@chromvoid/headless-ui 0.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 +21 -0
- package/README.md +99 -0
- package/dist/a11y-contracts/index.d.ts +23 -0
- package/dist/a11y-contracts/index.js +1 -0
- package/dist/accordion/index.d.ts +78 -0
- package/dist/accordion/index.js +264 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.js +1 -0
- package/dist/alert/index.d.ts +33 -0
- package/dist/alert/index.js +54 -0
- package/dist/alert-dialog/index.d.ts +69 -0
- package/dist/alert-dialog/index.js +94 -0
- package/dist/badge/index.d.ts +48 -0
- package/dist/badge/index.js +89 -0
- package/dist/breadcrumb/index.d.ts +55 -0
- package/dist/breadcrumb/index.js +77 -0
- package/dist/button/index.d.ts +46 -0
- package/dist/button/index.js +86 -0
- package/dist/callout/index.d.ts +41 -0
- package/dist/callout/index.js +63 -0
- package/dist/card/index.d.ts +54 -0
- package/dist/card/index.js +103 -0
- package/dist/carousel/index.d.ts +98 -0
- package/dist/carousel/index.js +243 -0
- package/dist/checkbox/index.d.ts +50 -0
- package/dist/checkbox/index.js +87 -0
- package/dist/combobox/index.d.ts +114 -0
- package/dist/combobox/index.js +431 -0
- package/dist/command-palette/index.d.ts +73 -0
- package/dist/command-palette/index.js +147 -0
- package/dist/context-menu/index.d.ts +111 -0
- package/dist/context-menu/index.js +372 -0
- package/dist/copy-button/index.d.ts +62 -0
- package/dist/copy-button/index.js +183 -0
- package/dist/core/index.d.ts +20 -0
- package/dist/core/index.js +2 -0
- package/dist/core/selection.d.ts +5 -0
- package/dist/core/selection.js +39 -0
- package/dist/core/value-range.d.ts +49 -0
- package/dist/core/value-range.js +134 -0
- package/dist/date-picker/index.d.ts +210 -0
- package/dist/date-picker/index.js +895 -0
- package/dist/dialog/index.d.ts +95 -0
- package/dist/dialog/index.js +153 -0
- package/dist/disclosure/index.d.ts +52 -0
- package/dist/disclosure/index.js +159 -0
- package/dist/drawer/index.d.ts +30 -0
- package/dist/drawer/index.js +39 -0
- package/dist/feed/index.d.ts +77 -0
- package/dist/feed/index.js +260 -0
- package/dist/grid/index.d.ts +103 -0
- package/dist/grid/index.js +415 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +51 -0
- package/dist/input/index.d.ts +86 -0
- package/dist/input/index.js +156 -0
- package/dist/interactions/composite-navigation.d.ts +69 -0
- package/dist/interactions/composite-navigation.js +169 -0
- package/dist/interactions/index.d.ts +15 -0
- package/dist/interactions/index.js +4 -0
- package/dist/interactions/keyboard-intents.d.ts +16 -0
- package/dist/interactions/keyboard-intents.js +33 -0
- package/dist/interactions/overlay-focus.d.ts +40 -0
- package/dist/interactions/overlay-focus.js +93 -0
- package/dist/interactions/typeahead.d.ts +20 -0
- package/dist/interactions/typeahead.js +41 -0
- package/dist/landmarks/index.d.ts +39 -0
- package/dist/landmarks/index.js +58 -0
- package/dist/link/index.d.ts +34 -0
- package/dist/link/index.js +39 -0
- package/dist/listbox/index.d.ts +92 -0
- package/dist/listbox/index.js +337 -0
- package/dist/menu/index.d.ts +132 -0
- package/dist/menu/index.js +541 -0
- package/dist/menu-button/index.d.ts +71 -0
- package/dist/menu-button/index.js +121 -0
- package/dist/meter/index.d.ts +45 -0
- package/dist/meter/index.js +106 -0
- package/dist/number/index.d.ts +113 -0
- package/dist/number/index.js +252 -0
- package/dist/popover/index.d.ts +70 -0
- package/dist/popover/index.js +126 -0
- package/dist/progress/index.d.ts +49 -0
- package/dist/progress/index.js +79 -0
- package/dist/radio-group/index.d.ts +61 -0
- package/dist/radio-group/index.js +150 -0
- package/dist/select/index.d.ts +92 -0
- package/dist/select/index.js +239 -0
- package/dist/sidebar/index.d.ts +74 -0
- package/dist/sidebar/index.js +186 -0
- package/dist/slider/index.d.ts +61 -0
- package/dist/slider/index.js +150 -0
- package/dist/slider-multi-thumb/index.d.ts +70 -0
- package/dist/slider-multi-thumb/index.js +222 -0
- package/dist/spinbutton/index.d.ts +75 -0
- package/dist/spinbutton/index.js +214 -0
- package/dist/spinner/index.d.ts +1 -0
- package/dist/spinner/index.js +1 -0
- package/dist/spinner/spinner.d.ts +23 -0
- package/dist/spinner/spinner.js +25 -0
- package/dist/switch/index.d.ts +40 -0
- package/dist/switch/index.js +61 -0
- package/dist/table/index.d.ts +117 -0
- package/dist/table/index.js +377 -0
- package/dist/tabs/index.d.ts +63 -0
- package/dist/tabs/index.js +174 -0
- package/dist/textarea/index.d.ts +68 -0
- package/dist/textarea/index.js +137 -0
- package/dist/toast/index.d.ts +67 -0
- package/dist/toast/index.js +145 -0
- package/dist/toolbar/index.d.ts +59 -0
- package/dist/toolbar/index.js +139 -0
- package/dist/tooltip/index.d.ts +52 -0
- package/dist/tooltip/index.js +169 -0
- package/dist/treegrid/index.d.ts +101 -0
- package/dist/treegrid/index.js +463 -0
- package/dist/treeview/index.d.ts +68 -0
- package/dist/treeview/index.js +370 -0
- package/dist/window-splitter/index.d.ts +65 -0
- package/dist/window-splitter/index.js +204 -0
- package/package.json +92 -0
- package/specs/ADR-001-headless-architecture.md +461 -0
- package/specs/ADR-002-repo-release-model.md +108 -0
- package/specs/ADR-003-public-api-versioning.md +136 -0
- package/specs/ADR-004-focus-selection-policy.md +117 -0
- package/specs/IMPLEMENTATION-ROADMAP.md +237 -0
- package/specs/ISSUE-BACKLOG.md +681 -0
- package/specs/RELEASE-CANDIDATE.md +30 -0
- package/specs/components/accordion.md +130 -0
- package/specs/components/alert-dialog.md +72 -0
- package/specs/components/alert.md +65 -0
- package/specs/components/badge.md +220 -0
- package/specs/components/breadcrumb.md +74 -0
- package/specs/components/button.md +115 -0
- package/specs/components/callout.md +195 -0
- package/specs/components/card.md +280 -0
- package/specs/components/carousel.md +140 -0
- package/specs/components/checkbox.md +172 -0
- package/specs/components/combobox.md +423 -0
- package/specs/components/command-palette.md +92 -0
- package/specs/components/context-menu.md +556 -0
- package/specs/components/copy-button.md +293 -0
- package/specs/components/date-picker.md +400 -0
- package/specs/components/dialog.md +298 -0
- package/specs/components/disclosure.md +257 -0
- package/specs/components/drawer.md +353 -0
- package/specs/components/feed.md +265 -0
- package/specs/components/grid.md +186 -0
- package/specs/components/input.md +254 -0
- package/specs/components/landmarks.md +136 -0
- package/specs/components/link.md +134 -0
- package/specs/components/listbox.md +351 -0
- package/specs/components/menu-button.md +76 -0
- package/specs/components/menu.md +623 -0
- package/specs/components/meter.md +149 -0
- package/specs/components/number.md +393 -0
- package/specs/components/popover.md +252 -0
- package/specs/components/progress.md +188 -0
- package/specs/components/radio-group.md +151 -0
- package/specs/components/select.md +144 -0
- package/specs/components/sidebar.md +321 -0
- package/specs/components/slider-multi-thumb.md +78 -0
- package/specs/components/slider.md +84 -0
- package/specs/components/spinbutton.md +140 -0
- package/specs/components/spinner.md +132 -0
- package/specs/components/switch.md +175 -0
- package/specs/components/table.md +403 -0
- package/specs/components/tabs.md +265 -0
- package/specs/components/textarea.md +185 -0
- package/specs/components/toast.md +198 -0
- package/specs/components/toolbar.md +278 -0
- package/specs/components/tooltip.md +252 -0
- package/specs/components/treegrid.md +281 -0
- package/specs/components/treeview.md +91 -0
- package/specs/components/window-splitter.md +297 -0
- package/specs/ops/git-shard-sync.md +107 -0
- package/specs/ops/release-checklist.md +76 -0
- package/specs/release/GAP-TO-GREEN-ISSUES.md +88 -0
- package/specs/release/api-freeze-candidate.md +54 -0
- package/specs/release/changelog-automation.md +76 -0
- package/specs/release/changelog.generated.md +53 -0
- package/specs/release/changelog.patch.generated.md +46 -0
- package/specs/release/consumer-integration.md +53 -0
- package/specs/release/migration-notes-pre-v1.md +40 -0
- package/specs/release/mvp-changelog.md +57 -0
- package/specs/release/release-notes-template.md +61 -0
- package/specs/release/release-rehearsal.md +113 -0
- package/specs/release/semver-deprecation-dry-run.md +89 -0
- package/specs/release/shard-release-drill-report.md +50 -0
- package/specs/release/shard-release-follow-ups.md +31 -0
- package/specs/signals.md +208 -0
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
import { action, atom, computed } from '@reatom/core';
|
|
2
|
+
const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
3
|
+
const DATETIME_RE = /^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2})$/;
|
|
4
|
+
const twoDigits = (value) => String(value).padStart(2, '0');
|
|
5
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
6
|
+
const parseDateOnly = (value) => {
|
|
7
|
+
const match = value.match(DATE_RE);
|
|
8
|
+
if (!match)
|
|
9
|
+
return null;
|
|
10
|
+
const year = Number(match[1]);
|
|
11
|
+
const month = Number(match[2]);
|
|
12
|
+
const day = Number(match[3]);
|
|
13
|
+
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day))
|
|
14
|
+
return null;
|
|
15
|
+
if (month < 1 || month > 12 || day < 1 || day > 31)
|
|
16
|
+
return null;
|
|
17
|
+
const date = new Date(Date.UTC(year, month - 1, day, 12, 0, 0, 0));
|
|
18
|
+
if (date.getUTCFullYear() !== year)
|
|
19
|
+
return null;
|
|
20
|
+
if (date.getUTCMonth() + 1 !== month)
|
|
21
|
+
return null;
|
|
22
|
+
if (date.getUTCDate() !== day)
|
|
23
|
+
return null;
|
|
24
|
+
return { year, month, day };
|
|
25
|
+
};
|
|
26
|
+
const parseTimeOnly = (value) => {
|
|
27
|
+
const match = value.match(/^(\d{2}):(\d{2})$/);
|
|
28
|
+
if (!match)
|
|
29
|
+
return null;
|
|
30
|
+
const hours = Number(match[1]);
|
|
31
|
+
const minutes = Number(match[2]);
|
|
32
|
+
if (!Number.isInteger(hours) || !Number.isInteger(minutes))
|
|
33
|
+
return null;
|
|
34
|
+
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59)
|
|
35
|
+
return null;
|
|
36
|
+
return `${twoDigits(hours)}:${twoDigits(minutes)}`;
|
|
37
|
+
};
|
|
38
|
+
const parseDateTimeDefault = (value) => {
|
|
39
|
+
const trimmed = value.trim();
|
|
40
|
+
if (trimmed.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
const dtMatch = trimmed.match(DATETIME_RE);
|
|
43
|
+
if (dtMatch) {
|
|
44
|
+
const date = `${dtMatch[1]}-${dtMatch[2]}-${dtMatch[3]}`;
|
|
45
|
+
const time = `${dtMatch[4]}:${dtMatch[5]}`;
|
|
46
|
+
if (!parseDateOnly(date))
|
|
47
|
+
return null;
|
|
48
|
+
if (!parseTimeOnly(time))
|
|
49
|
+
return null;
|
|
50
|
+
return { date, time, full: `${date}T${time}` };
|
|
51
|
+
}
|
|
52
|
+
const dateParts = parseDateOnly(trimmed);
|
|
53
|
+
if (!dateParts)
|
|
54
|
+
return null;
|
|
55
|
+
const date = `${dateParts.year}-${twoDigits(dateParts.month)}-${twoDigits(dateParts.day)}`;
|
|
56
|
+
const time = '00:00';
|
|
57
|
+
return { date, time, full: `${date}T${time}` };
|
|
58
|
+
};
|
|
59
|
+
const formatDateTimeDefault = (value) => `${value.date}T${value.time}`;
|
|
60
|
+
const splitDateTime = (value, parse) => {
|
|
61
|
+
if (!value)
|
|
62
|
+
return { date: null, time: null };
|
|
63
|
+
const parsed = parse(value);
|
|
64
|
+
if (!parsed)
|
|
65
|
+
return { date: null, time: null };
|
|
66
|
+
return { date: parsed.date, time: parsed.time };
|
|
67
|
+
};
|
|
68
|
+
const normalizeYearMonth = (year, month) => {
|
|
69
|
+
let nextYear = year;
|
|
70
|
+
let nextMonth = month;
|
|
71
|
+
while (nextMonth < 1) {
|
|
72
|
+
nextMonth += 12;
|
|
73
|
+
nextYear -= 1;
|
|
74
|
+
}
|
|
75
|
+
while (nextMonth > 12) {
|
|
76
|
+
nextMonth -= 12;
|
|
77
|
+
nextYear += 1;
|
|
78
|
+
}
|
|
79
|
+
return { year: nextYear, month: nextMonth };
|
|
80
|
+
};
|
|
81
|
+
const toDateStringUtc = (date) => `${date.getUTCFullYear()}-${twoDigits(date.getUTCMonth() + 1)}-${twoDigits(date.getUTCDate())}`;
|
|
82
|
+
const getTodayDate = (timeZone) => {
|
|
83
|
+
const now = new Date();
|
|
84
|
+
if (timeZone === 'utc') {
|
|
85
|
+
return `${now.getUTCFullYear()}-${twoDigits(now.getUTCMonth() + 1)}-${twoDigits(now.getUTCDate())}`;
|
|
86
|
+
}
|
|
87
|
+
return `${now.getFullYear()}-${twoDigits(now.getMonth() + 1)}-${twoDigits(now.getDate())}`;
|
|
88
|
+
};
|
|
89
|
+
const getNowTime = (timeZone, minuteStep) => {
|
|
90
|
+
const now = new Date();
|
|
91
|
+
const rawHours = timeZone === 'utc' ? now.getUTCHours() : now.getHours();
|
|
92
|
+
const rawMinutes = timeZone === 'utc' ? now.getUTCMinutes() : now.getMinutes();
|
|
93
|
+
const step = clamp(Math.floor(minuteStep), 1, 60);
|
|
94
|
+
let snappedMinutes = Math.round(rawMinutes / step) * step;
|
|
95
|
+
let hours = rawHours;
|
|
96
|
+
if (snappedMinutes >= 60) {
|
|
97
|
+
snappedMinutes = 0;
|
|
98
|
+
hours = (hours + 1) % 24;
|
|
99
|
+
}
|
|
100
|
+
return `${twoDigits(hours)}:${twoDigits(snappedMinutes)}`;
|
|
101
|
+
};
|
|
102
|
+
const addDaysUtc = (date, delta) => {
|
|
103
|
+
const parts = parseDateOnly(date);
|
|
104
|
+
if (!parts)
|
|
105
|
+
return null;
|
|
106
|
+
const cursor = new Date(Date.UTC(parts.year, parts.month - 1, parts.day, 12, 0, 0, 0));
|
|
107
|
+
cursor.setUTCDate(cursor.getUTCDate() + delta);
|
|
108
|
+
return toDateStringUtc(cursor);
|
|
109
|
+
};
|
|
110
|
+
const isDateWithinBounds = (date, min, max) => {
|
|
111
|
+
const minDate = min?.slice(0, 10) ?? null;
|
|
112
|
+
const maxDate = max?.slice(0, 10) ?? null;
|
|
113
|
+
if (minDate && date < minDate)
|
|
114
|
+
return false;
|
|
115
|
+
if (maxDate && date > maxDate)
|
|
116
|
+
return false;
|
|
117
|
+
return true;
|
|
118
|
+
};
|
|
119
|
+
const isDateTimeWithinBounds = (value, min, max) => {
|
|
120
|
+
if (min && value < min)
|
|
121
|
+
return false;
|
|
122
|
+
if (max && value > max)
|
|
123
|
+
return false;
|
|
124
|
+
return true;
|
|
125
|
+
};
|
|
126
|
+
const normalizeTime = (value, minuteStep) => {
|
|
127
|
+
const match = value.match(/^(\d{1,2}):(\d{1,2})$/);
|
|
128
|
+
if (!match)
|
|
129
|
+
return null;
|
|
130
|
+
let hours = Number(match[1]);
|
|
131
|
+
let minutes = Number(match[2]);
|
|
132
|
+
if (!Number.isInteger(hours) || !Number.isInteger(minutes))
|
|
133
|
+
return null;
|
|
134
|
+
if (hours < 0 || hours > 23)
|
|
135
|
+
return null;
|
|
136
|
+
if (minutes < 0 || minutes > 59)
|
|
137
|
+
return null;
|
|
138
|
+
const step = clamp(Math.floor(minuteStep), 1, 60);
|
|
139
|
+
let snappedMinutes = Math.round(minutes / step) * step;
|
|
140
|
+
if (snappedMinutes >= 60) {
|
|
141
|
+
snappedMinutes = 0;
|
|
142
|
+
hours = (hours + 1) % 24;
|
|
143
|
+
}
|
|
144
|
+
return `${twoDigits(hours)}:${twoDigits(snappedMinutes)}`;
|
|
145
|
+
};
|
|
146
|
+
const buildVisibleDays = (year, month, today, min, max) => {
|
|
147
|
+
const firstOfMonth = new Date(Date.UTC(year, month - 1, 1, 12, 0, 0, 0));
|
|
148
|
+
const firstWeekday = firstOfMonth.getUTCDay();
|
|
149
|
+
const start = new Date(firstOfMonth);
|
|
150
|
+
start.setUTCDate(start.getUTCDate() - firstWeekday);
|
|
151
|
+
const days = [];
|
|
152
|
+
for (let index = 0; index < 42; index += 1) {
|
|
153
|
+
const cursor = new Date(start);
|
|
154
|
+
cursor.setUTCDate(start.getUTCDate() + index);
|
|
155
|
+
const date = toDateStringUtc(cursor);
|
|
156
|
+
const cursorMonth = cursor.getUTCMonth() + 1;
|
|
157
|
+
const monthKind = cursorMonth < month ? 'prev' : cursorMonth > month ? 'next' : 'current';
|
|
158
|
+
const inRange = isDateWithinBounds(date, min, max);
|
|
159
|
+
days.push({
|
|
160
|
+
date,
|
|
161
|
+
month: monthKind,
|
|
162
|
+
inRange,
|
|
163
|
+
isToday: date === today,
|
|
164
|
+
disabled: !inRange,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return days;
|
|
168
|
+
};
|
|
169
|
+
export function createDatePicker(options = {}) {
|
|
170
|
+
const idBase = options.idBase ?? 'date-picker';
|
|
171
|
+
const parseDateTime = options.parseDateTime ?? ((value) => parseDateTimeDefault(value));
|
|
172
|
+
const formatDateTime = options.formatDateTime ?? ((value) => formatDateTimeDefault(value));
|
|
173
|
+
const localeAtom = atom(options.locale ?? 'en-US', `${idBase}.locale`);
|
|
174
|
+
const timeZoneAtom = atom(options.timeZone ?? 'local', `${idBase}.timeZone`);
|
|
175
|
+
const minuteStepAtom = atom(clamp(Math.floor(options.minuteStep ?? 1), 1, 60), `${idBase}.minuteStep`);
|
|
176
|
+
const initial = splitDateTime(options.value ?? null, (input) => parseDateTime(input, localeAtom()));
|
|
177
|
+
const initialDate = initial.date;
|
|
178
|
+
const initialTime = initial.time;
|
|
179
|
+
const today = getTodayDate(timeZoneAtom());
|
|
180
|
+
const todayParts = parseDateOnly(today) ?? { year: 1970, month: 1, day: 1 };
|
|
181
|
+
const initialDisplayDate = initialDate ?? today;
|
|
182
|
+
const initialDisplayParts = parseDateOnly(initialDisplayDate) ?? todayParts;
|
|
183
|
+
const committedDateAtom = atom(initialDate, `${idBase}.committedDate`);
|
|
184
|
+
const committedTimeAtom = atom(initialTime, `${idBase}.committedTime`);
|
|
185
|
+
const draftDateAtom = atom(initialDate, `${idBase}.draftDate`);
|
|
186
|
+
const draftTimeAtom = atom(initialTime, `${idBase}.draftTime`);
|
|
187
|
+
const inputValueAtom = atom(initialDate && initialTime
|
|
188
|
+
? formatDateTime({ date: initialDate, time: initialTime, full: `${initialDate}T${initialTime}` }, localeAtom())
|
|
189
|
+
: '', `${idBase}.inputValue`);
|
|
190
|
+
const isOpenAtom = atom(false, `${idBase}.isOpen`);
|
|
191
|
+
const focusedDateAtom = atom(initialDate ?? today, `${idBase}.focusedDate`);
|
|
192
|
+
const displayedYearAtom = atom(initialDisplayParts.year, `${idBase}.displayedYear`);
|
|
193
|
+
const displayedMonthAtom = atom(initialDisplayParts.month, `${idBase}.displayedMonth`);
|
|
194
|
+
const isInputFocusedAtom = atom(false, `${idBase}.isInputFocused`);
|
|
195
|
+
const isCalendarFocusedAtom = atom(false, `${idBase}.isCalendarFocused`);
|
|
196
|
+
const disabledAtom = atom(options.disabled ?? false, `${idBase}.disabled`);
|
|
197
|
+
const readonlyAtom = atom(options.readonly ?? false, `${idBase}.readonly`);
|
|
198
|
+
const requiredAtom = atom(options.required ?? false, `${idBase}.required`);
|
|
199
|
+
const placeholderAtom = atom(options.placeholder ?? 'Select date and time', `${idBase}.placeholder`);
|
|
200
|
+
const minAtom = atom(options.min ?? null, `${idBase}.min`);
|
|
201
|
+
const maxAtom = atom(options.max ?? null, `${idBase}.max`);
|
|
202
|
+
const hourCycleAtom = atom(options.hourCycle ?? 24, `${idBase}.hourCycle`);
|
|
203
|
+
const hasCommittedSelectionAtom = computed(() => committedDateAtom() != null && committedTimeAtom() != null, `${idBase}.hasCommittedSelection`);
|
|
204
|
+
const hasDraftSelectionAtom = computed(() => draftDateAtom() != null && draftTimeAtom() != null, `${idBase}.hasDraftSelection`);
|
|
205
|
+
const committedValueAtom = computed(() => {
|
|
206
|
+
const date = committedDateAtom();
|
|
207
|
+
const time = committedTimeAtom();
|
|
208
|
+
return date && time ? `${date}T${time}` : null;
|
|
209
|
+
}, `${idBase}.committedValue`);
|
|
210
|
+
const draftValueAtom = computed(() => {
|
|
211
|
+
const date = draftDateAtom();
|
|
212
|
+
const time = draftTimeAtom();
|
|
213
|
+
return date && time ? `${date}T${time}` : null;
|
|
214
|
+
}, `${idBase}.draftValue`);
|
|
215
|
+
const parsedValueAtom = computed(() => parseDateTime(inputValueAtom(), localeAtom()), `${idBase}.parsedValue`);
|
|
216
|
+
const canCommitInputAtom = computed(() => {
|
|
217
|
+
const parsed = parsedValueAtom();
|
|
218
|
+
if (!parsed)
|
|
219
|
+
return false;
|
|
220
|
+
return isDateTimeWithinBounds(parsed.full, minAtom(), maxAtom());
|
|
221
|
+
}, `${idBase}.canCommitInput`);
|
|
222
|
+
const inputInvalidAtom = computed(() => {
|
|
223
|
+
const value = inputValueAtom().trim();
|
|
224
|
+
return value.length > 0 && !canCommitInputAtom();
|
|
225
|
+
}, `${idBase}.inputInvalid`);
|
|
226
|
+
const todayAtom = computed(() => getTodayDate(timeZoneAtom()), `${idBase}.today`);
|
|
227
|
+
const visibleDaysAtom = computed(() => buildVisibleDays(displayedYearAtom(), displayedMonthAtom(), todayAtom(), minAtom(), maxAtom()), `${idBase}.visibleDays`);
|
|
228
|
+
const selectedCellIdAtom = computed(() => {
|
|
229
|
+
const selected = isOpenAtom() ? draftDateAtom() : committedDateAtom();
|
|
230
|
+
return selected ? `${idBase}-day-${selected}` : null;
|
|
231
|
+
}, `${idBase}.selectedCellId`);
|
|
232
|
+
const isDualCommitAtom = computed(() => true, `${idBase}.isDualCommit`);
|
|
233
|
+
const syncInputFromCommitted = () => {
|
|
234
|
+
const date = committedDateAtom();
|
|
235
|
+
const time = committedTimeAtom();
|
|
236
|
+
if (date && time) {
|
|
237
|
+
inputValueAtom.set(formatDateTime({ date, time, full: `${date}T${time}` }, localeAtom()));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
inputValueAtom.set('');
|
|
241
|
+
};
|
|
242
|
+
const setCommitted = (parsed) => {
|
|
243
|
+
const previous = committedValueAtom();
|
|
244
|
+
if (parsed) {
|
|
245
|
+
committedDateAtom.set(parsed.date);
|
|
246
|
+
committedTimeAtom.set(parsed.time);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
committedDateAtom.set(null);
|
|
250
|
+
committedTimeAtom.set(null);
|
|
251
|
+
}
|
|
252
|
+
const next = committedValueAtom();
|
|
253
|
+
if (previous !== next) {
|
|
254
|
+
options.onCommit?.(next);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
const ensureDisplayedFromDate = (date) => {
|
|
258
|
+
const parsed = date ? parseDateOnly(date) : null;
|
|
259
|
+
if (!parsed)
|
|
260
|
+
return;
|
|
261
|
+
displayedYearAtom.set(parsed.year);
|
|
262
|
+
displayedMonthAtom.set(parsed.month);
|
|
263
|
+
};
|
|
264
|
+
const ensureFocusedDate = () => {
|
|
265
|
+
const preferred = draftDateAtom() ?? committedDateAtom() ?? todayAtom();
|
|
266
|
+
if (preferred && isDateWithinBounds(preferred, minAtom(), maxAtom())) {
|
|
267
|
+
focusedDateAtom.set(preferred);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const firstEnabled = visibleDaysAtom().find((day) => !day.disabled);
|
|
271
|
+
focusedDateAtom.set(firstEnabled?.date ?? null);
|
|
272
|
+
};
|
|
273
|
+
const commitDraftInternal = () => {
|
|
274
|
+
if (disabledAtom() || readonlyAtom())
|
|
275
|
+
return false;
|
|
276
|
+
const date = draftDateAtom();
|
|
277
|
+
const time = draftTimeAtom();
|
|
278
|
+
if (!date || !time)
|
|
279
|
+
return false;
|
|
280
|
+
const full = `${date}T${time}`;
|
|
281
|
+
if (!isDateTimeWithinBounds(full, minAtom(), maxAtom())) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
const parsed = { date, time, full };
|
|
285
|
+
setCommitted(parsed);
|
|
286
|
+
inputValueAtom.set(formatDateTime(parsed, localeAtom()));
|
|
287
|
+
isOpenAtom.set(false);
|
|
288
|
+
isCalendarFocusedAtom.set(false);
|
|
289
|
+
focusedDateAtom.set(date);
|
|
290
|
+
return true;
|
|
291
|
+
};
|
|
292
|
+
const open = action(() => {
|
|
293
|
+
if (disabledAtom())
|
|
294
|
+
return;
|
|
295
|
+
isOpenAtom.set(true);
|
|
296
|
+
isCalendarFocusedAtom.set(true);
|
|
297
|
+
const committedDate = committedDateAtom();
|
|
298
|
+
const committedTime = committedTimeAtom();
|
|
299
|
+
if (committedDate && committedTime) {
|
|
300
|
+
draftDateAtom.set(committedDate);
|
|
301
|
+
draftTimeAtom.set(committedTime);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const parsedInput = parsedValueAtom();
|
|
305
|
+
if (parsedInput && isDateTimeWithinBounds(parsedInput.full, minAtom(), maxAtom())) {
|
|
306
|
+
draftDateAtom.set(parsedInput.date);
|
|
307
|
+
draftTimeAtom.set(parsedInput.time);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
draftDateAtom.set(todayAtom());
|
|
311
|
+
draftTimeAtom.set(getNowTime(timeZoneAtom(), minuteStepAtom()));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
ensureDisplayedFromDate(draftDateAtom());
|
|
315
|
+
ensureFocusedDate();
|
|
316
|
+
}, `${idBase}.open`);
|
|
317
|
+
const close = action(() => {
|
|
318
|
+
isOpenAtom.set(false);
|
|
319
|
+
isCalendarFocusedAtom.set(false);
|
|
320
|
+
draftDateAtom.set(committedDateAtom());
|
|
321
|
+
draftTimeAtom.set(committedTimeAtom());
|
|
322
|
+
syncInputFromCommitted();
|
|
323
|
+
}, `${idBase}.close`);
|
|
324
|
+
const toggle = action(() => {
|
|
325
|
+
if (isOpenAtom()) {
|
|
326
|
+
close();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
open();
|
|
330
|
+
}, `${idBase}.toggle`);
|
|
331
|
+
const setInputValue = action((value) => {
|
|
332
|
+
if (disabledAtom() || readonlyAtom())
|
|
333
|
+
return;
|
|
334
|
+
inputValueAtom.set(value);
|
|
335
|
+
options.onInput?.(value);
|
|
336
|
+
}, `${idBase}.setInputValue`);
|
|
337
|
+
const commitInput = action(() => {
|
|
338
|
+
if (disabledAtom() || readonlyAtom())
|
|
339
|
+
return;
|
|
340
|
+
const parsed = parsedValueAtom();
|
|
341
|
+
if (!parsed)
|
|
342
|
+
return;
|
|
343
|
+
if (!isDateTimeWithinBounds(parsed.full, minAtom(), maxAtom()))
|
|
344
|
+
return;
|
|
345
|
+
setCommitted(parsed);
|
|
346
|
+
draftDateAtom.set(parsed.date);
|
|
347
|
+
draftTimeAtom.set(parsed.time);
|
|
348
|
+
inputValueAtom.set(formatDateTime(parsed, localeAtom()));
|
|
349
|
+
ensureDisplayedFromDate(parsed.date);
|
|
350
|
+
focusedDateAtom.set(parsed.date);
|
|
351
|
+
isOpenAtom.set(false);
|
|
352
|
+
isCalendarFocusedAtom.set(false);
|
|
353
|
+
}, `${idBase}.commitInput`);
|
|
354
|
+
const clear = action(() => {
|
|
355
|
+
if (disabledAtom() || readonlyAtom())
|
|
356
|
+
return;
|
|
357
|
+
setCommitted(null);
|
|
358
|
+
draftDateAtom.set(null);
|
|
359
|
+
draftTimeAtom.set(null);
|
|
360
|
+
focusedDateAtom.set(null);
|
|
361
|
+
inputValueAtom.set('');
|
|
362
|
+
options.onClear?.();
|
|
363
|
+
}, `${idBase}.clear`);
|
|
364
|
+
const setDisabled = action((value) => {
|
|
365
|
+
disabledAtom.set(value);
|
|
366
|
+
if (value)
|
|
367
|
+
close();
|
|
368
|
+
}, `${idBase}.setDisabled`);
|
|
369
|
+
const setReadonly = action((value) => {
|
|
370
|
+
readonlyAtom.set(value);
|
|
371
|
+
}, `${idBase}.setReadonly`);
|
|
372
|
+
const setRequired = action((value) => {
|
|
373
|
+
requiredAtom.set(value);
|
|
374
|
+
}, `${idBase}.setRequired`);
|
|
375
|
+
const setPlaceholder = action((value) => {
|
|
376
|
+
placeholderAtom.set(value);
|
|
377
|
+
}, `${idBase}.setPlaceholder`);
|
|
378
|
+
const setLocale = action((value) => {
|
|
379
|
+
localeAtom.set(value);
|
|
380
|
+
syncInputFromCommitted();
|
|
381
|
+
}, `${idBase}.setLocale`);
|
|
382
|
+
const setTimeZone = action((value) => {
|
|
383
|
+
timeZoneAtom.set(value);
|
|
384
|
+
}, `${idBase}.setTimeZone`);
|
|
385
|
+
const setMin = action((value) => {
|
|
386
|
+
minAtom.set(value);
|
|
387
|
+
}, `${idBase}.setMin`);
|
|
388
|
+
const setMax = action((value) => {
|
|
389
|
+
maxAtom.set(value);
|
|
390
|
+
}, `${idBase}.setMax`);
|
|
391
|
+
const setMinuteStep = action((value) => {
|
|
392
|
+
minuteStepAtom.set(clamp(Math.floor(value), 1, 60));
|
|
393
|
+
const currentDraft = draftTimeAtom();
|
|
394
|
+
if (currentDraft) {
|
|
395
|
+
const normalized = normalizeTime(currentDraft, minuteStepAtom());
|
|
396
|
+
if (normalized)
|
|
397
|
+
draftTimeAtom.set(normalized);
|
|
398
|
+
}
|
|
399
|
+
const currentCommitted = committedTimeAtom();
|
|
400
|
+
if (currentCommitted) {
|
|
401
|
+
const normalized = normalizeTime(currentCommitted, minuteStepAtom());
|
|
402
|
+
if (normalized)
|
|
403
|
+
committedTimeAtom.set(normalized);
|
|
404
|
+
}
|
|
405
|
+
}, `${idBase}.setMinuteStep`);
|
|
406
|
+
const setHourCycle = action((value) => {
|
|
407
|
+
hourCycleAtom.set(value);
|
|
408
|
+
}, `${idBase}.setHourCycle`);
|
|
409
|
+
const setDisplayedMonth = action((year, month) => {
|
|
410
|
+
const normalized = normalizeYearMonth(year, month);
|
|
411
|
+
displayedYearAtom.set(normalized.year);
|
|
412
|
+
displayedMonthAtom.set(normalized.month);
|
|
413
|
+
ensureFocusedDate();
|
|
414
|
+
}, `${idBase}.setDisplayedMonth`);
|
|
415
|
+
const moveMonth = action((offset) => {
|
|
416
|
+
const normalized = normalizeYearMonth(displayedYearAtom(), displayedMonthAtom() + offset);
|
|
417
|
+
displayedYearAtom.set(normalized.year);
|
|
418
|
+
displayedMonthAtom.set(normalized.month);
|
|
419
|
+
ensureFocusedDate();
|
|
420
|
+
}, `${idBase}.moveMonth`);
|
|
421
|
+
const moveYear = action((offset) => {
|
|
422
|
+
const normalized = normalizeYearMonth(displayedYearAtom() + offset, displayedMonthAtom());
|
|
423
|
+
displayedYearAtom.set(normalized.year);
|
|
424
|
+
displayedMonthAtom.set(normalized.month);
|
|
425
|
+
ensureFocusedDate();
|
|
426
|
+
}, `${idBase}.moveYear`);
|
|
427
|
+
const setFocusedDate = action((date) => {
|
|
428
|
+
if (date == null) {
|
|
429
|
+
focusedDateAtom.set(null);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (!isDateWithinBounds(date, minAtom(), maxAtom()))
|
|
433
|
+
return;
|
|
434
|
+
focusedDateAtom.set(date);
|
|
435
|
+
}, `${idBase}.setFocusedDate`);
|
|
436
|
+
const moveFocusedBy = (delta) => {
|
|
437
|
+
const fallback = focusedDateAtom() ?? draftDateAtom() ?? committedDateAtom() ?? todayAtom();
|
|
438
|
+
const next = addDaysUtc(fallback, delta);
|
|
439
|
+
if (!next)
|
|
440
|
+
return;
|
|
441
|
+
if (!isDateWithinBounds(next, minAtom(), maxAtom()))
|
|
442
|
+
return;
|
|
443
|
+
focusedDateAtom.set(next);
|
|
444
|
+
ensureDisplayedFromDate(next);
|
|
445
|
+
};
|
|
446
|
+
const moveFocusPreviousDay = action(() => {
|
|
447
|
+
moveFocusedBy(-1);
|
|
448
|
+
}, `${idBase}.moveFocusPreviousDay`);
|
|
449
|
+
const moveFocusNextDay = action(() => {
|
|
450
|
+
moveFocusedBy(1);
|
|
451
|
+
}, `${idBase}.moveFocusNextDay`);
|
|
452
|
+
const moveFocusPreviousWeek = action(() => {
|
|
453
|
+
moveFocusedBy(-7);
|
|
454
|
+
}, `${idBase}.moveFocusPreviousWeek`);
|
|
455
|
+
const moveFocusNextWeek = action(() => {
|
|
456
|
+
moveFocusedBy(7);
|
|
457
|
+
}, `${idBase}.moveFocusNextWeek`);
|
|
458
|
+
const selectDraftDate = action((date) => {
|
|
459
|
+
if (disabledAtom() || readonlyAtom())
|
|
460
|
+
return;
|
|
461
|
+
if (!isDateWithinBounds(date, minAtom(), maxAtom()))
|
|
462
|
+
return;
|
|
463
|
+
draftDateAtom.set(date);
|
|
464
|
+
focusedDateAtom.set(date);
|
|
465
|
+
}, `${idBase}.selectDraftDate`);
|
|
466
|
+
const setDraftTime = action((time) => {
|
|
467
|
+
if (disabledAtom() || readonlyAtom())
|
|
468
|
+
return;
|
|
469
|
+
const normalized = normalizeTime(time, minuteStepAtom());
|
|
470
|
+
if (!normalized)
|
|
471
|
+
return;
|
|
472
|
+
draftTimeAtom.set(normalized);
|
|
473
|
+
}, `${idBase}.setDraftTime`);
|
|
474
|
+
const jumpToNow = action(() => {
|
|
475
|
+
if (disabledAtom() || readonlyAtom())
|
|
476
|
+
return;
|
|
477
|
+
const date = getTodayDate(timeZoneAtom());
|
|
478
|
+
const time = getNowTime(timeZoneAtom(), minuteStepAtom());
|
|
479
|
+
draftDateAtom.set(date);
|
|
480
|
+
draftTimeAtom.set(time);
|
|
481
|
+
focusedDateAtom.set(date);
|
|
482
|
+
ensureDisplayedFromDate(date);
|
|
483
|
+
}, `${idBase}.jumpToNow`);
|
|
484
|
+
const commitDraft = action(() => {
|
|
485
|
+
commitDraftInternal();
|
|
486
|
+
}, `${idBase}.commitDraft`);
|
|
487
|
+
const cancelDraft = action(() => {
|
|
488
|
+
if (!isOpenAtom())
|
|
489
|
+
return;
|
|
490
|
+
draftDateAtom.set(committedDateAtom());
|
|
491
|
+
draftTimeAtom.set(committedTimeAtom());
|
|
492
|
+
focusedDateAtom.set(committedDateAtom());
|
|
493
|
+
syncInputFromCommitted();
|
|
494
|
+
}, `${idBase}.cancelDraft`);
|
|
495
|
+
const handleInputKeyDown = action((event) => {
|
|
496
|
+
if (disabledAtom())
|
|
497
|
+
return;
|
|
498
|
+
if (event.key === 'Escape' && isOpenAtom() && (options.closeOnEscape ?? true)) {
|
|
499
|
+
event.preventDefault?.();
|
|
500
|
+
close();
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (event.key === 'Enter') {
|
|
504
|
+
event.preventDefault?.();
|
|
505
|
+
commitInput();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (event.key === 'ArrowDown' ||
|
|
509
|
+
event.key === 'ArrowUp' ||
|
|
510
|
+
event.key === ' ' ||
|
|
511
|
+
event.key === 'Spacebar') {
|
|
512
|
+
event.preventDefault?.();
|
|
513
|
+
open();
|
|
514
|
+
}
|
|
515
|
+
}, `${idBase}.handleInputKeyDown`);
|
|
516
|
+
const handleDialogKeyDown = action((event) => {
|
|
517
|
+
if (event.key === 'Escape' && (options.closeOnEscape ?? true)) {
|
|
518
|
+
event.preventDefault?.();
|
|
519
|
+
close();
|
|
520
|
+
}
|
|
521
|
+
}, `${idBase}.handleDialogKeyDown`);
|
|
522
|
+
const handleCalendarKeyDown = action((event) => {
|
|
523
|
+
if (!isOpenAtom())
|
|
524
|
+
return;
|
|
525
|
+
switch (event.key) {
|
|
526
|
+
case 'ArrowLeft':
|
|
527
|
+
event.preventDefault?.();
|
|
528
|
+
moveFocusPreviousDay();
|
|
529
|
+
return;
|
|
530
|
+
case 'ArrowRight':
|
|
531
|
+
event.preventDefault?.();
|
|
532
|
+
moveFocusNextDay();
|
|
533
|
+
return;
|
|
534
|
+
case 'ArrowUp':
|
|
535
|
+
event.preventDefault?.();
|
|
536
|
+
moveFocusPreviousWeek();
|
|
537
|
+
return;
|
|
538
|
+
case 'ArrowDown':
|
|
539
|
+
event.preventDefault?.();
|
|
540
|
+
moveFocusNextWeek();
|
|
541
|
+
return;
|
|
542
|
+
case 'PageUp':
|
|
543
|
+
event.preventDefault?.();
|
|
544
|
+
if (event.shiftKey) {
|
|
545
|
+
moveYear(-1);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
moveMonth(-1);
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
case 'PageDown':
|
|
552
|
+
event.preventDefault?.();
|
|
553
|
+
if (event.shiftKey) {
|
|
554
|
+
moveYear(1);
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
moveMonth(1);
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
case 'Home': {
|
|
561
|
+
event.preventDefault?.();
|
|
562
|
+
const focused = focusedDateAtom();
|
|
563
|
+
if (!focused)
|
|
564
|
+
return;
|
|
565
|
+
const parts = parseDateOnly(focused);
|
|
566
|
+
if (!parts)
|
|
567
|
+
return;
|
|
568
|
+
const date = new Date(Date.UTC(parts.year, parts.month - 1, parts.day, 12, 0, 0, 0));
|
|
569
|
+
const weekday = date.getUTCDay();
|
|
570
|
+
const next = addDaysUtc(focused, -weekday);
|
|
571
|
+
if (next)
|
|
572
|
+
setFocusedDate(next);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
case 'End': {
|
|
576
|
+
event.preventDefault?.();
|
|
577
|
+
const focused = focusedDateAtom();
|
|
578
|
+
if (!focused)
|
|
579
|
+
return;
|
|
580
|
+
const parts = parseDateOnly(focused);
|
|
581
|
+
if (!parts)
|
|
582
|
+
return;
|
|
583
|
+
const date = new Date(Date.UTC(parts.year, parts.month - 1, parts.day, 12, 0, 0, 0));
|
|
584
|
+
const weekday = date.getUTCDay();
|
|
585
|
+
const next = addDaysUtc(focused, 6 - weekday);
|
|
586
|
+
if (next)
|
|
587
|
+
setFocusedDate(next);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
case 'Enter': {
|
|
591
|
+
event.preventDefault?.();
|
|
592
|
+
if (event.ctrlKey) {
|
|
593
|
+
commitDraft();
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const focused = focusedDateAtom();
|
|
597
|
+
if (!focused)
|
|
598
|
+
return;
|
|
599
|
+
selectDraftDate(focused);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
case ' ':
|
|
603
|
+
case 'Spacebar': {
|
|
604
|
+
event.preventDefault?.();
|
|
605
|
+
const focused = focusedDateAtom();
|
|
606
|
+
if (!focused)
|
|
607
|
+
return;
|
|
608
|
+
selectDraftDate(focused);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
case 'Escape':
|
|
612
|
+
if (options.closeOnEscape ?? true) {
|
|
613
|
+
event.preventDefault?.();
|
|
614
|
+
close();
|
|
615
|
+
}
|
|
616
|
+
return;
|
|
617
|
+
default:
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
}, `${idBase}.handleCalendarKeyDown`);
|
|
621
|
+
const handleTimeKeyDown = action((event) => {
|
|
622
|
+
if (event.key === 'Enter') {
|
|
623
|
+
event.preventDefault?.();
|
|
624
|
+
commitDraft();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (event.key === 'Escape' && (options.closeOnEscape ?? true)) {
|
|
628
|
+
event.preventDefault?.();
|
|
629
|
+
close();
|
|
630
|
+
}
|
|
631
|
+
}, `${idBase}.handleTimeKeyDown`);
|
|
632
|
+
const handleOutsidePointer = action(() => {
|
|
633
|
+
if (!isOpenAtom())
|
|
634
|
+
return;
|
|
635
|
+
close();
|
|
636
|
+
}, `${idBase}.handleOutsidePointer`);
|
|
637
|
+
const getDraftTimeOrDefault = () => draftTimeAtom() ?? committedTimeAtom() ?? '00:00';
|
|
638
|
+
const updateHourSegment = (segment) => {
|
|
639
|
+
const digits = segment.replace(/\D/g, '');
|
|
640
|
+
if (digits.length === 0)
|
|
641
|
+
return;
|
|
642
|
+
const hour = clamp(Number(digits.slice(-2)), 0, 23);
|
|
643
|
+
const current = getDraftTimeOrDefault();
|
|
644
|
+
const minute = current.split(':')[1] ?? '00';
|
|
645
|
+
setDraftTime(`${twoDigits(hour)}:${minute}`);
|
|
646
|
+
};
|
|
647
|
+
const updateMinuteSegment = (segment) => {
|
|
648
|
+
const digits = segment.replace(/\D/g, '');
|
|
649
|
+
if (digits.length === 0)
|
|
650
|
+
return;
|
|
651
|
+
const minute = clamp(Number(digits.slice(-2)), 0, 59);
|
|
652
|
+
const current = getDraftTimeOrDefault();
|
|
653
|
+
const hour = current.split(':')[0] ?? '00';
|
|
654
|
+
setDraftTime(`${hour}:${twoDigits(minute)}`);
|
|
655
|
+
};
|
|
656
|
+
const contracts = {
|
|
657
|
+
getInputProps() {
|
|
658
|
+
return {
|
|
659
|
+
id: `${idBase}-input`,
|
|
660
|
+
role: 'combobox',
|
|
661
|
+
tabindex: '0',
|
|
662
|
+
autocomplete: 'off',
|
|
663
|
+
disabled: disabledAtom(),
|
|
664
|
+
readonly: readonlyAtom() ? true : undefined,
|
|
665
|
+
required: requiredAtom() ? true : undefined,
|
|
666
|
+
value: inputValueAtom(),
|
|
667
|
+
placeholder: placeholderAtom(),
|
|
668
|
+
'aria-haspopup': 'dialog',
|
|
669
|
+
'aria-expanded': isOpenAtom() ? 'true' : 'false',
|
|
670
|
+
'aria-controls': `${idBase}-dialog`,
|
|
671
|
+
'aria-activedescendant': isOpenAtom() ? (selectedCellIdAtom() ?? undefined) : undefined,
|
|
672
|
+
'aria-invalid': inputInvalidAtom() ? 'true' : undefined,
|
|
673
|
+
'aria-label': options.ariaLabel,
|
|
674
|
+
onInput: setInputValue,
|
|
675
|
+
onKeyDown: handleInputKeyDown,
|
|
676
|
+
onFocus: () => {
|
|
677
|
+
isInputFocusedAtom.set(true);
|
|
678
|
+
},
|
|
679
|
+
onBlur: () => {
|
|
680
|
+
isInputFocusedAtom.set(false);
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
},
|
|
684
|
+
getDialogProps() {
|
|
685
|
+
return {
|
|
686
|
+
id: `${idBase}-dialog`,
|
|
687
|
+
role: 'dialog',
|
|
688
|
+
tabindex: '-1',
|
|
689
|
+
hidden: !isOpenAtom(),
|
|
690
|
+
'aria-modal': 'true',
|
|
691
|
+
'aria-label': options.ariaLabel ?? 'Select date and time',
|
|
692
|
+
onKeyDown: handleDialogKeyDown,
|
|
693
|
+
onPointerDownOutside: handleOutsidePointer,
|
|
694
|
+
};
|
|
695
|
+
},
|
|
696
|
+
getCalendarGridProps() {
|
|
697
|
+
return {
|
|
698
|
+
id: `${idBase}-grid`,
|
|
699
|
+
role: 'grid',
|
|
700
|
+
tabindex: '-1',
|
|
701
|
+
'aria-label': 'Calendar',
|
|
702
|
+
onKeyDown: handleCalendarKeyDown,
|
|
703
|
+
};
|
|
704
|
+
},
|
|
705
|
+
getCalendarDayProps(date) {
|
|
706
|
+
const day = visibleDaysAtom().find((item) => item.date === date);
|
|
707
|
+
const dayDisabled = !day || day.disabled || disabledAtom() || readonlyAtom();
|
|
708
|
+
return {
|
|
709
|
+
id: `${idBase}-day-${date}`,
|
|
710
|
+
role: 'gridcell',
|
|
711
|
+
tabindex: focusedDateAtom() === date ? '0' : '-1',
|
|
712
|
+
'aria-selected': draftDateAtom() === date ? 'true' : 'false',
|
|
713
|
+
'aria-disabled': dayDisabled ? 'true' : undefined,
|
|
714
|
+
'aria-current': day?.isToday ? 'date' : undefined,
|
|
715
|
+
'data-date': date,
|
|
716
|
+
onClick: () => {
|
|
717
|
+
selectDraftDate(date);
|
|
718
|
+
},
|
|
719
|
+
onMouseEnter: () => {
|
|
720
|
+
setFocusedDate(date);
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
},
|
|
724
|
+
getMonthNavButtonProps(direction) {
|
|
725
|
+
return {
|
|
726
|
+
id: `${idBase}-month-${direction}`,
|
|
727
|
+
role: 'button',
|
|
728
|
+
tabindex: '0',
|
|
729
|
+
'aria-label': direction === 'prev' ? 'Previous month' : 'Next month',
|
|
730
|
+
onClick: () => {
|
|
731
|
+
moveMonth(direction === 'prev' ? -1 : 1);
|
|
732
|
+
},
|
|
733
|
+
};
|
|
734
|
+
},
|
|
735
|
+
getYearNavButtonProps(direction) {
|
|
736
|
+
return {
|
|
737
|
+
id: `${idBase}-year-${direction}`,
|
|
738
|
+
role: 'button',
|
|
739
|
+
tabindex: '0',
|
|
740
|
+
'aria-label': direction === 'prev' ? 'Previous year' : 'Next year',
|
|
741
|
+
onClick: () => {
|
|
742
|
+
moveYear(direction === 'prev' ? -1 : 1);
|
|
743
|
+
},
|
|
744
|
+
};
|
|
745
|
+
},
|
|
746
|
+
getHourInputProps() {
|
|
747
|
+
const [hour] = getDraftTimeOrDefault().split(':');
|
|
748
|
+
return {
|
|
749
|
+
id: `${idBase}-time-hour`,
|
|
750
|
+
type: 'text',
|
|
751
|
+
inputmode: 'numeric',
|
|
752
|
+
'aria-label': 'Hours',
|
|
753
|
+
value: hour ?? '00',
|
|
754
|
+
minlength: '2',
|
|
755
|
+
maxlength: '2',
|
|
756
|
+
disabled: disabledAtom(),
|
|
757
|
+
readonly: readonlyAtom(),
|
|
758
|
+
onInput: updateHourSegment,
|
|
759
|
+
onKeyDown: handleTimeKeyDown,
|
|
760
|
+
};
|
|
761
|
+
},
|
|
762
|
+
getMinuteInputProps() {
|
|
763
|
+
const [, minute] = getDraftTimeOrDefault().split(':');
|
|
764
|
+
return {
|
|
765
|
+
id: `${idBase}-time-minute`,
|
|
766
|
+
type: 'text',
|
|
767
|
+
inputmode: 'numeric',
|
|
768
|
+
'aria-label': 'Minutes',
|
|
769
|
+
value: minute ?? '00',
|
|
770
|
+
minlength: '2',
|
|
771
|
+
maxlength: '2',
|
|
772
|
+
disabled: disabledAtom(),
|
|
773
|
+
readonly: readonlyAtom(),
|
|
774
|
+
onInput: updateMinuteSegment,
|
|
775
|
+
onKeyDown: handleTimeKeyDown,
|
|
776
|
+
};
|
|
777
|
+
},
|
|
778
|
+
getApplyButtonProps() {
|
|
779
|
+
const draft = draftValueAtom();
|
|
780
|
+
const valid = draft ? isDateTimeWithinBounds(draft, minAtom(), maxAtom()) : false;
|
|
781
|
+
return {
|
|
782
|
+
id: `${idBase}-apply`,
|
|
783
|
+
role: 'button',
|
|
784
|
+
tabindex: '0',
|
|
785
|
+
'aria-label': 'Apply',
|
|
786
|
+
disabled: disabledAtom() || readonlyAtom() || !valid,
|
|
787
|
+
onClick: () => {
|
|
788
|
+
commitDraft();
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
},
|
|
792
|
+
getCancelButtonProps() {
|
|
793
|
+
return {
|
|
794
|
+
id: `${idBase}-cancel`,
|
|
795
|
+
role: 'button',
|
|
796
|
+
tabindex: '0',
|
|
797
|
+
'aria-label': 'Cancel',
|
|
798
|
+
disabled: disabledAtom(),
|
|
799
|
+
onClick: () => {
|
|
800
|
+
cancelDraft();
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
},
|
|
804
|
+
getClearButtonProps() {
|
|
805
|
+
return {
|
|
806
|
+
id: `${idBase}-clear`,
|
|
807
|
+
role: 'button',
|
|
808
|
+
tabindex: '0',
|
|
809
|
+
'aria-label': 'Clear',
|
|
810
|
+
disabled: disabledAtom() || readonlyAtom() || !hasCommittedSelectionAtom(),
|
|
811
|
+
onClick: () => {
|
|
812
|
+
clear();
|
|
813
|
+
},
|
|
814
|
+
};
|
|
815
|
+
},
|
|
816
|
+
getVisibleDays() {
|
|
817
|
+
return visibleDaysAtom();
|
|
818
|
+
},
|
|
819
|
+
};
|
|
820
|
+
const state = {
|
|
821
|
+
inputValue: inputValueAtom,
|
|
822
|
+
isOpen: isOpenAtom,
|
|
823
|
+
focusedDate: focusedDateAtom,
|
|
824
|
+
committedDate: committedDateAtom,
|
|
825
|
+
committedTime: committedTimeAtom,
|
|
826
|
+
draftDate: draftDateAtom,
|
|
827
|
+
draftTime: draftTimeAtom,
|
|
828
|
+
displayedYear: displayedYearAtom,
|
|
829
|
+
displayedMonth: displayedMonthAtom,
|
|
830
|
+
isInputFocused: isInputFocusedAtom,
|
|
831
|
+
isCalendarFocused: isCalendarFocusedAtom,
|
|
832
|
+
disabled: disabledAtom,
|
|
833
|
+
readonly: readonlyAtom,
|
|
834
|
+
required: requiredAtom,
|
|
835
|
+
placeholder: placeholderAtom,
|
|
836
|
+
locale: localeAtom,
|
|
837
|
+
timeZone: timeZoneAtom,
|
|
838
|
+
min: minAtom,
|
|
839
|
+
max: maxAtom,
|
|
840
|
+
minuteStep: minuteStepAtom,
|
|
841
|
+
hourCycle: hourCycleAtom,
|
|
842
|
+
isDualCommit: isDualCommitAtom,
|
|
843
|
+
hasCommittedSelection: hasCommittedSelectionAtom,
|
|
844
|
+
hasDraftSelection: hasDraftSelectionAtom,
|
|
845
|
+
committedValue: committedValueAtom,
|
|
846
|
+
draftValue: draftValueAtom,
|
|
847
|
+
parsedValue: parsedValueAtom,
|
|
848
|
+
canCommitInput: canCommitInputAtom,
|
|
849
|
+
inputInvalid: inputInvalidAtom,
|
|
850
|
+
visibleDays: visibleDaysAtom,
|
|
851
|
+
today: todayAtom,
|
|
852
|
+
selectedCellId: selectedCellIdAtom,
|
|
853
|
+
};
|
|
854
|
+
const actions = {
|
|
855
|
+
open,
|
|
856
|
+
close,
|
|
857
|
+
toggle,
|
|
858
|
+
setInputValue,
|
|
859
|
+
commitInput,
|
|
860
|
+
clear,
|
|
861
|
+
setDisabled,
|
|
862
|
+
setReadonly,
|
|
863
|
+
setRequired,
|
|
864
|
+
setPlaceholder,
|
|
865
|
+
setLocale,
|
|
866
|
+
setTimeZone,
|
|
867
|
+
setMin,
|
|
868
|
+
setMax,
|
|
869
|
+
setMinuteStep,
|
|
870
|
+
setHourCycle,
|
|
871
|
+
setDisplayedMonth,
|
|
872
|
+
moveMonth,
|
|
873
|
+
moveYear,
|
|
874
|
+
setFocusedDate,
|
|
875
|
+
moveFocusPreviousDay,
|
|
876
|
+
moveFocusNextDay,
|
|
877
|
+
moveFocusPreviousWeek,
|
|
878
|
+
moveFocusNextWeek,
|
|
879
|
+
selectDraftDate,
|
|
880
|
+
setDraftTime,
|
|
881
|
+
jumpToNow,
|
|
882
|
+
commitDraft,
|
|
883
|
+
cancelDraft,
|
|
884
|
+
handleInputKeyDown,
|
|
885
|
+
handleDialogKeyDown,
|
|
886
|
+
handleCalendarKeyDown,
|
|
887
|
+
handleTimeKeyDown,
|
|
888
|
+
handleOutsidePointer,
|
|
889
|
+
};
|
|
890
|
+
return {
|
|
891
|
+
state,
|
|
892
|
+
actions,
|
|
893
|
+
contracts,
|
|
894
|
+
};
|
|
895
|
+
}
|