UNPKG

7.24 kBTypeScriptView Raw
1import React, {
2 memo,
3 forwardRef,
4 ReactNode,
5 useId,
6 type InputHTMLAttributes,
7 type TextareaHTMLAttributes,
8 type DetailedHTMLProps,
9 type CSSProperties
10} from "react";
11import { symToStr } from "tsafe/symToStr";
12import { assert } from "tsafe/assert";
13import type { Equals } from "tsafe";
14import { fr } from "./fr";
15import { cx } from "./tools/cx";
16import type { FrIconClassName, RiIconClassName } from "./fr/generatedFromCss/classNames";
17
18export type InputProps = InputProps.RegularInput | InputProps.TextArea;
19
20export namespace InputProps {
21 export type Common = {
22 className?: string;
23 id?: string;
24 label: ReactNode;
25 hintText?: ReactNode;
26 hideLabel?: boolean;
27 /** default: false */
28 disabled?: boolean;
29 iconId?: FrIconClassName | RiIconClassName;
30 classes?: Partial<
31 Record<"root" | "label" | "description" | "nativeInputOrTextArea" | "message", string>
32 >;
33 style?: CSSProperties;
34 /** Default: "default" */
35 state?: "success" | "error" | "default";
36 /** The message won't be displayed if state is "default" */
37 stateRelatedMessage?: ReactNode;
38 addon?: ReactNode;
39 };
40
41 export type RegularInput = Common & {
42 /** Default: false */
43 textArea?: false;
44 /** Props forwarded to the underlying <input /> element */
45 nativeInputProps?: DetailedHTMLProps<
46 InputHTMLAttributes<HTMLInputElement>,
47 HTMLInputElement
48 >;
49
50 nativeTextAreaProps?: never;
51 };
52
53 export type TextArea = Common & {
54 /** Default: false */
55 textArea: true;
56 /** Props forwarded to the underlying <textarea /> element */
57 nativeTextAreaProps?: DetailedHTMLProps<
58 TextareaHTMLAttributes<HTMLTextAreaElement>,
59 HTMLTextAreaElement
60 >;
61
62 nativeInputProps?: never;
63 };
64}
65
66/**
67 * @see <https://components.react-dsfr.codegouv.studio/?path=/docs/components-input>
68 * */
69export const Input = memo(
70 forwardRef<HTMLDivElement, InputProps>((props, ref) => {
71 const {
72 className,
73 id,
74 label,
75 hintText,
76 hideLabel,
77 disabled = false,
78 iconId,
79 classes = {},
80 style,
81 state = "default",
82 stateRelatedMessage,
83 textArea = false,
84 nativeTextAreaProps,
85 nativeInputProps,
86 addon,
87 ...rest
88 } = props;
89
90 const nativeInputOrTextAreaProps =
91 (textArea ? nativeTextAreaProps : nativeInputProps) ?? {};
92
93 const NativeInputOrTextArea = textArea ? "textarea" : "input";
94
95 assert<Equals<keyof typeof rest, never>>();
96
97 const inputId = (function useClosure() {
98 const id = useId();
99
100 return nativeInputOrTextAreaProps.id ?? `input-${id}`;
101 })();
102
103 const messageId = `${inputId}-desc-error`;
104
105 return (
106 <div
107 className={cx(
108 fr.cx(
109 nativeInputProps?.type === "file" ? "fr-upload-group" : "fr-input-group",
110 disabled && "fr-input-group--disabled",
111 (() => {
112 switch (state) {
113 case "error":
114 return "fr-input-group--error";
115 case "success":
116 return "fr-input-group--valid";
117 case "default":
118 return undefined;
119 }
120 })()
121 ),
122 classes.root,
123 className
124 )}
125 style={style}
126 ref={ref}
127 id={id}
128 {...rest}
129 >
130 <label
131 className={cx(fr.cx("fr-label", hideLabel && "fr-sr-only"), classes.label)}
132 htmlFor={inputId}
133 >
134 {label}
135 {hintText !== undefined && <span className="fr-hint-text">{hintText}</span>}
136 </label>
137 {(() => {
138 const nativeInputOrTextArea = (
139 <NativeInputOrTextArea
140 {...(nativeInputOrTextAreaProps as {})}
141 className={cx(
142 fr.cx(
143 "fr-input",
144 (() => {
145 switch (state) {
146 case "error":
147 return "fr-input--error";
148 case "success":
149 return "fr-input--valid";
150 case "default":
151 return undefined;
152 }
153 })()
154 ),
155 classes.nativeInputOrTextArea
156 )}
157 disabled={disabled || undefined}
158 aria-describedby={messageId}
159 type={textArea ? undefined : nativeInputProps?.type ?? "text"}
160 id={inputId}
161 />
162 );
163
164 const hasIcon = iconId !== undefined;
165 const hasAddon = addon !== undefined;
166 return hasIcon || hasAddon ? (
167 <div
168 className={fr.cx(
169 "fr-input-wrap",
170 hasIcon && iconId,
171 hasAddon && "fr-input-wrap--addon"
172 )}
173 >
174 {nativeInputOrTextArea}
175 {hasAddon && addon}
176 </div>
177 ) : (
178 nativeInputOrTextArea
179 );
180 })()}
181 {state !== "default" && (
182 <p
183 id={messageId}
184 className={cx(
185 fr.cx(
186 (() => {
187 switch (state) {
188 case "error":
189 return "fr-error-text";
190 case "success":
191 return "fr-valid-text";
192 }
193 })()
194 ),
195 classes.message
196 )}
197 >
198 {stateRelatedMessage}
199 </p>
200 )}
201 </div>
202 );
203 })
204);
205
206Input.displayName = symToStr({ Input });
207
208export default Input;
209
\No newline at end of file