1 | "use client";
|
2 |
|
3 | import React, { memo, forwardRef, ReactNode, useId, type CSSProperties } from "react";
|
4 | import { symToStr } from "tsafe/symToStr";
|
5 | import { assert } from "tsafe/assert";
|
6 | import type { Equals } from "tsafe";
|
7 | import { fr } from "./fr";
|
8 | import { cx } from "./tools/cx";
|
9 | import { useAnalyticsId } from "./tools/useAnalyticsId";
|
10 |
|
11 | export 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 |
|
22 | disabled?: boolean;
|
23 |
|
24 | state?: "success" | "error" | "default";
|
25 |
|
26 | stateRelatedMessage?: ReactNode;
|
27 | style?: CSSProperties;
|
28 | };
|
29 |
|
30 |
|
31 |
|
32 |
|
33 | export 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 |
|
129 | Select.displayName = symToStr({ Select });
|
130 |
|
131 | export default Select;
|
132 |
|
\ | No newline at end of file |