@comitor/ui 2.0.0 → 2.1.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/LICENSE +35 -0
- package/README.md +14 -6
- package/dist/components/ui/action-bar-constants.d.ts +31 -0
- package/dist/components/ui/action-bar-constants.js +9 -0
- package/dist/components/ui/action-bar-constants.js.map +1 -0
- package/dist/components/ui/action-bar.d.ts +67 -0
- package/dist/components/ui/action-bar.js +522 -0
- package/dist/components/ui/action-bar.js.map +1 -0
- package/dist/components/ui/badge-overflow.d.ts +30 -0
- package/dist/components/ui/badge-overflow.js +179 -0
- package/dist/components/ui/badge-overflow.js.map +1 -0
- package/dist/components/ui/banner.d.ts +79 -0
- package/dist/components/ui/banner.js +518 -0
- package/dist/components/ui/banner.js.map +1 -0
- package/dist/components/ui/circular-progress.d.ts +50 -0
- package/dist/components/ui/circular-progress.js +266 -0
- package/dist/components/ui/circular-progress.js.map +1 -0
- package/dist/components/ui/cropper-constants.d.ts +34 -0
- package/dist/components/ui/cropper-constants.js +10 -0
- package/dist/components/ui/cropper-constants.js.map +1 -0
- package/dist/components/ui/cropper.d.ts +106 -0
- package/dist/components/ui/cropper.js +1269 -0
- package/dist/components/ui/cropper.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +1 -1
- package/dist/components/ui/drawer.js +2 -2
- package/dist/components/ui/drawer.js.map +1 -1
- package/dist/components/ui/kanban-constants.d.ts +61 -0
- package/dist/components/ui/kanban-constants.js +21 -0
- package/dist/components/ui/kanban-constants.js.map +1 -0
- package/dist/components/ui/kanban.d.ts +72 -0
- package/dist/components/ui/kanban.js +787 -0
- package/dist/components/ui/kanban.js.map +1 -0
- package/dist/components/ui/key-value-constants.d.ts +33 -0
- package/dist/components/ui/key-value-constants.js +11 -0
- package/dist/components/ui/key-value-constants.js.map +1 -0
- package/dist/components/ui/key-value.d.ts +96 -0
- package/dist/components/ui/key-value.js +641 -0
- package/dist/components/ui/key-value.js.map +1 -0
- package/dist/components/ui/mask-input-constants.d.ts +99 -0
- package/dist/components/ui/mask-input-constants.js +601 -0
- package/dist/components/ui/mask-input-constants.js.map +1 -0
- package/dist/components/ui/mask-input.d.ts +36 -0
- package/dist/components/ui/mask-input.js +558 -0
- package/dist/components/ui/mask-input.js.map +1 -0
- package/dist/components/ui/mention.d.ts +31 -0
- package/dist/components/ui/mention.js +128 -0
- package/dist/components/ui/mention.js.map +1 -0
- package/dist/components/ui/phone-input-constants.d.ts +54 -0
- package/dist/components/ui/phone-input-constants.js +9 -0
- package/dist/components/ui/phone-input-constants.js.map +1 -0
- package/dist/components/ui/phone-input.d.ts +85 -0
- package/dist/components/ui/phone-input.js +743 -0
- package/dist/components/ui/phone-input.js.map +1 -0
- package/dist/components/ui/rating.d.ts +56 -0
- package/dist/components/ui/rating.js +646 -0
- package/dist/components/ui/rating.js.map +1 -0
- package/dist/components/ui/responsive-dialog.d.ts +35 -0
- package/dist/components/ui/responsive-dialog.js +193 -0
- package/dist/components/ui/responsive-dialog.js.map +1 -0
- package/dist/components/ui/scroll-spy.d.ts +42 -0
- package/dist/components/ui/scroll-spy.js +334 -0
- package/dist/components/ui/scroll-spy.js.map +1 -0
- package/dist/components/ui/stepper-constants.d.ts +29 -0
- package/dist/components/ui/stepper-constants.js +9 -0
- package/dist/components/ui/stepper-constants.js.map +1 -0
- package/dist/components/ui/stepper.d.ts +80 -0
- package/dist/components/ui/stepper.js +902 -0
- package/dist/components/ui/stepper.js.map +1 -0
- package/dist/components/ui/tags-input.d.ts +41 -0
- package/dist/components/ui/tags-input.js +127 -0
- package/dist/components/ui/tags-input.js.map +1 -0
- package/dist/components/ui/time-picker-constants.d.ts +48 -0
- package/dist/components/ui/time-picker-constants.js +14 -0
- package/dist/components/ui/time-picker-constants.js.map +1 -0
- package/dist/components/ui/time-picker.d.ts +115 -0
- package/dist/components/ui/time-picker.js +1620 -0
- package/dist/components/ui/time-picker.js.map +1 -0
- package/dist/components/ui/timeline.d.ts +32 -0
- package/dist/components/ui/timeline.js +559 -0
- package/dist/components/ui/timeline.js.map +1 -0
- package/dist/components/ui/tour-constants.d.ts +39 -0
- package/dist/components/ui/tour-constants.js +12 -0
- package/dist/components/ui/tour-constants.js.map +1 -0
- package/dist/components/ui/tour.d.ts +124 -0
- package/dist/components/ui/tour.js +1207 -0
- package/dist/components/ui/tour.js.map +1 -0
- package/dist/data-grid/data-grid-cell-wrapper.js +1 -1
- package/dist/data-grid/data-grid-cell-wrapper.js.map +1 -1
- package/dist/data-grid/data-grid-context-menu.js +1 -1
- package/dist/data-grid/data-grid-context-menu.js.map +1 -1
- package/dist/data-grid/data-grid-paste-dialog.js +1 -1
- package/dist/data-grid/data-grid-paste-dialog.js.map +1 -1
- package/dist/data-grid/data-grid-row.js +1 -1
- package/dist/data-grid/data-grid-row.js.map +1 -1
- package/dist/data-grid/data-grid-search.js +1 -1
- package/dist/data-grid/data-grid-search.js.map +1 -1
- package/dist/data-grid/data-grid.js +1 -1
- package/dist/data-grid/data-grid.js.map +1 -1
- package/dist/data-grid/sortable.js +1 -1
- package/dist/data-grid/sortable.js.map +1 -1
- package/dist/data-grid/use-data-grid-undo-redo.js +2 -2
- package/dist/data-grid/use-data-grid-undo-redo.js.map +1 -1
- package/dist/data-grid/use-data-grid.js +3 -3
- package/dist/data-grid/use-data-grid.js.map +1 -1
- package/dist/index.d.ts +31 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/compose-refs.js.map +1 -0
- package/dist/lib/use-as-ref.js.map +1 -0
- package/dist/lib/use-is-mobile.d.ts +10 -0
- package/dist/lib/use-is-mobile.js +21 -0
- package/dist/lib/use-is-mobile.js.map +1 -0
- package/dist/lib/use-isomorphic-layout-effect.js.map +1 -0
- package/dist/lib/use-lazy-ref.js.map +1 -0
- package/dist/lib/visually-hidden-input.d.ts +12 -0
- package/dist/lib/visually-hidden-input.js +105 -0
- package/dist/lib/visually-hidden-input.js.map +1 -0
- package/package.json +5 -1
- package/styles.css +19 -0
- package/dist/data-grid/compose-refs.js.map +0 -1
- package/dist/data-grid/use-as-ref.js.map +0 -1
- package/dist/data-grid/use-isomorphic-layout-effect.js.map +0 -1
- package/dist/data-grid/use-lazy-ref.js.map +0 -1
- /package/dist/{data-grid → lib}/compose-refs.d.ts +0 -0
- /package/dist/{data-grid → lib}/compose-refs.js +0 -0
- /package/dist/{data-grid → lib}/use-as-ref.d.ts +0 -0
- /package/dist/{data-grid → lib}/use-as-ref.js +0 -0
- /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.d.ts +0 -0
- /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.js +0 -0
- /package/dist/{data-grid → lib}/use-lazy-ref.d.ts +0 -0
- /package/dist/{data-grid → lib}/use-lazy-ref.js +0 -0
|
@@ -0,0 +1,1620 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
4
|
+
import { Clock } from "lucide-react";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { cn } from "../../lib/cn.js";
|
|
7
|
+
import { useComposedRefs } from "../../lib/compose-refs.js";
|
|
8
|
+
import { useAsRef } from "../../lib/use-as-ref.js";
|
|
9
|
+
import { useIsomorphicLayoutEffect } from "../../lib/use-isomorphic-layout-effect.js";
|
|
10
|
+
import { useLazyRef } from "../../lib/use-lazy-ref.js";
|
|
11
|
+
import { VisuallyHiddenInput } from "../../lib/visually-hidden-input.js";
|
|
12
|
+
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "./popover.js";
|
|
13
|
+
import { DEFAULT_TIME_PICKER_LABELS } from "./time-picker-constants.js";
|
|
14
|
+
const ROOT_NAME = "TimePicker";
|
|
15
|
+
const LABEL_NAME = "TimePickerLabel";
|
|
16
|
+
const INPUT_GROUP_NAME = "TimePickerInputGroup";
|
|
17
|
+
const INPUT_NAME = "TimePickerInput";
|
|
18
|
+
const TRIGGER_NAME = "TimePickerTrigger";
|
|
19
|
+
const CONTENT_NAME = "TimePickerContent";
|
|
20
|
+
const COLUMN_NAME = "TimePickerColumn";
|
|
21
|
+
const COLUMN_ITEM_NAME = "TimePickerColumnItem";
|
|
22
|
+
const HOUR_NAME = "TimePickerHour";
|
|
23
|
+
const MINUTE_NAME = "TimePickerMinute";
|
|
24
|
+
const SECOND_NAME = "TimePickerSecond";
|
|
25
|
+
const PERIOD_NAME = "TimePickerPeriod";
|
|
26
|
+
const CLEAR_NAME = "TimePickerClear";
|
|
27
|
+
const DEFAULT_STEP = 1;
|
|
28
|
+
const DEFAULT_SEGMENT_PLACEHOLDER = "--";
|
|
29
|
+
const DEFAULT_LOCALE = "vi-VN";
|
|
30
|
+
const SEGMENTS = ["hour", "minute", "second", "period"];
|
|
31
|
+
const PERIODS = ["AM", "PM"];
|
|
32
|
+
function focusFirst(candidates, preventScroll = false) {
|
|
33
|
+
const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement;
|
|
34
|
+
for (const candidateRef of candidates) {
|
|
35
|
+
const candidate = candidateRef.current;
|
|
36
|
+
if (!candidate) continue;
|
|
37
|
+
if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return;
|
|
38
|
+
candidate.focus({ preventScroll });
|
|
39
|
+
if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function sortNodes(items) {
|
|
43
|
+
return items.sort((a, b) => {
|
|
44
|
+
const elementA = a.ref.current;
|
|
45
|
+
const elementB = b.ref.current;
|
|
46
|
+
if (!elementA || !elementB) return 0;
|
|
47
|
+
const position = elementA.compareDocumentPosition(elementB);
|
|
48
|
+
if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
|
|
49
|
+
return -1;
|
|
50
|
+
}
|
|
51
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING) {
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
return 0;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function getIs12Hour(locale) {
|
|
58
|
+
const testDate = new Date(2e3, 0, 1, 13, 0, 0);
|
|
59
|
+
const formatted = new Intl.DateTimeFormat(locale, {
|
|
60
|
+
hour: "numeric"
|
|
61
|
+
}).format(testDate);
|
|
62
|
+
return /am|pm/i.test(formatted) || !formatted.includes("13");
|
|
63
|
+
}
|
|
64
|
+
function parseTimeString(timeString) {
|
|
65
|
+
if (!timeString) return null;
|
|
66
|
+
const parts = timeString.split(":");
|
|
67
|
+
if (parts.length < 2) return null;
|
|
68
|
+
const result = {};
|
|
69
|
+
if (parts[0] && parts[0] !== DEFAULT_SEGMENT_PLACEHOLDER) {
|
|
70
|
+
const hour = Number.parseInt(parts[0], 10);
|
|
71
|
+
if (!Number.isNaN(hour) && hour >= 0 && hour <= 23) {
|
|
72
|
+
result.hour = hour;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (parts[1] && parts[1] !== DEFAULT_SEGMENT_PLACEHOLDER) {
|
|
76
|
+
const minute = Number.parseInt(parts[1], 10);
|
|
77
|
+
if (!Number.isNaN(minute) && minute >= 0 && minute <= 59) {
|
|
78
|
+
result.minute = minute;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (parts[2] && parts[2] !== DEFAULT_SEGMENT_PLACEHOLDER) {
|
|
82
|
+
const second = Number.parseInt(parts[2], 10);
|
|
83
|
+
if (!Number.isNaN(second) && second >= 0 && second <= 59) {
|
|
84
|
+
result.second = second;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (result.hour === void 0 && result.minute === void 0 && result.second === void 0) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
function formatTimeValue(value, showSeconds) {
|
|
93
|
+
const hourStr = value.hour !== void 0 ? value.hour.toString().padStart(2, "0") : DEFAULT_SEGMENT_PLACEHOLDER;
|
|
94
|
+
const minuteStr = value.minute !== void 0 ? value.minute.toString().padStart(2, "0") : DEFAULT_SEGMENT_PLACEHOLDER;
|
|
95
|
+
const secondStr = value.second !== void 0 ? value.second.toString().padStart(2, "0") : DEFAULT_SEGMENT_PLACEHOLDER;
|
|
96
|
+
if (showSeconds) {
|
|
97
|
+
return `${hourStr}:${minuteStr}:${secondStr}`;
|
|
98
|
+
}
|
|
99
|
+
return `${hourStr}:${minuteStr}`;
|
|
100
|
+
}
|
|
101
|
+
function to12Hour(hour24) {
|
|
102
|
+
const period = hour24 >= 12 ? "PM" : "AM";
|
|
103
|
+
const hour = hour24 % 12 || 12;
|
|
104
|
+
return { hour, period };
|
|
105
|
+
}
|
|
106
|
+
function to24Hour(hour12, period) {
|
|
107
|
+
if (hour12 === 12) {
|
|
108
|
+
return period === "PM" ? 12 : 0;
|
|
109
|
+
}
|
|
110
|
+
return period === "PM" ? hour12 + 12 : hour12;
|
|
111
|
+
}
|
|
112
|
+
function clamp(value, min, max) {
|
|
113
|
+
return Math.min(Math.max(value, min), max);
|
|
114
|
+
}
|
|
115
|
+
function getPeriodLabel(value, labels) {
|
|
116
|
+
if (value === "AM") return labels.am;
|
|
117
|
+
if (value === "PM") return labels.pm;
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
function getPeriodFromKey(key, labels) {
|
|
121
|
+
const lowerKey = key.toLowerCase();
|
|
122
|
+
if (lowerKey === "a" || lowerKey === "1") return "AM";
|
|
123
|
+
if (lowerKey === "p" || lowerKey === "2") return "PM";
|
|
124
|
+
if (lowerKey.length !== 1) return null;
|
|
125
|
+
if (lowerKey === labels.am.charAt(0).toLowerCase()) return "AM";
|
|
126
|
+
if (lowerKey === labels.pm.charAt(0).toLowerCase()) return "PM";
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const StoreContext = React.createContext(null);
|
|
130
|
+
function useStoreContext(consumerName) {
|
|
131
|
+
const context = React.useContext(StoreContext);
|
|
132
|
+
if (!context) {
|
|
133
|
+
throw new Error(`\`${consumerName}\` ph\u1EA3i n\u1EB1m b\xEAn trong \`${ROOT_NAME}\``);
|
|
134
|
+
}
|
|
135
|
+
return context;
|
|
136
|
+
}
|
|
137
|
+
function useStore(selector, ogStore) {
|
|
138
|
+
const contextStore = React.useContext(StoreContext);
|
|
139
|
+
const store = ogStore ?? contextStore;
|
|
140
|
+
if (!store) {
|
|
141
|
+
throw new Error(`\`useTimePicker\` ph\u1EA3i \u0111\u01B0\u1EE3c g\u1ECDi b\xEAn trong \`${ROOT_NAME}\``);
|
|
142
|
+
}
|
|
143
|
+
const getSnapshot = React.useCallback(() => selector(store.getState()), [store, selector]);
|
|
144
|
+
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
|
|
145
|
+
}
|
|
146
|
+
const TimePickerContext = React.createContext(null);
|
|
147
|
+
function useTimePickerContext(consumerName) {
|
|
148
|
+
const context = React.useContext(TimePickerContext);
|
|
149
|
+
if (!context) {
|
|
150
|
+
throw new Error(`\`${consumerName}\` ph\u1EA3i n\u1EB1m b\xEAn trong \`${ROOT_NAME}\``);
|
|
151
|
+
}
|
|
152
|
+
return context;
|
|
153
|
+
}
|
|
154
|
+
function TimePicker(props) {
|
|
155
|
+
const {
|
|
156
|
+
value: valueProp,
|
|
157
|
+
defaultValue,
|
|
158
|
+
onValueChange,
|
|
159
|
+
open,
|
|
160
|
+
defaultOpen,
|
|
161
|
+
onOpenChange,
|
|
162
|
+
openOnFocus = false,
|
|
163
|
+
inputGroupClickAction = "focus",
|
|
164
|
+
min,
|
|
165
|
+
max,
|
|
166
|
+
hourStep = DEFAULT_STEP,
|
|
167
|
+
minuteStep = DEFAULT_STEP,
|
|
168
|
+
secondStep = DEFAULT_STEP,
|
|
169
|
+
segmentPlaceholder = DEFAULT_SEGMENT_PLACEHOLDER,
|
|
170
|
+
locale = DEFAULT_LOCALE,
|
|
171
|
+
labels: labelsProp,
|
|
172
|
+
name,
|
|
173
|
+
asChild,
|
|
174
|
+
disabled = false,
|
|
175
|
+
invalid = false,
|
|
176
|
+
readOnly = false,
|
|
177
|
+
required = false,
|
|
178
|
+
showSeconds = false,
|
|
179
|
+
className,
|
|
180
|
+
children,
|
|
181
|
+
id,
|
|
182
|
+
...rootProps
|
|
183
|
+
} = props;
|
|
184
|
+
const instanceId = React.useId();
|
|
185
|
+
const rootId = id ?? instanceId;
|
|
186
|
+
const inputGroupId = React.useId();
|
|
187
|
+
const labelId = React.useId();
|
|
188
|
+
const triggerId = React.useId();
|
|
189
|
+
const inputGroupRef = React.useRef(null);
|
|
190
|
+
const triggerRef = React.useRef(null);
|
|
191
|
+
const [inputGroup, setInputGroup] = React.useState(null);
|
|
192
|
+
const isFormControl = inputGroup ? !!inputGroup.closest("form") : true;
|
|
193
|
+
const listenersRef = useLazyRef(() => /* @__PURE__ */ new Set());
|
|
194
|
+
const stateRef = useLazyRef(() => ({
|
|
195
|
+
value: valueProp ?? defaultValue ?? "",
|
|
196
|
+
open: open ?? defaultOpen ?? false,
|
|
197
|
+
openedViaFocus: false
|
|
198
|
+
}));
|
|
199
|
+
const propsRef = useAsRef({ onValueChange, onOpenChange });
|
|
200
|
+
const store = React.useMemo(() => {
|
|
201
|
+
return {
|
|
202
|
+
subscribe: (cb) => {
|
|
203
|
+
listenersRef.current.add(cb);
|
|
204
|
+
return () => listenersRef.current.delete(cb);
|
|
205
|
+
},
|
|
206
|
+
getState: () => stateRef.current,
|
|
207
|
+
setState: (key, value2) => {
|
|
208
|
+
if (Object.is(stateRef.current[key], value2)) return;
|
|
209
|
+
if (key === "value" && typeof value2 === "string") {
|
|
210
|
+
stateRef.current.value = value2;
|
|
211
|
+
propsRef.current.onValueChange?.(value2);
|
|
212
|
+
} else if (key === "open" && typeof value2 === "boolean") {
|
|
213
|
+
stateRef.current.open = value2;
|
|
214
|
+
propsRef.current.onOpenChange?.(value2);
|
|
215
|
+
if (!value2) {
|
|
216
|
+
stateRef.current.openedViaFocus = false;
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
stateRef.current[key] = value2;
|
|
220
|
+
}
|
|
221
|
+
store.notify();
|
|
222
|
+
},
|
|
223
|
+
notify: () => {
|
|
224
|
+
for (const cb of listenersRef.current) {
|
|
225
|
+
cb();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}, [listenersRef, stateRef, propsRef]);
|
|
230
|
+
const value = useStore((state) => state.value, store);
|
|
231
|
+
useIsomorphicLayoutEffect(() => {
|
|
232
|
+
if (valueProp !== void 0) {
|
|
233
|
+
store.setState("value", valueProp);
|
|
234
|
+
}
|
|
235
|
+
}, [valueProp]);
|
|
236
|
+
useIsomorphicLayoutEffect(() => {
|
|
237
|
+
if (open !== void 0) {
|
|
238
|
+
store.setState("open", open);
|
|
239
|
+
}
|
|
240
|
+
}, [open]);
|
|
241
|
+
const storeOpen = useStore((state) => state.open, store);
|
|
242
|
+
const onPopoverOpenChange = React.useCallback((newOpen) => store.setState("open", newOpen), [store]);
|
|
243
|
+
const is12Hour = React.useMemo(() => getIs12Hour(locale), [locale]);
|
|
244
|
+
const normalizedPlaceholder = React.useMemo(() => {
|
|
245
|
+
if (typeof segmentPlaceholder === "string") {
|
|
246
|
+
return {
|
|
247
|
+
hour: segmentPlaceholder,
|
|
248
|
+
minute: segmentPlaceholder,
|
|
249
|
+
second: segmentPlaceholder,
|
|
250
|
+
period: segmentPlaceholder
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
hour: segmentPlaceholder.hour ?? DEFAULT_SEGMENT_PLACEHOLDER,
|
|
255
|
+
minute: segmentPlaceholder.minute ?? DEFAULT_SEGMENT_PLACEHOLDER,
|
|
256
|
+
second: segmentPlaceholder.second ?? DEFAULT_SEGMENT_PLACEHOLDER,
|
|
257
|
+
period: segmentPlaceholder.period ?? DEFAULT_SEGMENT_PLACEHOLDER
|
|
258
|
+
};
|
|
259
|
+
}, [segmentPlaceholder]);
|
|
260
|
+
const labels = React.useMemo(
|
|
261
|
+
() => ({ ...DEFAULT_TIME_PICKER_LABELS, ...labelsProp }),
|
|
262
|
+
[labelsProp]
|
|
263
|
+
);
|
|
264
|
+
const rootContext = React.useMemo(
|
|
265
|
+
() => ({
|
|
266
|
+
id: rootId,
|
|
267
|
+
inputGroupId,
|
|
268
|
+
labelId,
|
|
269
|
+
triggerId,
|
|
270
|
+
inputGroupRef,
|
|
271
|
+
triggerRef,
|
|
272
|
+
openOnFocus,
|
|
273
|
+
inputGroupClickAction,
|
|
274
|
+
onInputGroupChange: setInputGroup,
|
|
275
|
+
disabled,
|
|
276
|
+
readOnly,
|
|
277
|
+
required,
|
|
278
|
+
invalid,
|
|
279
|
+
showSeconds,
|
|
280
|
+
is12Hour,
|
|
281
|
+
minuteStep,
|
|
282
|
+
secondStep,
|
|
283
|
+
hourStep,
|
|
284
|
+
segmentPlaceholder: normalizedPlaceholder,
|
|
285
|
+
labels,
|
|
286
|
+
min,
|
|
287
|
+
max
|
|
288
|
+
}),
|
|
289
|
+
[
|
|
290
|
+
rootId,
|
|
291
|
+
inputGroupId,
|
|
292
|
+
labelId,
|
|
293
|
+
triggerId,
|
|
294
|
+
openOnFocus,
|
|
295
|
+
inputGroupClickAction,
|
|
296
|
+
disabled,
|
|
297
|
+
readOnly,
|
|
298
|
+
required,
|
|
299
|
+
invalid,
|
|
300
|
+
showSeconds,
|
|
301
|
+
is12Hour,
|
|
302
|
+
minuteStep,
|
|
303
|
+
secondStep,
|
|
304
|
+
hourStep,
|
|
305
|
+
normalizedPlaceholder,
|
|
306
|
+
labels,
|
|
307
|
+
min,
|
|
308
|
+
max
|
|
309
|
+
]
|
|
310
|
+
);
|
|
311
|
+
const RootPrimitive = asChild ? Slot : "div";
|
|
312
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
313
|
+
/* @__PURE__ */ jsx(StoreContext.Provider, { value: store, children: /* @__PURE__ */ jsx(TimePickerContext.Provider, { value: rootContext, children: /* @__PURE__ */ jsx(Popover, { open: storeOpen, onOpenChange: onPopoverOpenChange, children: /* @__PURE__ */ jsx(
|
|
314
|
+
RootPrimitive,
|
|
315
|
+
{
|
|
316
|
+
"data-slot": "time-picker",
|
|
317
|
+
"data-disabled": disabled ? "" : void 0,
|
|
318
|
+
"data-invalid": invalid ? "" : void 0,
|
|
319
|
+
...rootProps,
|
|
320
|
+
className: cn("relative", className),
|
|
321
|
+
children
|
|
322
|
+
}
|
|
323
|
+
) }) }) }),
|
|
324
|
+
isFormControl && /* @__PURE__ */ jsx(
|
|
325
|
+
VisuallyHiddenInput,
|
|
326
|
+
{
|
|
327
|
+
type: "hidden",
|
|
328
|
+
control: inputGroup,
|
|
329
|
+
name,
|
|
330
|
+
value,
|
|
331
|
+
disabled,
|
|
332
|
+
readOnly,
|
|
333
|
+
required
|
|
334
|
+
}
|
|
335
|
+
)
|
|
336
|
+
] });
|
|
337
|
+
}
|
|
338
|
+
function TimePickerLabel(props) {
|
|
339
|
+
const { asChild, className, ...labelProps } = props;
|
|
340
|
+
const { labelId } = useTimePickerContext(LABEL_NAME);
|
|
341
|
+
const LabelPrimitive = asChild ? Slot : "label";
|
|
342
|
+
return /* @__PURE__ */ jsx(
|
|
343
|
+
LabelPrimitive,
|
|
344
|
+
{
|
|
345
|
+
"data-slot": "time-picker-label",
|
|
346
|
+
...labelProps,
|
|
347
|
+
id: labelId,
|
|
348
|
+
className: cn(
|
|
349
|
+
"text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
350
|
+
className
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
const TimePickerInputGroupContext = React.createContext(null);
|
|
356
|
+
function useTimePickerInputGroupContext(consumerName) {
|
|
357
|
+
const context = React.useContext(TimePickerInputGroupContext);
|
|
358
|
+
if (!context) {
|
|
359
|
+
throw new Error(`\`${consumerName}\` ph\u1EA3i n\u1EB1m b\xEAn trong \`${INPUT_GROUP_NAME}\``);
|
|
360
|
+
}
|
|
361
|
+
return context;
|
|
362
|
+
}
|
|
363
|
+
function TimePickerInputGroup(props) {
|
|
364
|
+
const {
|
|
365
|
+
onPointerDown: onPointerDownProp,
|
|
366
|
+
onClick: onClickProp,
|
|
367
|
+
asChild,
|
|
368
|
+
className,
|
|
369
|
+
style,
|
|
370
|
+
ref,
|
|
371
|
+
...inputGroupProps
|
|
372
|
+
} = props;
|
|
373
|
+
const {
|
|
374
|
+
inputGroupId,
|
|
375
|
+
labelId,
|
|
376
|
+
onInputGroupChange,
|
|
377
|
+
disabled,
|
|
378
|
+
readOnly,
|
|
379
|
+
invalid,
|
|
380
|
+
segmentPlaceholder,
|
|
381
|
+
labels,
|
|
382
|
+
inputGroupRef,
|
|
383
|
+
triggerRef,
|
|
384
|
+
inputGroupClickAction
|
|
385
|
+
} = useTimePickerContext(INPUT_GROUP_NAME);
|
|
386
|
+
const store = useStoreContext(INPUT_GROUP_NAME);
|
|
387
|
+
const composedRef = useComposedRefs(ref, inputGroupRef, onInputGroupChange);
|
|
388
|
+
const inputRefsMap = React.useRef(/* @__PURE__ */ new Map());
|
|
389
|
+
const onInputRegister = React.useCallback((segment, ref2) => {
|
|
390
|
+
inputRefsMap.current.set(segment, ref2);
|
|
391
|
+
}, []);
|
|
392
|
+
const onInputUnregister = React.useCallback((segment) => {
|
|
393
|
+
inputRefsMap.current.delete(segment);
|
|
394
|
+
}, []);
|
|
395
|
+
const getNextInput = React.useCallback((currentSegment) => {
|
|
396
|
+
const segmentOrder = ["hour", "minute", "second", "period"];
|
|
397
|
+
const currentIndex = segmentOrder.indexOf(currentSegment);
|
|
398
|
+
if (currentIndex === -1 || currentIndex === segmentOrder.length - 1) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
for (let i = currentIndex + 1; i < segmentOrder.length; i++) {
|
|
402
|
+
const nextSegment = segmentOrder[i];
|
|
403
|
+
if (nextSegment) {
|
|
404
|
+
const nextRef = inputRefsMap.current.get(nextSegment);
|
|
405
|
+
if (nextRef?.current) {
|
|
406
|
+
return nextRef;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
}, []);
|
|
412
|
+
const onPointerDown = React.useCallback(
|
|
413
|
+
(event) => {
|
|
414
|
+
onPointerDownProp?.(event);
|
|
415
|
+
if (disabled || readOnly || event.defaultPrevented) return;
|
|
416
|
+
const target = event.target;
|
|
417
|
+
if (target.tagName === "INPUT" || target.closest("input")) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (triggerRef.current?.contains(target)) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
event.preventDefault();
|
|
424
|
+
},
|
|
425
|
+
[onPointerDownProp, disabled, readOnly, triggerRef]
|
|
426
|
+
);
|
|
427
|
+
const onClick = React.useCallback(
|
|
428
|
+
(event) => {
|
|
429
|
+
onClickProp?.(event);
|
|
430
|
+
if (disabled || readOnly || event.defaultPrevented) return;
|
|
431
|
+
const target = event.target;
|
|
432
|
+
if (target.tagName === "INPUT" || target.closest("input")) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (triggerRef.current?.contains(target)) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (inputGroupClickAction === "open") {
|
|
439
|
+
store.setState("open", true);
|
|
440
|
+
} else {
|
|
441
|
+
const activeElement = document.activeElement;
|
|
442
|
+
const isInputAlreadyFocused = activeElement && activeElement.tagName === "INPUT" && inputGroupRef.current?.contains(activeElement);
|
|
443
|
+
if (!isInputAlreadyFocused) {
|
|
444
|
+
for (const segment of SEGMENTS) {
|
|
445
|
+
const inputRef = inputRefsMap.current.get(segment);
|
|
446
|
+
if (inputRef?.current) {
|
|
447
|
+
inputRef.current.focus();
|
|
448
|
+
inputRef.current.select();
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
[onClickProp, disabled, readOnly, inputGroupClickAction, store, triggerRef, inputGroupRef]
|
|
456
|
+
);
|
|
457
|
+
const inputGroupContextValue = React.useMemo(
|
|
458
|
+
() => ({
|
|
459
|
+
onInputRegister,
|
|
460
|
+
onInputUnregister,
|
|
461
|
+
getNextInput
|
|
462
|
+
}),
|
|
463
|
+
[onInputRegister, onInputUnregister, getNextInput]
|
|
464
|
+
);
|
|
465
|
+
const InputGroupPrimitive = asChild ? Slot : "div";
|
|
466
|
+
return /* @__PURE__ */ jsx(TimePickerInputGroupContext.Provider, { value: inputGroupContextValue, children: /* @__PURE__ */ jsx(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
467
|
+
InputGroupPrimitive,
|
|
468
|
+
{
|
|
469
|
+
role: "group",
|
|
470
|
+
id: inputGroupId,
|
|
471
|
+
"aria-labelledby": labelId,
|
|
472
|
+
"data-slot": "time-picker-input-group",
|
|
473
|
+
"data-disabled": disabled ? "" : void 0,
|
|
474
|
+
"data-invalid": invalid ? "" : void 0,
|
|
475
|
+
...inputGroupProps,
|
|
476
|
+
className: cn(
|
|
477
|
+
/*
|
|
478
|
+
* ⚠ Khác thượng nguồn: khung ô đi theo `Input` của gói (ô ngày của `DatePicker` là
|
|
479
|
+
* một `Input`, và hai thứ này hay đứng chung một hàng biểu mẫu): `h-9` thay `h-10`,
|
|
480
|
+
* bỏ `shadow-xs`, hover đậm viền `--control-edge-strong`, focus viền
|
|
481
|
+
* `--primary-ink` + quầng `ring-2 ring-ring/50` KHÔNG offset (quy ước họ ô nhập —
|
|
482
|
+
* xem `InputGroup`), lỗi viền `--destructive-ink`. Số đo ở chú thích đầu `input.tsx`.
|
|
483
|
+
*/
|
|
484
|
+
"flex h-9 w-full cursor-text items-center gap-0.5 rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors duration-150 ease-out outline-none hover:border-control-edge-strong",
|
|
485
|
+
"has-[input:focus]:border-primary-ink has-[input:focus]:ring-2 has-[input:focus]:ring-ring/50",
|
|
486
|
+
invalid && "border-destructive-ink hover:border-destructive-ink has-[input:focus]:border-destructive-ink has-[input:focus]:ring-destructive/30",
|
|
487
|
+
/*
|
|
488
|
+
* ⚠ Khác thượng nguồn: bản gốc ghép `disabled && "cursor-not-allowed opacity-50"` —
|
|
489
|
+
* lớp gắn VÔ ĐIỀU KIỆN vào chuỗi, nên `pnpm lint:a11y` đo viền/chữ lúc NGHỈ đã nhân
|
|
490
|
+
* 50% (viền 1,75:1, chữ 3,49:1 ✗). Neo vào `:disabled` gốc của các ô con (cổng miễn
|
|
491
|
+
* trừ đúng nhánh này — WCAG 1.4.3/1.4.11 không đòi tương phản cho control vô hiệu);
|
|
492
|
+
* `disabled` ở gốc khoá MỌI ô nên hai cách cho cùng kết quả. Thêm `bg-muted` theo `Input`.
|
|
493
|
+
*/
|
|
494
|
+
"has-[input:disabled]:cursor-not-allowed has-[input:disabled]:bg-muted has-[input:disabled]:opacity-50",
|
|
495
|
+
className
|
|
496
|
+
),
|
|
497
|
+
style: {
|
|
498
|
+
"--time-picker-hour-input-width": `${segmentPlaceholder.hour.length}ch`,
|
|
499
|
+
"--time-picker-minute-input-width": `${segmentPlaceholder.minute.length}ch`,
|
|
500
|
+
"--time-picker-second-input-width": `${segmentPlaceholder.second.length}ch`,
|
|
501
|
+
// ⚠ Khác thượng nguồn: tính cả độ dài nhãn buổi (`labels.am`/`labels.pm`) — bản gốc
|
|
502
|
+
// chỉ có "AM"/"PM" 2 ký tự nên ghim sàn 2; nhãn do app đặt có thể dài hơn.
|
|
503
|
+
"--time-picker-period-input-width": `${Math.max(segmentPlaceholder.period.length, labels.am.length, labels.pm.length, 2) + 0.5}ch`,
|
|
504
|
+
...style
|
|
505
|
+
},
|
|
506
|
+
ref: composedRef,
|
|
507
|
+
onPointerDown,
|
|
508
|
+
onClick
|
|
509
|
+
}
|
|
510
|
+
) }) });
|
|
511
|
+
}
|
|
512
|
+
function TimePickerInput(props) {
|
|
513
|
+
const {
|
|
514
|
+
segment,
|
|
515
|
+
onBlur: onBlurProp,
|
|
516
|
+
onChange: onChangeProp,
|
|
517
|
+
onClick: onClickProp,
|
|
518
|
+
onFocus: onFocusProp,
|
|
519
|
+
onKeyDown: onKeyDownProp,
|
|
520
|
+
disabled: disabledProp,
|
|
521
|
+
readOnly: readOnlyProp,
|
|
522
|
+
className,
|
|
523
|
+
style,
|
|
524
|
+
ref,
|
|
525
|
+
...inputProps
|
|
526
|
+
} = props;
|
|
527
|
+
const { is12Hour, showSeconds, disabled, readOnly, invalid, segmentPlaceholder, labels, openOnFocus } = useTimePickerContext(INPUT_NAME);
|
|
528
|
+
const store = useStoreContext(INPUT_NAME);
|
|
529
|
+
const inputGroupContext = useTimePickerInputGroupContext(INPUT_NAME);
|
|
530
|
+
const isDisabled = disabledProp || disabled;
|
|
531
|
+
const isReadOnly = readOnlyProp || readOnly;
|
|
532
|
+
const value = useStore((state) => state.value);
|
|
533
|
+
const timeValue = parseTimeString(value);
|
|
534
|
+
const inputRef = React.useRef(null);
|
|
535
|
+
const composedRef = useComposedRefs(ref, inputRef);
|
|
536
|
+
useIsomorphicLayoutEffect(() => {
|
|
537
|
+
if (segment) {
|
|
538
|
+
inputGroupContext.onInputRegister(segment, inputRef);
|
|
539
|
+
return () => inputGroupContext.onInputUnregister(segment);
|
|
540
|
+
}
|
|
541
|
+
}, [inputGroupContext, segment]);
|
|
542
|
+
const getSegmentValue = React.useCallback(() => {
|
|
543
|
+
if (!timeValue) {
|
|
544
|
+
if (!segment) return "";
|
|
545
|
+
return segmentPlaceholder[segment];
|
|
546
|
+
}
|
|
547
|
+
switch (segment) {
|
|
548
|
+
case "hour": {
|
|
549
|
+
if (timeValue.hour === void 0) return segmentPlaceholder.hour;
|
|
550
|
+
if (is12Hour) {
|
|
551
|
+
return to12Hour(timeValue.hour).hour.toString().padStart(2, "0");
|
|
552
|
+
}
|
|
553
|
+
return timeValue.hour.toString().padStart(2, "0");
|
|
554
|
+
}
|
|
555
|
+
case "minute":
|
|
556
|
+
if (timeValue.minute === void 0) return segmentPlaceholder.minute;
|
|
557
|
+
return timeValue.minute.toString().padStart(2, "0");
|
|
558
|
+
case "second":
|
|
559
|
+
if (timeValue.second === void 0) return segmentPlaceholder.second;
|
|
560
|
+
return timeValue.second.toString().padStart(2, "0");
|
|
561
|
+
case "period": {
|
|
562
|
+
if (!timeValue || timeValue.hour === void 0) return segmentPlaceholder.period;
|
|
563
|
+
return to12Hour(timeValue.hour).period;
|
|
564
|
+
}
|
|
565
|
+
default:
|
|
566
|
+
return "";
|
|
567
|
+
}
|
|
568
|
+
}, [timeValue, segment, is12Hour, segmentPlaceholder]);
|
|
569
|
+
const [editValue, setEditValue] = React.useState(getSegmentValue());
|
|
570
|
+
const [isEditing, setIsEditing] = React.useState(false);
|
|
571
|
+
const [pendingDigit, setPendingDigit] = React.useState(null);
|
|
572
|
+
React.useEffect(() => {
|
|
573
|
+
if (!isEditing) {
|
|
574
|
+
setEditValue(getSegmentValue());
|
|
575
|
+
setPendingDigit(null);
|
|
576
|
+
}
|
|
577
|
+
}, [getSegmentValue, isEditing]);
|
|
578
|
+
const updateTimeValue = React.useCallback(
|
|
579
|
+
(newSegmentValue, shouldCreateIfEmpty = false) => {
|
|
580
|
+
const placeholder = segment ? segmentPlaceholder[segment] : DEFAULT_SEGMENT_PLACEHOLDER;
|
|
581
|
+
if (!newSegmentValue || newSegmentValue === placeholder) return;
|
|
582
|
+
if (!timeValue && !shouldCreateIfEmpty) return;
|
|
583
|
+
const currentTime = timeValue ?? {};
|
|
584
|
+
const newTime = { ...currentTime };
|
|
585
|
+
switch (segment) {
|
|
586
|
+
case "hour": {
|
|
587
|
+
const displayHour = Number.parseInt(newSegmentValue, 10);
|
|
588
|
+
if (!Number.isNaN(displayHour)) {
|
|
589
|
+
if (is12Hour) {
|
|
590
|
+
const clampedHour = clamp(displayHour, 1, 12);
|
|
591
|
+
let currentPeriod;
|
|
592
|
+
if (timeValue?.period !== void 0) {
|
|
593
|
+
currentPeriod = timeValue.period;
|
|
594
|
+
} else if (timeValue?.hour !== void 0) {
|
|
595
|
+
currentPeriod = to12Hour(timeValue.hour).period;
|
|
596
|
+
} else {
|
|
597
|
+
const now = /* @__PURE__ */ new Date();
|
|
598
|
+
currentPeriod = to12Hour(now.getHours()).period;
|
|
599
|
+
}
|
|
600
|
+
const hour24 = to24Hour(clampedHour, currentPeriod);
|
|
601
|
+
newTime.hour = hour24;
|
|
602
|
+
if (timeValue?.period !== void 0) {
|
|
603
|
+
newTime.period = timeValue.period;
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
newTime.hour = clamp(displayHour, 0, 23);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
case "minute": {
|
|
612
|
+
const minute = Number.parseInt(newSegmentValue, 10);
|
|
613
|
+
if (!Number.isNaN(minute)) {
|
|
614
|
+
newTime.minute = clamp(minute, 0, 59);
|
|
615
|
+
}
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
case "second": {
|
|
619
|
+
const second = Number.parseInt(newSegmentValue, 10);
|
|
620
|
+
if (!Number.isNaN(second)) {
|
|
621
|
+
newTime.second = clamp(second, 0, 59);
|
|
622
|
+
}
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
case "period": {
|
|
626
|
+
if (newSegmentValue === "AM" || newSegmentValue === "PM") {
|
|
627
|
+
newTime.period = newSegmentValue;
|
|
628
|
+
if (timeValue && timeValue.hour !== void 0) {
|
|
629
|
+
const currentDisplay = to12Hour(timeValue.hour);
|
|
630
|
+
newTime.hour = to24Hour(currentDisplay.hour, newSegmentValue);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const newValue = formatTimeValue(newTime, showSeconds);
|
|
637
|
+
store.setState("value", newValue);
|
|
638
|
+
},
|
|
639
|
+
[timeValue, segment, is12Hour, showSeconds, store, segmentPlaceholder]
|
|
640
|
+
);
|
|
641
|
+
const onBlur = React.useCallback(
|
|
642
|
+
(event) => {
|
|
643
|
+
onBlurProp?.(event);
|
|
644
|
+
if (event.defaultPrevented) return;
|
|
645
|
+
setIsEditing(false);
|
|
646
|
+
const placeholder = segment ? segmentPlaceholder[segment] : DEFAULT_SEGMENT_PLACEHOLDER;
|
|
647
|
+
if (editValue && editValue !== placeholder && editValue.length > 0) {
|
|
648
|
+
let valueToUpdate = editValue;
|
|
649
|
+
if (segment !== "period") {
|
|
650
|
+
if (editValue.length === 2) {
|
|
651
|
+
valueToUpdate = editValue;
|
|
652
|
+
} else if (editValue.length === 1) {
|
|
653
|
+
const numValue = Number.parseInt(editValue, 10);
|
|
654
|
+
if (!Number.isNaN(numValue)) {
|
|
655
|
+
valueToUpdate = numValue.toString().padStart(2, "0");
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
updateTimeValue(valueToUpdate, true);
|
|
660
|
+
queueMicrotask(() => {
|
|
661
|
+
const currentTimeValue = parseTimeString(store.getState().value);
|
|
662
|
+
if (currentTimeValue) {
|
|
663
|
+
const now = /* @__PURE__ */ new Date();
|
|
664
|
+
const newTime = { ...currentTimeValue };
|
|
665
|
+
let needsUpdate = false;
|
|
666
|
+
if (newTime.hour === void 0) {
|
|
667
|
+
newTime.hour = now.getHours();
|
|
668
|
+
needsUpdate = true;
|
|
669
|
+
}
|
|
670
|
+
if (newTime.minute === void 0) {
|
|
671
|
+
newTime.minute = now.getMinutes();
|
|
672
|
+
needsUpdate = true;
|
|
673
|
+
}
|
|
674
|
+
if (showSeconds && newTime.second === void 0) {
|
|
675
|
+
newTime.second = now.getSeconds();
|
|
676
|
+
needsUpdate = true;
|
|
677
|
+
}
|
|
678
|
+
if (needsUpdate) {
|
|
679
|
+
const newValue = formatTimeValue(newTime, showSeconds);
|
|
680
|
+
store.setState("value", newValue);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
setEditValue(getSegmentValue());
|
|
686
|
+
setPendingDigit(null);
|
|
687
|
+
},
|
|
688
|
+
[onBlurProp, editValue, updateTimeValue, getSegmentValue, segment, segmentPlaceholder, showSeconds, store]
|
|
689
|
+
);
|
|
690
|
+
const onChange = React.useCallback(
|
|
691
|
+
(event) => {
|
|
692
|
+
onChangeProp?.(event);
|
|
693
|
+
if (event.defaultPrevented) return;
|
|
694
|
+
let newValue = event.target.value;
|
|
695
|
+
const placeholder = segment ? segmentPlaceholder[segment] : DEFAULT_SEGMENT_PLACEHOLDER;
|
|
696
|
+
if (editValue === placeholder && newValue.length > 0 && newValue !== placeholder) {
|
|
697
|
+
newValue = newValue.replace(new RegExp(`^${placeholder}`), "");
|
|
698
|
+
}
|
|
699
|
+
if (segment === "period") {
|
|
700
|
+
const newPeriod = getPeriodFromKey(newValue.charAt(0), labels);
|
|
701
|
+
if (newPeriod) {
|
|
702
|
+
setEditValue(newPeriod);
|
|
703
|
+
updateTimeValue(newPeriod, true);
|
|
704
|
+
queueMicrotask(() => {
|
|
705
|
+
inputRef.current?.select();
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (segment === "hour" || segment === "minute" || segment === "second") {
|
|
711
|
+
newValue = newValue.replace(/\D/g, "");
|
|
712
|
+
}
|
|
713
|
+
if (newValue.length > 2) {
|
|
714
|
+
newValue = newValue.slice(0, 2);
|
|
715
|
+
}
|
|
716
|
+
if (segment === "hour" || segment === "minute" || segment === "second") {
|
|
717
|
+
const numValue = Number.parseInt(newValue, 10);
|
|
718
|
+
if (!Number.isNaN(numValue) && newValue.length > 0) {
|
|
719
|
+
if (pendingDigit !== null && newValue.length === 1) {
|
|
720
|
+
const twoDigitValue = pendingDigit + newValue;
|
|
721
|
+
const combinedNum = Number.parseInt(twoDigitValue, 10);
|
|
722
|
+
if (!Number.isNaN(combinedNum)) {
|
|
723
|
+
const paddedValue = combinedNum.toString().padStart(2, "0");
|
|
724
|
+
setEditValue(paddedValue);
|
|
725
|
+
updateTimeValue(paddedValue, true);
|
|
726
|
+
setPendingDigit(null);
|
|
727
|
+
queueMicrotask(() => {
|
|
728
|
+
if (segment) {
|
|
729
|
+
const nextInputRef = inputGroupContext.getNextInput(segment);
|
|
730
|
+
if (nextInputRef?.current) {
|
|
731
|
+
nextInputRef.current.focus();
|
|
732
|
+
nextInputRef.current.select();
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const maxFirstDigit = segment === "hour" ? is12Hour ? 1 : 2 : 5;
|
|
740
|
+
const firstDigit = Number.parseInt(newValue[0] ?? "0", 10);
|
|
741
|
+
const shouldAutoAdvance = firstDigit > maxFirstDigit;
|
|
742
|
+
if (newValue.length === 1) {
|
|
743
|
+
if (shouldAutoAdvance) {
|
|
744
|
+
const paddedValue = numValue.toString().padStart(2, "0");
|
|
745
|
+
setEditValue(paddedValue);
|
|
746
|
+
updateTimeValue(paddedValue, true);
|
|
747
|
+
setPendingDigit(null);
|
|
748
|
+
queueMicrotask(() => {
|
|
749
|
+
if (segment) {
|
|
750
|
+
const nextInputRef = inputGroupContext.getNextInput(segment);
|
|
751
|
+
if (nextInputRef?.current) {
|
|
752
|
+
nextInputRef.current.focus();
|
|
753
|
+
nextInputRef.current.select();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
} else {
|
|
758
|
+
const paddedValue = numValue.toString().padStart(2, "0");
|
|
759
|
+
setEditValue(paddedValue);
|
|
760
|
+
setPendingDigit(newValue);
|
|
761
|
+
queueMicrotask(() => {
|
|
762
|
+
inputRef.current?.select();
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
} else if (newValue.length === 2) {
|
|
766
|
+
const paddedValue = numValue.toString().padStart(2, "0");
|
|
767
|
+
setEditValue(paddedValue);
|
|
768
|
+
updateTimeValue(paddedValue, true);
|
|
769
|
+
setPendingDigit(null);
|
|
770
|
+
queueMicrotask(() => {
|
|
771
|
+
if (segment) {
|
|
772
|
+
const nextInputRef = inputGroupContext.getNextInput(segment);
|
|
773
|
+
if (nextInputRef?.current) {
|
|
774
|
+
nextInputRef.current.focus();
|
|
775
|
+
nextInputRef.current.select();
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
} else if (newValue.length === 0) {
|
|
781
|
+
setEditValue("");
|
|
782
|
+
setPendingDigit(null);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
[
|
|
787
|
+
segment,
|
|
788
|
+
updateTimeValue,
|
|
789
|
+
onChangeProp,
|
|
790
|
+
editValue,
|
|
791
|
+
is12Hour,
|
|
792
|
+
inputGroupContext,
|
|
793
|
+
pendingDigit,
|
|
794
|
+
segmentPlaceholder,
|
|
795
|
+
labels
|
|
796
|
+
]
|
|
797
|
+
);
|
|
798
|
+
const onClick = React.useCallback(
|
|
799
|
+
(event) => {
|
|
800
|
+
onClickProp?.(event);
|
|
801
|
+
if (event.defaultPrevented) return;
|
|
802
|
+
event.currentTarget.select();
|
|
803
|
+
},
|
|
804
|
+
[onClickProp]
|
|
805
|
+
);
|
|
806
|
+
const onFocus = React.useCallback(
|
|
807
|
+
(event) => {
|
|
808
|
+
onFocusProp?.(event);
|
|
809
|
+
if (event.defaultPrevented) return;
|
|
810
|
+
setIsEditing(true);
|
|
811
|
+
setPendingDigit(null);
|
|
812
|
+
if (openOnFocus && !store.getState().open) {
|
|
813
|
+
store.setState("openedViaFocus", true);
|
|
814
|
+
store.setState("open", true);
|
|
815
|
+
}
|
|
816
|
+
queueMicrotask(() => event.target.select());
|
|
817
|
+
},
|
|
818
|
+
[onFocusProp, openOnFocus, store]
|
|
819
|
+
);
|
|
820
|
+
const onKeyDown = React.useCallback(
|
|
821
|
+
(event) => {
|
|
822
|
+
onKeyDownProp?.(event);
|
|
823
|
+
if (event.defaultPrevented) return;
|
|
824
|
+
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
825
|
+
event.preventDefault();
|
|
826
|
+
const goToPrevious = event.key === "ArrowLeft";
|
|
827
|
+
const inputGroup = inputRef.current?.closest('[data-slot="time-picker-input-group"]');
|
|
828
|
+
if (inputGroup && inputRef.current) {
|
|
829
|
+
const allInputs = Array.from(inputGroup.querySelectorAll('input[type="text"]'));
|
|
830
|
+
const currentIdx = allInputs.indexOf(inputRef.current);
|
|
831
|
+
if (currentIdx !== -1) {
|
|
832
|
+
const targetIdx = goToPrevious ? Math.max(0, currentIdx - 1) : Math.min(allInputs.length - 1, currentIdx + 1);
|
|
833
|
+
const targetInput = allInputs[targetIdx];
|
|
834
|
+
if (targetInput && targetInput !== inputRef.current) {
|
|
835
|
+
targetInput.focus();
|
|
836
|
+
targetInput.select();
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (event.key === "Backspace" || event.key === "Delete") {
|
|
843
|
+
const input = inputRef.current;
|
|
844
|
+
if (input && input.selectionStart === 0 && input.selectionEnd === input.value.length) {
|
|
845
|
+
event.preventDefault();
|
|
846
|
+
const placeholder = segment ? segmentPlaceholder[segment] : DEFAULT_SEGMENT_PLACEHOLDER;
|
|
847
|
+
setEditValue(placeholder);
|
|
848
|
+
setPendingDigit(null);
|
|
849
|
+
if (timeValue) {
|
|
850
|
+
const newTime = { ...timeValue };
|
|
851
|
+
switch (segment) {
|
|
852
|
+
case "hour":
|
|
853
|
+
delete newTime.hour;
|
|
854
|
+
break;
|
|
855
|
+
case "minute":
|
|
856
|
+
delete newTime.minute;
|
|
857
|
+
break;
|
|
858
|
+
case "second":
|
|
859
|
+
delete newTime.second;
|
|
860
|
+
break;
|
|
861
|
+
case "period":
|
|
862
|
+
delete newTime.period;
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
if (newTime.hour !== void 0 || newTime.minute !== void 0 || newTime.second !== void 0 || newTime.period !== void 0) {
|
|
866
|
+
const newValue = formatTimeValue(newTime, showSeconds);
|
|
867
|
+
store.setState("value", newValue);
|
|
868
|
+
} else {
|
|
869
|
+
store.setState("value", "");
|
|
870
|
+
}
|
|
871
|
+
} else {
|
|
872
|
+
store.setState("value", "");
|
|
873
|
+
}
|
|
874
|
+
queueMicrotask(() => {
|
|
875
|
+
inputRef.current?.select();
|
|
876
|
+
});
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (segment === "period") {
|
|
881
|
+
const newPeriodFromKey = getPeriodFromKey(event.key, labels);
|
|
882
|
+
if (newPeriodFromKey) {
|
|
883
|
+
event.preventDefault();
|
|
884
|
+
setEditValue(newPeriodFromKey);
|
|
885
|
+
updateTimeValue(newPeriodFromKey, true);
|
|
886
|
+
queueMicrotask(() => {
|
|
887
|
+
inputRef.current?.select();
|
|
888
|
+
});
|
|
889
|
+
} else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
890
|
+
event.preventDefault();
|
|
891
|
+
const placeholder = segmentPlaceholder.period;
|
|
892
|
+
const currentPeriod = editValue === placeholder || editValue === "" ? "AM" : editValue;
|
|
893
|
+
const newPeriod = currentPeriod === "AM" || currentPeriod === "A" ? "PM" : "AM";
|
|
894
|
+
setEditValue(newPeriod);
|
|
895
|
+
updateTimeValue(newPeriod, true);
|
|
896
|
+
queueMicrotask(() => {
|
|
897
|
+
inputRef.current?.select();
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (event.key === "Tab") {
|
|
903
|
+
const placeholder = segment ? segmentPlaceholder[segment] : DEFAULT_SEGMENT_PLACEHOLDER;
|
|
904
|
+
if (editValue && editValue.length > 0 && editValue !== placeholder) {
|
|
905
|
+
if (editValue.length === 2) {
|
|
906
|
+
updateTimeValue(editValue, true);
|
|
907
|
+
} else if (editValue.length === 1) {
|
|
908
|
+
const numValue = Number.parseInt(editValue, 10);
|
|
909
|
+
if (!Number.isNaN(numValue)) {
|
|
910
|
+
const paddedValue = numValue.toString().padStart(2, "0");
|
|
911
|
+
updateTimeValue(paddedValue, true);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
if (event.key === "Enter") {
|
|
918
|
+
event.preventDefault();
|
|
919
|
+
const placeholder = segment ? segmentPlaceholder[segment] : DEFAULT_SEGMENT_PLACEHOLDER;
|
|
920
|
+
if (editValue && editValue.length > 0 && editValue !== placeholder) {
|
|
921
|
+
if (editValue.length === 2) {
|
|
922
|
+
updateTimeValue(editValue, true);
|
|
923
|
+
} else if (editValue.length === 1) {
|
|
924
|
+
const numValue = Number.parseInt(editValue, 10);
|
|
925
|
+
if (!Number.isNaN(numValue)) {
|
|
926
|
+
const paddedValue = numValue.toString().padStart(2, "0");
|
|
927
|
+
updateTimeValue(paddedValue, true);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
queueMicrotask(() => {
|
|
932
|
+
inputRef.current?.select();
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
if (event.key === "Escape") {
|
|
936
|
+
event.preventDefault();
|
|
937
|
+
setEditValue(getSegmentValue());
|
|
938
|
+
inputRef.current?.blur();
|
|
939
|
+
}
|
|
940
|
+
if (event.key === "ArrowUp") {
|
|
941
|
+
event.preventDefault();
|
|
942
|
+
const placeholder = segment ? segmentPlaceholder[segment] : DEFAULT_SEGMENT_PLACEHOLDER;
|
|
943
|
+
if (editValue === placeholder || editValue === "") {
|
|
944
|
+
const defaultValue = segment === "hour" ? is12Hour ? 12 : 0 : 0;
|
|
945
|
+
const formattedValue = defaultValue.toString().padStart(2, "0");
|
|
946
|
+
setEditValue(formattedValue);
|
|
947
|
+
updateTimeValue(formattedValue, true);
|
|
948
|
+
queueMicrotask(() => {
|
|
949
|
+
inputRef.current?.select();
|
|
950
|
+
});
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const currentValue = Number.parseInt(editValue, 10);
|
|
954
|
+
if (!Number.isNaN(currentValue)) {
|
|
955
|
+
let newValue;
|
|
956
|
+
switch (segment) {
|
|
957
|
+
case "hour":
|
|
958
|
+
if (is12Hour) {
|
|
959
|
+
newValue = currentValue === 12 ? 1 : currentValue + 1;
|
|
960
|
+
} else {
|
|
961
|
+
newValue = currentValue === 23 ? 0 : currentValue + 1;
|
|
962
|
+
}
|
|
963
|
+
break;
|
|
964
|
+
case "minute":
|
|
965
|
+
case "second":
|
|
966
|
+
newValue = currentValue === 59 ? 0 : currentValue + 1;
|
|
967
|
+
break;
|
|
968
|
+
default:
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
const formattedValue = newValue.toString().padStart(2, "0");
|
|
972
|
+
setEditValue(formattedValue);
|
|
973
|
+
updateTimeValue(formattedValue, true);
|
|
974
|
+
queueMicrotask(() => {
|
|
975
|
+
inputRef.current?.select();
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (event.key === "ArrowDown") {
|
|
980
|
+
event.preventDefault();
|
|
981
|
+
const placeholder = segment ? segmentPlaceholder[segment] : DEFAULT_SEGMENT_PLACEHOLDER;
|
|
982
|
+
if (editValue === placeholder || editValue === "") {
|
|
983
|
+
const defaultValue = segment === "hour" ? is12Hour ? 12 : 23 : 59;
|
|
984
|
+
const formattedValue = defaultValue.toString().padStart(2, "0");
|
|
985
|
+
setEditValue(formattedValue);
|
|
986
|
+
updateTimeValue(formattedValue, true);
|
|
987
|
+
queueMicrotask(() => {
|
|
988
|
+
inputRef.current?.select();
|
|
989
|
+
});
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const currentValue = Number.parseInt(editValue, 10);
|
|
993
|
+
if (!Number.isNaN(currentValue)) {
|
|
994
|
+
let newValue;
|
|
995
|
+
switch (segment) {
|
|
996
|
+
case "hour":
|
|
997
|
+
if (is12Hour) {
|
|
998
|
+
newValue = currentValue === 1 ? 12 : currentValue - 1;
|
|
999
|
+
} else {
|
|
1000
|
+
newValue = currentValue === 0 ? 23 : currentValue - 1;
|
|
1001
|
+
}
|
|
1002
|
+
break;
|
|
1003
|
+
case "minute":
|
|
1004
|
+
case "second":
|
|
1005
|
+
newValue = currentValue === 0 ? 59 : currentValue - 1;
|
|
1006
|
+
break;
|
|
1007
|
+
default:
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const formattedValue = newValue.toString().padStart(2, "0");
|
|
1011
|
+
setEditValue(formattedValue);
|
|
1012
|
+
updateTimeValue(formattedValue, true);
|
|
1013
|
+
queueMicrotask(() => {
|
|
1014
|
+
inputRef.current?.select();
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
[
|
|
1020
|
+
onKeyDownProp,
|
|
1021
|
+
editValue,
|
|
1022
|
+
segment,
|
|
1023
|
+
is12Hour,
|
|
1024
|
+
getSegmentValue,
|
|
1025
|
+
updateTimeValue,
|
|
1026
|
+
showSeconds,
|
|
1027
|
+
timeValue,
|
|
1028
|
+
store,
|
|
1029
|
+
segmentPlaceholder,
|
|
1030
|
+
labels
|
|
1031
|
+
]
|
|
1032
|
+
);
|
|
1033
|
+
if (segment === "period" && !is12Hour) return null;
|
|
1034
|
+
const displayValue = isEditing ? editValue : getSegmentValue();
|
|
1035
|
+
const segmentWidth = segment ? `var(--time-picker-${segment}-input-width)` : "2ch";
|
|
1036
|
+
return /* @__PURE__ */ jsx(
|
|
1037
|
+
"input",
|
|
1038
|
+
{
|
|
1039
|
+
type: "text",
|
|
1040
|
+
inputMode: segment === "period" ? "text" : "numeric",
|
|
1041
|
+
autoComplete: "off",
|
|
1042
|
+
autoCorrect: "off",
|
|
1043
|
+
autoCapitalize: "off",
|
|
1044
|
+
spellCheck: false,
|
|
1045
|
+
translate: "no",
|
|
1046
|
+
"aria-label": labels[segment],
|
|
1047
|
+
"aria-invalid": invalid || void 0,
|
|
1048
|
+
...inputProps,
|
|
1049
|
+
disabled: isDisabled,
|
|
1050
|
+
readOnly: isReadOnly,
|
|
1051
|
+
className: cn(
|
|
1052
|
+
"inline-flex h-full items-center justify-center border-0 bg-transparent text-center text-sm tabular-nums transition-colors outline-none focus:bg-transparent disabled:cursor-not-allowed disabled:opacity-50",
|
|
1053
|
+
className
|
|
1054
|
+
),
|
|
1055
|
+
style: { width: segmentWidth, ...style },
|
|
1056
|
+
ref: composedRef,
|
|
1057
|
+
value: segment === "period" ? getPeriodLabel(displayValue, labels) : displayValue,
|
|
1058
|
+
onBlur,
|
|
1059
|
+
onChange,
|
|
1060
|
+
onClick,
|
|
1061
|
+
onFocus,
|
|
1062
|
+
onKeyDown
|
|
1063
|
+
}
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
function TimePickerTrigger(props) {
|
|
1067
|
+
const { className, children, disabled: disabledProp, ref, ...triggerProps } = props;
|
|
1068
|
+
const { triggerId, disabled, labels, triggerRef } = useTimePickerContext(TRIGGER_NAME);
|
|
1069
|
+
const isDisabled = disabledProp || disabled;
|
|
1070
|
+
const composedRef = useComposedRefs(ref, triggerRef);
|
|
1071
|
+
return /* @__PURE__ */ jsx(
|
|
1072
|
+
PopoverTrigger,
|
|
1073
|
+
{
|
|
1074
|
+
type: "button",
|
|
1075
|
+
id: triggerId,
|
|
1076
|
+
"data-slot": "time-picker-trigger",
|
|
1077
|
+
disabled: isDisabled,
|
|
1078
|
+
ref: composedRef,
|
|
1079
|
+
...triggerProps,
|
|
1080
|
+
className: cn(
|
|
1081
|
+
// ⚠ Khác thượng nguồn: thêm viền focus theo khuôn `Button` của gói — bản gốc không có kiểu
|
|
1082
|
+
// focus nào, và viền mặc định của trình duyệt bị lớp nền `outline-ring/50` pha còn gold 50%.
|
|
1083
|
+
"ml-auto flex items-center rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2 disabled:pointer-events-none [&>svg:not([class*='size-'])]:size-4",
|
|
1084
|
+
className
|
|
1085
|
+
),
|
|
1086
|
+
children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1087
|
+
/* @__PURE__ */ jsx(Clock, { "aria-hidden": "true" }),
|
|
1088
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: labels.openPicker })
|
|
1089
|
+
] })
|
|
1090
|
+
}
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
const TimePickerGroupContext = React.createContext(null);
|
|
1094
|
+
function useTimePickerGroupContext(consumerName) {
|
|
1095
|
+
const context = React.useContext(TimePickerGroupContext);
|
|
1096
|
+
if (!context) {
|
|
1097
|
+
throw new Error(`\`${consumerName}\` ph\u1EA3i n\u1EB1m b\xEAn trong \`${CONTENT_NAME}\``);
|
|
1098
|
+
}
|
|
1099
|
+
return context;
|
|
1100
|
+
}
|
|
1101
|
+
function TimePickerContent(props) {
|
|
1102
|
+
const {
|
|
1103
|
+
side = "bottom",
|
|
1104
|
+
align = "start",
|
|
1105
|
+
sideOffset = 6,
|
|
1106
|
+
className,
|
|
1107
|
+
onOpenAutoFocus: onOpenAutoFocusProp,
|
|
1108
|
+
onInteractOutside: onInteractOutsideProp,
|
|
1109
|
+
...contentProps
|
|
1110
|
+
} = props;
|
|
1111
|
+
const store = useStoreContext(CONTENT_NAME);
|
|
1112
|
+
const { openOnFocus, inputGroupRef } = useTimePickerContext(CONTENT_NAME);
|
|
1113
|
+
const columnsRef = React.useRef(/* @__PURE__ */ new Map());
|
|
1114
|
+
const onColumnRegister = React.useCallback((column) => {
|
|
1115
|
+
columnsRef.current.set(column.id, column);
|
|
1116
|
+
}, []);
|
|
1117
|
+
const onColumnUnregister = React.useCallback((id) => {
|
|
1118
|
+
columnsRef.current.delete(id);
|
|
1119
|
+
}, []);
|
|
1120
|
+
const getColumns = React.useCallback(() => {
|
|
1121
|
+
const columns = Array.from(columnsRef.current.entries()).map(([id, { ref, getSelectedItemRef, getItems }]) => ({
|
|
1122
|
+
id,
|
|
1123
|
+
ref,
|
|
1124
|
+
getSelectedItemRef,
|
|
1125
|
+
getItems
|
|
1126
|
+
})).filter((c) => c.ref.current !== null);
|
|
1127
|
+
return sortNodes(columns);
|
|
1128
|
+
}, []);
|
|
1129
|
+
const groupContextValue = React.useMemo(
|
|
1130
|
+
() => ({
|
|
1131
|
+
getColumns,
|
|
1132
|
+
onColumnRegister,
|
|
1133
|
+
onColumnUnregister
|
|
1134
|
+
}),
|
|
1135
|
+
[getColumns, onColumnRegister, onColumnUnregister]
|
|
1136
|
+
);
|
|
1137
|
+
const onOpenAutoFocus = React.useCallback(
|
|
1138
|
+
(event) => {
|
|
1139
|
+
onOpenAutoFocusProp?.(event);
|
|
1140
|
+
if (event.defaultPrevented) return;
|
|
1141
|
+
event.preventDefault();
|
|
1142
|
+
const { openedViaFocus } = store.getState();
|
|
1143
|
+
if (openedViaFocus) {
|
|
1144
|
+
store.setState("openedViaFocus", false);
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
const columns = getColumns();
|
|
1148
|
+
const firstColumn = columns[0];
|
|
1149
|
+
if (!firstColumn) return;
|
|
1150
|
+
const items = firstColumn.getItems();
|
|
1151
|
+
const selectedItem = items.find((item) => item.selected);
|
|
1152
|
+
const candidateRefs = selectedItem ? [selectedItem.ref, ...items.map((item) => item.ref)] : items.map((item) => item.ref);
|
|
1153
|
+
focusFirst(candidateRefs, false);
|
|
1154
|
+
},
|
|
1155
|
+
[onOpenAutoFocusProp, getColumns, store]
|
|
1156
|
+
);
|
|
1157
|
+
const onInteractOutside = React.useCallback(
|
|
1158
|
+
(event) => {
|
|
1159
|
+
onInteractOutsideProp?.(event);
|
|
1160
|
+
if (event.defaultPrevented) return;
|
|
1161
|
+
if (openOnFocus && inputGroupRef.current) {
|
|
1162
|
+
const target = event.target;
|
|
1163
|
+
if (!(target instanceof Node)) return;
|
|
1164
|
+
const isInsideInputGroup = inputGroupRef.current.contains(target);
|
|
1165
|
+
if (isInsideInputGroup) {
|
|
1166
|
+
event.preventDefault();
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
},
|
|
1170
|
+
[onInteractOutsideProp, openOnFocus, inputGroupRef]
|
|
1171
|
+
);
|
|
1172
|
+
return /* @__PURE__ */ jsx(TimePickerGroupContext.Provider, { value: groupContextValue, children: /* @__PURE__ */ jsx(
|
|
1173
|
+
PopoverContent,
|
|
1174
|
+
{
|
|
1175
|
+
"data-slot": "time-picker-content",
|
|
1176
|
+
side,
|
|
1177
|
+
align,
|
|
1178
|
+
sideOffset,
|
|
1179
|
+
...contentProps,
|
|
1180
|
+
className: cn("flex w-auto max-w-(--radix-popover-trigger-width) p-0", className),
|
|
1181
|
+
onOpenAutoFocus,
|
|
1182
|
+
onInteractOutside
|
|
1183
|
+
}
|
|
1184
|
+
) });
|
|
1185
|
+
}
|
|
1186
|
+
const TimePickerColumnContext = React.createContext(null);
|
|
1187
|
+
function useTimePickerColumnContext(consumerName) {
|
|
1188
|
+
const context = React.useContext(TimePickerColumnContext);
|
|
1189
|
+
if (!context) {
|
|
1190
|
+
throw new Error(`\`${consumerName}\` ph\u1EA3i n\u1EB1m b\xEAn trong m\u1ED9t c\u1ED9t (\`${COLUMN_NAME}\`)`);
|
|
1191
|
+
}
|
|
1192
|
+
return context;
|
|
1193
|
+
}
|
|
1194
|
+
function TimePickerColumn(props) {
|
|
1195
|
+
const { children, className, ref, ...columnProps } = props;
|
|
1196
|
+
const columnId = React.useId();
|
|
1197
|
+
const columnRef = React.useRef(null);
|
|
1198
|
+
const composedRef = useComposedRefs(ref, columnRef);
|
|
1199
|
+
const itemsRef = React.useRef(/* @__PURE__ */ new Map());
|
|
1200
|
+
const groupContext = useTimePickerGroupContext(COLUMN_NAME);
|
|
1201
|
+
const onItemRegister = React.useCallback(
|
|
1202
|
+
(value, ref2, selected) => {
|
|
1203
|
+
itemsRef.current.set(value, { ref: ref2, selected });
|
|
1204
|
+
},
|
|
1205
|
+
[]
|
|
1206
|
+
);
|
|
1207
|
+
const onItemUnregister = React.useCallback((value) => {
|
|
1208
|
+
itemsRef.current.delete(value);
|
|
1209
|
+
}, []);
|
|
1210
|
+
const getItems = React.useCallback(() => {
|
|
1211
|
+
const items = Array.from(itemsRef.current.entries()).map(([value, { ref: ref2, selected }]) => ({
|
|
1212
|
+
value,
|
|
1213
|
+
ref: ref2,
|
|
1214
|
+
selected
|
|
1215
|
+
})).filter((item) => item.ref.current);
|
|
1216
|
+
return sortNodes(items);
|
|
1217
|
+
}, []);
|
|
1218
|
+
const getSelectedItemRef = React.useCallback(() => {
|
|
1219
|
+
const items = getItems();
|
|
1220
|
+
return items.find((item) => item.selected)?.ref ?? null;
|
|
1221
|
+
}, [getItems]);
|
|
1222
|
+
useIsomorphicLayoutEffect(() => {
|
|
1223
|
+
groupContext.onColumnRegister({
|
|
1224
|
+
id: columnId,
|
|
1225
|
+
ref: columnRef,
|
|
1226
|
+
getSelectedItemRef,
|
|
1227
|
+
getItems
|
|
1228
|
+
});
|
|
1229
|
+
return () => groupContext.onColumnUnregister(columnId);
|
|
1230
|
+
}, [groupContext, columnId, getSelectedItemRef, getItems]);
|
|
1231
|
+
const columnContextValue = React.useMemo(
|
|
1232
|
+
() => ({
|
|
1233
|
+
getItems,
|
|
1234
|
+
onItemRegister,
|
|
1235
|
+
onItemUnregister
|
|
1236
|
+
}),
|
|
1237
|
+
[getItems, onItemRegister, onItemUnregister]
|
|
1238
|
+
);
|
|
1239
|
+
return /* @__PURE__ */ jsx(TimePickerColumnContext.Provider, { value: columnContextValue, children: /* @__PURE__ */ jsx(
|
|
1240
|
+
"div",
|
|
1241
|
+
{
|
|
1242
|
+
role: "listbox",
|
|
1243
|
+
ref: composedRef,
|
|
1244
|
+
"data-slot": "time-picker-column",
|
|
1245
|
+
...columnProps,
|
|
1246
|
+
className: cn("flex flex-col gap-1 p-1 not-last:border-r", className),
|
|
1247
|
+
children
|
|
1248
|
+
}
|
|
1249
|
+
) });
|
|
1250
|
+
}
|
|
1251
|
+
function TimePickerColumnItem(props) {
|
|
1252
|
+
const { value, selected = false, format = "numeric", className, children, ref, ...itemProps } = props;
|
|
1253
|
+
const itemRef = React.useRef(null);
|
|
1254
|
+
const composedRef = useComposedRefs(ref, itemRef);
|
|
1255
|
+
const columnContext = useTimePickerColumnContext(COLUMN_ITEM_NAME);
|
|
1256
|
+
const groupContext = useTimePickerGroupContext(COLUMN_ITEM_NAME);
|
|
1257
|
+
useIsomorphicLayoutEffect(() => {
|
|
1258
|
+
columnContext.onItemRegister(value, itemRef, selected);
|
|
1259
|
+
return () => columnContext.onItemUnregister(value);
|
|
1260
|
+
}, [columnContext, value, selected]);
|
|
1261
|
+
useIsomorphicLayoutEffect(() => {
|
|
1262
|
+
if (selected && itemRef.current) {
|
|
1263
|
+
itemRef.current.scrollIntoView({ block: "nearest" });
|
|
1264
|
+
}
|
|
1265
|
+
}, [selected]);
|
|
1266
|
+
const onClick = React.useCallback(
|
|
1267
|
+
(event) => {
|
|
1268
|
+
itemProps.onClick?.(event);
|
|
1269
|
+
if (event.defaultPrevented) return;
|
|
1270
|
+
itemRef.current?.focus();
|
|
1271
|
+
},
|
|
1272
|
+
[itemProps.onClick]
|
|
1273
|
+
);
|
|
1274
|
+
const onKeyDown = React.useCallback(
|
|
1275
|
+
(event) => {
|
|
1276
|
+
itemProps.onKeyDown?.(event);
|
|
1277
|
+
if (event.defaultPrevented) return;
|
|
1278
|
+
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
1279
|
+
event.preventDefault();
|
|
1280
|
+
const items = columnContext.getItems().sort((a, b) => {
|
|
1281
|
+
if (typeof a.value === "number" && typeof b.value === "number") {
|
|
1282
|
+
return a.value - b.value;
|
|
1283
|
+
}
|
|
1284
|
+
return 0;
|
|
1285
|
+
});
|
|
1286
|
+
const currentIndex = items.findIndex((item) => item.value === value);
|
|
1287
|
+
let nextIndex;
|
|
1288
|
+
if (event.key === "ArrowUp") {
|
|
1289
|
+
nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
1290
|
+
} else {
|
|
1291
|
+
nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
1292
|
+
}
|
|
1293
|
+
const nextItem = items[nextIndex];
|
|
1294
|
+
nextItem?.ref.current?.focus();
|
|
1295
|
+
nextItem?.ref.current?.click();
|
|
1296
|
+
} else if ((event.key === "Tab" || event.key === "ArrowLeft" || event.key === "ArrowRight") && groupContext) {
|
|
1297
|
+
event.preventDefault();
|
|
1298
|
+
queueMicrotask(() => {
|
|
1299
|
+
const columns = groupContext.getColumns();
|
|
1300
|
+
if (columns.length === 0) return;
|
|
1301
|
+
const currentColumnIndex = columns.findIndex((c) => c.ref.current?.contains(itemRef.current) ?? false);
|
|
1302
|
+
if (currentColumnIndex === -1) return;
|
|
1303
|
+
const goToPrevious = event.key === "ArrowLeft" || event.key === "Tab" && event.shiftKey;
|
|
1304
|
+
const nextColumnIndex = goToPrevious ? currentColumnIndex > 0 ? currentColumnIndex - 1 : columns.length - 1 : currentColumnIndex < columns.length - 1 ? currentColumnIndex + 1 : 0;
|
|
1305
|
+
const nextColumn = columns[nextColumnIndex];
|
|
1306
|
+
if (nextColumn?.ref.current) {
|
|
1307
|
+
const items = nextColumn.getItems();
|
|
1308
|
+
const selectedItem = items.find((item) => item.selected);
|
|
1309
|
+
const candidateRefs = selectedItem ? [selectedItem.ref, ...items.map((item) => item.ref)] : items.map((item) => item.ref);
|
|
1310
|
+
focusFirst(candidateRefs, false);
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
},
|
|
1315
|
+
[itemProps.onKeyDown, columnContext, groupContext, value]
|
|
1316
|
+
);
|
|
1317
|
+
const formattedValue = typeof value === "number" && format === "2-digit" ? value.toString().padStart(2, "0") : value.toString();
|
|
1318
|
+
return /* @__PURE__ */ jsx(
|
|
1319
|
+
"button",
|
|
1320
|
+
{
|
|
1321
|
+
type: "button",
|
|
1322
|
+
role: "option",
|
|
1323
|
+
"aria-selected": selected,
|
|
1324
|
+
...itemProps,
|
|
1325
|
+
ref: composedRef,
|
|
1326
|
+
"data-selected": selected ? "" : void 0,
|
|
1327
|
+
className: cn(
|
|
1328
|
+
/*
|
|
1329
|
+
* ⚠ Khác thượng nguồn: focus đi theo khuôn `Checkbox`/`Calendar` của gói (`focus-visible`,
|
|
1330
|
+
* có offset — mục đang chọn tô nền vàng, quầng vàng dính sát nền vàng thì không thấy);
|
|
1331
|
+
* mục đang chọn dùng cặp token của control BẬT `--control-on` / `--control-check` thay
|
|
1332
|
+
* `bg-primary text-primary-foreground` — cùng lý do và số đo với ô ngày đang chọn của
|
|
1333
|
+
* `Calendar` (chú thích đầu `calendar.tsx`). Hover chỉ áp cho mục CHƯA chọn
|
|
1334
|
+
* (`not-data-selected:`) thay vì để `data-selected:hover:` ghi đè lại: cùng kết quả trên
|
|
1335
|
+
* màn hình, nhưng cách cũ khiến `pnpm lint:a11y` ghép chữ hover `--accent-foreground`
|
|
1336
|
+
* với nền mục đang chọn (1,39:1 ở dark) — một cặp không bao giờ xuất hiện.
|
|
1337
|
+
*/
|
|
1338
|
+
"w-full rounded px-3 py-1.5 text-left text-sm not-data-selected:hover:bg-accent not-data-selected:hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
1339
|
+
"data-selected:bg-control-on data-selected:text-control-check",
|
|
1340
|
+
className
|
|
1341
|
+
),
|
|
1342
|
+
onClick,
|
|
1343
|
+
onKeyDown,
|
|
1344
|
+
children: children ?? formattedValue
|
|
1345
|
+
}
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
function TimePickerHour(props) {
|
|
1349
|
+
const { asChild, format = "numeric", className, ...hourProps } = props;
|
|
1350
|
+
const { is12Hour, hourStep, showSeconds, disabled, readOnly, labels } = useTimePickerContext(HOUR_NAME);
|
|
1351
|
+
const store = useStoreContext(HOUR_NAME);
|
|
1352
|
+
const value = useStore((state) => state.value);
|
|
1353
|
+
const timeValue = parseTimeString(value);
|
|
1354
|
+
const hours = Array.from(
|
|
1355
|
+
{
|
|
1356
|
+
length: is12Hour ? Math.ceil(12 / hourStep) : Math.ceil(24 / hourStep)
|
|
1357
|
+
},
|
|
1358
|
+
(_, i) => {
|
|
1359
|
+
if (is12Hour) {
|
|
1360
|
+
const hour = i * hourStep % 12;
|
|
1361
|
+
return hour === 0 ? 12 : hour;
|
|
1362
|
+
}
|
|
1363
|
+
return i * hourStep;
|
|
1364
|
+
}
|
|
1365
|
+
);
|
|
1366
|
+
const onHourSelect = React.useCallback(
|
|
1367
|
+
(displayHour2) => {
|
|
1368
|
+
if (disabled || readOnly) return;
|
|
1369
|
+
const now2 = /* @__PURE__ */ new Date();
|
|
1370
|
+
const currentTime = timeValue ?? {};
|
|
1371
|
+
let hour24 = displayHour2;
|
|
1372
|
+
if (is12Hour) {
|
|
1373
|
+
let currentPeriod;
|
|
1374
|
+
if (timeValue?.period !== void 0) {
|
|
1375
|
+
currentPeriod = timeValue.period;
|
|
1376
|
+
} else if (timeValue?.hour !== void 0) {
|
|
1377
|
+
currentPeriod = to12Hour(timeValue.hour).period;
|
|
1378
|
+
} else {
|
|
1379
|
+
currentPeriod = to12Hour(now2.getHours()).period;
|
|
1380
|
+
}
|
|
1381
|
+
hour24 = to24Hour(displayHour2, currentPeriod);
|
|
1382
|
+
}
|
|
1383
|
+
const newTime = { ...currentTime, hour: hour24 };
|
|
1384
|
+
if (timeValue && timeValue.period !== void 0) {
|
|
1385
|
+
newTime.period = timeValue.period;
|
|
1386
|
+
}
|
|
1387
|
+
if (newTime.minute === void 0) {
|
|
1388
|
+
newTime.minute = now2.getMinutes();
|
|
1389
|
+
}
|
|
1390
|
+
if (showSeconds && newTime.second === void 0) {
|
|
1391
|
+
newTime.second = now2.getSeconds();
|
|
1392
|
+
}
|
|
1393
|
+
const newValue = formatTimeValue(newTime, showSeconds);
|
|
1394
|
+
store.setState("value", newValue);
|
|
1395
|
+
},
|
|
1396
|
+
[timeValue, showSeconds, is12Hour, disabled, readOnly, store]
|
|
1397
|
+
);
|
|
1398
|
+
const now = /* @__PURE__ */ new Date();
|
|
1399
|
+
const referenceHour = timeValue?.hour ?? now.getHours();
|
|
1400
|
+
const displayHour = is12Hour ? to12Hour(referenceHour).hour : referenceHour;
|
|
1401
|
+
const HourPrimitive = asChild ? Slot : TimePickerColumn;
|
|
1402
|
+
return /* @__PURE__ */ jsx(
|
|
1403
|
+
HourPrimitive,
|
|
1404
|
+
{
|
|
1405
|
+
"data-slot": "time-picker-hour",
|
|
1406
|
+
"aria-label": labels.hour,
|
|
1407
|
+
...hourProps,
|
|
1408
|
+
className: cn("scrollbar-none flex max-h-[200px] flex-col gap-1 overflow-y-auto p-1", className),
|
|
1409
|
+
children: hours.map((hour) => /* @__PURE__ */ jsx(
|
|
1410
|
+
TimePickerColumnItem,
|
|
1411
|
+
{
|
|
1412
|
+
value: hour,
|
|
1413
|
+
selected: displayHour === hour,
|
|
1414
|
+
format,
|
|
1415
|
+
onClick: () => onHourSelect(hour)
|
|
1416
|
+
},
|
|
1417
|
+
hour
|
|
1418
|
+
))
|
|
1419
|
+
}
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
function TimePickerMinute(props) {
|
|
1423
|
+
const { asChild, format = "2-digit", className, ...minuteProps } = props;
|
|
1424
|
+
const { minuteStep, showSeconds, disabled, readOnly, labels } = useTimePickerContext(MINUTE_NAME);
|
|
1425
|
+
const store = useStoreContext(MINUTE_NAME);
|
|
1426
|
+
const value = useStore((state) => state.value);
|
|
1427
|
+
const timeValue = parseTimeString(value);
|
|
1428
|
+
const minutes = Array.from({ length: Math.ceil(60 / minuteStep) }, (_, i) => i * minuteStep);
|
|
1429
|
+
const onMinuteSelect = React.useCallback(
|
|
1430
|
+
(minute) => {
|
|
1431
|
+
if (disabled || readOnly) return;
|
|
1432
|
+
const now2 = /* @__PURE__ */ new Date();
|
|
1433
|
+
const currentTime = timeValue ?? {};
|
|
1434
|
+
const newTime = { ...currentTime, minute };
|
|
1435
|
+
if (newTime.hour === void 0) {
|
|
1436
|
+
newTime.hour = now2.getHours();
|
|
1437
|
+
}
|
|
1438
|
+
if (showSeconds && newTime.second === void 0) {
|
|
1439
|
+
newTime.second = now2.getSeconds();
|
|
1440
|
+
}
|
|
1441
|
+
const newValue = formatTimeValue(newTime, showSeconds);
|
|
1442
|
+
store.setState("value", newValue);
|
|
1443
|
+
},
|
|
1444
|
+
[timeValue, showSeconds, disabled, readOnly, store]
|
|
1445
|
+
);
|
|
1446
|
+
const MinutePrimitive = asChild ? Slot : TimePickerColumn;
|
|
1447
|
+
const now = /* @__PURE__ */ new Date();
|
|
1448
|
+
const referenceMinute = timeValue?.minute ?? now.getMinutes();
|
|
1449
|
+
return /* @__PURE__ */ jsx(
|
|
1450
|
+
MinutePrimitive,
|
|
1451
|
+
{
|
|
1452
|
+
"data-slot": "time-picker-minute",
|
|
1453
|
+
"aria-label": labels.minute,
|
|
1454
|
+
...minuteProps,
|
|
1455
|
+
className: cn("scrollbar-none flex max-h-[200px] flex-col gap-1 overflow-y-auto p-1", className),
|
|
1456
|
+
children: minutes.map((minute) => /* @__PURE__ */ jsx(
|
|
1457
|
+
TimePickerColumnItem,
|
|
1458
|
+
{
|
|
1459
|
+
value: minute,
|
|
1460
|
+
selected: referenceMinute === minute,
|
|
1461
|
+
format,
|
|
1462
|
+
onClick: () => onMinuteSelect(minute)
|
|
1463
|
+
},
|
|
1464
|
+
minute
|
|
1465
|
+
))
|
|
1466
|
+
}
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
function TimePickerSecond(props) {
|
|
1470
|
+
const { asChild, format = "2-digit", className, ...secondProps } = props;
|
|
1471
|
+
const { secondStep, disabled, readOnly, labels } = useTimePickerContext(SECOND_NAME);
|
|
1472
|
+
const store = useStoreContext(SECOND_NAME);
|
|
1473
|
+
const value = useStore((state) => state.value);
|
|
1474
|
+
const timeValue = parseTimeString(value);
|
|
1475
|
+
const seconds = Array.from({ length: Math.ceil(60 / secondStep) }, (_, i) => i * secondStep);
|
|
1476
|
+
const onSecondSelect = React.useCallback(
|
|
1477
|
+
(second) => {
|
|
1478
|
+
if (disabled || readOnly) return;
|
|
1479
|
+
const now2 = /* @__PURE__ */ new Date();
|
|
1480
|
+
const currentTime = timeValue ?? {};
|
|
1481
|
+
const newTime = { ...currentTime, second };
|
|
1482
|
+
if (newTime.hour === void 0) {
|
|
1483
|
+
newTime.hour = now2.getHours();
|
|
1484
|
+
}
|
|
1485
|
+
if (newTime.minute === void 0) {
|
|
1486
|
+
newTime.minute = now2.getMinutes();
|
|
1487
|
+
}
|
|
1488
|
+
const newValue = formatTimeValue(newTime, true);
|
|
1489
|
+
store.setState("value", newValue);
|
|
1490
|
+
},
|
|
1491
|
+
[timeValue, disabled, readOnly, store]
|
|
1492
|
+
);
|
|
1493
|
+
const SecondPrimitive = asChild ? Slot : TimePickerColumn;
|
|
1494
|
+
const now = /* @__PURE__ */ new Date();
|
|
1495
|
+
const referenceSecond = timeValue?.second ?? now.getSeconds();
|
|
1496
|
+
return /* @__PURE__ */ jsx(
|
|
1497
|
+
SecondPrimitive,
|
|
1498
|
+
{
|
|
1499
|
+
"data-slot": "time-picker-second",
|
|
1500
|
+
"aria-label": labels.second,
|
|
1501
|
+
...secondProps,
|
|
1502
|
+
className: cn("scrollbar-none flex max-h-[200px] flex-col gap-1 overflow-y-auto p-1", className),
|
|
1503
|
+
children: seconds.map((second) => /* @__PURE__ */ jsx(
|
|
1504
|
+
TimePickerColumnItem,
|
|
1505
|
+
{
|
|
1506
|
+
value: second,
|
|
1507
|
+
selected: referenceSecond === second,
|
|
1508
|
+
format,
|
|
1509
|
+
onClick: () => onSecondSelect(second)
|
|
1510
|
+
},
|
|
1511
|
+
second
|
|
1512
|
+
))
|
|
1513
|
+
}
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
function TimePickerPeriod(props) {
|
|
1517
|
+
const { asChild, className, ...periodProps } = props;
|
|
1518
|
+
const { is12Hour, showSeconds, disabled, readOnly, labels } = useTimePickerContext(PERIOD_NAME);
|
|
1519
|
+
const store = useStoreContext(PERIOD_NAME);
|
|
1520
|
+
const value = useStore((state) => state.value);
|
|
1521
|
+
const timeValue = parseTimeString(value);
|
|
1522
|
+
const onPeriodToggle = React.useCallback(
|
|
1523
|
+
(period) => {
|
|
1524
|
+
if (disabled || readOnly) return;
|
|
1525
|
+
const now2 = /* @__PURE__ */ new Date();
|
|
1526
|
+
const currentTime = timeValue ?? {};
|
|
1527
|
+
const currentHour = currentTime.hour !== void 0 ? currentTime.hour : now2.getHours();
|
|
1528
|
+
const currentDisplay = to12Hour(currentHour);
|
|
1529
|
+
const new24Hour = to24Hour(currentDisplay.hour, period);
|
|
1530
|
+
const newTime = { ...currentTime, hour: new24Hour };
|
|
1531
|
+
if (newTime.minute === void 0) {
|
|
1532
|
+
newTime.minute = now2.getMinutes();
|
|
1533
|
+
}
|
|
1534
|
+
if (showSeconds && newTime.second === void 0) {
|
|
1535
|
+
newTime.second = now2.getSeconds();
|
|
1536
|
+
}
|
|
1537
|
+
const newValue = formatTimeValue(newTime, showSeconds);
|
|
1538
|
+
store.setState("value", newValue);
|
|
1539
|
+
},
|
|
1540
|
+
[timeValue, showSeconds, disabled, readOnly, store]
|
|
1541
|
+
);
|
|
1542
|
+
if (!is12Hour) return null;
|
|
1543
|
+
const now = /* @__PURE__ */ new Date();
|
|
1544
|
+
const referenceHour = timeValue?.hour ?? now.getHours();
|
|
1545
|
+
const currentPeriod = to12Hour(referenceHour).period;
|
|
1546
|
+
const PeriodPrimitive = asChild ? Slot : TimePickerColumn;
|
|
1547
|
+
return /* @__PURE__ */ jsx(
|
|
1548
|
+
PeriodPrimitive,
|
|
1549
|
+
{
|
|
1550
|
+
"data-slot": "time-picker-period",
|
|
1551
|
+
"aria-label": labels.period,
|
|
1552
|
+
...periodProps,
|
|
1553
|
+
className: cn("flex flex-col gap-1 p-1", className),
|
|
1554
|
+
children: PERIODS.map((period) => /* @__PURE__ */ jsx(
|
|
1555
|
+
TimePickerColumnItem,
|
|
1556
|
+
{
|
|
1557
|
+
value: period,
|
|
1558
|
+
selected: currentPeriod === period,
|
|
1559
|
+
onClick: () => onPeriodToggle(period),
|
|
1560
|
+
children: getPeriodLabel(period, labels)
|
|
1561
|
+
},
|
|
1562
|
+
period
|
|
1563
|
+
))
|
|
1564
|
+
}
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
function TimePickerSeparator(props) {
|
|
1568
|
+
const { asChild, children, ...separatorProps } = props;
|
|
1569
|
+
const SeparatorPrimitive = asChild ? Slot : "span";
|
|
1570
|
+
return /* @__PURE__ */ jsx(SeparatorPrimitive, { "aria-hidden": "true", "data-slot": "time-picker-separator", ...separatorProps, children: children ?? ":" });
|
|
1571
|
+
}
|
|
1572
|
+
function TimePickerClear(props) {
|
|
1573
|
+
const { asChild, className, children, disabled: disabledProp, ...clearProps } = props;
|
|
1574
|
+
const { disabled, readOnly, labels } = useTimePickerContext(CLEAR_NAME);
|
|
1575
|
+
const store = useStoreContext(CLEAR_NAME);
|
|
1576
|
+
const isDisabled = disabledProp || disabled;
|
|
1577
|
+
const onClick = React.useCallback(
|
|
1578
|
+
(event) => {
|
|
1579
|
+
clearProps.onClick?.(event);
|
|
1580
|
+
if (event.defaultPrevented) return;
|
|
1581
|
+
event.preventDefault();
|
|
1582
|
+
if (disabled || readOnly) return;
|
|
1583
|
+
store.setState("value", "");
|
|
1584
|
+
},
|
|
1585
|
+
[clearProps.onClick, disabled, readOnly, store]
|
|
1586
|
+
);
|
|
1587
|
+
const ClearPrimitive = asChild ? Slot : "button";
|
|
1588
|
+
return /* @__PURE__ */ jsx(
|
|
1589
|
+
ClearPrimitive,
|
|
1590
|
+
{
|
|
1591
|
+
type: "button",
|
|
1592
|
+
"data-slot": "time-picker-clear",
|
|
1593
|
+
disabled: isDisabled,
|
|
1594
|
+
...clearProps,
|
|
1595
|
+
className: cn(
|
|
1596
|
+
// Viền focus theo khuôn `Button` — xem `TimePickerTrigger`.
|
|
1597
|
+
"inline-flex items-center justify-center rounded-sm text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
1598
|
+
className
|
|
1599
|
+
),
|
|
1600
|
+
onClick,
|
|
1601
|
+
children: children ?? labels.clear
|
|
1602
|
+
}
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
export {
|
|
1606
|
+
TimePicker,
|
|
1607
|
+
TimePickerClear,
|
|
1608
|
+
TimePickerContent,
|
|
1609
|
+
TimePickerHour,
|
|
1610
|
+
TimePickerInput,
|
|
1611
|
+
TimePickerInputGroup,
|
|
1612
|
+
TimePickerLabel,
|
|
1613
|
+
TimePickerMinute,
|
|
1614
|
+
TimePickerPeriod,
|
|
1615
|
+
TimePickerSecond,
|
|
1616
|
+
TimePickerSeparator,
|
|
1617
|
+
TimePickerTrigger,
|
|
1618
|
+
useStore as useTimePicker
|
|
1619
|
+
};
|
|
1620
|
+
//# sourceMappingURL=time-picker.js.map
|