@adia-ai/web-components 0.6.33 → 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 +22 -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 +28 -28
- 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/table.css +162 -162
- 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/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,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* date-range-picker-ui — unit tests covering the SPEC-037 contract.
|
|
3
|
+
*
|
|
4
|
+
* Run under happy-dom (per vitest.config.js). showPopover/hidePopover are
|
|
5
|
+
* stubbed to no-ops by `packages/web-components/test-setup.js`, so popover
|
|
6
|
+
* state is tracked via the `open` reflected attribute and the `<popover>`
|
|
7
|
+
* div's presence, not via `:popover-open` selector matching.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
11
|
+
import '../../core/element.js';
|
|
12
|
+
import '../button/button.js';
|
|
13
|
+
import '../popover/popover.js';
|
|
14
|
+
import '../calendar-picker/calendar-picker.js';
|
|
15
|
+
import '../divider/divider.js';
|
|
16
|
+
import './date-range-picker.js';
|
|
17
|
+
|
|
18
|
+
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
19
|
+
|
|
20
|
+
function mount(html) {
|
|
21
|
+
const wrap = document.createElement('div');
|
|
22
|
+
wrap.innerHTML = html.trim();
|
|
23
|
+
document.body.appendChild(wrap);
|
|
24
|
+
return wrap.firstElementChild;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('date-range-picker-ui — structure + defaults', () => {
|
|
28
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
29
|
+
|
|
30
|
+
it('registers the custom element', () => {
|
|
31
|
+
expect(customElements.get('date-range-picker-ui')).toBeTruthy();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('stamps a default <button-ui slot="trigger"> when no slot="trigger" is authored', async () => {
|
|
35
|
+
const el = mount('<date-range-picker-ui></date-range-picker-ui>');
|
|
36
|
+
await tick();
|
|
37
|
+
const trigger = el.querySelector(':scope > [slot="trigger"]');
|
|
38
|
+
expect(trigger).not.toBeNull();
|
|
39
|
+
expect(trigger.localName).toBe('button-ui');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('stamps a popover container with role=dialog', async () => {
|
|
43
|
+
const el = mount('<date-range-picker-ui></date-range-picker-ui>');
|
|
44
|
+
await tick();
|
|
45
|
+
const popover = el.querySelector(':scope > [slot="popover"]');
|
|
46
|
+
expect(popover).not.toBeNull();
|
|
47
|
+
expect(popover.getAttribute('role')).toBe('dialog');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('stamps TWO <calendar-grid-ui> instances inside the popover', async () => {
|
|
51
|
+
const el = mount('<date-range-picker-ui></date-range-picker-ui>');
|
|
52
|
+
await tick();
|
|
53
|
+
const calArea = el.querySelector(':scope > [slot="popover"] > [data-calendar-area]');
|
|
54
|
+
expect(calArea).not.toBeNull();
|
|
55
|
+
const cals = calArea.querySelectorAll(':scope > calendar-grid-ui');
|
|
56
|
+
expect(cals.length).toBe(2);
|
|
57
|
+
expect(cals[0].hasAttribute('data-cal-from')).toBe(true);
|
|
58
|
+
expect(cals[1].hasAttribute('data-cal-to')).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('stamps a preset rail with the default 5 presets', async () => {
|
|
62
|
+
const el = mount('<date-range-picker-ui></date-range-picker-ui>');
|
|
63
|
+
await tick();
|
|
64
|
+
const rail = el.querySelector(':scope > [slot="popover"] > [data-preset-rail]');
|
|
65
|
+
expect(rail).not.toBeNull();
|
|
66
|
+
const buttons = rail.querySelectorAll('button-ui[data-preset-label]');
|
|
67
|
+
expect(buttons.length).toBe(5);
|
|
68
|
+
expect(buttons[0].dataset.presetLabel).toBe('Today');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('host carries role=combobox + aria-haspopup=dialog', async () => {
|
|
72
|
+
const el = mount('<date-range-picker-ui></date-range-picker-ui>');
|
|
73
|
+
await tick();
|
|
74
|
+
expect(el.getAttribute('role')).toBe('combobox');
|
|
75
|
+
expect(el.getAttribute('aria-haspopup')).toBe('dialog');
|
|
76
|
+
expect(el.getAttribute('aria-expanded')).toBe('false');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('date-range-picker-ui — props + reflection', () => {
|
|
81
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
82
|
+
|
|
83
|
+
it('reflects [min] and [max] onto both calendar panes', async () => {
|
|
84
|
+
const el = mount('<date-range-picker-ui min="2026-01-01" max="2026-12-31"></date-range-picker-ui>');
|
|
85
|
+
await tick();
|
|
86
|
+
const cals = el.querySelectorAll('calendar-grid-ui');
|
|
87
|
+
expect(cals[0].getAttribute('min')).toBe('2026-01-01');
|
|
88
|
+
expect(cals[0].getAttribute('max')).toBe('2026-12-31');
|
|
89
|
+
expect(cals[1].getAttribute('min')).toBe('2026-01-01');
|
|
90
|
+
expect(cals[1].getAttribute('max')).toBe('2026-12-31');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('parses JSON `value` into rangeValue', async () => {
|
|
94
|
+
const el = mount('<date-range-picker-ui value=\'{"from":"2026-01-01","to":"2026-01-07"}\'></date-range-picker-ui>');
|
|
95
|
+
await tick();
|
|
96
|
+
expect(el.rangeValue).toEqual({ from: '2026-01-01', to: '2026-01-07' });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('renders formatted-range text in the trigger when value is set', async () => {
|
|
100
|
+
const el = mount('<date-range-picker-ui value=\'{"from":"2026-01-01","to":"2026-01-07"}\'></date-range-picker-ui>');
|
|
101
|
+
await tick();
|
|
102
|
+
const trigger = el.querySelector(':scope > [slot="trigger"]');
|
|
103
|
+
expect(trigger.getAttribute('text')).toContain('Jan 1, 2026');
|
|
104
|
+
expect(trigger.getAttribute('text')).toContain('Jan 7, 2026');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('uses [placeholder] in the trigger when value is empty', async () => {
|
|
108
|
+
const el = mount('<date-range-picker-ui placeholder="Choose range"></date-range-picker-ui>');
|
|
109
|
+
await tick();
|
|
110
|
+
const trigger = el.querySelector(':scope > [slot="trigger"]');
|
|
111
|
+
expect(trigger.getAttribute('text')).toBe('Choose range');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('reflects [comparison] as an attribute', async () => {
|
|
115
|
+
const el = mount('<date-range-picker-ui comparison></date-range-picker-ui>');
|
|
116
|
+
await tick();
|
|
117
|
+
expect(el.hasAttribute('comparison')).toBe(true);
|
|
118
|
+
expect(el.comparison).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('[no-presets] removes the preset rail', async () => {
|
|
122
|
+
const el = mount('<date-range-picker-ui no-presets></date-range-picker-ui>');
|
|
123
|
+
await tick();
|
|
124
|
+
expect(el.querySelector(':scope > [slot="popover"] > [data-preset-rail]')).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('custom `presets` property overrides the default list', async () => {
|
|
128
|
+
const el = mount('<date-range-picker-ui></date-range-picker-ui>');
|
|
129
|
+
await tick();
|
|
130
|
+
el.presets = [
|
|
131
|
+
{ label: 'Q1', range: { from: '2026-01-01', to: '2026-03-31' } },
|
|
132
|
+
{ label: 'Q2', range: { from: '2026-04-01', to: '2026-06-30' } },
|
|
133
|
+
];
|
|
134
|
+
await tick();
|
|
135
|
+
const rail = el.querySelector(':scope > [slot="popover"] > [data-preset-rail]');
|
|
136
|
+
const buttons = rail.querySelectorAll('button-ui[data-preset-label]');
|
|
137
|
+
expect(buttons.length).toBe(2);
|
|
138
|
+
expect(buttons[0].dataset.presetLabel).toBe('Q1');
|
|
139
|
+
expect(buttons[1].dataset.presetLabel).toBe('Q2');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('date-range-picker-ui — open/close behavior', () => {
|
|
144
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
145
|
+
|
|
146
|
+
it('opens on trigger click + emits `open` event', async () => {
|
|
147
|
+
const el = mount('<date-range-picker-ui></date-range-picker-ui>');
|
|
148
|
+
await tick();
|
|
149
|
+
let openDetail = null;
|
|
150
|
+
el.addEventListener('open', (e) => { openDetail = e.detail; });
|
|
151
|
+
const trigger = el.querySelector(':scope > [slot="trigger"]');
|
|
152
|
+
trigger.click();
|
|
153
|
+
await tick();
|
|
154
|
+
expect(el.open).toBe(true);
|
|
155
|
+
expect(el.hasAttribute('open')).toBe(true);
|
|
156
|
+
expect(el.getAttribute('aria-expanded')).toBe('true');
|
|
157
|
+
expect(openDetail?.trigger).toBe('click');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('closes on second trigger click + emits `close`', async () => {
|
|
161
|
+
const el = mount('<date-range-picker-ui></date-range-picker-ui>');
|
|
162
|
+
await tick();
|
|
163
|
+
const trigger = el.querySelector(':scope > [slot="trigger"]');
|
|
164
|
+
let closeDetail = null;
|
|
165
|
+
el.addEventListener('close', (e) => { closeDetail = e.detail; });
|
|
166
|
+
trigger.click();
|
|
167
|
+
await tick();
|
|
168
|
+
trigger.click();
|
|
169
|
+
await tick();
|
|
170
|
+
expect(el.open).toBe(false);
|
|
171
|
+
expect(closeDetail).not.toBeNull();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('Escape closes the popover + emits close with reason "escape"', async () => {
|
|
175
|
+
const el = mount('<date-range-picker-ui></date-range-picker-ui>');
|
|
176
|
+
await tick();
|
|
177
|
+
el.open = true;
|
|
178
|
+
await tick();
|
|
179
|
+
let reason = null;
|
|
180
|
+
el.addEventListener('close', (e) => { reason = e.detail?.reason; });
|
|
181
|
+
const popover = el.querySelector(':scope > [slot="popover"]');
|
|
182
|
+
const ev = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true });
|
|
183
|
+
popover.dispatchEvent(ev);
|
|
184
|
+
await tick();
|
|
185
|
+
expect(el.open).toBe(false);
|
|
186
|
+
expect(reason).toBe('escape');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('Enter on the trigger opens the popover with trigger=keyboard', async () => {
|
|
190
|
+
const el = mount('<date-range-picker-ui></date-range-picker-ui>');
|
|
191
|
+
await tick();
|
|
192
|
+
const trigger = el.querySelector(':scope > [slot="trigger"]');
|
|
193
|
+
let openDetail = null;
|
|
194
|
+
el.addEventListener('open', (e) => { openDetail = e.detail; });
|
|
195
|
+
trigger.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }));
|
|
196
|
+
await tick();
|
|
197
|
+
expect(el.open).toBe(true);
|
|
198
|
+
expect(openDetail?.trigger).toBe('keyboard');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('[disabled] blocks the trigger from opening', async () => {
|
|
202
|
+
const el = mount('<date-range-picker-ui disabled></date-range-picker-ui>');
|
|
203
|
+
await tick();
|
|
204
|
+
const trigger = el.querySelector(':scope > [slot="trigger"]');
|
|
205
|
+
trigger.click();
|
|
206
|
+
await tick();
|
|
207
|
+
expect(el.open).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('[readonly] blocks the popover from opening', async () => {
|
|
211
|
+
const el = mount('<date-range-picker-ui readonly></date-range-picker-ui>');
|
|
212
|
+
await tick();
|
|
213
|
+
const trigger = el.querySelector(':scope > [slot="trigger"]');
|
|
214
|
+
trigger.click();
|
|
215
|
+
await tick();
|
|
216
|
+
expect(el.open).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('date-range-picker-ui — selection + commit', () => {
|
|
221
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
222
|
+
|
|
223
|
+
it('clicking a preset commits value + closes popover + fires `change` with presetLabel', async () => {
|
|
224
|
+
const el = mount('<date-range-picker-ui></date-range-picker-ui>');
|
|
225
|
+
await tick();
|
|
226
|
+
el.open = true;
|
|
227
|
+
await tick();
|
|
228
|
+
let detail = null;
|
|
229
|
+
el.addEventListener('change', (e) => { detail = e.detail; });
|
|
230
|
+
const rail = el.querySelector(':scope > [slot="popover"] > [data-preset-rail]');
|
|
231
|
+
const todayBtn = rail.querySelector('button-ui[data-preset-label="Today"]');
|
|
232
|
+
todayBtn.click();
|
|
233
|
+
await tick();
|
|
234
|
+
expect(detail).not.toBeNull();
|
|
235
|
+
expect(detail.presetLabel).toBe('Today');
|
|
236
|
+
expect(detail.value.from).toBe(detail.value.to);
|
|
237
|
+
expect(el.open).toBe(false);
|
|
238
|
+
expect(el.rangeValue).not.toBeNull();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('applyPreset(label) commits the matching preset', async () => {
|
|
242
|
+
const el = mount('<date-range-picker-ui></date-range-picker-ui>');
|
|
243
|
+
await tick();
|
|
244
|
+
let detail = null;
|
|
245
|
+
el.addEventListener('change', (e) => { detail = e.detail; });
|
|
246
|
+
const result = el.applyPreset('Last 7 days');
|
|
247
|
+
expect(result).toBe(true);
|
|
248
|
+
await tick();
|
|
249
|
+
expect(detail).not.toBeNull();
|
|
250
|
+
expect(detail.presetLabel).toBe('Last 7 days');
|
|
251
|
+
expect(detail.value.from < detail.value.to).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('applyPreset(unknown) returns false and does not commit', async () => {
|
|
255
|
+
const el = mount('<date-range-picker-ui></date-range-picker-ui>');
|
|
256
|
+
await tick();
|
|
257
|
+
let fired = false;
|
|
258
|
+
el.addEventListener('change', () => { fired = true; });
|
|
259
|
+
expect(el.applyPreset('NonExistent')).toBe(false);
|
|
260
|
+
expect(fired).toBe(false);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('emits `input` (partial state) when only `from` is selected', async () => {
|
|
264
|
+
const el = mount('<date-range-picker-ui></date-range-picker-ui>');
|
|
265
|
+
await tick();
|
|
266
|
+
let inputDetail = null;
|
|
267
|
+
let changeFired = false;
|
|
268
|
+
el.addEventListener('input', (e) => { inputDetail = e.detail; });
|
|
269
|
+
el.addEventListener('change', () => { changeFired = true; });
|
|
270
|
+
const calFrom = el.querySelector('calendar-grid-ui[data-cal-from]');
|
|
271
|
+
calFrom.value = '2026-03-15';
|
|
272
|
+
calFrom.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-03-15' } }));
|
|
273
|
+
await tick();
|
|
274
|
+
expect(inputDetail).not.toBeNull();
|
|
275
|
+
expect(inputDetail.value.from).toBe('2026-03-15');
|
|
276
|
+
expect(inputDetail.value.to).toBeNull();
|
|
277
|
+
expect(changeFired).toBe(false);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('commits `change` when both panes have selections in order', async () => {
|
|
281
|
+
const el = mount('<date-range-picker-ui></date-range-picker-ui>');
|
|
282
|
+
await tick();
|
|
283
|
+
let detail = null;
|
|
284
|
+
el.addEventListener('change', (e) => { detail = e.detail; });
|
|
285
|
+
const calFrom = el.querySelector('calendar-grid-ui[data-cal-from]');
|
|
286
|
+
const calTo = el.querySelector('calendar-grid-ui[data-cal-to]');
|
|
287
|
+
calFrom.value = '2026-03-01';
|
|
288
|
+
calFrom.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-03-01' } }));
|
|
289
|
+
await tick();
|
|
290
|
+
calTo.value = '2026-03-15';
|
|
291
|
+
calTo.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-03-15' } }));
|
|
292
|
+
await tick();
|
|
293
|
+
expect(detail).not.toBeNull();
|
|
294
|
+
expect(detail.value).toEqual({ from: '2026-03-01', to: '2026-03-15' });
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('reversed range fires `invalid`, not `change`', async () => {
|
|
298
|
+
const el = mount('<date-range-picker-ui></date-range-picker-ui>');
|
|
299
|
+
await tick();
|
|
300
|
+
let invalidDetail = null;
|
|
301
|
+
let changeFired = false;
|
|
302
|
+
el.addEventListener('invalid', (e) => { invalidDetail = e.detail; });
|
|
303
|
+
el.addEventListener('change', () => { changeFired = true; });
|
|
304
|
+
const calFrom = el.querySelector('calendar-grid-ui[data-cal-from]');
|
|
305
|
+
const calTo = el.querySelector('calendar-grid-ui[data-cal-to]');
|
|
306
|
+
// Click `to` first (with a date), then `from` AFTER it (reversed).
|
|
307
|
+
calTo.value = '2026-03-01';
|
|
308
|
+
calTo.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-03-01' } }));
|
|
309
|
+
await tick();
|
|
310
|
+
calFrom.value = '2026-03-15';
|
|
311
|
+
calFrom.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-03-15' } }));
|
|
312
|
+
await tick();
|
|
313
|
+
expect(invalidDetail).not.toBeNull();
|
|
314
|
+
expect(invalidDetail.reason).toBe('reversed');
|
|
315
|
+
expect(changeFired).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('clear() resets the value', async () => {
|
|
319
|
+
const el = mount('<date-range-picker-ui value=\'{"from":"2026-01-01","to":"2026-01-07"}\'></date-range-picker-ui>');
|
|
320
|
+
await tick();
|
|
321
|
+
el.clear();
|
|
322
|
+
await tick();
|
|
323
|
+
expect(el.value).toBe('');
|
|
324
|
+
expect(el.rangeValue).toBeNull();
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe('date-range-picker-ui — comparison mode', () => {
|
|
329
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
330
|
+
|
|
331
|
+
it('adds a comparison preset cluster when [comparison] is set', async () => {
|
|
332
|
+
const el = mount('<date-range-picker-ui comparison></date-range-picker-ui>');
|
|
333
|
+
await tick();
|
|
334
|
+
const rail = el.querySelector(':scope > [slot="popover"] > [data-preset-rail]');
|
|
335
|
+
const cmpButtons = rail.querySelectorAll('button-ui[data-comparison-preset-label]');
|
|
336
|
+
expect(cmpButtons.length).toBe(3);
|
|
337
|
+
expect(cmpButtons[0].dataset.comparisonPresetLabel).toBe('Previous period');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('parses compare-value into rangeCompareValue', async () => {
|
|
341
|
+
const el = mount('<date-range-picker-ui comparison compare-value=\'{"from":"2026-04-01","to":"2026-04-30"}\'></date-range-picker-ui>');
|
|
342
|
+
await tick();
|
|
343
|
+
expect(el.rangeCompareValue).toEqual({ from: '2026-04-01', to: '2026-04-30' });
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('change detail carries compareValue when comparison is set', async () => {
|
|
347
|
+
const el = mount('<date-range-picker-ui comparison compare-value=\'{"from":"2026-04-01","to":"2026-04-30"}\'></date-range-picker-ui>');
|
|
348
|
+
await tick();
|
|
349
|
+
let detail = null;
|
|
350
|
+
el.addEventListener('change', (e) => { detail = e.detail; });
|
|
351
|
+
const calFrom = el.querySelector('calendar-grid-ui[data-cal-from]');
|
|
352
|
+
const calTo = el.querySelector('calendar-grid-ui[data-cal-to]');
|
|
353
|
+
calFrom.value = '2026-05-01';
|
|
354
|
+
calFrom.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-05-01' } }));
|
|
355
|
+
await tick();
|
|
356
|
+
calTo.value = '2026-05-31';
|
|
357
|
+
calTo.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: '2026-05-31' } }));
|
|
358
|
+
await tick();
|
|
359
|
+
expect(detail).not.toBeNull();
|
|
360
|
+
expect(detail.value).toEqual({ from: '2026-05-01', to: '2026-05-31' });
|
|
361
|
+
expect(detail.compareValue).toEqual({ from: '2026-04-01', to: '2026-04-30' });
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe('date-range-picker-ui — form participation', () => {
|
|
366
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
367
|
+
|
|
368
|
+
it('is form-associated', () => {
|
|
369
|
+
expect(customElements.get('date-range-picker-ui').formAssociated).toBe(true);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('exposes ElementInternals via the host', async () => {
|
|
373
|
+
const el = mount('<date-range-picker-ui name="period"></date-range-picker-ui>');
|
|
374
|
+
await tick();
|
|
375
|
+
expect(el.internals).toBeDefined();
|
|
376
|
+
expect(typeof el.internals.setFormValue).toBe('function');
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('runs constraint validation on a reversed range (post-commit attempt is rejected)', async () => {
|
|
380
|
+
const el = mount('<date-range-picker-ui name="period" value=\'{"from":"2026-01-01","to":"2026-01-07"}\'></date-range-picker-ui>');
|
|
381
|
+
await tick();
|
|
382
|
+
// Apply a reversed range programmatically via direct value write,
|
|
383
|
+
// then sync — happy-dom's setValidity is a no-op, so we just confirm
|
|
384
|
+
// syncValue doesn't throw.
|
|
385
|
+
expect(() => el.syncValue('{"from":"2026-12-31","to":"2026-01-01"}')).not.toThrow();
|
|
386
|
+
});
|
|
387
|
+
});
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
2
|
+
name: UIDateRangePicker
|
|
3
|
+
tag: date-range-picker-ui
|
|
4
|
+
status: experimental
|
|
5
|
+
component: DateRangePicker
|
|
6
|
+
category: input
|
|
7
|
+
version: 1
|
|
8
|
+
description: >-
|
|
9
|
+
Compound form primitive for selecting a start + end date pair with
|
|
10
|
+
optional named preset shortcuts ("Today", "Last 7 days", "This month",
|
|
11
|
+
...). Default presentation is a trigger button showing the formatted
|
|
12
|
+
range; on activation a popover opens with two synchronized calendar
|
|
13
|
+
panes + a preset side rail. Composes <calendar-picker-ui> twice;
|
|
14
|
+
emits {from, to} ISO 8601 dates. Optional comparison-range mode
|
|
15
|
+
emits a secondary range for analytics surfaces.
|
|
16
|
+
# Per ADR-0027 — primitives that programmatically create other primitives
|
|
17
|
+
# do NOT auto-import them. Consumer (or demo shell) must explicitly import.
|
|
18
|
+
composes:
|
|
19
|
+
- calendar-grid-ui # two grids (start + end) — substrate primitive
|
|
20
|
+
- button-ui # trigger + preset rail + footer actions
|
|
21
|
+
- popover-ui # the floating panel
|
|
22
|
+
- text-ui # trigger label + preset labels
|
|
23
|
+
- divider-ui # separator between preset rail and calendars
|
|
24
|
+
props:
|
|
25
|
+
name:
|
|
26
|
+
description: Form field name for form data submission. The selected range serializes as JSON `{"from":"...","to":"..."}` under this key.
|
|
27
|
+
type: string
|
|
28
|
+
default: ''
|
|
29
|
+
reflect: true
|
|
30
|
+
value:
|
|
31
|
+
description: 'Selected range as `{from, to}` ISO 8601 dates. Null when empty. Emitted on commit.'
|
|
32
|
+
type: string
|
|
33
|
+
default: ''
|
|
34
|
+
dynamic: true
|
|
35
|
+
compareValue:
|
|
36
|
+
description: 'Comparison range when `[comparison]` is set. Same `{from, to}` shape as `value`.'
|
|
37
|
+
type: string
|
|
38
|
+
default: ''
|
|
39
|
+
attribute: compare-value
|
|
40
|
+
dynamic: true
|
|
41
|
+
comparison:
|
|
42
|
+
description: Enable comparison-range mode. Renders a second preset cluster and emits a secondary range under `<name>-compare`.
|
|
43
|
+
type: boolean
|
|
44
|
+
default: false
|
|
45
|
+
reflect: true
|
|
46
|
+
min:
|
|
47
|
+
description: Earliest selectable ISO date (inclusive). Days before are disabled in both panes.
|
|
48
|
+
type: string
|
|
49
|
+
default: ''
|
|
50
|
+
reflect: true
|
|
51
|
+
max:
|
|
52
|
+
description: Latest selectable ISO date (inclusive). Days after are disabled in both panes.
|
|
53
|
+
type: string
|
|
54
|
+
default: ''
|
|
55
|
+
reflect: true
|
|
56
|
+
disabled:
|
|
57
|
+
description: Block all interaction. Trigger remains rendered but does not open the popover.
|
|
58
|
+
type: boolean
|
|
59
|
+
default: false
|
|
60
|
+
reflect: true
|
|
61
|
+
readonly:
|
|
62
|
+
description: Block edits; allow keyboard navigation for screen-reader inspection.
|
|
63
|
+
type: boolean
|
|
64
|
+
default: false
|
|
65
|
+
reflect: true
|
|
66
|
+
required:
|
|
67
|
+
description: Required for validation when inside a `<form>`.
|
|
68
|
+
type: boolean
|
|
69
|
+
default: false
|
|
70
|
+
reflect: true
|
|
71
|
+
open:
|
|
72
|
+
description: Whether the popover is currently open.
|
|
73
|
+
type: boolean
|
|
74
|
+
default: false
|
|
75
|
+
reflect: true
|
|
76
|
+
placeholder:
|
|
77
|
+
description: Text shown in the trigger when the value is empty.
|
|
78
|
+
type: string
|
|
79
|
+
default: Select range
|
|
80
|
+
format:
|
|
81
|
+
description: 'Trigger button date format. `short`: `Jan 1, 2026`; `long`: `January 1, 2026`; `iso`: `2026-01-01`.'
|
|
82
|
+
type: string
|
|
83
|
+
default: short
|
|
84
|
+
enum:
|
|
85
|
+
- short
|
|
86
|
+
- long
|
|
87
|
+
- iso
|
|
88
|
+
noPresets:
|
|
89
|
+
description: Hide the preset side rail.
|
|
90
|
+
type: boolean
|
|
91
|
+
default: false
|
|
92
|
+
attribute: no-presets
|
|
93
|
+
reflect: true
|
|
94
|
+
events:
|
|
95
|
+
change:
|
|
96
|
+
description: Fired when the range commits (both `from` AND `to` selected, OR a preset clicked).
|
|
97
|
+
detail:
|
|
98
|
+
value: object
|
|
99
|
+
compareValue: object
|
|
100
|
+
presetLabel: string
|
|
101
|
+
input:
|
|
102
|
+
description: Fired during selection — e.g. user clicked `from` but has not yet picked `to`.
|
|
103
|
+
detail:
|
|
104
|
+
value: object
|
|
105
|
+
open:
|
|
106
|
+
description: Popover opened (user clicked the trigger or pressed Enter / Space).
|
|
107
|
+
detail:
|
|
108
|
+
trigger: string
|
|
109
|
+
close:
|
|
110
|
+
description: Popover closed (without commit, or after commit).
|
|
111
|
+
detail:
|
|
112
|
+
reason: string
|
|
113
|
+
invalid:
|
|
114
|
+
description: A constraint failed (out of range, end < start).
|
|
115
|
+
detail:
|
|
116
|
+
value: object
|
|
117
|
+
reason: string
|
|
118
|
+
slots:
|
|
119
|
+
trigger:
|
|
120
|
+
description: Custom trigger replacement. When omitted, a default `<button-ui>` is stamped.
|
|
121
|
+
presets:
|
|
122
|
+
description: Custom preset content replacing the default preset rail.
|
|
123
|
+
footer:
|
|
124
|
+
description: Optional footer area below the calendars (e.g., "Apply" / "Cancel" buttons for explicit-commit flows).
|
|
125
|
+
states:
|
|
126
|
+
- name: idle
|
|
127
|
+
description: Closed, no pending selection.
|
|
128
|
+
- name: open-pending-from
|
|
129
|
+
description: Popover open, user has not yet clicked a start date.
|
|
130
|
+
attribute: open
|
|
131
|
+
- name: open-pending-to
|
|
132
|
+
description: Start clicked, awaiting end click; hover preview active.
|
|
133
|
+
attribute: open
|
|
134
|
+
- name: open-committed
|
|
135
|
+
description: Both ends clicked, awaiting Apply (only in explicit-commit mode with `footer` slot).
|
|
136
|
+
attribute: open
|
|
137
|
+
- name: disabled
|
|
138
|
+
description: Non-interactive; dimmed.
|
|
139
|
+
attribute: disabled
|
|
140
|
+
- name: readonly
|
|
141
|
+
description: Read-only; trigger keyboard-focusable but popover does not open.
|
|
142
|
+
attribute: readonly
|
|
143
|
+
traits: []
|
|
144
|
+
tokens:
|
|
145
|
+
--date-range-picker-bg:
|
|
146
|
+
description: Host background.
|
|
147
|
+
default: var(--a-bg)
|
|
148
|
+
--date-range-picker-popover-bg:
|
|
149
|
+
description: Popover background.
|
|
150
|
+
default: var(--a-bg-subtle)
|
|
151
|
+
--date-range-picker-popover-border:
|
|
152
|
+
description: Popover border color.
|
|
153
|
+
default: var(--a-border-subtle)
|
|
154
|
+
--date-range-picker-popover-radius:
|
|
155
|
+
description: Popover border radius.
|
|
156
|
+
default: var(--a-radius-lg)
|
|
157
|
+
--date-range-picker-popover-shadow:
|
|
158
|
+
description: Popover box shadow.
|
|
159
|
+
default: var(--a-shadow-lg)
|
|
160
|
+
--date-range-picker-preset-bg:
|
|
161
|
+
description: Preset row background (idle).
|
|
162
|
+
default: transparent
|
|
163
|
+
--date-range-picker-preset-bg-hover:
|
|
164
|
+
description: Preset row background on hover.
|
|
165
|
+
default: var(--a-bg-hover)
|
|
166
|
+
--date-range-picker-selected-bg:
|
|
167
|
+
description: Selected day-cell background (cascaded into the inner calendar via --calendar-picker-day-bg-selected).
|
|
168
|
+
default: var(--a-accent)
|
|
169
|
+
--date-range-picker-selected-fg:
|
|
170
|
+
description: Selected day-cell foreground.
|
|
171
|
+
default: var(--a-accent-fg)
|
|
172
|
+
--date-range-picker-preview-bg:
|
|
173
|
+
description: In-range preview background (between from and pending to).
|
|
174
|
+
default: var(--a-accent-muted)
|
|
175
|
+
--date-range-picker-px:
|
|
176
|
+
description: Horizontal popover padding.
|
|
177
|
+
default: var(--a-space-3)
|
|
178
|
+
--date-range-picker-py:
|
|
179
|
+
description: Vertical popover padding.
|
|
180
|
+
default: var(--a-space-2)
|
|
181
|
+
requiredIcons:
|
|
182
|
+
- calendar
|
|
183
|
+
- caret-down
|
|
184
|
+
- arrow-right
|
|
185
|
+
a2ui:
|
|
186
|
+
rules:
|
|
187
|
+
- rule: 'DateRangePicker.value MUST be `{from, to}` with both ISO 8601 dates, OR null. Either side null is invalid mid-state and the validator should reject it (use `input` event for partial state).'
|
|
188
|
+
reason: 'Range integrity contract.'
|
|
189
|
+
- rule: 'DateRangePicker.value.to MUST be `>=` value.from lexicographically. Reversed ranges trigger `invalid` and do NOT commit.'
|
|
190
|
+
reason: 'Range ordering constraint.'
|
|
191
|
+
- rule: 'When `comparison: true`, both `value` AND `compareValue` MUST be set on commit. If only one is set, the form participation emits the set one and omits the other.'
|
|
192
|
+
reason: 'Comparison-mode pairing.'
|
|
193
|
+
- rule: 'presets array entries each require both `label` (string) and `range` (`{from, to}`). Empty preset arrays are valid (rail renders empty).'
|
|
194
|
+
reason: 'Preset shape contract.'
|
|
195
|
+
- rule: 'Use DateRangePicker for date ranges. Do NOT compose two adjacent `<calendar-picker-ui>` instances + JS synchronization — that is the pattern this primitive replaces.'
|
|
196
|
+
reason: 'Adoption signal: prefer the canonical primitive over two-calendar composition.'
|
|
197
|
+
anti_patterns:
|
|
198
|
+
- wrong: |
|
|
199
|
+
{"component": "DateRangePicker", "value": "2026-01-01 to 2026-01-31"}
|
|
200
|
+
why: |
|
|
201
|
+
`value` is a string, not an object. The contract is `{from, to}` ISO 8601 dates.
|
|
202
|
+
fix: |
|
|
203
|
+
{"component": "DateRangePicker", "value": {"from": "2026-01-01", "to": "2026-01-31"}}
|
|
204
|
+
- wrong: |
|
|
205
|
+
{"component": "DateRangePicker", "value": {"from": "2026-12-31", "to": "2026-01-01"}}
|
|
206
|
+
why: |
|
|
207
|
+
Reversed range — `to < from`. This fires `invalid` and never commits.
|
|
208
|
+
fix: |
|
|
209
|
+
{"component": "DateRangePicker", "value": {"from": "2026-01-01", "to": "2026-12-31"}}
|
|
210
|
+
- wrong: |
|
|
211
|
+
Two adjacent <calendar-picker-ui> instances + JS synchronization.
|
|
212
|
+
why: |
|
|
213
|
+
This is the pattern the primitive replaces. Reinvents linked-pane behavior,
|
|
214
|
+
misses preset rail, has no a11y range-announcement.
|
|
215
|
+
fix: |
|
|
216
|
+
Use <date-range-picker-ui>.
|
|
217
|
+
examples:
|
|
218
|
+
- name: canonical
|
|
219
|
+
description: Form-field-wrapped picker with default presets.
|
|
220
|
+
a2ui: |
|
|
221
|
+
[
|
|
222
|
+
{
|
|
223
|
+
"id": "field",
|
|
224
|
+
"component": "Field",
|
|
225
|
+
"label": "Report period",
|
|
226
|
+
"children": ["dr"]
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"id": "dr",
|
|
230
|
+
"component": "DateRangePicker",
|
|
231
|
+
"name": "report-period",
|
|
232
|
+
"min": "2026-01-01",
|
|
233
|
+
"max": "2026-12-31"
|
|
234
|
+
}
|
|
235
|
+
]
|
|
236
|
+
- name: comparison
|
|
237
|
+
description: Analytics-surface picker with comparison range enabled.
|
|
238
|
+
a2ui: |
|
|
239
|
+
[
|
|
240
|
+
{
|
|
241
|
+
"id": "dr",
|
|
242
|
+
"component": "DateRangePicker",
|
|
243
|
+
"name": "period",
|
|
244
|
+
"comparison": true,
|
|
245
|
+
"value": {"from": "2026-05-01", "to": "2026-05-31"},
|
|
246
|
+
"compareValue": {"from": "2026-04-01", "to": "2026-04-30"}
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
- name: no-presets
|
|
250
|
+
description: Custom-only picker with the preset rail hidden.
|
|
251
|
+
a2ui: |
|
|
252
|
+
[
|
|
253
|
+
{
|
|
254
|
+
"id": "dr",
|
|
255
|
+
"component": "DateRangePicker",
|
|
256
|
+
"name": "period",
|
|
257
|
+
"noPresets": true
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
keywords:
|
|
261
|
+
- daterangepicker
|
|
262
|
+
- date-range-picker
|
|
263
|
+
- date-range
|
|
264
|
+
- range
|
|
265
|
+
- daterange
|
|
266
|
+
- period
|
|
267
|
+
- reporting-period
|
|
268
|
+
synonyms:
|
|
269
|
+
tags:
|
|
270
|
+
- DateRangePicker
|
|
271
|
+
- DateRange
|
|
272
|
+
- DateRangeInput
|
|
273
|
+
range:
|
|
274
|
+
- date-range
|
|
275
|
+
- period
|
|
276
|
+
- report-period
|
|
277
|
+
period:
|
|
278
|
+
- date-range
|
|
279
|
+
- range
|
|
280
|
+
- reporting-period
|
|
281
|
+
related:
|
|
282
|
+
- calendar-picker
|
|
283
|
+
- popover
|
|
284
|
+
- field
|
|
285
|
+
- button
|