@uniai-fe/uds-primitives 0.6.15 → 0.7.1
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/dist/styles.css +455 -0
- package/package.json +5 -5
- package/src/components/input/img/clock.svg +4 -0
- package/src/components/input/index.scss +1 -0
- package/src/components/input/markup/index.tsx +2 -0
- package/src/components/input/markup/time/Picker.tsx +378 -0
- package/src/components/input/markup/time/index.tsx +6 -0
- package/src/components/input/styles/time.scss +420 -0
- package/src/components/input/styles/variables.scss +101 -0
- package/src/components/input/types/index.ts +1 -0
- package/src/components/input/types/time.ts +265 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { TimePicker as MantineTimePicker } from "@mantine/dates";
|
|
4
|
+
import { useUncontrolled } from "@mantine/hooks";
|
|
5
|
+
import clsx from "clsx";
|
|
6
|
+
import type { CSSProperties, ChangeEvent, FocusEvent, MouseEvent } from "react";
|
|
7
|
+
import { forwardRef, useCallback, useRef, useState } from "react";
|
|
8
|
+
import type { TimePickerStylesNames } from "@mantine/dates";
|
|
9
|
+
import {
|
|
10
|
+
getFormFieldWidthAttr,
|
|
11
|
+
getFormFieldWidthValue,
|
|
12
|
+
} from "../../../form/utils/form-field";
|
|
13
|
+
import type { InputTimePickerProps } from "../../types";
|
|
14
|
+
import ClockIcon from "../../img/clock.svg";
|
|
15
|
+
import ResetIcon from "../../img/reset.svg";
|
|
16
|
+
|
|
17
|
+
const INPUT_TIME_CLASS_NAMES = {
|
|
18
|
+
input: "input-time-input",
|
|
19
|
+
wrapper: "input-time-wrapper",
|
|
20
|
+
section: "input-time-section",
|
|
21
|
+
fieldsRoot: "input-time-fields-root",
|
|
22
|
+
fieldsGroup: "input-time-fields-group",
|
|
23
|
+
field: "input-time-field",
|
|
24
|
+
dropdown: "input-time-dropdown",
|
|
25
|
+
controlsListGroup: "input-time-controls-list-group",
|
|
26
|
+
controlsList: "input-time-controls-list",
|
|
27
|
+
control: "input-time-option",
|
|
28
|
+
presetControl: "input-time-preset-option",
|
|
29
|
+
presetsGroup: "input-time-presets-group",
|
|
30
|
+
presetsGroupLabel: "input-time-presets-group-label",
|
|
31
|
+
scrollarea: "input-time-scrollarea",
|
|
32
|
+
} satisfies Partial<Record<TimePickerStylesNames, string>>;
|
|
33
|
+
|
|
34
|
+
const INPUT_TIME_DROPDOWN_CONTENT_HEIGHT = 200;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Input Time Picker; Mantine TimePicker 기반 UDS adapter.
|
|
38
|
+
* `Input.Time.Picker` public namespace에서 사용하며 Calendar/date 하위에 속하지 않는다.
|
|
39
|
+
* @component
|
|
40
|
+
* @param {InputTimePickerProps} props
|
|
41
|
+
* @param {string} [props.value] 제어형 시간 값
|
|
42
|
+
* @param {string} [props.defaultValue] 비제어 초기 시간 값
|
|
43
|
+
* @param {(value:string)=>void} [props.onChange] 값 변경 핸들러
|
|
44
|
+
* @param {(value:string)=>void} [props.onValueChange] 값 변경 별칭
|
|
45
|
+
* @param {UseFormRegisterReturn} [props.register] react-hook-form register 결과
|
|
46
|
+
* @param {"primary" | "secondary" | "tertiary" | "table"} [props.priority="primary"] input priority
|
|
47
|
+
* @param {"small" | "medium" | "large"} [props.size="medium"] input size
|
|
48
|
+
* @param {"default" | "active" | "focused" | "success" | "error" | "disabled" | "loading"} [props.state="default"] input state
|
|
49
|
+
* @param {"12h" | "24h"} [props.format="24h"] 표시/입력 시간 포맷
|
|
50
|
+
* @param {boolean} [props.withSeconds=false] seconds 입력 노출 여부
|
|
51
|
+
* @param {boolean} [props.withDropdown=false] dropdown 노출 여부
|
|
52
|
+
* @param {boolean} [props.clearable=true] clear 버튼 노출 여부
|
|
53
|
+
* @param {"left" | "right"} [props.iconPosition] clock icon 위치
|
|
54
|
+
*/
|
|
55
|
+
const InputTimePicker = forwardRef<HTMLDivElement, InputTimePickerProps>(
|
|
56
|
+
(
|
|
57
|
+
{
|
|
58
|
+
value,
|
|
59
|
+
defaultValue,
|
|
60
|
+
onChange,
|
|
61
|
+
onValueChange,
|
|
62
|
+
name,
|
|
63
|
+
form,
|
|
64
|
+
register,
|
|
65
|
+
priority = "primary",
|
|
66
|
+
size = "medium",
|
|
67
|
+
state: stateProp = "default",
|
|
68
|
+
block = false,
|
|
69
|
+
width,
|
|
70
|
+
className,
|
|
71
|
+
disabled,
|
|
72
|
+
readOnly,
|
|
73
|
+
required,
|
|
74
|
+
id,
|
|
75
|
+
format = "24h",
|
|
76
|
+
withSeconds = false,
|
|
77
|
+
withDropdown = false,
|
|
78
|
+
clearable = true,
|
|
79
|
+
min,
|
|
80
|
+
max,
|
|
81
|
+
hoursStep,
|
|
82
|
+
minutesStep,
|
|
83
|
+
secondsStep,
|
|
84
|
+
icon,
|
|
85
|
+
iconPosition,
|
|
86
|
+
iconLabel = "시간 선택",
|
|
87
|
+
clearButtonLabel = "시간 지우기",
|
|
88
|
+
popoverProps,
|
|
89
|
+
hiddenInputProps,
|
|
90
|
+
hoursInputLabel,
|
|
91
|
+
minutesInputLabel,
|
|
92
|
+
secondsInputLabel,
|
|
93
|
+
amPmInputLabel,
|
|
94
|
+
amPmLabels,
|
|
95
|
+
hoursPlaceholder,
|
|
96
|
+
minutesPlaceholder,
|
|
97
|
+
secondsPlaceholder,
|
|
98
|
+
pasteSplit,
|
|
99
|
+
presets,
|
|
100
|
+
maxDropdownContentHeight,
|
|
101
|
+
scrollAreaProps,
|
|
102
|
+
reverseTimeControlsList,
|
|
103
|
+
onFocus,
|
|
104
|
+
onBlur,
|
|
105
|
+
},
|
|
106
|
+
ref,
|
|
107
|
+
) => {
|
|
108
|
+
const hoursInputRef = useRef<HTMLInputElement | null>(null);
|
|
109
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
110
|
+
const [timeValue, setTimeValue] = useUncontrolled<string>({
|
|
111
|
+
value,
|
|
112
|
+
defaultValue,
|
|
113
|
+
finalValue: "",
|
|
114
|
+
onChange: nextValue => {
|
|
115
|
+
onChange?.(nextValue);
|
|
116
|
+
onValueChange?.(nextValue);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const emitRegisterChange = useCallback(
|
|
121
|
+
(nextValue: string) => {
|
|
122
|
+
if (!register?.onChange) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const syntheticEvent = {
|
|
126
|
+
target: {
|
|
127
|
+
name: register.name,
|
|
128
|
+
value: nextValue,
|
|
129
|
+
},
|
|
130
|
+
currentTarget: {
|
|
131
|
+
name: register.name,
|
|
132
|
+
value: nextValue,
|
|
133
|
+
},
|
|
134
|
+
} as unknown as ChangeEvent<HTMLInputElement>;
|
|
135
|
+
register.onChange(syntheticEvent);
|
|
136
|
+
},
|
|
137
|
+
[register],
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const emitRegisterBlur = useCallback(
|
|
141
|
+
(nextValue: string) => {
|
|
142
|
+
if (!register?.onBlur) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const syntheticEvent = {
|
|
146
|
+
target: {
|
|
147
|
+
name: register.name,
|
|
148
|
+
value: nextValue,
|
|
149
|
+
},
|
|
150
|
+
currentTarget: {
|
|
151
|
+
name: register.name,
|
|
152
|
+
value: nextValue,
|
|
153
|
+
},
|
|
154
|
+
} as unknown as FocusEvent<HTMLInputElement>;
|
|
155
|
+
register.onBlur(syntheticEvent);
|
|
156
|
+
},
|
|
157
|
+
[register],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const currentState = disabled ? "disabled" : stateProp;
|
|
161
|
+
const isDisabled =
|
|
162
|
+
currentState === "disabled" || currentState === "loading";
|
|
163
|
+
const visualState =
|
|
164
|
+
priority === "tertiary"
|
|
165
|
+
? isDisabled
|
|
166
|
+
? "disabled"
|
|
167
|
+
: "default"
|
|
168
|
+
: !isDisabled && isFocused
|
|
169
|
+
? "active"
|
|
170
|
+
: currentState;
|
|
171
|
+
const resolvedIconPosition =
|
|
172
|
+
iconPosition ?? (priority === "table" ? "left" : "right");
|
|
173
|
+
const resolvedBlock =
|
|
174
|
+
block || (priority === "table" && width === undefined);
|
|
175
|
+
const widthAttr =
|
|
176
|
+
width !== undefined
|
|
177
|
+
? getFormFieldWidthAttr(width)
|
|
178
|
+
: resolvedBlock
|
|
179
|
+
? "full"
|
|
180
|
+
: undefined;
|
|
181
|
+
const widthValue =
|
|
182
|
+
width !== undefined ? getFormFieldWidthValue(width) : undefined;
|
|
183
|
+
const dropdownContentHeight =
|
|
184
|
+
maxDropdownContentHeight ?? INPUT_TIME_DROPDOWN_CONTENT_HEIGHT;
|
|
185
|
+
const containerStyle: CSSProperties | undefined =
|
|
186
|
+
widthValue !== undefined || maxDropdownContentHeight !== undefined
|
|
187
|
+
? ({
|
|
188
|
+
"--input-time-width": widthValue,
|
|
189
|
+
"--input-time-dropdown-content-height":
|
|
190
|
+
maxDropdownContentHeight !== undefined
|
|
191
|
+
? `${maxDropdownContentHeight}px`
|
|
192
|
+
: undefined,
|
|
193
|
+
} as CSSProperties)
|
|
194
|
+
: undefined;
|
|
195
|
+
const resolvedIcon = icon === undefined ? <ClockIcon /> : icon;
|
|
196
|
+
const showClearButton = Boolean(
|
|
197
|
+
clearable && timeValue && isFocused && !isDisabled && !readOnly,
|
|
198
|
+
);
|
|
199
|
+
const reserveClearSpace = Boolean(clearable && !isDisabled && !readOnly);
|
|
200
|
+
const rightSectionWidth =
|
|
201
|
+
resolvedIconPosition === "right"
|
|
202
|
+
? reserveClearSpace
|
|
203
|
+
? "var(--input-time-section-width-with-clear)"
|
|
204
|
+
: "var(--input-time-section-width)"
|
|
205
|
+
: showClearButton
|
|
206
|
+
? "var(--input-time-section-width)"
|
|
207
|
+
: undefined;
|
|
208
|
+
|
|
209
|
+
const handleChange = (nextValue: string) => {
|
|
210
|
+
setTimeValue(nextValue);
|
|
211
|
+
emitRegisterChange(nextValue);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const handleFocus = (event: FocusEvent<HTMLInputElement>) => {
|
|
215
|
+
setIsFocused(true);
|
|
216
|
+
onFocus?.(event);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const handleBlur = (event: FocusEvent<HTMLDivElement>) => {
|
|
220
|
+
setIsFocused(false);
|
|
221
|
+
emitRegisterBlur(timeValue);
|
|
222
|
+
onBlur?.(event);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const handleIconClick = (event: MouseEvent<HTMLButtonElement>) => {
|
|
226
|
+
event.preventDefault();
|
|
227
|
+
if (isDisabled || readOnly) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
hoursInputRef.current?.focus();
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const handleClearClick = (event: MouseEvent<HTMLButtonElement>) => {
|
|
234
|
+
event.preventDefault();
|
|
235
|
+
if (isDisabled || readOnly) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
handleChange("");
|
|
239
|
+
hoursInputRef.current?.focus();
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const iconButton = resolvedIcon ? (
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
className="input-time-section-button input-time-icon-button"
|
|
246
|
+
tabIndex={-1}
|
|
247
|
+
aria-label={iconLabel}
|
|
248
|
+
aria-disabled={isDisabled || readOnly ? true : undefined}
|
|
249
|
+
onMouseDown={event => event.preventDefault()}
|
|
250
|
+
onClick={handleIconClick}
|
|
251
|
+
>
|
|
252
|
+
<span className="input-time-icon-graphic" aria-hidden="true">
|
|
253
|
+
{resolvedIcon}
|
|
254
|
+
</span>
|
|
255
|
+
</button>
|
|
256
|
+
) : null;
|
|
257
|
+
|
|
258
|
+
const clearButton = showClearButton ? (
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
className="input-time-section-button input-time-clear-button"
|
|
262
|
+
tabIndex={-1}
|
|
263
|
+
aria-label={clearButtonLabel}
|
|
264
|
+
onMouseDown={event => event.preventDefault()}
|
|
265
|
+
onClick={handleClearClick}
|
|
266
|
+
>
|
|
267
|
+
<ResetIcon aria-hidden="true" />
|
|
268
|
+
</button>
|
|
269
|
+
) : null;
|
|
270
|
+
|
|
271
|
+
const rightSection =
|
|
272
|
+
resolvedIconPosition === "right" ? (
|
|
273
|
+
<span className="input-time-section-group">
|
|
274
|
+
{clearButton}
|
|
275
|
+
{iconButton}
|
|
276
|
+
</span>
|
|
277
|
+
) : (
|
|
278
|
+
clearButton
|
|
279
|
+
);
|
|
280
|
+
const resolvedScrollAreaProps = {
|
|
281
|
+
h: "var(--input-time-dropdown-content-height)",
|
|
282
|
+
mah: "var(--input-time-dropdown-content-height)",
|
|
283
|
+
...scrollAreaProps,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<div
|
|
288
|
+
id={id}
|
|
289
|
+
className={clsx("input-time", className)}
|
|
290
|
+
data-priority={priority}
|
|
291
|
+
data-size={size}
|
|
292
|
+
data-state={visualState}
|
|
293
|
+
data-readonly={readOnly ? "true" : undefined}
|
|
294
|
+
data-block={resolvedBlock ? "true" : undefined}
|
|
295
|
+
data-icon-position={resolvedIconPosition}
|
|
296
|
+
data-has-value={timeValue ? "true" : "false"}
|
|
297
|
+
data-clearable={reserveClearSpace ? "true" : undefined}
|
|
298
|
+
data-has-clear={showClearButton ? "true" : undefined}
|
|
299
|
+
data-width={widthAttr}
|
|
300
|
+
style={containerStyle}
|
|
301
|
+
>
|
|
302
|
+
<MantineTimePicker
|
|
303
|
+
ref={ref}
|
|
304
|
+
className="input-time-picker"
|
|
305
|
+
classNames={INPUT_TIME_CLASS_NAMES}
|
|
306
|
+
value={timeValue}
|
|
307
|
+
onChange={handleChange}
|
|
308
|
+
name={register ? undefined : name}
|
|
309
|
+
form={register ? undefined : form}
|
|
310
|
+
required={required}
|
|
311
|
+
disabled={isDisabled}
|
|
312
|
+
readOnly={readOnly}
|
|
313
|
+
format={format}
|
|
314
|
+
withSeconds={withSeconds}
|
|
315
|
+
withDropdown={withDropdown}
|
|
316
|
+
clearable={false}
|
|
317
|
+
min={min}
|
|
318
|
+
max={max}
|
|
319
|
+
hoursStep={hoursStep}
|
|
320
|
+
minutesStep={minutesStep}
|
|
321
|
+
secondsStep={secondsStep}
|
|
322
|
+
hoursInputLabel={hoursInputLabel}
|
|
323
|
+
minutesInputLabel={minutesInputLabel}
|
|
324
|
+
secondsInputLabel={secondsInputLabel}
|
|
325
|
+
amPmInputLabel={amPmInputLabel}
|
|
326
|
+
amPmLabels={amPmLabels}
|
|
327
|
+
hoursPlaceholder={hoursPlaceholder}
|
|
328
|
+
minutesPlaceholder={minutesPlaceholder}
|
|
329
|
+
secondsPlaceholder={secondsPlaceholder}
|
|
330
|
+
pasteSplit={pasteSplit}
|
|
331
|
+
presets={presets}
|
|
332
|
+
maxDropdownContentHeight={dropdownContentHeight}
|
|
333
|
+
scrollAreaProps={resolvedScrollAreaProps}
|
|
334
|
+
reverseTimeControlsList={reverseTimeControlsList}
|
|
335
|
+
popoverProps={{
|
|
336
|
+
width: "target",
|
|
337
|
+
withinPortal: false,
|
|
338
|
+
...popoverProps,
|
|
339
|
+
}}
|
|
340
|
+
hiddenInputProps={register ? undefined : hiddenInputProps}
|
|
341
|
+
size="sm"
|
|
342
|
+
variant="unstyled"
|
|
343
|
+
withErrorStyles={false}
|
|
344
|
+
aria-invalid={visualState === "error" ? true : undefined}
|
|
345
|
+
leftSection={resolvedIconPosition === "left" ? iconButton : undefined}
|
|
346
|
+
rightSection={rightSection}
|
|
347
|
+
leftSectionWidth={
|
|
348
|
+
resolvedIconPosition === "left"
|
|
349
|
+
? "var(--input-time-left-section-width)"
|
|
350
|
+
: undefined
|
|
351
|
+
}
|
|
352
|
+
rightSectionWidth={rightSectionWidth}
|
|
353
|
+
leftSectionPointerEvents={
|
|
354
|
+
resolvedIconPosition === "left" ? "auto" : undefined
|
|
355
|
+
}
|
|
356
|
+
rightSectionPointerEvents={rightSection ? "auto" : undefined}
|
|
357
|
+
hoursRef={hoursInputRef}
|
|
358
|
+
onFocus={handleFocus}
|
|
359
|
+
onBlur={handleBlur}
|
|
360
|
+
/>
|
|
361
|
+
{register ? (
|
|
362
|
+
<input
|
|
363
|
+
{...hiddenInputProps}
|
|
364
|
+
{...register}
|
|
365
|
+
type="hidden"
|
|
366
|
+
name={register.name}
|
|
367
|
+
form={form ?? hiddenInputProps?.form}
|
|
368
|
+
value={timeValue}
|
|
369
|
+
/>
|
|
370
|
+
) : null}
|
|
371
|
+
</div>
|
|
372
|
+
);
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
InputTimePicker.displayName = "InputTimePicker";
|
|
377
|
+
|
|
378
|
+
export default InputTimePicker;
|