UNPKG

4.25 kBTypeScriptView Raw
1"use client";
2
3import React, { memo, forwardRef, ReactNode, useId, type CSSProperties } from "react";
4import { symToStr } from "tsafe/symToStr";
5import { assert } from "tsafe/assert";
6import type { Equals } from "tsafe";
7import { fr } from "./fr";
8import { cx } from "./tools/cx";
9import { useAnalyticsId } from "./tools/useAnalyticsId";
10
11export type SelectProps = {
12 id?: string;
13 className?: string;
14 label: ReactNode;
15 hint?: ReactNode;
16 nativeSelectProps: React.DetailedHTMLProps<
17 React.SelectHTMLAttributes<HTMLSelectElement>,
18 HTMLSelectElement
19 >;
20 children: ReactNode;
21 /** Default: false */
22 disabled?: boolean;
23 /** Default: "default" */
24 state?: "success" | "error" | "default";
25 /** The message won't be displayed if state is "default" */
26 stateRelatedMessage?: ReactNode;
27 style?: CSSProperties;
28};
29
30/**
31 * @see <https://components.react-dsfr.codegouv.studio/?path=/docs/components-select>
32 * */
33export const Select = memo(
34 forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
35 const {
36 id: id_props,
37 className,
38 label,
39 hint,
40 nativeSelectProps,
41 disabled = false,
42 children,
43 state = "default",
44 stateRelatedMessage,
45 style,
46 ...rest
47 } = props;
48
49 assert<Equals<keyof typeof rest, never>>();
50
51 const id = useAnalyticsId({
52 "defaultIdPrefix": "fr-select-group",
53 "explicitlyProvidedId": id_props
54 });
55
56 const selectId = (function useClosure() {
57 const id = useId();
58
59 if (nativeSelectProps.id !== undefined) {
60 return nativeSelectProps.id;
61 }
62
63 return `select-${id}`;
64 })();
65
66 const stateDescriptionId = `select-${useId()}-desc`;
67
68 return (
69 <div
70 id={id}
71 className={cx(
72 fr.cx(
73 "fr-select-group",
74 disabled && "fr-select-group--disabled",
75 (() => {
76 switch (state) {
77 case "error":
78 return "fr-select-group--error";
79 case "success":
80 return "fr-select-group--valid";
81 case "default":
82 return undefined;
83 }
84 assert<Equals<typeof state, never>>(false);
85 })()
86 ),
87 className
88 )}
89 ref={ref}
90 style={style}
91 {...rest}
92 >
93 <label className={fr.cx("fr-label")} htmlFor={selectId}>
94 {label}
95 {hint !== undefined && <span className={fr.cx("fr-hint-text")}>{hint}</span>}
96 </label>
97 <select
98 {...nativeSelectProps}
99 className={cx(fr.cx("fr-select"), nativeSelectProps.className)}
100 id={selectId}
101 aria-describedby={stateDescriptionId}
102 disabled={disabled}
103 >
104 {children}
105 </select>
106 {state !== "default" && (
107 <p
108 id={stateDescriptionId}
109 className={fr.cx(
110 (() => {
111 switch (state) {
112 case "error":
113 return "fr-error-text";
114 case "success":
115 return "fr-valid-text";
116 }
117 assert<Equals<typeof state, never>>(false);
118 })()
119 )}
120 >
121 {stateRelatedMessage}
122 </p>
123 )}
124 </div>
125 );
126 })
127);
128
129Select.displayName = symToStr({ Select });
130
131export default Select;
132
\No newline at end of file