1 | import React, {
|
2 | memo,
|
3 | forwardRef,
|
4 | ReactNode,
|
5 | useId,
|
6 | type InputHTMLAttributes,
|
7 | type TextareaHTMLAttributes,
|
8 | type DetailedHTMLProps,
|
9 | type CSSProperties
|
10 | } from "react";
|
11 | import { symToStr } from "tsafe/symToStr";
|
12 | import { assert } from "tsafe/assert";
|
13 | import type { Equals } from "tsafe";
|
14 | import { fr } from "./fr";
|
15 | import { cx } from "./tools/cx";
|
16 | import type { FrIconClassName, RiIconClassName } from "./fr/generatedFromCss/classNames";
|
17 |
|
18 | export type InputProps = InputProps.RegularInput | InputProps.TextArea;
|
19 |
|
20 | export namespace InputProps {
|
21 | export type Common = {
|
22 | className?: string;
|
23 | id?: string;
|
24 | label: ReactNode;
|
25 | hintText?: ReactNode;
|
26 | hideLabel?: boolean;
|
27 |
|
28 | disabled?: boolean;
|
29 | iconId?: FrIconClassName | RiIconClassName;
|
30 | classes?: Partial<
|
31 | Record<"root" | "label" | "description" | "nativeInputOrTextArea" | "message", string>
|
32 | >;
|
33 | style?: CSSProperties;
|
34 |
|
35 | state?: "success" | "error" | "default";
|
36 |
|
37 | stateRelatedMessage?: ReactNode;
|
38 | addon?: ReactNode;
|
39 | };
|
40 |
|
41 | export type RegularInput = Common & {
|
42 |
|
43 | textArea?: false;
|
44 |
|
45 | nativeInputProps?: DetailedHTMLProps<
|
46 | InputHTMLAttributes<HTMLInputElement>,
|
47 | HTMLInputElement
|
48 | >;
|
49 |
|
50 | nativeTextAreaProps?: never;
|
51 | };
|
52 |
|
53 | export type TextArea = Common & {
|
54 |
|
55 | textArea: true;
|
56 |
|
57 | nativeTextAreaProps?: DetailedHTMLProps<
|
58 | TextareaHTMLAttributes<HTMLTextAreaElement>,
|
59 | HTMLTextAreaElement
|
60 | >;
|
61 |
|
62 | nativeInputProps?: never;
|
63 | };
|
64 | }
|
65 |
|
66 |
|
67 |
|
68 |
|
69 | export 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 |
|
206 | Input.displayName = symToStr({ Input });
|
207 |
|
208 | export default Input;
|
209 |
|
\ | No newline at end of file |