@astryxdesign/core 0.4.6 → 0.4.7-canary.405f1ee

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 (71) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts +1 -0
  3. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheet.js +37 -11
  5. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +6 -0
  6. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -0
  7. package/dist/BottomSheet/BottomSheetEdgeTint.js +62 -0
  8. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  9. package/dist/BottomSheet/BottomSheetPanel.js +1 -0
  10. package/dist/BottomSheet/BottomSheetSwitcher.d.ts +1 -0
  11. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  12. package/dist/BottomSheet/BottomSheetSwitcher.js +10 -4
  13. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  14. package/dist/BottomSheet/useSheetGestures.js +23 -5
  15. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  16. package/dist/DateInput/TouchDateField.js +34 -1
  17. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  18. package/dist/MultiSelector/MultiSelector.js +6 -0
  19. package/dist/Selector/Selector.d.ts.map +1 -1
  20. package/dist/Selector/Selector.js +5 -0
  21. package/dist/TabList/Tab.d.ts.map +1 -1
  22. package/dist/TabList/Tab.js +5 -1
  23. package/dist/Table/BaseTable.d.ts.map +1 -1
  24. package/dist/Table/BaseTable.js +4 -1
  25. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  26. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +20 -8
  27. package/dist/Table/plugins/selection/useTableSelection.d.ts +16 -0
  28. package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
  29. package/dist/Table/plugins/selection/useTableSelection.js +19 -5
  30. package/dist/Table/types.d.ts +22 -4
  31. package/dist/Table/types.d.ts.map +1 -1
  32. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  33. package/dist/Table/useBaseTablePlugins.js +5 -0
  34. package/dist/astryx.css +4 -0
  35. package/dist/hooks/useListFocus.d.ts +5 -2
  36. package/dist/hooks/useListFocus.d.ts.map +1 -1
  37. package/dist/hooks/useListFocus.js +12 -6
  38. package/package.json +3 -3
  39. package/src/Avatar/Avatar.doc.mjs +2 -1
  40. package/src/BottomSheet/BottomSheet.tsx +19 -0
  41. package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +225 -0
  42. package/src/BottomSheet/BottomSheetEdgeTint.tsx +82 -0
  43. package/src/BottomSheet/BottomSheetPanel.test.tsx +66 -0
  44. package/src/BottomSheet/BottomSheetPanel.tsx +19 -0
  45. package/src/BottomSheet/BottomSheetSwitcher.tsx +13 -0
  46. package/src/BottomSheet/useSheetGestures.test.ts +27 -0
  47. package/src/BottomSheet/useSheetGestures.ts +25 -5
  48. package/src/DateInput/DateInputTouch.test.tsx +36 -0
  49. package/src/DateInput/TouchDateField.tsx +35 -1
  50. package/src/MultiSelector/MultiSelector.test.tsx +21 -0
  51. package/src/MultiSelector/MultiSelector.tsx +10 -1
  52. package/src/Popover/Popover.test.tsx +27 -1
  53. package/src/Selector/Selector.test.tsx +21 -0
  54. package/src/Selector/Selector.tsx +11 -1
  55. package/src/TabList/Tab.tsx +5 -1
  56. package/src/TabList/TabList.test.tsx +21 -4
  57. package/src/Table/BaseTable.tsx +6 -3
  58. package/src/Table/Table.test.tsx +35 -0
  59. package/src/Table/plugins/groupedRows/useTableGroupedRows-perf.test.tsx +112 -0
  60. package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +100 -0
  61. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +17 -8
  62. package/src/Table/plugins/selection/useTableSelection.test.tsx +76 -0
  63. package/src/Table/plugins/selection/useTableSelection.tsx +40 -7
  64. package/src/Table/types.ts +22 -4
  65. package/src/Table/useBaseTablePlugins.ts +5 -0
  66. package/src/Table/useTableGroupedRows.doc.mjs +5 -4
  67. package/src/Table/useTableSelection.doc.mjs +31 -0
  68. package/src/hooks/useListFocus.doc.mjs +2 -2
  69. package/src/hooks/useListFocus.test.tsx +65 -3
  70. package/src/hooks/useListFocus.ts +15 -7
  71. package/src/theme/MediaTheme.doc.mjs +5 -5
@@ -26,7 +26,13 @@
26
26
  * DOM elements and no central element tracking. Each subscription
27
27
  * self-cleans when the row disconnects.
28
28
  *
29
+ * Because the background is an inline style, it outranks anything
30
+ * userland can layer on with StyleX. `hasRowHighlight: false` is the
31
+ * supported way to reclaim the row background; `aria-selected` is
32
+ * unaffected by it.
33
+ *
29
34
  * SYNC: When modified, update these files to stay in sync:
35
+ * - /packages/core/src/Table/useTableSelection.doc.mjs (config prop table)
30
36
  * - /packages/core/src/Table/Table.doc.mjs (selection documentation)
31
37
  * - /packages/core/src/Table/index.ts (exports)
32
38
  */
@@ -81,6 +87,22 @@ export interface UseTableSelectionConfig<T extends Record<string, unknown>> {
81
87
  * ```
82
88
  */
83
89
  getRowLabel?: (item: T) => string;
90
+ /**
91
+ * Paint checked rows with the accent wash. Set false when the surrounding
92
+ * UI already uses row background to mean something else — a row that is
93
+ * open in a detail panel, for instance. The wash is an inline style, so
94
+ * it cannot be overridden from userland; this flag is the way off it.
95
+ *
96
+ * Only the background is dropped. `aria-selected` is still set on checked
97
+ * rows either way, so the selection stays legible to screen readers.
98
+ *
99
+ * @default true
100
+ * @example
101
+ * ```
102
+ * useTableSelection({...config, hasRowHighlight: false})
103
+ * ```
104
+ */
105
+ hasRowHighlight?: boolean;
84
106
  }
85
107
 
86
108
  // =============================================================================
@@ -121,18 +143,24 @@ function createSelectionStore<T extends Record<string, unknown>>(
121
143
 
122
144
  /**
123
145
  * Apply or remove selection styling on a <tr> element.
146
+ *
147
+ * `aria-selected` tracks selection on its own: it is the semantic half of
148
+ * the state and stays correct whether or not the row is painted.
124
149
  */
125
150
  function applyRowSelectionStyle(
126
151
  el: HTMLTableRowElement,
127
152
  isSelected: boolean,
153
+ hasRowHighlight: boolean,
128
154
  ): void {
129
155
  if (isSelected) {
130
156
  el.setAttribute('aria-selected', 'true');
131
- el.style.backgroundColor = selectedBgColor;
132
157
  } else {
133
158
  el.removeAttribute('aria-selected');
134
- el.style.backgroundColor = '';
135
159
  }
160
+ // Written on every pass, not just when painting: the flag can flip while a
161
+ // row is already selected, and the wash has to come back off.
162
+ el.style.backgroundColor =
163
+ isSelected && hasRowHighlight ? selectedBgColor : '';
136
164
  }
137
165
 
138
166
  // =============================================================================
@@ -358,18 +386,23 @@ export function useTableSelection<T extends Record<string, unknown>>(
358
386
  if (!el) {
359
387
  return;
360
388
  }
389
+ const applyStyle = () => {
390
+ const config = store.getConfig();
391
+ applyRowSelectionStyle(
392
+ el,
393
+ config.getIsItemSelected(item),
394
+ config.hasRowHighlight ?? true,
395
+ );
396
+ };
361
397
  // Apply initial style
362
- applyRowSelectionStyle(el, store.getConfig().getIsItemSelected(item));
398
+ applyStyle();
363
399
  // Subscribe for future changes
364
400
  const unsub = store.subscribe(() => {
365
401
  if (!el.isConnected) {
366
402
  unsub();
367
403
  return;
368
404
  }
369
- applyRowSelectionStyle(
370
- el,
371
- store.getConfig().getIsItemSelected(item),
372
- );
405
+ applyStyle();
373
406
  });
374
407
  return () => {
375
408
  unsub();
@@ -322,6 +322,19 @@ export interface BodyCellRenderProps {
322
322
  * sticky offsets. Optional for backward compatibility.
323
323
  */
324
324
  columns?: ReadonlyArray<TableColumn<Record<string, unknown>>>;
325
+ /**
326
+ * When true, the cell renders empty: BaseTable calls neither the column's
327
+ * `renderCell` nor the default renderer. Set it for a row whose cells a
328
+ * plugin is going to replace wholesale in `transformBodyRow` (a grouped-rows
329
+ * section header, a summary row) — the row is not one the consumer supplied,
330
+ * so their renderer can only misread it or throw, and its output is
331
+ * discarded moments later anyway.
332
+ *
333
+ * Applies to this cell only, and is decided per render against the final
334
+ * column list, so it covers columns other plugins contributed regardless of
335
+ * where in the pipeline this plugin sits.
336
+ */
337
+ isContentSuppressed?: boolean;
325
338
  }
326
339
 
327
340
  /**
@@ -414,13 +427,18 @@ export type TableContextActions =
414
427
  *
415
428
  * 1. `transformColumns` — filter, reorder, or inject columns before rendering
416
429
  * 2. `transformTable` — transform the root `<table>` element props
417
- * 3. `transformHeaderRow` — transform the header `<tr>` props
418
- * 4. `transformHeaderCell` — transform each `<th>` props
419
- * 5. `transformBodyRow` — transform each body `<tr>` props
420
- * 6. `transformBodyCell` — transform each body `<td>` props
430
+ * 3. `transformHeaderCell` — transform each `<th>` props
431
+ * 4. `transformHeaderRow` — transform the header `<tr>` props
432
+ * 5. `transformBodyCell` — transform each body `<td>` props
433
+ * 6. `transformBodyRow` — transform each body `<tr>` props
421
434
  * 7. `transformScrollWrapper` — transform the scroll-container wrapper around the table
422
435
  * 8. `transformTableContext` — wrap the table output in context providers
423
436
  *
437
+ * A row's cells are built before its row transform runs, and reach it as
438
+ * `children`: the cell hook is the earliest per-cell interception point, and a
439
+ * row transform can discard cells but cannot stop them being built (see
440
+ * `isContentSuppressed` on `BodyCellRenderProps` for that).
441
+ *
424
442
  * Plugins may also contribute right-click menu actions by appending to
425
443
  * `contextMenuActions` in `transformHeaderCell` / `transformBodyCell`
426
444
  * (aggregated into one menu per header cell / row).
@@ -46,6 +46,11 @@ import {devWarn} from '../utils/devWarning';
46
46
  * Canonical ordering for first-party plugin names.
47
47
  * Plugins are sorted by their position in this array.
48
48
  * Unknown names are appended after the known set.
49
+ *
50
+ * This decides LAYOUT — which column lands left of which, who wraps whom. It
51
+ * must never decide whether the table works: a plugin that renders in one
52
+ * order and crashes in the other is broken, and adding it here buys a lucky
53
+ * order rather than a fix.
49
54
  */
50
55
  const PLUGIN_ORDER: ReadonlyArray<string> = [
51
56
  'columnSettings',
@@ -7,7 +7,7 @@ export const docs = {
7
7
  subComponentOf: 'Table',
8
8
  displayName: 'useTableGroupedRows',
9
9
  description:
10
- 'Hook that groups a flat data array into collapsible section rows. Each distinct groupBy value becomes a full-width section-header row with a chevron toggle, the group label, and a member count; collapsing hides that group\'s data rows while keeping the header visible. Mirrors useTableTreeState: the consumer owns the collapsedGroups set and the hook returns {data, plugin, idKey}: pass all three to Table (data, plugins, and idKey respectively).',
10
+ "Hook that groups a flat data array into collapsible section rows. Each distinct groupBy value becomes a full-width section-header row with a chevron toggle, the group label, and a member count; collapsing hides that group's data rows while keeping the header visible. Mirrors useTableTreeState: the consumer owns the collapsedGroups set and the hook returns {data, plugin, idKey}: pass them to Table as data, plugins, and idKey respectively.",
11
11
  props: [
12
12
  {
13
13
  name: 'data',
@@ -58,13 +58,14 @@ export const docs = {
58
58
  /** @type {import('@astryxdesign/cli/authoring').ComponentTranslationDoc} */
59
59
  export const docsDense = {
60
60
  description:
61
- 'Groups a flat data array into collapsible section rows. Each groupBy value becomes a full-width header (chevron + label + count); collapsing hides its rows. Returns {data, plugin, idKey}: pass to Table data / plugins / idKey. Consumer owns the collapsedGroups set.',
61
+ 'Groups a flat data array into collapsible section rows. Each groupBy value becomes a full-width header (chevron + label + count); collapsing hides its rows. Returns {data, plugin, idKey}: pass them to Table data / plugins / idKey. Consumer owns the collapsedGroups set.',
62
62
  propDescriptions: {
63
63
  data: 'The flat data to group.',
64
- groupBy: 'Derive a row\'s group key. Same key = same section.',
64
+ groupBy: "Derive a row's group key. Same key = same section.",
65
65
  collapsedGroups: 'Set of currently-collapsed group keys.',
66
66
  onToggleGroup: 'Called with the group key when a header is toggled.',
67
- renderGroupHeader: "Custom header content (right of chevron). Default '<key> (<count>)'.",
67
+ renderGroupHeader:
68
+ "Custom header content (right of chevron). Default '<key> (<count>)'.",
68
69
  getRowKey: 'Stable key for a real row; positional fallback when omitted.',
69
70
  groupOrder: 'Pin these group keys first; others keep first-seen order.',
70
71
  },
@@ -61,6 +61,13 @@ export const docs = {
61
61
  description:
62
62
  'Derives a human-readable identity for a row; the row checkbox\'s hidden label becomes `Select ${getRowLabel(item)}` so screen readers announce which row each checkbox selects. Falls back to "Select row" when omitted.',
63
63
  },
64
+ {
65
+ name: 'hasRowHighlight',
66
+ type: 'boolean',
67
+ description:
68
+ 'Paints checked rows with the accent wash. Set false when the surrounding UI already uses row background to mean something else (a row open in a detail panel, say) — the wash is an inline style, so it cannot be overridden from userland. Only the background is dropped: aria-selected is still set on checked rows either way.',
69
+ default: 'true',
70
+ },
64
71
  ],
65
72
  examples: [
66
73
  {
@@ -82,6 +89,21 @@ const selectionPlugin = useTableSelection({
82
89
  idKey="id"
83
90
  plugins={{selection: selectionPlugin}}
84
91
  />;
92
+ `,
93
+ },
94
+ {
95
+ label: 'Opt out of the checked-row wash',
96
+ code: `
97
+ // This table already uses row background to mean "open in the detail
98
+ // panel". Turning the selection wash off keeps that meaning unambiguous;
99
+ // the checkbox and aria-selected still carry the selection.
100
+ const selectionPlugin = useTableSelection({
101
+ getIsItemSelected: item => selectedIds.has(item.id),
102
+ onSelectItem: ({item, isSelected}) => toggle(item.id, isSelected),
103
+ onSelectAll: ({isAllSelected}) => selectAll(isAllSelected),
104
+ getIsAllSelected: () => selectedIds.size === users.length,
105
+ hasRowHighlight: false,
106
+ });
85
107
  `,
86
108
  },
87
109
  ],
@@ -142,6 +164,13 @@ export const docsZh = {
142
164
  description:
143
165
  '为行派生人类可读的标识;行复选框的隐藏标签变为 `Select ${getRowLabel(item)}`,让屏幕阅读器播报每个复选框选择的是哪一行。省略时回退为 "Select row"。',
144
166
  },
167
+ {
168
+ name: 'hasRowHighlight',
169
+ type: 'boolean',
170
+ description:
171
+ '为选中的行绘制强调色背景。当周围的界面已用行背景表达其他含义(例如该行已在详情面板中打开)时设为 false —— 该背景是内联样式,无法从业务代码覆盖。仅去掉背景:无论如何选中的行仍会设置 aria-selected。',
172
+ default: 'true',
173
+ },
145
174
  ],
146
175
  };
147
176
 
@@ -164,5 +193,7 @@ export const docsDense = {
164
193
  'Returns whether row checkbox is interactive; disabled rows show disabled checkbox.',
165
194
  getRowLabel:
166
195
  'Derives row identity for the checkbox\'s hidden label: `Select ${getRowLabel(item)}`. Falls back to "Select row" when omitted.',
196
+ hasRowHighlight:
197
+ 'false => skip the accent wash on checked rows (inline style, not overridable from userland). aria-selected is unaffected. Defaults to true.',
167
198
  },
168
199
  };
@@ -36,7 +36,7 @@ export const docs = {
36
36
  {
37
37
  name: 'options.onEscape',
38
38
  type: '() => void',
39
- description: 'Callback when Escape key is pressed (e.g., close menu).',
39
+ description: 'Callback when Escape key is pressed (e.g., close menu). Supplying it also consumes the key (preventDefault); without it Escape passes through to the surrounding layer.',
40
40
  required: false,
41
41
  },
42
42
  {
@@ -144,7 +144,7 @@ export const docsDense = {
144
144
  'options.itemSelector': 'selector for focusable items in list.',
145
145
  'options.boundarySelector': "boundary selector for lists that contain nested lists (e.g. submenu flyouts); scopes items + key handling to this level.",
146
146
  'options.wrap': 'whether arrow navigation wraps around at ends.',
147
- 'options.onEscape': 'callback when Escape key pressed (e.g. close menu).',
147
+ 'options.onEscape': 'callback when Escape key pressed (e.g. close menu). Also consumes the key; without it Escape passes through to the surrounding layer.',
148
148
  'options.orientation': "navigation orientation. 'horizontal' uses ArrowLeft/ArrowRight, 'vertical' uses ArrowUp/ArrowDown, 'both' accepts all four arrows.",
149
149
  'options.hasHomeEnd': 'whether Home/End jump to first/last enabled item.',
150
150
  'options.isRtl': 'ArrowLeft/ArrowRight swap for horizontal nav (RTL). default: auto-detect from container computed direction; explicit boolean wins.',
@@ -3,14 +3,14 @@
3
3
  /**
4
4
  * @file useListFocus.test.tsx
5
5
  * @input Uses vitest, @testing-library/react, useListFocus hook
6
- * @output Unit tests for useListFocus disabled-item skipping, navigation, and
7
- * RTL auto-detection
6
+ * @output Unit tests for useListFocus disabled-item skipping, navigation,
7
+ * Escape consumption, and RTL auto-detection
8
8
  * @position Testing; validates useListFocus.ts keyboard navigation
9
9
  *
10
10
  * SYNC: When useListFocus.ts changes, update tests to match new behavior
11
11
  */
12
12
 
13
- import {describe, it, expect} from 'vitest';
13
+ import {describe, it, expect, vi} from 'vitest';
14
14
  import type {KeyboardEvent as ReactKeyboardEvent} from 'react';
15
15
  import {render, screen, fireEvent} from '@testing-library/react';
16
16
  import {useListFocus} from './useListFocus';
@@ -525,3 +525,65 @@ describe('useListFocus boundarySelector (nested lists)', () => {
525
525
  expect(innerProbe).toHaveAttribute('data-owns', 'false');
526
526
  });
527
527
  });
528
+
529
+ // A list inside a host that dismisses on Escape. The host's guard mirrors
530
+ // `useFocusTrap`: it acts only on a key no inner handler has consumed.
531
+ function EscapeHost({
532
+ onEscape,
533
+ onHostEscape,
534
+ }: {
535
+ onEscape?: () => void;
536
+ onHostEscape: () => void;
537
+ }) {
538
+ const {listRef, handleKeyDown} = useListFocus<HTMLDivElement>({onEscape});
539
+ return (
540
+ <div
541
+ data-testid="host"
542
+ onKeyDown={e => {
543
+ if (e.key === 'Escape' && !e.defaultPrevented) {
544
+ onHostEscape();
545
+ }
546
+ }}>
547
+ <div ref={listRef} role="menu" onKeyDown={handleKeyDown}>
548
+ <div role="menuitem" tabIndex={-1} data-testid="One">
549
+ One
550
+ </div>
551
+ <div role="menuitem" tabIndex={-1} data-testid="Two">
552
+ Two
553
+ </div>
554
+ </div>
555
+ </div>
556
+ );
557
+ }
558
+
559
+ describe('useListFocus Escape', () => {
560
+ it('leaves Escape to the host when no onEscape is supplied', () => {
561
+ const onHostEscape = vi.fn();
562
+ render(<EscapeHost onHostEscape={onHostEscape} />);
563
+
564
+ fireEvent.keyDown(screen.getByRole('menu'), {key: 'Escape'});
565
+ expect(onHostEscape).toHaveBeenCalledTimes(1);
566
+ });
567
+
568
+ it('consumes Escape and runs onEscape when one is supplied', () => {
569
+ const onEscape = vi.fn();
570
+ const onHostEscape = vi.fn();
571
+ render(<EscapeHost onEscape={onEscape} onHostEscape={onHostEscape} />);
572
+
573
+ fireEvent.keyDown(screen.getByRole('menu'), {key: 'Escape'});
574
+ expect(onEscape).toHaveBeenCalledTimes(1);
575
+ expect(onHostEscape).not.toHaveBeenCalled();
576
+ });
577
+
578
+ it('still consumes arrow keys with no onEscape (page-scroll suppression)', () => {
579
+ render(<EscapeHost onHostEscape={() => {}} />);
580
+ screen.getByTestId('One').focus();
581
+
582
+ // fireEvent returns false when a handler cancelled the event.
583
+ const wasCancelled = !fireEvent.keyDown(screen.getByRole('menu'), {
584
+ key: 'ArrowDown',
585
+ });
586
+ expect(wasCancelled).toBe(true);
587
+ expect(screen.getByTestId('Two')).toHaveFocus();
588
+ });
589
+ });
@@ -14,6 +14,8 @@
14
14
  *
15
15
  * SYNC: When modified, update:
16
16
  * - /packages/core/src/hooks/index.ts
17
+ * - /packages/core/src/hooks/useListFocus.doc.mjs
18
+ * - /packages/core/src/hooks/useListFocus.test.tsx
17
19
  */
18
20
 
19
21
  import {useCallback, useRef} from 'react';
@@ -64,7 +66,9 @@ export interface UseListFocusOptions {
64
66
  wrap?: boolean;
65
67
 
66
68
  /**
67
- * Callback when Escape key is pressed.
69
+ * Callback when Escape key is pressed. Supplying it also makes the list
70
+ * consume the key (`preventDefault`); without it Escape passes through to
71
+ * the surrounding layer.
68
72
  */
69
73
  onEscape?: () => void;
70
74
 
@@ -277,7 +281,8 @@ function shouldDeferToCaret(target: EventTarget | null, key: string): boolean {
277
281
  * - ArrowUp/ArrowLeft: Move to previous item (wraps to last)
278
282
  * - Home: Move to first item
279
283
  * - End: Move to last item
280
- * - Escape: Custom callback (e.g., close menu)
284
+ * - Escape: runs `onEscape` and consumes the key. With no `onEscape` the key
285
+ * is left alone, so a surrounding layer can still dismiss on it.
281
286
  *
282
287
  * By default the hook only *moves* focus and leaves `tabindex` management to
283
288
  * the caller. Opt into {@link UseListFocusOptions.hasRovingTabIndex} for a hook
@@ -557,12 +562,15 @@ export function useListFocus<T extends HTMLElement = HTMLElement>(
557
562
  return;
558
563
  }
559
564
 
560
- // Escape is handled regardless of orientation. Preserve the historical
561
- // behavior of always consuming Escape here (preventDefault) so consumers
562
- // that relied on it are unaffected.
565
+ // Escape is handled regardless of orientation, but only *consumed* when
566
+ // a handler asked for it: a list with no dismissal to perform must leave
567
+ // the key to whatever host layer does have one, and those defer to
568
+ // `defaultPrevented` (see `useFocusTrap`) or to the native popover.
563
569
  if (e.key === 'Escape') {
564
- e.preventDefault();
565
- onEscape?.();
570
+ if (onEscape) {
571
+ e.preventDefault();
572
+ onEscape();
573
+ }
566
574
  return;
567
575
  }
568
576
 
@@ -68,7 +68,7 @@ export const docs = {
68
68
  {
69
69
  guidance: true,
70
70
  description:
71
- 'Prefer mode="auto" when the surface color comes from a theme token. A token named "inverted" is not guaranteed to be inverted, and auto measures what was actually painted instead of trusting the name — including deciding that a surface needs no media context at all.',
71
+ 'Prefer mode="auto" when the surface color comes from a theme token. A token named "inverted" is not guaranteed to be inverted, and auto measures what was actually painted instead of trusting the name. It can even decide that a surface needs no media context at all.',
72
72
  },
73
73
  {
74
74
  guidance: true,
@@ -93,14 +93,14 @@ export const docs = {
93
93
  type: "'dark' | 'light' | 'auto' | 'off'",
94
94
  required: true,
95
95
  description:
96
- 'Surface luminance context: dark for content over dark backgrounds (light text, white-tinted interactions), light for content over light backgrounds (dark text, black-tinted interactions), auto to decide from the painted surface — no media context when the ambient text already reads on the surface (3:1), otherwise the side that reads better — and off to turn it off explicitly. The element renders either way, so a surface can switch contexts without remounting children.',
96
+ 'Surface luminance context: dark for content over dark backgrounds (light text, white-tinted interactions), light for content over light backgrounds (dark text, black-tinted interactions), auto to decide from the painted surface (no media context when the ambient text already reads on the surface at 3:1, otherwise the side that reads better), and off to turn it off explicitly. The element renders either way, so a surface can switch contexts without remounting children.',
97
97
  },
98
98
  {
99
99
  name: 'fallback',
100
100
  type: "'dark' | 'light'",
101
101
  default: "'dark'",
102
102
  description:
103
- 'Which side auto uses when the surface cannot be measured: during SSR, on the first client frame, and whenever the backdrop is not knowable from CSS — most often a background-image, whose pixels need sampling (useImageMode) rather than a computed style. Ignored unless mode is auto.',
103
+ 'Which side auto uses when the surface cannot be measured: during SSR, on the first client frame, and whenever the backdrop is not knowable from CSS, most often a background-image, whose pixels need sampling (useImageMode) rather than a computed style. Ignored unless mode is auto.',
104
104
  },
105
105
  {
106
106
  name: 'children',
@@ -126,7 +126,7 @@ export const docsDense = {
126
126
  {
127
127
  guidance: true,
128
128
  description:
129
- 'Prefer mode="auto" when surface color comes from a theme token — a token named "inverted" is not guaranteed to be; auto measures what was painted.',
129
+ 'Prefer mode="auto" when surface color comes from a theme token; a token named "inverted" is not guaranteed to be; auto measures what was painted.',
130
130
  },
131
131
  {
132
132
  guidance: true,
@@ -148,6 +148,6 @@ export const docsDense = {
148
148
  propDescriptions: {
149
149
  mode: 'surface luminance context: dark for content over dark backgrounds (light text, white-tinted interactions), light for content over light backgrounds (dark text, black-tinted interactions), auto to decide from painted surface (none if ambient text already reads at 3:1, else better-reading side), off to turn off explicitly (element still renders, so children never remount)',
150
150
  fallback:
151
- 'side auto uses when surface is unmeasurable (SSR, first frame, background-image — those need useImageMode sampling); ignored unless mode is auto',
151
+ 'side auto uses when surface is unmeasurable (SSR, first frame, background-image, which needs useImageMode sampling); ignored unless mode is auto',
152
152
  },
153
153
  };