aplance-ui 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/registry/components/primitives/icon/icon.tsx +4 -6
- package/registry/components/primitives/modal/modal.module.css +5 -0
- package/registry/components/primitives/modal/modal.tsx +32 -34
- package/registry/components/primitives/popover/popover-context.ts +33 -29
- package/registry/components/primitives/popover/popover.module.css +4 -2
- package/registry/components/primitives/popover/popover.tsx +150 -37
- package/registry/components/ui/accordion/accordion.module.css +52 -0
- package/registry/components/ui/accordion/accordion.tsx +97 -25
- package/registry/components/ui/avatar/avatar.tsx +2 -2
- package/registry/components/ui/breadcrumbs/breadcrumbs.module.css +9 -9
- package/registry/components/ui/button/button.module.css +43 -5
- package/registry/components/ui/button/button.tsx +2 -1
- package/registry/components/ui/copy-button/copy-button.module.css +2 -0
- package/registry/components/ui/copy-button/copy-button.tsx +5 -0
- package/registry/components/ui/data-table/data-table.module.css +786 -710
- package/registry/components/ui/data-table/data-table.tsx +1238 -1211
- package/registry/components/ui/data-table/filter-modal.tsx +86 -0
- package/registry/components/ui/data-table/filters.tsx +21 -0
- package/registry/components/ui/data-table/pagination-bar.tsx +31 -17
- package/registry/components/ui/data-table/types.ts +19 -1
- package/registry/components/ui/data-table/useDataTable.tsx +2 -2
- package/registry/components/ui/data-table/utils.ts +30 -4
- package/registry/components/ui/input-fields/common/input-field-common.module.css +8 -2
- package/registry/components/ui/input-fields/common/picker-popover-common.module.css +4 -0
- package/registry/components/ui/input-fields/custom-select/custom-select.module.css +118 -76
- package/registry/components/ui/input-fields/custom-select/custom-select.tsx +552 -484
- package/registry/components/ui/input-fields/date-picker/date-picker.tsx +936 -933
- package/registry/components/ui/input-fields/date-picker/types.ts +2 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-picker.tsx +8 -0
- package/registry/components/ui/input-fields/input/input.tsx +348 -348
- package/registry/components/ui/input-fields/media-input/media-input.tsx +3 -1
- package/registry/components/ui/input-fields/media-input/types.ts +8 -2
- package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +5 -5
- package/registry/components/ui/input-fields/media-input/utils.ts +18 -5
- package/registry/components/ui/scroll-rail/scroll-rail.module.css +104 -0
- package/registry/components/ui/scroll-rail/scroll-rail.tsx +159 -0
- package/registry/components/ui/tabs/tabs.module.css +8 -12
- package/registry/components/ui/tabs/tabs.tsx +94 -50
- package/registry/components/ui/tooltip/tooltip.module.css +8 -3
- package/registry/components/ui/tooltip/tooltip.tsx +16 -3
- package/registry/hooks/useMediaQuery.ts +38 -0
- package/registry/lib/string.ts +3 -3
- package/registry/registry.json +54 -4
|
@@ -1,348 +1,348 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type ComponentProps,
|
|
3
|
-
useCallback,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
| "
|
|
27
|
-
| "
|
|
28
|
-
| "
|
|
29
|
-
| "
|
|
30
|
-
| "
|
|
31
|
-
| "
|
|
32
|
-
| "
|
|
33
|
-
| "
|
|
34
|
-
| "
|
|
35
|
-
| "
|
|
36
|
-
| "
|
|
37
|
-
| "
|
|
38
|
-
| "
|
|
39
|
-
| "
|
|
40
|
-
| "
|
|
41
|
-
| "
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
]
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
type
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
131
|
-
|
|
132
|
-
const isPassword = type === "password";
|
|
133
|
-
const isSearch = type === "search";
|
|
134
|
-
|
|
135
|
-
const inputType = isPassword && eyeVisible ? "text" : type;
|
|
136
|
-
// useId() guarantees a stable, unique id per Input instance. Falling back
|
|
137
|
-
// to `name` previously broke radio/checkbox siblings (they share a name,
|
|
138
|
-
// so every label's htmlFor pointed to the first input in the DOM).
|
|
139
|
-
const generatedId = useId();
|
|
140
|
-
const inputId = props.id ?? generatedId;
|
|
141
|
-
|
|
142
|
-
const showPasswordToggle = isPassword && hasValue;
|
|
143
|
-
const showClearButton =
|
|
144
|
-
showClear && hasValue && clearableInputTypes.includes(type);
|
|
145
|
-
|
|
146
|
-
const stylesToApplyFields = styledInputTypes.includes(type);
|
|
147
|
-
const stylesFitWidth = fitWidthInputTypes.includes(type);
|
|
148
|
-
const hasButtonStyle = buttonStyle !== "none";
|
|
149
|
-
const buttonToneClass = hasButtonStyle
|
|
150
|
-
? sButton[`tone_${buttonStyle}`]
|
|
151
|
-
: undefined;
|
|
152
|
-
|
|
153
|
-
const isDateFamily = dateInputTypes.includes(type);
|
|
154
|
-
const isTimeFamily = type === "time";
|
|
155
|
-
|
|
156
|
-
const showDateCalendarIcon = isDateFamily && !iconEnd;
|
|
157
|
-
const showTimeClockIcon = isTimeFamily && !iconEnd;
|
|
158
|
-
|
|
159
|
-
const startActionCount = iconStart ? 1 : isSearch ? 1 : 0;
|
|
160
|
-
const endActionCount = [
|
|
161
|
-
showClearButton,
|
|
162
|
-
showPasswordToggle,
|
|
163
|
-
showDateCalendarIcon,
|
|
164
|
-
showTimeClockIcon,
|
|
165
|
-
iconEnd,
|
|
166
|
-
].filter(Boolean).length;
|
|
167
|
-
|
|
168
|
-
const dynamicInputStyle = {
|
|
169
|
-
...style,
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
173
|
-
setHasValue(e.target.value.length > 0);
|
|
174
|
-
onChange?.(e);
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
// A programmatic value set (e.g. react-hook-form populating a field on reset)
|
|
178
|
-
// sets the DOM value via the ref WITHOUT firing onChange, so `hasValue` can lag
|
|
179
|
-
// the real value and the reveal/clear actions stay hidden on a pre-filled
|
|
180
|
-
// field. Re-sync from the actual value on focus so they appear as soon as the
|
|
181
|
-
// field is interacted with.
|
|
182
|
-
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
183
|
-
setHasValue(e.target.value.length > 0);
|
|
184
|
-
props.onFocus?.(e);
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const handleClear = () => {
|
|
188
|
-
const element = inputRef.current;
|
|
189
|
-
if (!element) return;
|
|
190
|
-
|
|
191
|
-
if (onClear) {
|
|
192
|
-
onClear();
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (onChange) {
|
|
197
|
-
element.value = "";
|
|
198
|
-
setHasValue(false);
|
|
199
|
-
const event = {
|
|
200
|
-
target: element,
|
|
201
|
-
currentTarget: element,
|
|
202
|
-
} as React.ChangeEvent<HTMLInputElement>;
|
|
203
|
-
onChange(event);
|
|
204
|
-
element.focus();
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
element.value = "";
|
|
209
|
-
setHasValue(false);
|
|
210
|
-
element.focus();
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
// Stable ref callback - memoized so React doesn't treat it as a new ref
|
|
214
|
-
// on every render. Does NOT call setState (would cause infinite loop).
|
|
215
|
-
const refCallback = useCallback(
|
|
216
|
-
(element: HTMLInputElement | null) => {
|
|
217
|
-
inputRef.current = element;
|
|
218
|
-
if (ref) {
|
|
219
|
-
if (typeof ref === "function") {
|
|
220
|
-
ref(element);
|
|
221
|
-
} else {
|
|
222
|
-
ref.current = element;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
[ref],
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
return (
|
|
230
|
-
<div className={clsx(sCommon.container, className)} style={style}>
|
|
231
|
-
<div
|
|
232
|
-
className={clsx(
|
|
233
|
-
sCommon.field_container,
|
|
234
|
-
layout !== "default" && sCommon[layout],
|
|
235
|
-
layout !== "default" &&
|
|
236
|
-
layoutDirection !== "default" &&
|
|
237
|
-
sCommon[layoutDirection],
|
|
238
|
-
)}
|
|
239
|
-
>
|
|
240
|
-
{label && (
|
|
241
|
-
<label className={sCommon.label} htmlFor={inputId}>
|
|
242
|
-
{label}
|
|
243
|
-
</label>
|
|
244
|
-
)}
|
|
245
|
-
<div
|
|
246
|
-
className={clsx(
|
|
247
|
-
sCommon.field_wrapper,
|
|
248
|
-
s.native_field_wrapper,
|
|
249
|
-
hasButtonStyle && sCommon.field_wrapper_variant,
|
|
250
|
-
hasButtonStyle && buttonToneClass,
|
|
251
|
-
stylesFitWidth && sCommon.field_wrapper_fit,
|
|
252
|
-
)}
|
|
253
|
-
data-start-actions={
|
|
254
|
-
startActionCount > 0 ? startActionCount : undefined
|
|
255
|
-
}
|
|
256
|
-
data-end-actions={
|
|
257
|
-
endActionCount > 0 ? Math.min(3, endActionCount) : undefined
|
|
258
|
-
}
|
|
259
|
-
>
|
|
260
|
-
{iconStart ? (
|
|
261
|
-
<div className={clsx(sCommon.icon_start)}>{iconStart}</div>
|
|
262
|
-
) : isSearch ? (
|
|
263
|
-
<Icon className={clsx(sCommon.icon_start)}>
|
|
264
|
-
<IconSearch />
|
|
265
|
-
</Icon>
|
|
266
|
-
) : null}
|
|
267
|
-
|
|
268
|
-
<input
|
|
269
|
-
id={inputId}
|
|
270
|
-
name={props.name}
|
|
271
|
-
type={inputType}
|
|
272
|
-
value={value}
|
|
273
|
-
ref={refCallback}
|
|
274
|
-
className={clsx(
|
|
275
|
-
stylesToApplyFields &&
|
|
276
|
-
buttonStyle === "none" &&
|
|
277
|
-
sCommon.field_default,
|
|
278
|
-
stylesToApplyFields &&
|
|
279
|
-
hasButtonStyle && [
|
|
280
|
-
sCommon.field_full_width,
|
|
281
|
-
sCommon.field_cursor_text,
|
|
282
|
-
sCommon.field_button_variant,
|
|
283
|
-
buttonToneClass,
|
|
284
|
-
sButton.base,
|
|
285
|
-
sButton[buttonStyle],
|
|
286
|
-
sButton.input_focus,
|
|
287
|
-
],
|
|
288
|
-
)}
|
|
289
|
-
style={dynamicInputStyle}
|
|
290
|
-
{...props}
|
|
291
|
-
onChange={handleInputChange}
|
|
292
|
-
onFocus={handleFocus}
|
|
293
|
-
/>
|
|
294
|
-
|
|
295
|
-
<div className={clsx(sCommon.action_end_wrapper)}>
|
|
296
|
-
{showClearButton && (
|
|
297
|
-
<Icon onClick={handleClear} aria-label="Clear input">
|
|
298
|
-
<IconX />
|
|
299
|
-
</Icon>
|
|
300
|
-
)}
|
|
301
|
-
{showPasswordToggle && (
|
|
302
|
-
<Icon
|
|
303
|
-
onClick={(e) => {
|
|
304
|
-
e.preventDefault();
|
|
305
|
-
setEyeVisible((v) => !v);
|
|
306
|
-
}}
|
|
307
|
-
aria-label={eyeVisible ? "Hide password" : "Show password"}
|
|
308
|
-
>
|
|
309
|
-
{eyeVisible ? <IconKeyOff /> : <IconKey />}
|
|
310
|
-
</Icon>
|
|
311
|
-
)}
|
|
312
|
-
{showDateCalendarIcon && (
|
|
313
|
-
<Icon
|
|
314
|
-
onClick={() => {
|
|
315
|
-
try {
|
|
316
|
-
inputRef.current?.showPicker();
|
|
317
|
-
} catch {
|
|
318
|
-
// showPicker is not supported in all browsers.
|
|
319
|
-
}
|
|
320
|
-
}}
|
|
321
|
-
clickable
|
|
322
|
-
>
|
|
323
|
-
<IconCalendar />
|
|
324
|
-
</Icon>
|
|
325
|
-
)}
|
|
326
|
-
{showTimeClockIcon && (
|
|
327
|
-
<Icon
|
|
328
|
-
onClick={() => {
|
|
329
|
-
try {
|
|
330
|
-
inputRef.current?.showPicker();
|
|
331
|
-
} catch {
|
|
332
|
-
// showPicker is not supported in all browsers.
|
|
333
|
-
}
|
|
334
|
-
}}
|
|
335
|
-
clickable
|
|
336
|
-
>
|
|
337
|
-
<IconClock />
|
|
338
|
-
</Icon>
|
|
339
|
-
)}
|
|
340
|
-
{iconEnd}
|
|
341
|
-
</div>
|
|
342
|
-
</div>
|
|
343
|
-
</div>
|
|
344
|
-
|
|
345
|
-
<FieldMessage errors={errors} info={info} />
|
|
346
|
-
</div>
|
|
347
|
-
);
|
|
348
|
-
};
|
|
1
|
+
import {
|
|
2
|
+
type ComponentProps,
|
|
3
|
+
useCallback,
|
|
4
|
+
useId,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from "react";
|
|
8
|
+
import clsx from "clsx";
|
|
9
|
+
import {
|
|
10
|
+
IconKey,
|
|
11
|
+
IconKeyOff,
|
|
12
|
+
IconSearch,
|
|
13
|
+
IconX,
|
|
14
|
+
IconCalendar,
|
|
15
|
+
IconClock,
|
|
16
|
+
} from "@tabler/icons-react";
|
|
17
|
+
import sCommon from "../common/input-field-common.module.css";
|
|
18
|
+
import sButton from "../../button/button.module.css";
|
|
19
|
+
import s from "../common/input-field-native.module.css";
|
|
20
|
+
import { FieldMessage } from "../common/field-message/field-message";
|
|
21
|
+
import { Icon } from "../../../primitives/icon/icon";
|
|
22
|
+
import type { BaseInputFieldProps } from "../common/types";
|
|
23
|
+
|
|
24
|
+
export type InputType =
|
|
25
|
+
| "text"
|
|
26
|
+
| "email"
|
|
27
|
+
| "tel"
|
|
28
|
+
| "url"
|
|
29
|
+
| "password"
|
|
30
|
+
| "search"
|
|
31
|
+
| "number"
|
|
32
|
+
| "file"
|
|
33
|
+
| "radio"
|
|
34
|
+
| "date"
|
|
35
|
+
| "month"
|
|
36
|
+
| "week"
|
|
37
|
+
| "time"
|
|
38
|
+
| "datetime-local"
|
|
39
|
+
| "checkbox"
|
|
40
|
+
| "color"
|
|
41
|
+
| "range";
|
|
42
|
+
|
|
43
|
+
const styledInputTypes: readonly InputType[] = [
|
|
44
|
+
"text",
|
|
45
|
+
"email",
|
|
46
|
+
"tel",
|
|
47
|
+
"url",
|
|
48
|
+
"password",
|
|
49
|
+
"search",
|
|
50
|
+
"number",
|
|
51
|
+
"file",
|
|
52
|
+
"date",
|
|
53
|
+
"month",
|
|
54
|
+
"week",
|
|
55
|
+
"time",
|
|
56
|
+
"datetime-local",
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
const clearableInputTypes: readonly InputType[] = [
|
|
60
|
+
"text",
|
|
61
|
+
"email",
|
|
62
|
+
"tel",
|
|
63
|
+
"url",
|
|
64
|
+
"password",
|
|
65
|
+
"search",
|
|
66
|
+
"number",
|
|
67
|
+
"file",
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
70
|
+
const fitWidthInputTypes: readonly InputType[] = [
|
|
71
|
+
"checkbox",
|
|
72
|
+
"radio",
|
|
73
|
+
"color",
|
|
74
|
+
] as const;
|
|
75
|
+
const dateInputTypes: readonly InputType[] = [
|
|
76
|
+
"date",
|
|
77
|
+
"month",
|
|
78
|
+
"week",
|
|
79
|
+
"datetime-local",
|
|
80
|
+
] as const;
|
|
81
|
+
|
|
82
|
+
type BaseProps = BaseInputFieldProps & {
|
|
83
|
+
type?: InputType;
|
|
84
|
+
onClear?: () => void;
|
|
85
|
+
showClear?: boolean;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type InputProps = Omit<ComponentProps<"input">, "type"> & BaseProps;
|
|
89
|
+
|
|
90
|
+
export const Input = ({
|
|
91
|
+
type = "text",
|
|
92
|
+
label,
|
|
93
|
+
errors,
|
|
94
|
+
info,
|
|
95
|
+
className,
|
|
96
|
+
style,
|
|
97
|
+
value,
|
|
98
|
+
layout = "default",
|
|
99
|
+
layoutDirection = "default",
|
|
100
|
+
ref,
|
|
101
|
+
onClear,
|
|
102
|
+
showClear = false,
|
|
103
|
+
onChange,
|
|
104
|
+
buttonStyle = "none",
|
|
105
|
+
iconStart,
|
|
106
|
+
iconEnd,
|
|
107
|
+
...props
|
|
108
|
+
}: InputProps) => {
|
|
109
|
+
const [eyeVisible, setEyeVisible] = useState(false);
|
|
110
|
+
|
|
111
|
+
// Derive initial hasValue from controlled value or defaultValue.
|
|
112
|
+
// IMPORTANT: do NOT call setHasValue inside a ref callback - it triggers
|
|
113
|
+
// a re-render which creates a new ref callback, which React treats as a
|
|
114
|
+
// changed ref, repeating the cycle indefinitely (infinite loop).
|
|
115
|
+
const [hasValue, setHasValue] = useState(() => {
|
|
116
|
+
if (value !== undefined && value !== null && value !== "") return true;
|
|
117
|
+
const dv = props.defaultValue;
|
|
118
|
+
if (dv !== undefined && dv !== null && dv !== "") return true;
|
|
119
|
+
return false;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Sync hasValue when controlled value changes externally (e.g. form reset),
|
|
123
|
+
// in the render that sees the change.
|
|
124
|
+
const [seenValue, setSeenValue] = useState(value);
|
|
125
|
+
if (!Object.is(value, seenValue)) {
|
|
126
|
+
setSeenValue(value);
|
|
127
|
+
setHasValue(value !== undefined && String(value).length > 0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
131
|
+
|
|
132
|
+
const isPassword = type === "password";
|
|
133
|
+
const isSearch = type === "search";
|
|
134
|
+
|
|
135
|
+
const inputType = isPassword && eyeVisible ? "text" : type;
|
|
136
|
+
// useId() guarantees a stable, unique id per Input instance. Falling back
|
|
137
|
+
// to `name` previously broke radio/checkbox siblings (they share a name,
|
|
138
|
+
// so every label's htmlFor pointed to the first input in the DOM).
|
|
139
|
+
const generatedId = useId();
|
|
140
|
+
const inputId = props.id ?? generatedId;
|
|
141
|
+
|
|
142
|
+
const showPasswordToggle = isPassword && hasValue;
|
|
143
|
+
const showClearButton =
|
|
144
|
+
showClear && hasValue && clearableInputTypes.includes(type);
|
|
145
|
+
|
|
146
|
+
const stylesToApplyFields = styledInputTypes.includes(type);
|
|
147
|
+
const stylesFitWidth = fitWidthInputTypes.includes(type);
|
|
148
|
+
const hasButtonStyle = buttonStyle !== "none";
|
|
149
|
+
const buttonToneClass = hasButtonStyle
|
|
150
|
+
? sButton[`tone_${buttonStyle}`]
|
|
151
|
+
: undefined;
|
|
152
|
+
|
|
153
|
+
const isDateFamily = dateInputTypes.includes(type);
|
|
154
|
+
const isTimeFamily = type === "time";
|
|
155
|
+
|
|
156
|
+
const showDateCalendarIcon = isDateFamily && !iconEnd;
|
|
157
|
+
const showTimeClockIcon = isTimeFamily && !iconEnd;
|
|
158
|
+
|
|
159
|
+
const startActionCount = iconStart ? 1 : isSearch ? 1 : 0;
|
|
160
|
+
const endActionCount = [
|
|
161
|
+
showClearButton,
|
|
162
|
+
showPasswordToggle,
|
|
163
|
+
showDateCalendarIcon,
|
|
164
|
+
showTimeClockIcon,
|
|
165
|
+
iconEnd,
|
|
166
|
+
].filter(Boolean).length;
|
|
167
|
+
|
|
168
|
+
const dynamicInputStyle = {
|
|
169
|
+
...style,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
173
|
+
setHasValue(e.target.value.length > 0);
|
|
174
|
+
onChange?.(e);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// A programmatic value set (e.g. react-hook-form populating a field on reset)
|
|
178
|
+
// sets the DOM value via the ref WITHOUT firing onChange, so `hasValue` can lag
|
|
179
|
+
// the real value and the reveal/clear actions stay hidden on a pre-filled
|
|
180
|
+
// field. Re-sync from the actual value on focus so they appear as soon as the
|
|
181
|
+
// field is interacted with.
|
|
182
|
+
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
183
|
+
setHasValue(e.target.value.length > 0);
|
|
184
|
+
props.onFocus?.(e);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const handleClear = () => {
|
|
188
|
+
const element = inputRef.current;
|
|
189
|
+
if (!element) return;
|
|
190
|
+
|
|
191
|
+
if (onClear) {
|
|
192
|
+
onClear();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (onChange) {
|
|
197
|
+
element.value = "";
|
|
198
|
+
setHasValue(false);
|
|
199
|
+
const event = {
|
|
200
|
+
target: element,
|
|
201
|
+
currentTarget: element,
|
|
202
|
+
} as React.ChangeEvent<HTMLInputElement>;
|
|
203
|
+
onChange(event);
|
|
204
|
+
element.focus();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
element.value = "";
|
|
209
|
+
setHasValue(false);
|
|
210
|
+
element.focus();
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Stable ref callback - memoized so React doesn't treat it as a new ref
|
|
214
|
+
// on every render. Does NOT call setState (would cause infinite loop).
|
|
215
|
+
const refCallback = useCallback(
|
|
216
|
+
(element: HTMLInputElement | null) => {
|
|
217
|
+
inputRef.current = element;
|
|
218
|
+
if (ref) {
|
|
219
|
+
if (typeof ref === "function") {
|
|
220
|
+
ref(element);
|
|
221
|
+
} else {
|
|
222
|
+
ref.current = element;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
[ref],
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div className={clsx(sCommon.container, className)} style={style}>
|
|
231
|
+
<div
|
|
232
|
+
className={clsx(
|
|
233
|
+
sCommon.field_container,
|
|
234
|
+
layout !== "default" && sCommon[layout],
|
|
235
|
+
layout !== "default" &&
|
|
236
|
+
layoutDirection !== "default" &&
|
|
237
|
+
sCommon[layoutDirection],
|
|
238
|
+
)}
|
|
239
|
+
>
|
|
240
|
+
{label && (
|
|
241
|
+
<label className={sCommon.label} htmlFor={inputId}>
|
|
242
|
+
{label}
|
|
243
|
+
</label>
|
|
244
|
+
)}
|
|
245
|
+
<div
|
|
246
|
+
className={clsx(
|
|
247
|
+
sCommon.field_wrapper,
|
|
248
|
+
s.native_field_wrapper,
|
|
249
|
+
hasButtonStyle && sCommon.field_wrapper_variant,
|
|
250
|
+
hasButtonStyle && buttonToneClass,
|
|
251
|
+
stylesFitWidth && sCommon.field_wrapper_fit,
|
|
252
|
+
)}
|
|
253
|
+
data-start-actions={
|
|
254
|
+
startActionCount > 0 ? startActionCount : undefined
|
|
255
|
+
}
|
|
256
|
+
data-end-actions={
|
|
257
|
+
endActionCount > 0 ? Math.min(3, endActionCount) : undefined
|
|
258
|
+
}
|
|
259
|
+
>
|
|
260
|
+
{iconStart ? (
|
|
261
|
+
<div className={clsx(sCommon.icon_start)}>{iconStart}</div>
|
|
262
|
+
) : isSearch ? (
|
|
263
|
+
<Icon className={clsx(sCommon.icon_start)}>
|
|
264
|
+
<IconSearch />
|
|
265
|
+
</Icon>
|
|
266
|
+
) : null}
|
|
267
|
+
|
|
268
|
+
<input
|
|
269
|
+
id={inputId}
|
|
270
|
+
name={props.name}
|
|
271
|
+
type={inputType}
|
|
272
|
+
value={value}
|
|
273
|
+
ref={refCallback}
|
|
274
|
+
className={clsx(
|
|
275
|
+
stylesToApplyFields &&
|
|
276
|
+
buttonStyle === "none" &&
|
|
277
|
+
sCommon.field_default,
|
|
278
|
+
stylesToApplyFields &&
|
|
279
|
+
hasButtonStyle && [
|
|
280
|
+
sCommon.field_full_width,
|
|
281
|
+
sCommon.field_cursor_text,
|
|
282
|
+
sCommon.field_button_variant,
|
|
283
|
+
buttonToneClass,
|
|
284
|
+
sButton.base,
|
|
285
|
+
sButton[buttonStyle],
|
|
286
|
+
sButton.input_focus,
|
|
287
|
+
],
|
|
288
|
+
)}
|
|
289
|
+
style={dynamicInputStyle}
|
|
290
|
+
{...props}
|
|
291
|
+
onChange={handleInputChange}
|
|
292
|
+
onFocus={handleFocus}
|
|
293
|
+
/>
|
|
294
|
+
|
|
295
|
+
<div className={clsx(sCommon.action_end_wrapper)}>
|
|
296
|
+
{showClearButton && (
|
|
297
|
+
<Icon onClick={handleClear} aria-label="Clear input">
|
|
298
|
+
<IconX />
|
|
299
|
+
</Icon>
|
|
300
|
+
)}
|
|
301
|
+
{showPasswordToggle && (
|
|
302
|
+
<Icon
|
|
303
|
+
onClick={(e) => {
|
|
304
|
+
e.preventDefault();
|
|
305
|
+
setEyeVisible((v) => !v);
|
|
306
|
+
}}
|
|
307
|
+
aria-label={eyeVisible ? "Hide password" : "Show password"}
|
|
308
|
+
>
|
|
309
|
+
{eyeVisible ? <IconKeyOff /> : <IconKey />}
|
|
310
|
+
</Icon>
|
|
311
|
+
)}
|
|
312
|
+
{showDateCalendarIcon && (
|
|
313
|
+
<Icon
|
|
314
|
+
onClick={() => {
|
|
315
|
+
try {
|
|
316
|
+
inputRef.current?.showPicker();
|
|
317
|
+
} catch {
|
|
318
|
+
// showPicker is not supported in all browsers.
|
|
319
|
+
}
|
|
320
|
+
}}
|
|
321
|
+
clickable
|
|
322
|
+
>
|
|
323
|
+
<IconCalendar />
|
|
324
|
+
</Icon>
|
|
325
|
+
)}
|
|
326
|
+
{showTimeClockIcon && (
|
|
327
|
+
<Icon
|
|
328
|
+
onClick={() => {
|
|
329
|
+
try {
|
|
330
|
+
inputRef.current?.showPicker();
|
|
331
|
+
} catch {
|
|
332
|
+
// showPicker is not supported in all browsers.
|
|
333
|
+
}
|
|
334
|
+
}}
|
|
335
|
+
clickable
|
|
336
|
+
>
|
|
337
|
+
<IconClock />
|
|
338
|
+
</Icon>
|
|
339
|
+
)}
|
|
340
|
+
{iconEnd}
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<FieldMessage errors={errors} info={info} />
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
};
|