@adia-ai/web-components 0.6.32 → 0.6.34
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/CHANGELOG.md +44 -0
- package/components/accordion/accordion.css +2 -2
- package/components/action-list/action-list.css +2 -2
- package/components/agent-artifact/agent-artifact.css +31 -31
- package/components/agent-feedback-bar/agent-feedback-bar.css +10 -10
- package/components/agent-questions/agent-questions.css +57 -57
- package/components/agent-reasoning/agent-reasoning.css +62 -62
- package/components/agent-suggestions/agent-suggestions.css +4 -4
- package/components/agent-trace/agent-trace.css +53 -53
- package/components/alert/alert.css +41 -41
- package/components/avatar/avatar.css +27 -27
- package/components/badge/badge.css +27 -27
- package/components/block/block.css +16 -16
- package/components/breadcrumb/breadcrumb.css +23 -23
- package/components/button/button.css +101 -91
- package/components/calendar-grid/calendar-grid.a2ui.json +136 -0
- package/components/calendar-grid/calendar-grid.css +226 -0
- package/components/calendar-grid/calendar-grid.d.ts +37 -0
- package/components/calendar-grid/calendar-grid.js +17 -0
- package/components/calendar-grid/calendar-grid.yaml +116 -0
- package/components/calendar-grid/class.js +300 -0
- package/components/calendar-picker/calendar-picker.css +139 -139
- package/components/canvas/canvas.css +12 -12
- package/components/card/card.css +83 -83
- package/components/chart/chart.css +224 -224
- package/components/chart-legend/chart-legend.css +26 -26
- package/components/check/check.css +40 -40
- package/components/code/code.css +125 -125
- package/components/col/col.css +15 -15
- package/components/color-picker/color-picker.css +55 -55
- package/components/combobox/class.js +861 -0
- package/components/combobox/combobox.a2ui.json +363 -0
- package/components/combobox/combobox.css +244 -0
- package/components/combobox/combobox.d.ts +113 -0
- package/components/combobox/combobox.examples.md +59 -0
- package/components/combobox/combobox.js +17 -0
- package/components/combobox/combobox.test.js +181 -0
- package/components/combobox/combobox.yaml +369 -0
- package/components/command/command.css +90 -90
- package/components/date-range-picker/class.js +775 -0
- package/components/date-range-picker/date-range-picker.a2ui.json +300 -0
- package/components/date-range-picker/date-range-picker.css +178 -0
- package/components/date-range-picker/date-range-picker.d.ts +82 -0
- package/components/date-range-picker/date-range-picker.examples.md +37 -0
- package/components/date-range-picker/date-range-picker.js +17 -0
- package/components/date-range-picker/date-range-picker.test.js +387 -0
- package/components/date-range-picker/date-range-picker.yaml +285 -0
- package/components/datetime-picker/class.js +706 -0
- package/components/datetime-picker/datetime-picker.a2ui.json +334 -0
- package/components/datetime-picker/datetime-picker.css +150 -0
- package/components/datetime-picker/datetime-picker.d.ts +86 -0
- package/components/datetime-picker/datetime-picker.examples.md +46 -0
- package/components/datetime-picker/datetime-picker.js +17 -0
- package/components/datetime-picker/datetime-picker.test.js +454 -0
- package/components/datetime-picker/datetime-picker.yaml +332 -0
- package/components/demo-toggle/demo-toggle.css +27 -27
- package/components/description-list/description-list.css +18 -18
- package/components/divider/divider.css +24 -24
- package/components/embed/embed.css +6 -6
- package/components/empty-state/empty-state.css +27 -27
- package/components/feed/feed.css +12 -12
- package/components/field/field.css +37 -28
- package/components/field/field.test.js +32 -0
- package/components/fields/fields.css +5 -5
- package/components/grid/grid.css +5 -5
- package/components/heatmap/heatmap.css +63 -63
- package/components/icon/icon.css +12 -12
- package/components/image/image.css +14 -14
- package/components/index.js +8 -0
- package/components/input/input.css +66 -66
- package/components/inspector/inspector.css +6 -6
- package/components/integration-card/class.js +410 -0
- package/components/integration-card/integration-card.a2ui.json +268 -0
- package/components/integration-card/integration-card.css +169 -0
- package/components/integration-card/integration-card.d.ts +63 -0
- package/components/integration-card/integration-card.examples.md +41 -0
- package/components/integration-card/integration-card.js +17 -0
- package/components/integration-card/integration-card.test.js +306 -0
- package/components/integration-card/integration-card.yaml +280 -0
- package/components/kbd/kbd.css +32 -32
- package/components/link/link.css +12 -12
- package/components/list/list.css +8 -8
- package/components/list-window/class.js +688 -0
- package/components/list-window/list-window.a2ui.json +277 -0
- package/components/list-window/list-window.css +124 -0
- package/components/list-window/list-window.d.ts +84 -0
- package/components/list-window/list-window.examples.md +73 -0
- package/components/list-window/list-window.js +17 -0
- package/components/list-window/list-window.test.js +303 -0
- package/components/list-window/list-window.yaml +270 -0
- package/components/menu/menu.css +8 -8
- package/components/modal/modal.css +43 -43
- package/components/nav/nav.css +40 -40
- package/components/nav-group/nav-group.css +52 -52
- package/components/nav-item/nav-item.css +44 -44
- package/components/noodles/noodles.css +31 -31
- package/components/option-card/option-card.css +69 -69
- package/components/otp-input/otp-input.css +30 -30
- package/components/page/page.css +18 -18
- package/components/pagination/pagination.css +61 -61
- package/components/pane/pane.css +57 -57
- package/components/pipeline-status/pipeline-status.css +65 -65
- package/components/popover/popover.css +17 -17
- package/components/progress/progress.css +23 -23
- package/components/progress-row/progress-row.css +17 -17
- package/components/radio/radio.css +39 -39
- package/components/range/range.css +55 -55
- package/components/rating/rating.css +28 -28
- package/components/richtext/richtext.css +133 -133
- package/components/row/row.css +19 -19
- package/components/search/search.css +5 -5
- package/components/segment/segment.css +24 -24
- package/components/segmented/segmented.css +25 -25
- package/components/select/select.css +84 -84
- package/components/skeleton/skeleton.css +14 -14
- package/components/slider/slider.css +46 -46
- package/components/spinner/class.js +69 -0
- package/components/spinner/spinner.a2ui.json +197 -0
- package/components/spinner/spinner.css +165 -0
- package/components/spinner/spinner.d.ts +26 -0
- package/components/spinner/spinner.examples.md +26 -0
- package/components/spinner/spinner.js +17 -0
- package/components/spinner/spinner.test.js +234 -0
- package/components/spinner/spinner.yaml +230 -0
- package/components/stack/stack.css +11 -11
- package/components/stat/stat.css +25 -25
- package/components/step-progress/step-progress.css +20 -20
- package/components/stepper/stepper.css +29 -29
- package/components/stream/stream.css +12 -12
- package/components/swatch/swatch.css +68 -68
- package/components/swiper/swiper.css +57 -57
- package/components/switch/switch.css +52 -52
- package/components/table/class.js +9 -0
- package/components/table/table.a2ui.json +1 -1
- package/components/table/table.css +162 -162
- package/components/table/table.d.ts +1 -1
- package/components/table/table.test.js +53 -0
- package/components/table/table.yaml +13 -1
- package/components/table-toolbar/table-toolbar.css +32 -32
- package/components/tabs/tabs.css +51 -51
- package/components/tag/tag.css +48 -48
- package/components/text/text.css +44 -44
- package/components/textarea/textarea.css +46 -46
- package/components/time-picker/class.js +693 -0
- package/components/time-picker/time-picker.a2ui.json +267 -0
- package/components/time-picker/time-picker.css +122 -0
- package/components/time-picker/time-picker.d.ts +75 -0
- package/components/time-picker/time-picker.examples.md +35 -0
- package/components/time-picker/time-picker.js +17 -0
- package/components/time-picker/time-picker.test.js +287 -0
- package/components/time-picker/time-picker.yaml +256 -0
- package/components/timeline/timeline.css +50 -50
- package/components/toast/toast.css +58 -58
- package/components/toggle-group/toggle-group.css +6 -6
- package/components/toggle-scheme/toggle-scheme.css +2 -2
- package/components/toolbar/toolbar.css +17 -17
- package/components/tooltip/tooltip.css +2 -2
- package/components/tree/tree.css +37 -37
- package/components/upload/upload.css +49 -49
- package/dist/icons-manifest.js +3 -3
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +121 -83
- package/package.json +1 -1
- package/styles/components.css +8 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* time-picker-ui — SPEC-043
|
|
3
|
+
*
|
|
4
|
+
* Coverage:
|
|
5
|
+
* - Stamp shape (precision × hour-cycle)
|
|
6
|
+
* - Segment text sync from value
|
|
7
|
+
* - Arrow Up / Down increments
|
|
8
|
+
* - Page Up / Down + Home / End
|
|
9
|
+
* - Tab equivalents (ArrowLeft / ArrowRight)
|
|
10
|
+
* - Digit typing + auto-advance
|
|
11
|
+
* - Form participation (name + ISO value)
|
|
12
|
+
* - min / max → invalid event
|
|
13
|
+
* - disabled / readonly behavior
|
|
14
|
+
* - precision toggle re-stamps without losing hh/mm
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
18
|
+
import '../../core/element.js';
|
|
19
|
+
import './time-picker.js';
|
|
20
|
+
|
|
21
|
+
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
22
|
+
|
|
23
|
+
function mount(html) {
|
|
24
|
+
const wrap = document.createElement('div');
|
|
25
|
+
wrap.innerHTML = html;
|
|
26
|
+
document.body.appendChild(wrap);
|
|
27
|
+
return wrap.firstElementChild;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function segText(el, key) {
|
|
31
|
+
return el.querySelector(`[data-segment="${key}"]`)?.textContent || '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fireKey(el, key) {
|
|
35
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('time-picker-ui', () => {
|
|
39
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
40
|
+
|
|
41
|
+
it('renders with empty segments when no value is set', async () => {
|
|
42
|
+
const el = mount('<time-picker-ui></time-picker-ui>');
|
|
43
|
+
await tick();
|
|
44
|
+
expect(el.querySelector('[data-segment="hour"]')).not.toBeNull();
|
|
45
|
+
expect(el.querySelector('[data-segment="minute"]')).not.toBeNull();
|
|
46
|
+
expect(segText(el, 'hour')).toBe('');
|
|
47
|
+
expect(segText(el, 'minute')).toBe('');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('populates hour/minute segments from value="09:30"', async () => {
|
|
51
|
+
const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
|
|
52
|
+
await tick();
|
|
53
|
+
expect(segText(el, 'hour')).toBe('09');
|
|
54
|
+
expect(segText(el, 'minute')).toBe('30');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('precision="second" stamps a seconds segment + separator', async () => {
|
|
58
|
+
const el = mount('<time-picker-ui precision="second" value="14:30:45"></time-picker-ui>');
|
|
59
|
+
await tick();
|
|
60
|
+
expect(el.querySelector('[data-segment="second"]')).not.toBeNull();
|
|
61
|
+
expect(segText(el, 'second')).toBe('45');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('hour-cycle="h12" stamps a meridiem segment + 12h display', async () => {
|
|
65
|
+
const el = mount('<time-picker-ui hour-cycle="h12" value="14:30"></time-picker-ui>');
|
|
66
|
+
await tick();
|
|
67
|
+
const meridiem = el.querySelector('[data-segment="meridiem"]');
|
|
68
|
+
expect(meridiem).not.toBeNull();
|
|
69
|
+
expect(segText(el, 'hour')).toBe('02');
|
|
70
|
+
expect(segText(el, 'meridiem')).toBe('PM');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('hour-cycle="h12" displays midnight as 12 AM', async () => {
|
|
74
|
+
const el = mount('<time-picker-ui hour-cycle="h12" value="00:15"></time-picker-ui>');
|
|
75
|
+
await tick();
|
|
76
|
+
expect(segText(el, 'hour')).toBe('12');
|
|
77
|
+
expect(segText(el, 'meridiem')).toBe('AM');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('Arrow Up on hour segment increments by 1', async () => {
|
|
81
|
+
const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
|
|
82
|
+
await tick();
|
|
83
|
+
const hour = el.querySelector('[data-segment="hour"]');
|
|
84
|
+
hour.focus();
|
|
85
|
+
fireKey(hour, 'ArrowUp');
|
|
86
|
+
await tick();
|
|
87
|
+
expect(el.value).toBe('10:30');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('Arrow Down on minute segment decrements by step/60 minutes', async () => {
|
|
91
|
+
const el = mount('<time-picker-ui value="09:30" step="60"></time-picker-ui>');
|
|
92
|
+
await tick();
|
|
93
|
+
const min = el.querySelector('[data-segment="minute"]');
|
|
94
|
+
min.focus();
|
|
95
|
+
fireKey(min, 'ArrowDown');
|
|
96
|
+
await tick();
|
|
97
|
+
expect(el.value).toBe('09:29');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('step="900" increments minute by 15 on Arrow Up', async () => {
|
|
101
|
+
const el = mount('<time-picker-ui value="09:30" step="900"></time-picker-ui>');
|
|
102
|
+
await tick();
|
|
103
|
+
const min = el.querySelector('[data-segment="minute"]');
|
|
104
|
+
min.focus();
|
|
105
|
+
fireKey(min, 'ArrowUp');
|
|
106
|
+
await tick();
|
|
107
|
+
expect(el.value).toBe('09:45');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('Page Up increments by 10× the small step', async () => {
|
|
111
|
+
const el = mount('<time-picker-ui value="09:00"></time-picker-ui>');
|
|
112
|
+
await tick();
|
|
113
|
+
const min = el.querySelector('[data-segment="minute"]');
|
|
114
|
+
min.focus();
|
|
115
|
+
fireKey(min, 'PageUp');
|
|
116
|
+
await tick();
|
|
117
|
+
expect(el.value).toBe('09:10');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('Home jumps to segment min; End jumps to segment max', async () => {
|
|
121
|
+
const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
|
|
122
|
+
await tick();
|
|
123
|
+
const hour = el.querySelector('[data-segment="hour"]');
|
|
124
|
+
hour.focus();
|
|
125
|
+
fireKey(hour, 'Home');
|
|
126
|
+
await tick();
|
|
127
|
+
expect(el.value).toBe('00:30');
|
|
128
|
+
fireKey(hour, 'End');
|
|
129
|
+
await tick();
|
|
130
|
+
expect(el.value).toBe('23:30');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('ArrowRight moves focus to next segment; ArrowLeft to previous', async () => {
|
|
134
|
+
const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
|
|
135
|
+
await tick();
|
|
136
|
+
const hour = el.querySelector('[data-segment="hour"]');
|
|
137
|
+
const minute = el.querySelector('[data-segment="minute"]');
|
|
138
|
+
hour.focus();
|
|
139
|
+
fireKey(hour, 'ArrowRight');
|
|
140
|
+
await tick();
|
|
141
|
+
expect(document.activeElement).toBe(minute);
|
|
142
|
+
fireKey(minute, 'ArrowLeft');
|
|
143
|
+
await tick();
|
|
144
|
+
expect(document.activeElement).toBe(hour);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('disabled prop blocks Arrow-key step', async () => {
|
|
148
|
+
const el = mount('<time-picker-ui value="09:30" disabled></time-picker-ui>');
|
|
149
|
+
await tick();
|
|
150
|
+
const hour = el.querySelector('[data-segment="hour"]');
|
|
151
|
+
fireKey(hour, 'ArrowUp');
|
|
152
|
+
await tick();
|
|
153
|
+
expect(el.value).toBe('09:30');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('readonly prop blocks edits but keeps segments focusable', async () => {
|
|
157
|
+
const el = mount('<time-picker-ui value="09:30" readonly></time-picker-ui>');
|
|
158
|
+
await tick();
|
|
159
|
+
const hour = el.querySelector('[data-segment="hour"]');
|
|
160
|
+
expect(hour.getAttribute('tabindex')).toBe('0');
|
|
161
|
+
expect(hour.contentEditable).toBe('false');
|
|
162
|
+
hour.focus();
|
|
163
|
+
fireKey(hour, 'ArrowUp');
|
|
164
|
+
await tick();
|
|
165
|
+
expect(el.value).toBe('09:30');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('min violation fires invalid + does not mutate value', async () => {
|
|
169
|
+
const el = mount('<time-picker-ui value="08:00" min="08:00" max="18:00"></time-picker-ui>');
|
|
170
|
+
await tick();
|
|
171
|
+
let captured = null;
|
|
172
|
+
el.addEventListener('invalid', (e) => { captured = e; });
|
|
173
|
+
const hour = el.querySelector('[data-segment="hour"]');
|
|
174
|
+
hour.focus();
|
|
175
|
+
fireKey(hour, 'ArrowDown');
|
|
176
|
+
await tick();
|
|
177
|
+
expect(captured).not.toBeNull();
|
|
178
|
+
expect(captured.detail.reason).toBe('min');
|
|
179
|
+
expect(el.value).toBe('08:00');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('max violation fires invalid', async () => {
|
|
183
|
+
const el = mount('<time-picker-ui value="18:00" min="08:00" max="18:00"></time-picker-ui>');
|
|
184
|
+
await tick();
|
|
185
|
+
let captured = null;
|
|
186
|
+
el.addEventListener('invalid', (e) => { captured = e; });
|
|
187
|
+
const hour = el.querySelector('[data-segment="hour"]');
|
|
188
|
+
hour.focus();
|
|
189
|
+
fireKey(hour, 'ArrowUp');
|
|
190
|
+
await tick();
|
|
191
|
+
expect(captured).not.toBeNull();
|
|
192
|
+
expect(captured.detail.reason).toBe('max');
|
|
193
|
+
expect(el.value).toBe('18:00');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('emits change with detail.segment on Arrow step', async () => {
|
|
197
|
+
const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
|
|
198
|
+
await tick();
|
|
199
|
+
let captured = null;
|
|
200
|
+
el.addEventListener('change', (e) => { captured = e; });
|
|
201
|
+
const hour = el.querySelector('[data-segment="hour"]');
|
|
202
|
+
hour.focus();
|
|
203
|
+
fireKey(hour, 'ArrowUp');
|
|
204
|
+
await tick();
|
|
205
|
+
expect(captured).not.toBeNull();
|
|
206
|
+
expect(captured.detail).toEqual({ value: '10:30', segment: 'hour' });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('precision="minute" → "second" re-stamps without losing hh/mm', async () => {
|
|
210
|
+
const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
|
|
211
|
+
await tick();
|
|
212
|
+
expect(el.querySelector('[data-segment="second"]')).toBeNull();
|
|
213
|
+
el.precision = 'second';
|
|
214
|
+
await tick();
|
|
215
|
+
expect(el.querySelector('[data-segment="second"]')).not.toBeNull();
|
|
216
|
+
expect(segText(el, 'hour')).toBe('09');
|
|
217
|
+
expect(segText(el, 'minute')).toBe('30');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('aria-valuemax matches segment max (h23 → 23 for hour)', async () => {
|
|
221
|
+
const el = mount('<time-picker-ui hour-cycle="h23"></time-picker-ui>');
|
|
222
|
+
await tick();
|
|
223
|
+
const hour = el.querySelector('[data-segment="hour"]');
|
|
224
|
+
expect(hour.getAttribute('aria-valuemax')).toBe('23');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('aria-valuemax for hour is 12 in h12 cycle', async () => {
|
|
228
|
+
const el = mount('<time-picker-ui hour-cycle="h12"></time-picker-ui>');
|
|
229
|
+
await tick();
|
|
230
|
+
const hour = el.querySelector('[data-segment="hour"]');
|
|
231
|
+
expect(hour.getAttribute('aria-valuemax')).toBe('12');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('aria-valuenow + aria-valuetext are populated from value', async () => {
|
|
235
|
+
const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
|
|
236
|
+
await tick();
|
|
237
|
+
const hour = el.querySelector('[data-segment="hour"]');
|
|
238
|
+
const minute = el.querySelector('[data-segment="minute"]');
|
|
239
|
+
expect(hour.getAttribute('aria-valuenow')).toBe('9');
|
|
240
|
+
expect(minute.getAttribute('aria-valuenow')).toBe('30');
|
|
241
|
+
expect(hour.getAttribute('aria-valuetext')).toContain('hour');
|
|
242
|
+
expect(minute.getAttribute('aria-valuetext')).toContain('minute');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('host role="group" + aria-label default', async () => {
|
|
246
|
+
const el = mount('<time-picker-ui></time-picker-ui>');
|
|
247
|
+
await tick();
|
|
248
|
+
expect(el.getAttribute('role')).toBe('group');
|
|
249
|
+
expect(el.hasAttribute('aria-label')).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('clear() resets value to empty', async () => {
|
|
253
|
+
const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
|
|
254
|
+
await tick();
|
|
255
|
+
el.clear();
|
|
256
|
+
await tick();
|
|
257
|
+
expect(el.value).toBe('');
|
|
258
|
+
expect(segText(el, 'hour')).toBe('');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('stepUp() increments programmatically', async () => {
|
|
262
|
+
const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
|
|
263
|
+
await tick();
|
|
264
|
+
el.stepUp('hour');
|
|
265
|
+
await tick();
|
|
266
|
+
expect(el.value).toBe('10:30');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('value setter syncs segments', async () => {
|
|
270
|
+
const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
|
|
271
|
+
await tick();
|
|
272
|
+
el.value = '11:45';
|
|
273
|
+
await tick();
|
|
274
|
+
expect(segText(el, 'hour')).toBe('11');
|
|
275
|
+
expect(segText(el, 'minute')).toBe('45');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('focusing a segment sets [editing] + [segment="…"] on host', async () => {
|
|
279
|
+
const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
|
|
280
|
+
await tick();
|
|
281
|
+
const minute = el.querySelector('[data-segment="minute"]');
|
|
282
|
+
minute.focus();
|
|
283
|
+
await tick();
|
|
284
|
+
expect(el.hasAttribute('editing')).toBe(true);
|
|
285
|
+
expect(el.getAttribute('segment')).toBe('minute');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
2
|
+
name: UITimePicker
|
|
3
|
+
tag: time-picker-ui
|
|
4
|
+
status: stable
|
|
5
|
+
component: TimePicker
|
|
6
|
+
category: input
|
|
7
|
+
version: 1
|
|
8
|
+
description: >-
|
|
9
|
+
Time-of-day picker with discrete segments (hour, minute, optional second,
|
|
10
|
+
optional AM/PM). Each segment follows the WAI-ARIA Spinbutton pattern with
|
|
11
|
+
Arrow-key increment/decrement, Page Up/Down for larger jumps, Home/End for
|
|
12
|
+
segment min/max, and Tab to move between segments. Form-associated input
|
|
13
|
+
emitting ISO 8601 time string ("HH:mm" or "HH:mm:ss") via change events.
|
|
14
|
+
Per ADR-0025 no native form controls — segments are contenteditable spans
|
|
15
|
+
inside a custom host. SPEC-043.
|
|
16
|
+
props:
|
|
17
|
+
name:
|
|
18
|
+
description: Form field name for FormData submission
|
|
19
|
+
type: string
|
|
20
|
+
default: ''
|
|
21
|
+
value:
|
|
22
|
+
description: Current time value as ISO 8601 ("HH:mm" or "HH:mm:ss"). Empty when no value selected.
|
|
23
|
+
type: string
|
|
24
|
+
default: ''
|
|
25
|
+
min:
|
|
26
|
+
description: Earliest selectable time as ISO 8601. Empty disables the constraint.
|
|
27
|
+
type: string
|
|
28
|
+
default: ''
|
|
29
|
+
reflect: true
|
|
30
|
+
max:
|
|
31
|
+
description: Latest selectable time as ISO 8601. Empty disables the constraint.
|
|
32
|
+
type: string
|
|
33
|
+
default: ''
|
|
34
|
+
reflect: true
|
|
35
|
+
step:
|
|
36
|
+
description: Per-Arrow-keypress increment in seconds. 60 = minute precision (default), 900 = 15-minute, 1 = second precision.
|
|
37
|
+
type: number
|
|
38
|
+
default: 60
|
|
39
|
+
reflect: true
|
|
40
|
+
precision:
|
|
41
|
+
description: Whether the seconds segment is exposed. `minute` (default) hides seconds; `second` adds the seconds segment + separator.
|
|
42
|
+
type: string
|
|
43
|
+
default: minute
|
|
44
|
+
enum:
|
|
45
|
+
- minute
|
|
46
|
+
- second
|
|
47
|
+
reflect: true
|
|
48
|
+
hourcycle:
|
|
49
|
+
description: |
|
|
50
|
+
Hour cycle. Empty (default) derives from the document locale — most
|
|
51
|
+
en-* locales resolve to `h23`. `h12` forces a 12-hour cycle with an
|
|
52
|
+
AM/PM (meridiem) segment; `h23` forces 24-hour cycle.
|
|
53
|
+
type: string
|
|
54
|
+
default: ''
|
|
55
|
+
enum:
|
|
56
|
+
- ''
|
|
57
|
+
- h12
|
|
58
|
+
- h23
|
|
59
|
+
reflect: true
|
|
60
|
+
attribute: hour-cycle
|
|
61
|
+
placeholder:
|
|
62
|
+
description: Per-segment placeholder text shown when a segment is empty. Defaults to "--".
|
|
63
|
+
type: string
|
|
64
|
+
default: '--'
|
|
65
|
+
disabled:
|
|
66
|
+
description: Disables interaction; dims the control
|
|
67
|
+
type: boolean
|
|
68
|
+
default: false
|
|
69
|
+
reflect: true
|
|
70
|
+
readonly:
|
|
71
|
+
description: Prevents editing while keeping the field focusable. Segments stay focusable for inspection.
|
|
72
|
+
type: boolean
|
|
73
|
+
default: false
|
|
74
|
+
reflect: true
|
|
75
|
+
required:
|
|
76
|
+
description: Marks the field as required for form validation. Sets aria-required.
|
|
77
|
+
type: boolean
|
|
78
|
+
default: false
|
|
79
|
+
reflect: true
|
|
80
|
+
locale:
|
|
81
|
+
description: BCP-47 locale tag used to derive hour-cycle when `hour-cycle` is empty. Falls back to `<html lang>` then to browser default.
|
|
82
|
+
type: string
|
|
83
|
+
default: ''
|
|
84
|
+
events:
|
|
85
|
+
change:
|
|
86
|
+
description: Fired when the value commits (segment edit completes via Arrow / Tab / blur).
|
|
87
|
+
detail:
|
|
88
|
+
value:
|
|
89
|
+
type: string
|
|
90
|
+
description: Current value as ISO 8601 ("HH:mm" or "HH:mm:ss").
|
|
91
|
+
segment:
|
|
92
|
+
type: string
|
|
93
|
+
description: Which segment commit fired the event (`hour` | `minute` | `second` | `meridiem`).
|
|
94
|
+
input:
|
|
95
|
+
description: Fired on intermediate segment edits before commit.
|
|
96
|
+
detail:
|
|
97
|
+
value:
|
|
98
|
+
type: string
|
|
99
|
+
description: In-flight value as ISO 8601.
|
|
100
|
+
invalid:
|
|
101
|
+
description: Fired when a constraint is violated (min/max range, parse error).
|
|
102
|
+
detail:
|
|
103
|
+
value:
|
|
104
|
+
type: string
|
|
105
|
+
description: Value that failed validation.
|
|
106
|
+
reason:
|
|
107
|
+
type: string
|
|
108
|
+
description: Why it failed (`min` | `max` | `parse`).
|
|
109
|
+
enum:
|
|
110
|
+
- min
|
|
111
|
+
- max
|
|
112
|
+
- parse
|
|
113
|
+
slots:
|
|
114
|
+
prefix:
|
|
115
|
+
description: Leading content (e.g. an `<icon-ui name="clock">`); sits before the hour segment.
|
|
116
|
+
suffix:
|
|
117
|
+
description: Trailing content (e.g. a timezone label); sits after the last segment.
|
|
118
|
+
states:
|
|
119
|
+
- name: idle
|
|
120
|
+
description: Default, ready for interaction.
|
|
121
|
+
- name: editing
|
|
122
|
+
description: One segment is the focus target.
|
|
123
|
+
attribute: editing
|
|
124
|
+
- name: disabled
|
|
125
|
+
description: Non-interactive; dimmed.
|
|
126
|
+
attribute: disabled
|
|
127
|
+
- name: focused
|
|
128
|
+
description: Keyboard focus ring on the host.
|
|
129
|
+
selector: :focus-within
|
|
130
|
+
traits:
|
|
131
|
+
- focusable
|
|
132
|
+
tokens:
|
|
133
|
+
--time-picker-bg:
|
|
134
|
+
description: Host background color.
|
|
135
|
+
--time-picker-fg:
|
|
136
|
+
description: Host text color.
|
|
137
|
+
--time-picker-fg-muted:
|
|
138
|
+
description: Muted text color (empty placeholder + separator).
|
|
139
|
+
--time-picker-border:
|
|
140
|
+
description: Host border color.
|
|
141
|
+
--time-picker-radius:
|
|
142
|
+
description: Host border radius.
|
|
143
|
+
--time-picker-px:
|
|
144
|
+
description: Host inline padding.
|
|
145
|
+
--time-picker-py:
|
|
146
|
+
description: Host block padding.
|
|
147
|
+
--time-picker-segment-bg-focus:
|
|
148
|
+
description: Focused segment background.
|
|
149
|
+
--time-picker-segment-fg-focus:
|
|
150
|
+
description: Focused segment text color.
|
|
151
|
+
--time-picker-separator-fg:
|
|
152
|
+
description: Separator (`:`) text color.
|
|
153
|
+
requiredIcons: []
|
|
154
|
+
a2ui:
|
|
155
|
+
rules:
|
|
156
|
+
- >-
|
|
157
|
+
`<time-picker-ui>` is the canonical standalone time-of-day picker.
|
|
158
|
+
Per ADR-0025 NEVER wrap a native `<input type="time">` — segments
|
|
159
|
+
are contenteditable spans + ElementInternals provides form
|
|
160
|
+
participation.
|
|
161
|
+
- >-
|
|
162
|
+
`value` MUST be ISO 8601 time `HH:mm` or `HH:mm:ss` (24-hour),
|
|
163
|
+
or empty string. Localized formats (e.g. "9:30 AM") are not
|
|
164
|
+
accepted; display formatting is derived from `hour-cycle`
|
|
165
|
+
regardless of how the value is stored.
|
|
166
|
+
- >-
|
|
167
|
+
`step` is in seconds. 60 = minute precision (default); 900 =
|
|
168
|
+
15-minute precision (meeting-time common); 1 = second precision
|
|
169
|
+
(requires `precision="second"`).
|
|
170
|
+
- >-
|
|
171
|
+
`precision="second"` exposes the seconds segment AND emits
|
|
172
|
+
`HH:mm:ss`. Default `precision="minute"` emits `HH:mm`.
|
|
173
|
+
- >-
|
|
174
|
+
`hour-cycle` overrides locale-derived behavior. Set explicitly
|
|
175
|
+
(`h12` / `h23`) when the surface needs a specific cycle (cron
|
|
176
|
+
editors, log queries, system surfaces).
|
|
177
|
+
- >-
|
|
178
|
+
For datetime selection use `<datetime-picker-ui>` (SPEC-038) —
|
|
179
|
+
it composes this primitive as its time pane.
|
|
180
|
+
anti_patterns:
|
|
181
|
+
- wrong: |
|
|
182
|
+
{"component": "TimePicker", "value": "9:30 AM"}
|
|
183
|
+
why: |
|
|
184
|
+
Localized format is not a valid `value`. The contract requires ISO
|
|
185
|
+
8601 time (`HH:mm` or `HH:mm:ss`, 24-hour). The display format is
|
|
186
|
+
derived from `hourCycle` regardless of how the value is stored.
|
|
187
|
+
fix: |
|
|
188
|
+
{"component": "TimePicker", "value": "09:30"}
|
|
189
|
+
- wrong: |
|
|
190
|
+
{"component": "TimePicker", "value": "2026-05-24T09:30"}
|
|
191
|
+
why: |
|
|
192
|
+
Datetime string is invalid for TimePicker — the date portion is
|
|
193
|
+
not accepted. For datetime selection use the DatetimePicker
|
|
194
|
+
component.
|
|
195
|
+
fix: |
|
|
196
|
+
{"component": "DatetimePicker", "value": "2026-05-24T09:30"}
|
|
197
|
+
- wrong: |
|
|
198
|
+
{"component": "TimePicker", "children": [
|
|
199
|
+
{"component": "Input", "type": "time"}
|
|
200
|
+
]}
|
|
201
|
+
why: |
|
|
202
|
+
Nesting a native time input violates ADR-0025 and bypasses
|
|
203
|
+
ElementInternals form participation.
|
|
204
|
+
fix: |
|
|
205
|
+
{"component": "TimePicker", "name": "field-name"}
|
|
206
|
+
examples:
|
|
207
|
+
- name: meeting-time-15min-step
|
|
208
|
+
description: Time-of-day picker with 15-minute step and business-hours bounds.
|
|
209
|
+
a2ui: |
|
|
210
|
+
[
|
|
211
|
+
{"id": "root", "component": "Field", "label": "Start time",
|
|
212
|
+
"children": ["t"]},
|
|
213
|
+
{"id": "t", "component": "TimePicker",
|
|
214
|
+
"name": "start-time", "value": "09:30",
|
|
215
|
+
"min": "08:00", "max": "18:00", "step": 900}
|
|
216
|
+
]
|
|
217
|
+
- name: log-cursor-second-precision
|
|
218
|
+
description: Time picker with second precision for a log-cursor query.
|
|
219
|
+
a2ui: |
|
|
220
|
+
[
|
|
221
|
+
{"id": "root", "component": "Field", "label": "Cursor",
|
|
222
|
+
"children": ["t"]},
|
|
223
|
+
{"id": "t", "component": "TimePicker",
|
|
224
|
+
"name": "cursor", "precision": "second", "step": 1,
|
|
225
|
+
"value": "14:30:45"}
|
|
226
|
+
]
|
|
227
|
+
- name: alarm-12h-cycle
|
|
228
|
+
description: Forced 12-hour cycle with AM/PM segment for an alarm picker.
|
|
229
|
+
a2ui: |
|
|
230
|
+
[
|
|
231
|
+
{"id": "root", "component": "Field", "label": "Alarm",
|
|
232
|
+
"children": ["t"]},
|
|
233
|
+
{"id": "t", "component": "TimePicker",
|
|
234
|
+
"name": "alarm", "hourCycle": "h12", "value": "07:00"}
|
|
235
|
+
]
|
|
236
|
+
keywords:
|
|
237
|
+
- time
|
|
238
|
+
- timepicker
|
|
239
|
+
- time-picker
|
|
240
|
+
- clock
|
|
241
|
+
- hour
|
|
242
|
+
- minute
|
|
243
|
+
- spinbutton
|
|
244
|
+
synonyms:
|
|
245
|
+
tags:
|
|
246
|
+
- TimeInput
|
|
247
|
+
- TimePicker
|
|
248
|
+
time:
|
|
249
|
+
- clock
|
|
250
|
+
- hour
|
|
251
|
+
- minute
|
|
252
|
+
- alarm
|
|
253
|
+
related:
|
|
254
|
+
- CalendarPicker
|
|
255
|
+
- Field
|
|
256
|
+
- Input
|