@uniai-fe/uds-primitives 0.10.4 → 0.12.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.
Files changed (35) hide show
  1. package/README.md +38 -452
  2. package/dist/styles.css +1233 -508
  3. package/package.json +3 -3
  4. package/src/components/button/markup/Base.tsx +1 -1
  5. package/src/components/button/styles/button.scss +84 -1
  6. package/src/components/button/styles/variables.scss +73 -0
  7. package/src/components/button/types/options.ts +8 -0
  8. package/src/components/button/types/props.ts +30 -6
  9. package/src/components/input/markup/address/Button.tsx +1 -1
  10. package/src/components/input/markup/time/Template.tsx +423 -0
  11. package/src/components/input/markup/time/Trigger.tsx +289 -0
  12. package/src/components/input/markup/time/index.tsx +2 -2
  13. package/src/components/input/styles/time.scss +160 -317
  14. package/src/components/input/styles/variables.scss +9 -47
  15. package/src/components/input/types/time.ts +61 -120
  16. package/src/components/time-picker/img/minus.svg +3 -0
  17. package/src/components/time-picker/img/plus.svg +3 -0
  18. package/src/components/time-picker/index.scss +1 -0
  19. package/src/components/time-picker/index.tsx +11 -0
  20. package/src/components/time-picker/markup/Footer.tsx +71 -0
  21. package/src/components/time-picker/markup/HourSection.tsx +99 -0
  22. package/src/components/time-picker/markup/MinuteSection.tsx +86 -0
  23. package/src/components/time-picker/markup/Summary.tsx +24 -0
  24. package/src/components/time-picker/markup/Template.tsx +86 -0
  25. package/src/components/time-picker/markup/index.tsx +10 -0
  26. package/src/components/time-picker/styles/index.scss +2 -0
  27. package/src/components/time-picker/styles/template.scss +174 -0
  28. package/src/components/time-picker/styles/variables.scss +64 -0
  29. package/src/components/time-picker/types/index.ts +1 -0
  30. package/src/components/time-picker/types/time-picker.ts +94 -0
  31. package/src/components/time-picker/utils/index.ts +1 -0
  32. package/src/components/time-picker/utils/time.ts +110 -0
  33. package/src/index.scss +1 -0
  34. package/src/index.tsx +1 -0
  35. package/src/components/input/markup/time/Picker.tsx +0 -378
@@ -0,0 +1,423 @@
1
+ "use client";
2
+
3
+ import { useUncontrolled } from "@mantine/hooks";
4
+ import type { CSSProperties, FocusEvent, KeyboardEvent } from "react";
5
+ import { forwardRef, useCallback, useEffect, useState } from "react";
6
+ import { PopOver } from "../../../pop-over";
7
+ import {
8
+ getFormFieldWidthAttr,
9
+ getFormFieldWidthValue,
10
+ } from "../../../form/utils/form-field";
11
+ import { TimePicker } from "../../../time-picker";
12
+ import type {
13
+ TimePickerParts,
14
+ TimePickerPeriod,
15
+ TimePickerUnit,
16
+ } from "../../../time-picker/types";
17
+ import {
18
+ getTimePeriod,
19
+ parseTimeValue,
20
+ serializeTimeValue,
21
+ setTimePeriod,
22
+ stepTimeValue,
23
+ toDisplayHour,
24
+ } from "../../../time-picker/utils";
25
+ import ClockIcon from "../../img/clock.svg";
26
+ import type { InputTimeTemplateProps } from "../../types";
27
+ import InputTimeTrigger from "./Trigger";
28
+
29
+ interface InputTimeDraftFields {
30
+ hours: string;
31
+ minutes: string;
32
+ seconds: string;
33
+ }
34
+
35
+ interface InputTimeDraft {
36
+ fields: InputTimeDraftFields;
37
+ period: TimePickerPeriod;
38
+ }
39
+
40
+ const getInputTimeDraft = (
41
+ value: string,
42
+ format: "12h" | "24h",
43
+ ): InputTimeDraft => {
44
+ const parts = parseTimeValue(value);
45
+ if (!parts) {
46
+ return {
47
+ fields: { hours: "", minutes: "", seconds: "" },
48
+ period: "am",
49
+ };
50
+ }
51
+
52
+ return {
53
+ fields: {
54
+ hours: String(toDisplayHour(parts.hours, format)).padStart(2, "0"),
55
+ minutes: String(parts.minutes).padStart(2, "0"),
56
+ seconds: String(parts.seconds).padStart(2, "0"),
57
+ },
58
+ period: getTimePeriod(parts.hours),
59
+ };
60
+ };
61
+
62
+ /**
63
+ * Input Time Template; native trigger와 PopOver·TimePicker 선택 panel 조합.
64
+ * 저장값은 24시간 `HH:mm` 또는 `HH:mm:ss` 문자열로 유지한다.
65
+ * @component
66
+ * @param {InputTimeTemplateProps} props
67
+ * @param {"primary" | "secondary" | "tertiary" | "table"} [props.priority="primary"] input priority
68
+ * @param {"small" | "medium" | "large"} [props.size="medium"] input size
69
+ * @param {"12h" | "24h"} [props.format="24h"] 표시 형식
70
+ * @param {boolean} [props.withSeconds=false] 초 field 노출 여부
71
+ * @returns {ReactNode} 시간 입력과 선택 panel
72
+ */
73
+ const InputTimeTemplate = forwardRef<HTMLDivElement, InputTimeTemplateProps>(
74
+ (
75
+ {
76
+ value,
77
+ defaultValue,
78
+ onChange,
79
+ onValueChange,
80
+ name,
81
+ form,
82
+ register,
83
+ priority = "primary",
84
+ size = "medium",
85
+ state: stateProp = "default",
86
+ block = false,
87
+ width,
88
+ className,
89
+ disabled,
90
+ readOnly = false,
91
+ required,
92
+ id,
93
+ format = "24h",
94
+ withSeconds = false,
95
+ clearable = true,
96
+ hoursStep = 1,
97
+ minutesStep = 5,
98
+ secondsStep = 1,
99
+ icon,
100
+ iconPosition,
101
+ iconLabel = "시간 선택",
102
+ hiddenInputProps,
103
+ hoursInputLabel = "시",
104
+ minutesInputLabel = "분",
105
+ secondsInputLabel = "초",
106
+ amPmInputLabel = "오전 또는 오후",
107
+ hoursPlaceholder = "HH",
108
+ minutesPlaceholder = "MM",
109
+ secondsPlaceholder = "SS",
110
+ onFocus,
111
+ onBlur,
112
+ },
113
+ ref,
114
+ ) => {
115
+ const [isOpen, setIsOpen] = useState(false);
116
+ const [activeUnit, setActiveUnit] = useState<TimePickerUnit | null>(null);
117
+ const [timeValue, setTimeValue] = useUncontrolled<string>({
118
+ value,
119
+ defaultValue,
120
+ finalValue: "",
121
+ onChange: nextValue => {
122
+ onChange?.(nextValue);
123
+ onValueChange?.(nextValue);
124
+ },
125
+ });
126
+ const [draft, setDraft] = useState<InputTimeDraft>(() =>
127
+ getInputTimeDraft(value ?? defaultValue ?? "", format),
128
+ );
129
+
130
+ useEffect(() => {
131
+ setDraft(getInputTimeDraft(timeValue, format));
132
+ }, [format, timeValue]);
133
+
134
+ const isDisabled =
135
+ disabled || stateProp === "disabled" || stateProp === "loading";
136
+ const visualState =
137
+ priority === "tertiary"
138
+ ? isDisabled
139
+ ? "disabled"
140
+ : "default"
141
+ : !isDisabled && activeUnit
142
+ ? "active"
143
+ : stateProp;
144
+ const resolvedIconPosition =
145
+ iconPosition ?? (priority === "table" ? "left" : "right");
146
+ const resolvedBlock =
147
+ block || (priority === "table" && width === undefined);
148
+ const widthAttr =
149
+ width !== undefined
150
+ ? getFormFieldWidthAttr(width)
151
+ : resolvedBlock
152
+ ? "full"
153
+ : undefined;
154
+ const widthValue =
155
+ width !== undefined ? getFormFieldWidthValue(width) : undefined;
156
+ const triggerStyle: CSSProperties | undefined = widthValue
157
+ ? { width: widthValue }
158
+ : undefined;
159
+
160
+ const emitRegisterChange = useCallback(
161
+ (nextValue: string) => {
162
+ register?.onChange({
163
+ target: { name: register.name, value: nextValue },
164
+ type: "change",
165
+ });
166
+ },
167
+ [register],
168
+ );
169
+
170
+ const updateValue = useCallback(
171
+ (nextValue: string) => {
172
+ setDraft(getInputTimeDraft(nextValue, format));
173
+ setTimeValue(nextValue);
174
+ emitRegisterChange(nextValue);
175
+ },
176
+ [emitRegisterChange, format, setTimeValue],
177
+ );
178
+
179
+ const commitDraft = useCallback(
180
+ (nextDraft: InputTimeDraft) => {
181
+ const { hours, minutes, seconds } = nextDraft.fields;
182
+ if (
183
+ hours.length !== 2 ||
184
+ minutes.length !== 2 ||
185
+ (withSeconds && seconds.length !== 2)
186
+ ) {
187
+ return;
188
+ }
189
+
190
+ const displayHours = Number(hours);
191
+ const nextMinutes = Number(minutes);
192
+ const nextSeconds = withSeconds ? Number(seconds) : 0;
193
+ const validHours =
194
+ format === "12h"
195
+ ? displayHours >= 1 && displayHours <= 12
196
+ : displayHours >= 0 && displayHours <= 23;
197
+ if (!validHours || nextMinutes > 59 || nextSeconds > 59) {
198
+ return;
199
+ }
200
+
201
+ const storedHours =
202
+ format === "12h"
203
+ ? (displayHours % 12) + (nextDraft.period === "pm" ? 12 : 0)
204
+ : displayHours;
205
+ updateValue(
206
+ serializeTimeValue(
207
+ {
208
+ hours: storedHours,
209
+ minutes: nextMinutes,
210
+ seconds: nextSeconds,
211
+ },
212
+ withSeconds,
213
+ ),
214
+ );
215
+ },
216
+ [format, updateValue, withSeconds],
217
+ );
218
+
219
+ const handleFieldChange = (unit: TimePickerUnit, rawValue: string) => {
220
+ const nextValue = rawValue.replace(/\D/g, "").slice(0, 2);
221
+ const nextDraft = {
222
+ ...draft,
223
+ fields: { ...draft.fields, [unit]: nextValue },
224
+ };
225
+ setDraft(nextDraft);
226
+ commitDraft(nextDraft);
227
+ };
228
+
229
+ const handleFieldFocus = (
230
+ unit: TimePickerUnit,
231
+ event: FocusEvent<HTMLInputElement>,
232
+ ) => {
233
+ setActiveUnit(unit);
234
+ event.currentTarget.select();
235
+ onFocus?.(event);
236
+ };
237
+
238
+ const handleFieldBlur = (unit: TimePickerUnit) => {
239
+ const currentValue = draft.fields[unit];
240
+ if (!currentValue) {
241
+ return;
242
+ }
243
+
244
+ const max = unit === "hours" ? (format === "12h" ? 12 : 23) : 59;
245
+ const min = unit === "hours" && format === "12h" ? 1 : 0;
246
+ const normalized = String(
247
+ Math.min(max, Math.max(min, Number(currentValue))),
248
+ ).padStart(2, "0");
249
+ const nextDraft = {
250
+ ...draft,
251
+ fields: { ...draft.fields, [unit]: normalized },
252
+ };
253
+ setDraft(nextDraft);
254
+ commitDraft(nextDraft);
255
+ };
256
+
257
+ const handleFieldKeyDown = (
258
+ unit: TimePickerUnit,
259
+ event: KeyboardEvent<HTMLInputElement>,
260
+ ) => {
261
+ if (event.key !== "ArrowUp" && event.key !== "ArrowDown") {
262
+ return;
263
+ }
264
+ event.preventDefault();
265
+ const amount = event.key === "ArrowUp" ? 1 : -1;
266
+ const step =
267
+ unit === "hours"
268
+ ? hoursStep
269
+ : unit === "minutes"
270
+ ? minutesStep
271
+ : secondsStep;
272
+ updateValue(stepTimeValue(timeValue, unit, amount * step, withSeconds));
273
+ };
274
+
275
+ const handleStep = (amount: 1 | -1) => {
276
+ if (!activeUnit) {
277
+ return;
278
+ }
279
+ const step =
280
+ activeUnit === "hours"
281
+ ? hoursStep
282
+ : activeUnit === "minutes"
283
+ ? minutesStep
284
+ : secondsStep;
285
+ updateValue(
286
+ stepTimeValue(timeValue, activeUnit, amount * step, withSeconds),
287
+ );
288
+ };
289
+
290
+ const handlePeriodChange = (period: TimePickerPeriod) => {
291
+ const nextValue = setTimePeriod(timeValue, period, withSeconds);
292
+ updateValue(nextValue);
293
+ };
294
+
295
+ const handleHourSelect = (hours: number) => {
296
+ const parts = parseTimeValue(timeValue) ?? {
297
+ hours: 0,
298
+ minutes: 0,
299
+ seconds: 0,
300
+ };
301
+ const storedHours =
302
+ format === "12h"
303
+ ? (hours % 12) + (draft.period === "pm" ? 12 : 0)
304
+ : hours;
305
+ updateValue(
306
+ serializeTimeValue({ ...parts, hours: storedHours }, withSeconds),
307
+ );
308
+ };
309
+
310
+ const handleMinuteSelect = (minutes: number) => {
311
+ const parts: TimePickerParts = parseTimeValue(timeValue) ?? {
312
+ hours: 0,
313
+ minutes: 0,
314
+ seconds: 0,
315
+ };
316
+ updateValue(serializeTimeValue({ ...parts, minutes }, withSeconds));
317
+ };
318
+
319
+ const handleRootBlur = (event: FocusEvent<HTMLDivElement>) => {
320
+ if (
321
+ event.relatedTarget instanceof Node &&
322
+ event.currentTarget.contains(event.relatedTarget)
323
+ ) {
324
+ return;
325
+ }
326
+ setActiveUnit(null);
327
+ register?.onBlur({
328
+ target: { name: register.name, value: timeValue },
329
+ type: "blur",
330
+ });
331
+ onBlur?.(event);
332
+ };
333
+
334
+ const handleOpenChange = (nextOpen: boolean) => {
335
+ setIsOpen(nextOpen && !isDisabled && !readOnly);
336
+ };
337
+
338
+ return (
339
+ <>
340
+ <PopOver.Root open={isOpen} onOpenChange={handleOpenChange}>
341
+ <PopOver.Trigger asChild>
342
+ <InputTimeTrigger
343
+ ref={ref}
344
+ id={id}
345
+ className={className}
346
+ style={triggerStyle}
347
+ fields={draft.fields}
348
+ period={draft.period}
349
+ activeUnit={activeUnit}
350
+ priority={priority}
351
+ size={size}
352
+ state={visualState}
353
+ format={format}
354
+ withSeconds={withSeconds}
355
+ disabled={Boolean(isDisabled)}
356
+ readOnly={readOnly}
357
+ icon={
358
+ icon === undefined ? <ClockIcon aria-hidden="true" /> : icon
359
+ }
360
+ iconPosition={resolvedIconPosition}
361
+ iconLabel={iconLabel}
362
+ hoursInputLabel={hoursInputLabel}
363
+ minutesInputLabel={minutesInputLabel}
364
+ secondsInputLabel={secondsInputLabel}
365
+ amPmInputLabel={amPmInputLabel}
366
+ hoursPlaceholder={hoursPlaceholder}
367
+ minutesPlaceholder={minutesPlaceholder}
368
+ secondsPlaceholder={secondsPlaceholder}
369
+ data-block={resolvedBlock ? "true" : undefined}
370
+ data-width={widthAttr}
371
+ onFieldChange={handleFieldChange}
372
+ onFieldFocus={handleFieldFocus}
373
+ onFieldBlur={handleFieldBlur}
374
+ onFieldKeyDown={handleFieldKeyDown}
375
+ onPeriodChange={handlePeriodChange}
376
+ onStep={handleStep}
377
+ onRootBlur={handleRootBlur}
378
+ />
379
+ </PopOver.Trigger>
380
+ <PopOver.Content
381
+ className="input-time-pop-over"
382
+ width={336}
383
+ align="start"
384
+ sideOffset={8}
385
+ onOpenAutoFocus={event => event.preventDefault()}
386
+ >
387
+ <TimePicker.Template
388
+ value={timeValue}
389
+ format={format}
390
+ clearable={clearable}
391
+ disabled={Boolean(isDisabled || readOnly)}
392
+ onHourSelect={handleHourSelect}
393
+ onMinuteSelect={handleMinuteSelect}
394
+ onPeriodChange={handlePeriodChange}
395
+ onMinuteStep={amount =>
396
+ updateValue(
397
+ stepTimeValue(timeValue, "minutes", amount, withSeconds),
398
+ )
399
+ }
400
+ onClear={() => updateValue("")}
401
+ onApply={() => setIsOpen(false)}
402
+ />
403
+ </PopOver.Content>
404
+ </PopOver.Root>
405
+ {register || name ? (
406
+ <input
407
+ {...hiddenInputProps}
408
+ {...register}
409
+ type="hidden"
410
+ name={register?.name ?? name}
411
+ form={form ?? hiddenInputProps?.form}
412
+ required={required}
413
+ value={timeValue}
414
+ />
415
+ ) : null}
416
+ </>
417
+ );
418
+ },
419
+ );
420
+
421
+ InputTimeTemplate.displayName = "InputTimeTemplate";
422
+
423
+ export default InputTimeTemplate;
@@ -0,0 +1,289 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import type {
5
+ ChangeEvent,
6
+ ComponentPropsWithoutRef,
7
+ FocusEvent,
8
+ KeyboardEvent,
9
+ MouseEvent,
10
+ ReactNode,
11
+ } from "react";
12
+ import { forwardRef, useRef } from "react";
13
+ import { Calendar } from "../../../calendar";
14
+ import type {
15
+ TimePickerFormat,
16
+ TimePickerPeriod,
17
+ TimePickerUnit,
18
+ } from "../../../time-picker/types";
19
+ import type { InputPriority, InputSize, InputState } from "../../types";
20
+
21
+ interface InputTimeTriggerFields {
22
+ hours: string;
23
+ minutes: string;
24
+ seconds: string;
25
+ }
26
+
27
+ interface InputTimeTriggerProps extends Omit<
28
+ ComponentPropsWithoutRef<"div">,
29
+ "onBlur" | "onFocus"
30
+ > {
31
+ fields: InputTimeTriggerFields;
32
+ period: TimePickerPeriod;
33
+ activeUnit: TimePickerUnit | null;
34
+ priority: InputPriority;
35
+ size: InputSize;
36
+ state: InputState;
37
+ format: TimePickerFormat;
38
+ withSeconds: boolean;
39
+ disabled: boolean;
40
+ readOnly: boolean;
41
+ icon: ReactNode;
42
+ iconPosition: "left" | "right";
43
+ iconLabel: string;
44
+ hoursInputLabel: string;
45
+ minutesInputLabel: string;
46
+ secondsInputLabel: string;
47
+ amPmInputLabel: string;
48
+ hoursPlaceholder: string;
49
+ minutesPlaceholder: string;
50
+ secondsPlaceholder: string;
51
+ onFieldChange: (unit: TimePickerUnit, value: string) => void;
52
+ onFieldFocus: (
53
+ unit: TimePickerUnit,
54
+ event: FocusEvent<HTMLInputElement>,
55
+ ) => void;
56
+ onFieldBlur: (unit: TimePickerUnit) => void;
57
+ onFieldKeyDown: (
58
+ unit: TimePickerUnit,
59
+ event: KeyboardEvent<HTMLInputElement>,
60
+ ) => void;
61
+ onPeriodChange: (period: TimePickerPeriod) => void;
62
+ onStep: (amount: 1 | -1) => void;
63
+ onRootBlur: (event: FocusEvent<HTMLDivElement>) => void;
64
+ }
65
+
66
+ /**
67
+ * Input Time trigger; 시·분·초 field와 focus 단위 stepper를 렌더한다.
68
+ * @component
69
+ * @param {InputTimeTriggerProps} props
70
+ * @returns {ReactNode} 시간 입력 trigger
71
+ */
72
+ const InputTimeTrigger = forwardRef<HTMLDivElement, InputTimeTriggerProps>(
73
+ (
74
+ {
75
+ fields,
76
+ period,
77
+ activeUnit,
78
+ priority,
79
+ size,
80
+ state,
81
+ format,
82
+ withSeconds,
83
+ disabled,
84
+ readOnly,
85
+ icon,
86
+ iconPosition,
87
+ iconLabel,
88
+ hoursInputLabel,
89
+ minutesInputLabel,
90
+ secondsInputLabel,
91
+ amPmInputLabel,
92
+ hoursPlaceholder,
93
+ minutesPlaceholder,
94
+ secondsPlaceholder,
95
+ onFieldChange,
96
+ onFieldFocus,
97
+ onFieldBlur,
98
+ onFieldKeyDown,
99
+ onPeriodChange,
100
+ onStep,
101
+ onRootBlur,
102
+ className,
103
+ ...restProps
104
+ },
105
+ ref,
106
+ ) => {
107
+ const hoursRef = useRef<HTMLInputElement | null>(null);
108
+ const onChange =
109
+ (unit: TimePickerUnit) => (event: ChangeEvent<HTMLInputElement>) => {
110
+ onFieldChange(unit, event.currentTarget.value);
111
+ };
112
+ const onFocus =
113
+ (unit: TimePickerUnit) => (event: FocusEvent<HTMLInputElement>) => {
114
+ onFieldFocus(unit, event);
115
+ };
116
+ const onBlur = (unit: TimePickerUnit) => () => onFieldBlur(unit);
117
+ const onKeyDown =
118
+ (unit: TimePickerUnit) => (event: KeyboardEvent<HTMLInputElement>) => {
119
+ onFieldKeyDown(unit, event);
120
+ };
121
+ const onStepperMouseDown = (event: MouseEvent<HTMLButtonElement>) => {
122
+ event.preventDefault();
123
+ };
124
+ const onInteractiveClick = (event: MouseEvent<HTMLElement>) => {
125
+ event.stopPropagation();
126
+ };
127
+ const onClockClick = () => {
128
+ hoursRef.current?.focus();
129
+ };
130
+
131
+ return (
132
+ <div
133
+ {...restProps}
134
+ ref={ref}
135
+ className={clsx("input-time input-time-picker", className)}
136
+ data-priority={priority}
137
+ data-size={size}
138
+ data-state={state}
139
+ data-readonly={readOnly ? "true" : undefined}
140
+ data-active-unit={activeUnit ?? undefined}
141
+ data-icon-position={iconPosition}
142
+ onBlur={onRootBlur}
143
+ >
144
+ <div className="input-time-trigger-input">
145
+ {iconPosition === "left" ? (
146
+ <button
147
+ type="button"
148
+ className="input-time-icon-button"
149
+ tabIndex={-1}
150
+ aria-label={iconLabel}
151
+ disabled={disabled}
152
+ onMouseDown={onStepperMouseDown}
153
+ onClick={onClockClick}
154
+ >
155
+ {icon}
156
+ </button>
157
+ ) : null}
158
+ <div className="input-time-fields-group">
159
+ <input
160
+ ref={hoursRef}
161
+ className="input-time-field"
162
+ type="text"
163
+ inputMode="numeric"
164
+ autoComplete="off"
165
+ maxLength={2}
166
+ value={fields.hours}
167
+ placeholder={hoursPlaceholder}
168
+ aria-label={hoursInputLabel}
169
+ aria-invalid={state === "error" ? true : undefined}
170
+ disabled={disabled}
171
+ readOnly={readOnly}
172
+ onChange={onChange("hours")}
173
+ onClick={onInteractiveClick}
174
+ onFocus={onFocus("hours")}
175
+ onBlur={onBlur("hours")}
176
+ onKeyDown={onKeyDown("hours")}
177
+ />
178
+ <span aria-hidden="true">:</span>
179
+ <input
180
+ className="input-time-field"
181
+ type="text"
182
+ inputMode="numeric"
183
+ autoComplete="off"
184
+ maxLength={2}
185
+ value={fields.minutes}
186
+ placeholder={minutesPlaceholder}
187
+ aria-label={minutesInputLabel}
188
+ aria-invalid={state === "error" ? true : undefined}
189
+ disabled={disabled}
190
+ readOnly={readOnly}
191
+ onChange={onChange("minutes")}
192
+ onClick={onInteractiveClick}
193
+ onFocus={onFocus("minutes")}
194
+ onBlur={onBlur("minutes")}
195
+ onKeyDown={onKeyDown("minutes")}
196
+ />
197
+ {withSeconds ? (
198
+ <>
199
+ <span aria-hidden="true">:</span>
200
+ <input
201
+ className="input-time-field"
202
+ type="text"
203
+ inputMode="numeric"
204
+ autoComplete="off"
205
+ maxLength={2}
206
+ value={fields.seconds}
207
+ placeholder={secondsPlaceholder}
208
+ aria-label={secondsInputLabel}
209
+ aria-invalid={state === "error" ? true : undefined}
210
+ disabled={disabled}
211
+ readOnly={readOnly}
212
+ onChange={onChange("seconds")}
213
+ onClick={onInteractiveClick}
214
+ onFocus={onFocus("seconds")}
215
+ onBlur={onBlur("seconds")}
216
+ onKeyDown={onKeyDown("seconds")}
217
+ />
218
+ </>
219
+ ) : null}
220
+ {format === "12h" ? (
221
+ <select
222
+ className="input-time-period-field"
223
+ value={period}
224
+ aria-label={amPmInputLabel}
225
+ disabled={disabled}
226
+ onClick={onInteractiveClick}
227
+ onChange={event =>
228
+ onPeriodChange(
229
+ event.currentTarget.value === "pm" ? "pm" : "am",
230
+ )
231
+ }
232
+ >
233
+ <option value="am">오전</option>
234
+ <option value="pm">오후</option>
235
+ </select>
236
+ ) : null}
237
+ </div>
238
+ {iconPosition === "right" ? (
239
+ <button
240
+ type="button"
241
+ className="input-time-icon-button"
242
+ tabIndex={-1}
243
+ aria-label={iconLabel}
244
+ disabled={disabled}
245
+ onMouseDown={onStepperMouseDown}
246
+ onClick={onClockClick}
247
+ >
248
+ {icon}
249
+ </button>
250
+ ) : null}
251
+ </div>
252
+ {activeUnit && !disabled && !readOnly ? (
253
+ <div className="input-time-stepper" aria-label="시간 값 조절">
254
+ <button
255
+ type="button"
256
+ className="input-time-stepper-button"
257
+ tabIndex={-1}
258
+ aria-label={`${activeUnit} 증가`}
259
+ onMouseDown={onStepperMouseDown}
260
+ onClick={event => {
261
+ onInteractiveClick(event);
262
+ onStep(1);
263
+ }}
264
+ >
265
+ <Calendar.Icon.Chevron.up aria-hidden="true" />
266
+ </button>
267
+ <button
268
+ type="button"
269
+ className="input-time-stepper-button"
270
+ tabIndex={-1}
271
+ aria-label={`${activeUnit} 감소`}
272
+ onMouseDown={onStepperMouseDown}
273
+ onClick={event => {
274
+ onInteractiveClick(event);
275
+ onStep(-1);
276
+ }}
277
+ >
278
+ <Calendar.Icon.Chevron.down aria-hidden="true" />
279
+ </button>
280
+ </div>
281
+ ) : null}
282
+ </div>
283
+ );
284
+ },
285
+ );
286
+
287
+ InputTimeTrigger.displayName = "InputTimeTrigger";
288
+
289
+ export default InputTimeTrigger;