@astryxdesign/core 0.7.4-canary.faa934f → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  3. package/dist/CommandPalette/CommandPalette.js +6 -11
  4. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  5. package/dist/CommandPalette/CommandPaletteInput.js +7 -1
  6. package/dist/CommandPalette/CommandPaletteItem.d.ts +1 -0
  7. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  8. package/dist/CommandPalette/CommandPaletteItem.js +5 -1
  9. package/dist/DateInput/DateInput.d.ts +6 -4
  10. package/dist/DateInput/DateInput.d.ts.map +1 -1
  11. package/dist/DateInput/DateInput.js +5 -1
  12. package/dist/DateInput/NativeDateField.d.ts.map +1 -1
  13. package/dist/DateInput/NativeDateField.js +5 -1
  14. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  15. package/dist/DateInput/TouchDateField.js +5 -1
  16. package/dist/DateInput/tokens.stylex.d.ts.map +1 -1
  17. package/dist/DateInput/tokens.stylex.js +3 -1
  18. package/dist/FileInput/FileInput.d.ts.map +1 -1
  19. package/dist/FileInput/FileInput.js +16 -2
  20. package/package.json +4 -4
  21. package/src/BottomSheet/BottomSheet.spec.md +1 -0
  22. package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +25 -2
  23. package/src/BottomSheet/BottomSheetSwitcher.spec.md +7 -6
  24. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +221 -1
  25. package/src/BottomSheet/__tests__/BottomSheetNativeModal.a11y.browser.spec.ts +461 -0
  26. package/src/CommandPalette/CommandPalette.doc.mjs +4 -1
  27. package/src/CommandPalette/CommandPalette.test.tsx +4 -2
  28. package/src/CommandPalette/CommandPalette.tsx +6 -11
  29. package/src/CommandPalette/CommandPaletteInput.test.tsx +3 -1
  30. package/src/CommandPalette/CommandPaletteInput.tsx +10 -1
  31. package/src/CommandPalette/CommandPaletteItem.doc.mjs +17 -5
  32. package/src/CommandPalette/CommandPaletteItem.test.tsx +65 -12
  33. package/src/CommandPalette/CommandPaletteItem.tsx +6 -1
  34. package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +250 -20
  35. package/src/DateInput/DateInput.doc.mjs +23 -5
  36. package/src/DateInput/DateInput.spec.md +52 -45
  37. package/src/DateInput/DateInput.tsx +13 -7
  38. package/src/DateInput/NativeDateField.tsx +6 -2
  39. package/src/DateInput/TouchDateField.tsx +6 -2
  40. package/src/DateInput/__tests__/DateInputAudit.a11y.chromium.spec.ts +576 -0
  41. package/src/DateInput/tokens.stylex.ts +3 -1
  42. package/src/FileInput/FileInput.tsx +18 -2
  43. package/src/Overlay/Overlay.spec.md +304 -0
  44. package/src/__tests__/BusyOwnerAnnouncement.test.tsx +217 -1
@@ -195,17 +195,6 @@ describe('CommandPaletteItem', () => {
195
195
  expect(item).toHaveAttribute('aria-selected', 'true');
196
196
  });
197
197
 
198
- it('paints context-derived highlight when the item is not selected', () => {
199
- renderWithContext(
200
- <CommandPaletteItem value="test">Item</CommandPaletteItem>,
201
- {highlightedIndex: 0},
202
- );
203
-
204
- expect(getComputedStyle(screen.getByRole('option')).backgroundColor).toBe(
205
- 'var(--color-overlay-hover)',
206
- );
207
- });
208
-
209
198
  it('lets explicit visual state override context-derived state', () => {
210
199
  renderWithContext(
211
200
  <CommandPaletteItem value="test" isHighlighted={false} isSelected={false}>
@@ -216,7 +205,8 @@ describe('CommandPaletteItem', () => {
216
205
 
217
206
  const item = screen.getByRole('option');
218
207
  expect(item).toHaveAttribute('aria-selected', 'false');
219
- expect(getComputedStyle(item).backgroundColor).toBe('rgba(0, 0, 0, 0)');
208
+ expect(item).not.toHaveAttribute('data-selected');
209
+ expect(item).not.toHaveAttribute('data-highlighted');
220
210
  });
221
211
 
222
212
  it('protects owned option semantics from native passthrough props', () => {
@@ -354,6 +344,69 @@ describe('CommandPaletteItem', () => {
354
344
  expect(item).toHaveStyle({marginBlockStart: '8px'});
355
345
  });
356
346
 
347
+ it('reflects selected and disabled state on its theme target', () => {
348
+ render(
349
+ <>
350
+ <CommandPaletteItem value="rest">Rest</CommandPaletteItem>
351
+ <CommandPaletteItem value="selected" isSelected>
352
+ Selected
353
+ </CommandPaletteItem>
354
+ <CommandPaletteItem value="disabled" isDisabled>
355
+ Disabled
356
+ </CommandPaletteItem>
357
+ </>,
358
+ );
359
+
360
+ const rest = screen.getByRole('option', {name: 'Rest'});
361
+ expect(rest).not.toHaveAttribute('data-selected');
362
+ expect(rest).not.toHaveAttribute('data-disabled');
363
+ expect(screen.getByRole('option', {name: 'Selected'})).toHaveAttribute(
364
+ 'data-selected',
365
+ 'selected',
366
+ );
367
+ expect(screen.getByRole('option', {name: 'Disabled'})).toHaveAttribute(
368
+ 'data-disabled',
369
+ 'disabled',
370
+ );
371
+ });
372
+
373
+ it('reflects context-derived selection on its theme target', () => {
374
+ renderWithContext(
375
+ <CommandPaletteItem value="test">Item</CommandPaletteItem>,
376
+ {value: 'test'},
377
+ );
378
+
379
+ expect(screen.getByRole('option')).toHaveAttribute(
380
+ 'data-selected',
381
+ 'selected',
382
+ );
383
+ });
384
+
385
+ it('reflects explicit and context-derived highlight on its theme target', () => {
386
+ renderWithContext(
387
+ <>
388
+ <CommandPaletteItem value="test">
389
+ Context highlighted
390
+ </CommandPaletteItem>
391
+ <CommandPaletteItem value="other" isHighlighted>
392
+ Explicitly highlighted
393
+ </CommandPaletteItem>
394
+ <CommandPaletteItem value="rest">Rest</CommandPaletteItem>
395
+ </>,
396
+ {highlightedIndex: 0},
397
+ );
398
+
399
+ expect(
400
+ screen.getByRole('option', {name: 'Context highlighted'}),
401
+ ).toHaveAttribute('data-highlighted', 'highlighted');
402
+ expect(
403
+ screen.getByRole('option', {name: 'Explicitly highlighted'}),
404
+ ).toHaveAttribute('data-highlighted', 'highlighted');
405
+ expect(screen.getByRole('option', {name: 'Rest'})).not.toHaveAttribute(
406
+ 'data-highlighted',
407
+ );
408
+ });
409
+
357
410
  it('sets data-value attribute', () => {
358
411
  render(<CommandPaletteItem value="my-value">Item</CommandPaletteItem>);
359
412
  expect(screen.getByRole('option')).toHaveAttribute(
@@ -88,6 +88,7 @@ export interface CommandPaletteItemProps extends Omit<
88
88
  isHighlighted?: boolean;
89
89
  /**
90
90
  * Whether this item is currently selected (picker mode).
91
+ * When omitted inside CommandPalette, derived from the palette value.
91
92
  * @default false
92
93
  */
93
94
  isSelected?: boolean;
@@ -202,7 +203,11 @@ export function CommandPaletteItem({
202
203
  }
203
204
  onMouseEnter={onMouseEnterProp}
204
205
  {...mergeProps(
205
- themeProps('command-palette-item'),
206
+ themeProps('command-palette-item', {
207
+ highlighted: isHighlighted ? 'highlighted' : null,
208
+ selected: isSelected ? 'selected' : null,
209
+ disabled: isDisabled ? 'disabled' : null,
210
+ }),
206
211
  stylex.props(
207
212
  styles.item,
208
213
  !isDisabled && interactionOverlayStyles.backgroundColor,
@@ -2,7 +2,9 @@
2
2
 
3
3
  /**
4
4
  * @file CommandPaletteItemAudit.a11y.chromium.spec.ts
5
- * @input Exact-head CommandPaletteItem stories across semantics, states, themes, direction, and viewport sizes
5
+ * @input Exact-head CommandPaletteItem stories, plus the inline grouped palette in
6
+ * the CommandPalette audit matrix, across states, themes, direction, pointers,
7
+ * and viewport sizes
6
8
  * @output Screenshots and fail-closed sensor receipts for the CommandPaletteItem whole-component audit
7
9
  * @position Component-owned Night Watch browser evidence; CI runs it only for a CommandPaletteItem audit pull request
8
10
  */
@@ -11,7 +13,13 @@ import {createHash} from 'node:crypto';
11
13
  import {execFileSync} from 'node:child_process';
12
14
  import * as fs from 'node:fs';
13
15
  import * as path from 'node:path';
14
- import {expect, test, type Page} from '@playwright/test';
16
+ import {
17
+ expect,
18
+ test,
19
+ type CDPSession,
20
+ type Locator,
21
+ type Page,
22
+ } from '@playwright/test';
15
23
  import {holdMotionStill} from '@astryxdesign/a11y-spec/chromium';
16
24
  import {
17
25
  DEFAULT_STORYBOOK_DIR,
@@ -26,8 +34,17 @@ const NARROW = {width: 320, height: 720};
26
34
  type Mode = 'light' | 'dark';
27
35
  type Direction = 'ltr' | 'rtl';
28
36
  type Theme = 'neutral' | 'butter';
29
- type Story = 'states' | 'combined-and-long';
30
- type State = 'rest' | 'hover';
37
+ type Pointer = 'fine' | 'coarse';
38
+ type Story = 'states' | 'combined-and-long' | 'palette';
39
+ type State =
40
+ | 'rest'
41
+ | 'hover'
42
+ | 'pressed'
43
+ | 'pressed-highlighted'
44
+ | 'disabled-hover'
45
+ | 'touch-press'
46
+ | 'keyboard-highlight'
47
+ | 'keyboard-selected-highlight';
31
48
 
32
49
  type AuditCase = {
33
50
  key: string;
@@ -36,6 +53,8 @@ type AuditCase = {
36
53
  direction: Direction;
37
54
  theme: Theme;
38
55
  state: State;
56
+ pointer: Pointer;
57
+ forcedColors: boolean;
39
58
  viewport: {width: number; height: number};
40
59
  };
41
60
 
@@ -57,6 +76,8 @@ function addPair(
57
76
  direction: options.direction ?? 'ltr',
58
77
  theme: options.theme ?? 'neutral',
59
78
  state: options.state ?? 'rest',
79
+ pointer: options.pointer ?? 'fine',
80
+ forcedColors: options.forcedColors ?? false,
60
81
  viewport: options.viewport ?? WIDE,
61
82
  });
62
83
  }
@@ -64,9 +85,19 @@ function addPair(
64
85
 
65
86
  addPair('states-rest');
66
87
  addPair('states-hover', {state: 'hover'});
88
+ addPair('states-pressed', {state: 'pressed'});
89
+ addPair('states-pressed-highlighted', {state: 'pressed-highlighted'});
90
+ addPair('states-disabled-hover', {state: 'disabled-hover'});
67
91
  addPair('states-custom-theme', {theme: 'butter'});
68
92
  addPair('states-rtl', {direction: 'rtl'});
69
93
  addPair('states-narrow', {viewport: NARROW});
94
+ addPair('states-coarse', {viewport: NARROW, pointer: 'coarse'});
95
+ addPair('states-touch-press', {
96
+ viewport: NARROW,
97
+ pointer: 'coarse',
98
+ state: 'touch-press',
99
+ });
100
+ addPair('states-forced-colors', {forcedColors: true});
70
101
  addPair('combined-rest', {story: 'combined-and-long'});
71
102
  addPair('combined-custom-theme', {
72
103
  story: 'combined-and-long',
@@ -76,6 +107,45 @@ addPair('combined-narrow', {
76
107
  story: 'combined-and-long',
77
108
  viewport: NARROW,
78
109
  });
110
+ addPair('palette-keyboard-highlight', {
111
+ story: 'palette',
112
+ state: 'keyboard-highlight',
113
+ });
114
+ addPair('palette-keyboard-selected-highlight', {
115
+ story: 'palette',
116
+ state: 'keyboard-selected-highlight',
117
+ });
118
+ addPair('palette-keyboard-selected-highlight-custom-theme', {
119
+ story: 'palette',
120
+ state: 'keyboard-selected-highlight',
121
+ theme: 'butter',
122
+ });
123
+
124
+ addPair('palette-keyboard-highlight-forced-colors', {
125
+ story: 'palette',
126
+ state: 'keyboard-highlight',
127
+ forcedColors: true,
128
+ });
129
+ addPair('palette-keyboard-selected-highlight-forced-colors', {
130
+ story: 'palette',
131
+ state: 'keyboard-selected-highlight',
132
+ forcedColors: true,
133
+ });
134
+
135
+ /** Option each pointer state acts on, by its accessible text. */
136
+ const POINTER_TARGET: Partial<Record<State, string>> = {
137
+ hover: 'Open file',
138
+ pressed: 'Open file',
139
+ 'pressed-highlighted': 'Search files',
140
+ 'disabled-hover': 'Archived command',
141
+ 'touch-press': 'Open file',
142
+ };
143
+
144
+ /** Option the palette keyboard states move the active descendant to. */
145
+ const KEYBOARD_TARGET: Partial<Record<State, string>> = {
146
+ 'keyboard-highlight': 'Home',
147
+ 'keyboard-selected-highlight': 'Settings',
148
+ };
79
149
 
80
150
  test.describe.configure({mode: 'serial', retries: 0});
81
151
  let server: StaticServer;
@@ -83,7 +153,9 @@ let checkoutSha: string;
83
153
  let storybookSha: string;
84
154
 
85
155
  function storyId(item: AuditCase) {
86
- return `core-commandpaletteitem--${item.story}`;
156
+ return item.story === 'palette'
157
+ ? 'core-commandpalette--audit-matrix'
158
+ : `core-commandpaletteitem--${item.story}`;
87
159
  }
88
160
 
89
161
  function storyUrl(item: AuditCase) {
@@ -96,6 +168,38 @@ async function settle(page: Page) {
96
168
  await page.waitForTimeout(40);
97
169
  }
98
170
 
171
+ async function centerOf(locator: Locator) {
172
+ const box = await locator.boundingBox();
173
+ if (box == null) {
174
+ throw new Error('target option has no rendered bounds');
175
+ }
176
+ return {x: box.x + box.width / 2, y: box.y + box.height / 2};
177
+ }
178
+
179
+ async function highlightedName(input: Locator) {
180
+ return input.evaluate(element => {
181
+ const id = element.getAttribute('aria-activedescendant');
182
+ const option = id ? document.getElementById(id) : null;
183
+ return option?.textContent?.replace(/\s+/g, ' ').trim() ?? null;
184
+ });
185
+ }
186
+
187
+ /** PageUp moves the palette to its first result; ArrowDown walks forward. */
188
+ async function highlightFromKeyboard(
189
+ page: Page,
190
+ input: Locator,
191
+ target: string,
192
+ ) {
193
+ await input.focus();
194
+ await page.keyboard.press('PageUp');
195
+ for (let step = 0; step < 4; step++) {
196
+ if ((await highlightedName(input)) === target) {
197
+ return;
198
+ }
199
+ await page.keyboard.press('ArrowDown');
200
+ }
201
+ }
202
+
99
203
  test.beforeAll(async () => {
100
204
  fs.rmSync(OUTPUT, {recursive: true, force: true});
101
205
  fs.mkdirSync(OUTPUT, {recursive: true});
@@ -133,6 +237,8 @@ for (const item of cases) {
133
237
  viewport: item.viewport,
134
238
  colorScheme: item.mode,
135
239
  reducedMotion: 'reduce',
240
+ hasTouch: item.pointer === 'coarse',
241
+ forcedColors: item.forcedColors ? 'active' : 'none',
136
242
  });
137
243
  const page = await context.newPage();
138
244
  const pageErrors: string[] = [];
@@ -140,13 +246,20 @@ for (const item of cases) {
140
246
  await page.goto(storyUrl(item), {waitUntil: 'load'});
141
247
  await page.locator('#storybook-root').waitFor();
142
248
 
143
- const listName =
144
- item.story === 'states'
145
- ? 'Command item states'
146
- : 'Combined and long command item states';
147
- const list = page.getByRole('listbox', {name: listName});
249
+ const isPalette = item.story === 'palette';
250
+ const subject = isPalette
251
+ ? page.getByRole('region', {name: 'Grouped command palette'})
252
+ : page.locator('#storybook-root');
253
+ const list = isPalette
254
+ ? subject.getByRole('listbox')
255
+ : subject.getByRole('listbox', {
256
+ name:
257
+ item.story === 'states'
258
+ ? 'Command item states'
259
+ : 'Combined and long command item states',
260
+ });
148
261
  const options = list.getByRole('option');
149
- const expectedCount = item.story === 'states' ? 4 : 2;
262
+ const expectedCount = item.story === 'combined-and-long' ? 2 : 4;
150
263
  const expectedDisabled = item.story === 'states' ? 1 : 0;
151
264
  await expect(list).toBeVisible();
152
265
  await expect(options).toHaveCount(expectedCount);
@@ -156,12 +269,51 @@ for (const item of cases) {
156
269
  await expect(
157
270
  list.locator('[role="option"][aria-disabled="true"]'),
158
271
  ).toHaveCount(expectedDisabled);
159
- await expect(list.locator('button, a, [tabindex]')).toHaveCount(0);
272
+ await expect(
273
+ list.locator('[role="option"] :is(button, a, [tabindex])'),
274
+ ).toHaveCount(0);
275
+
276
+ const pointerTarget = POINTER_TARGET[item.state];
277
+ const keyboardTarget = KEYBOARD_TARGET[item.state];
278
+ const target = pointerTarget ?? keyboardTarget ?? null;
279
+ const targetOption =
280
+ target == null ? null : options.filter({hasText: target}).first();
281
+ let cdp: CDPSession | null = null;
160
282
 
161
- if (item.state === 'hover') {
162
- await options.first().hover();
163
- }
164
283
  await settle(page);
284
+ if (item.state === 'hover' || item.state === 'disabled-hover') {
285
+ await targetOption?.hover();
286
+ } else if (
287
+ item.state === 'pressed' ||
288
+ item.state === 'pressed-highlighted'
289
+ ) {
290
+ if (targetOption == null) {
291
+ throw new Error(`${item.key}: no press target`);
292
+ }
293
+ await targetOption.hover();
294
+ await page.mouse.down();
295
+ } else if (item.state === 'touch-press') {
296
+ if (targetOption == null) {
297
+ throw new Error(`${item.key}: no touch target`);
298
+ }
299
+ const point = await centerOf(targetOption);
300
+ cdp = await context.newCDPSession(page);
301
+ await cdp.send('Input.dispatchTouchEvent', {
302
+ type: 'touchStart',
303
+ touchPoints: [{x: point.x, y: point.y, id: 1}],
304
+ });
305
+ // The touch press controller believes a stationary press after its
306
+ // onset delay; hold past it before reading the painted state.
307
+ await page.waitForTimeout(320);
308
+ } else if (keyboardTarget != null) {
309
+ await highlightFromKeyboard(
310
+ page,
311
+ subject.getByRole('combobox'),
312
+ keyboardTarget,
313
+ );
314
+ }
315
+ await page.evaluate(async () => document.fonts.ready);
316
+ await page.waitForTimeout(40);
165
317
 
166
318
  const observed = await page.evaluate(() => ({
167
319
  theme: document
@@ -173,20 +325,41 @@ for (const item of cases) {
173
325
  ).direction,
174
326
  viewport: {width: innerWidth, height: innerHeight},
175
327
  reducedMotion: matchMedia('(prefers-reduced-motion: reduce)').matches,
328
+ coarsePointer: matchMedia('(pointer: coarse)').matches,
329
+ forcedColors: matchMedia('(forced-colors: active)').matches,
176
330
  horizontalOverflow: document.documentElement.scrollWidth > innerWidth + 1,
177
331
  fonts: document.fonts.status,
178
332
  }));
333
+ const listBackdrop = await list.evaluate(element => {
334
+ let node: Element | null = element;
335
+ while (node) {
336
+ const color = getComputedStyle(node).backgroundColor;
337
+ if (color !== 'rgba(0, 0, 0, 0)' && color !== 'transparent') {
338
+ return color;
339
+ }
340
+ node = node.parentElement;
341
+ }
342
+ return getComputedStyle(document.body).backgroundColor;
343
+ });
179
344
  const optionStates = await options.evaluateAll(nodes =>
180
345
  nodes.map(node => {
181
346
  const style = getComputedStyle(node);
182
347
  const box = node.getBoundingClientRect();
183
348
  return {
184
349
  name: node.textContent?.replace(/\s+/g, ' ').trim(),
350
+ id: node.id || null,
185
351
  selected: node.getAttribute('aria-selected'),
186
352
  disabled: node.getAttribute('aria-disabled'),
353
+ themeSelected: node.getAttribute('data-selected'),
354
+ themeDisabled: node.getAttribute('data-disabled'),
355
+ active: node.matches(':active'),
356
+ hovered: node.matches(':hover'),
357
+ press: node.getAttribute('data-astryx-press'),
187
358
  backgroundColor: style.backgroundColor,
359
+ backgroundImage: style.backgroundImage,
188
360
  color: style.color,
189
361
  opacity: style.opacity,
362
+ cursor: style.cursor,
190
363
  geometry: {
191
364
  x: box.x,
192
365
  y: box.y,
@@ -196,6 +369,18 @@ for (const item of cases) {
196
369
  };
197
370
  }),
198
371
  );
372
+ const combobox = isPalette
373
+ ? await subject.getByRole('combobox').evaluate(element => ({
374
+ focused: element === document.activeElement,
375
+ activeDescendant: element.getAttribute('aria-activedescendant'),
376
+ }))
377
+ : null;
378
+
379
+ const targetState =
380
+ target == null
381
+ ? null
382
+ : (optionStates.find(option => option.name?.startsWith(target)) ??
383
+ null);
199
384
  const semanticsPass =
200
385
  optionStates.length === expectedCount &&
201
386
  optionStates.filter(option => option.selected === 'true').length === 1 &&
@@ -204,11 +389,48 @@ for (const item of cases) {
204
389
  optionStates.every(
205
390
  option => option.geometry.width > 0 && option.geometry.height > 0,
206
391
  );
392
+ let statePass = true;
393
+ if (item.state === 'hover') {
394
+ statePass = targetState?.hovered === true;
395
+ } else if (item.state === 'disabled-hover') {
396
+ statePass =
397
+ targetState?.hovered === true &&
398
+ targetState.disabled === 'true' &&
399
+ targetState.backgroundColor === 'rgba(0, 0, 0, 0)' &&
400
+ targetState.cursor === 'default';
401
+ } else if (
402
+ item.state === 'pressed' ||
403
+ item.state === 'pressed-highlighted'
404
+ ) {
405
+ statePass = targetState?.active === true;
406
+ } else if (item.state === 'touch-press') {
407
+ statePass = targetState?.press === 'on';
408
+ } else if (keyboardTarget != null) {
409
+ statePass =
410
+ combobox?.focused === true &&
411
+ targetState?.id != null &&
412
+ combobox.activeDescendant === targetState.id &&
413
+ (item.state === 'keyboard-selected-highlight'
414
+ ? targetState.selected === 'true'
415
+ : targetState.selected === 'false');
416
+ }
417
+
418
+ const png = isPalette
419
+ ? await subject.screenshot({animations: 'disabled'})
420
+ : await page.screenshot({animations: 'disabled', fullPage: true});
421
+ if (item.state === 'pressed' || item.state === 'pressed-highlighted') {
422
+ await page.mouse.up();
423
+ }
424
+ if (cdp != null) {
425
+ await cdp.send('Input.dispatchTouchEvent', {
426
+ type: 'touchEnd',
427
+ touchPoints: [],
428
+ });
429
+ }
207
430
 
208
- const png = await page.screenshot({animations: 'disabled', fullPage: true});
209
431
  const image = `${item.key}.png`;
210
432
  const receipt = {
211
- schemaVersion: 1,
433
+ schemaVersion: 2,
212
434
  expected: {
213
435
  build: checkoutSha,
214
436
  storyId: storyId(item),
@@ -216,6 +438,9 @@ for (const item of cases) {
216
438
  direction: item.direction,
217
439
  theme: item.theme,
218
440
  state: item.state,
441
+ target,
442
+ pointer: item.pointer,
443
+ forcedColors: item.forcedColors,
219
444
  viewport: item.viewport,
220
445
  optionCount: expectedCount,
221
446
  disabledCount: expectedDisabled,
@@ -226,6 +451,8 @@ for (const item of cases) {
226
451
  ...observed,
227
452
  listRole: await list.getAttribute('role'),
228
453
  listName: await list.getAttribute('aria-label'),
454
+ listBackdrop,
455
+ combobox,
229
456
  optionStates,
230
457
  pageErrors,
231
458
  },
@@ -235,7 +462,7 @@ for (const item of cases) {
235
462
  width: png.readUInt32BE(16),
236
463
  height: png.readUInt32BE(20),
237
464
  },
238
- checks: {semanticsPass},
465
+ checks: {semanticsPass, statePass},
239
466
  passed:
240
467
  storybookSha === checkoutSha &&
241
468
  pageErrors.length === 0 &&
@@ -245,9 +472,12 @@ for (const item of cases) {
245
472
  observed.viewport.width === item.viewport.width &&
246
473
  observed.viewport.height === item.viewport.height &&
247
474
  observed.reducedMotion &&
475
+ observed.coarsePointer === (item.pointer === 'coarse') &&
476
+ observed.forcedColors === item.forcedColors &&
248
477
  !observed.horizontalOverflow &&
249
478
  observed.fonts === 'loaded' &&
250
- semanticsPass,
479
+ semanticsPass &&
480
+ statePass,
251
481
  };
252
482
  fs.writeFileSync(path.join(OUTPUT, image), png);
253
483
  fs.writeFileSync(
@@ -270,7 +500,7 @@ test('manifest is complete', async () => {
270
500
  ) as AuditReceipt,
271
501
  );
272
502
  const manifest = {
273
- schemaVersion: 1,
503
+ schemaVersion: 2,
274
504
  component: 'core/CommandPaletteItem',
275
505
  build: checkoutSha,
276
506
  frames: cases.map(item => ({
@@ -79,7 +79,7 @@ export const docs = {
79
79
  name: 'isLoading',
80
80
  type: 'boolean',
81
81
  description:
82
- 'Whether the input is in a loading state. Disables interaction and shows a spinner.',
82
+ 'Whether the input is in a loading state. The field stays focusable and exposes `aria-busy` with a spinner; typing, picking, and clearing are blocked until loading ends.',
83
83
  default: 'false',
84
84
  },
85
85
  {
@@ -253,6 +253,11 @@ export const docs = {
253
253
  description:
254
254
  'Show a loading state with changeAction when the date triggers a server-side save.',
255
255
  },
256
+ {
257
+ guidance: true,
258
+ description:
259
+ 'On a fine pointer the field follows the WAI-ARIA combobox pattern with a dialog popup, as in the APG date picker combobox: ArrowDown or Alt+ArrowDown opens the calendar, and Escape closes it and returns focus to the field.',
260
+ },
256
261
  {
257
262
  guidance: true,
258
263
  description:
@@ -301,7 +306,7 @@ export const docs = {
301
306
  name: 'Calendar popover',
302
307
  required: false,
303
308
  description:
304
- 'A month grid that appears when the icon is clicked or the input is focused.',
309
+ 'A month grid that appears when the icon or the input is clicked, or when ArrowDown is pressed in the input.',
305
310
  },
306
311
  {
307
312
  name: 'Clear button',
@@ -351,6 +356,11 @@ export const docsZh = {
351
356
  description:
352
357
  'Show a loading state with changeAction when the date triggers a server-side save.',
353
358
  },
359
+ {
360
+ guidance: true,
361
+ description:
362
+ 'On a fine pointer the field follows the WAI-ARIA combobox pattern with a dialog popup, as in the APG date picker combobox: ArrowDown or Alt+ArrowDown opens the calendar, and Escape closes it and returns focus to the field.',
363
+ },
354
364
  {
355
365
  guidance: false,
356
366
  description:
@@ -401,7 +411,8 @@ export const docsZh = {
401
411
  {
402
412
  name: 'isDisabled',
403
413
  type: 'boolean',
404
- description: '禁用输入框和日历。输入框仍可聚焦并暴露 `aria-disabled`,但不能输入或打开日历。',
414
+ description:
415
+ '禁用输入框和日历。输入框仍可聚焦并暴露 `aria-disabled`,但不能输入或打开日历。',
405
416
  default: 'false',
406
417
  },
407
418
  {
@@ -429,7 +440,8 @@ export const docsZh = {
429
440
  {
430
441
  name: 'isLoading',
431
442
  type: 'boolean',
432
- description: '输入框是否处于加载状态。禁用交互并显示加载指示器。',
443
+ description:
444
+ '输入框是否处于加载状态。字段仍可聚焦,并通过 `aria-busy` 和加载指示器表示忙碌;加载结束前不能输入、选择或清除日期。',
433
445
  default: 'false',
434
446
  },
435
447
  {
@@ -599,6 +611,11 @@ export const docsDense = {
599
611
  description:
600
612
  'Show a loading state with changeAction when the date triggers a server-side save.',
601
613
  },
614
+ {
615
+ guidance: true,
616
+ description:
617
+ 'On a fine pointer the field follows the WAI-ARIA combobox pattern with a dialog popup, as in the APG date picker combobox: ArrowDown or Alt+ArrowDown opens the calendar, and Escape closes it and returns focus to the field.',
618
+ },
602
619
  {
603
620
  guidance: true,
604
621
  description:
@@ -638,7 +655,8 @@ export const docsDense = {
638
655
  value: 'selected date YYYY-MM-DD',
639
656
  onChange: 'callback on date change',
640
657
  changeAction: 'async action after onChange; drives optimistic UI',
641
- isLoading: 'loading state; disables interaction, shows spinner',
658
+ isLoading:
659
+ 'loading state; stays focusable w/ aria-busy + spinner; blocks typing, picking, clearing',
642
660
  min: 'min selectable date (YYYY-MM-DD)',
643
661
  max: 'max selectable date (YYYY-MM-DD)',
644
662
  dateConstraints: 'custom constraint fns to disable specific dates',