fluentui-svelte 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/components/auto-suggest-box/auto-suggest-box.svelte +68 -56
  2. package/dist/components/auto-suggest-box/docs/llms.md +46 -14
  3. package/dist/components/auto-suggest-box/option.svelte +2 -1
  4. package/dist/components/avatar/docs/llms.md +20 -14
  5. package/dist/components/badge/docs/llms.md +29 -16
  6. package/dist/components/button/button.svelte +11 -5
  7. package/dist/components/button/docs/llms.md +33 -24
  8. package/dist/components/calendar-date-picker/docs/llms.md +17 -7
  9. package/dist/components/calendar-view/calendar-view-header.svelte +2 -2
  10. package/dist/components/calendar-view/calendar-view-header.svelte.d.ts +2 -17
  11. package/dist/components/calendar-view/calendar-view-months.svelte +1 -1
  12. package/dist/components/calendar-view/calendar-view-years.svelte +1 -1
  13. package/dist/components/calendar-view/calendar-view.svelte +66 -52
  14. package/dist/components/calendar-view/docs/llms.md +42 -30
  15. package/dist/components/card/docs/llms.md +54 -34
  16. package/dist/components/checkbox/docs/llms.md +18 -12
  17. package/dist/components/date-picker/docs/llms.md +25 -15
  18. package/dist/components/dialog/dialog-surface.svelte +8 -8
  19. package/dist/components/dialog/dialog.svelte +41 -39
  20. package/dist/components/dialog/docs/llms.md +59 -34
  21. package/dist/components/divider/docs/llms.md +14 -9
  22. package/dist/components/dropdown/docs/llms.md +34 -15
  23. package/dist/components/dropdown/dropdown.svelte +4 -2
  24. package/dist/components/dropdown-select/docs/llms.md +28 -14
  25. package/dist/components/expander/docs/llms.md +19 -12
  26. package/dist/components/flyout/docs/llms.md +17 -11
  27. package/dist/components/hyperlink/docs/llms.md +11 -7
  28. package/dist/components/info-bar/docs/llms.md +15 -9
  29. package/dist/components/label/docs/llms.md +15 -11
  30. package/dist/components/list-view/docs/llms.md +34 -24
  31. package/dist/components/list-view/list-view-item.svelte +10 -4
  32. package/dist/components/list-view/list-view.svelte +68 -63
  33. package/dist/components/menu/context-menu-trigger.svelte +45 -40
  34. package/dist/components/menu/docs/llms.md +63 -34
  35. package/dist/components/menu/menu-item-checkbox.svelte +3 -1
  36. package/dist/components/menu/menu-item-radio.svelte +3 -1
  37. package/dist/components/menu/menu-item-switch.svelte +3 -1
  38. package/dist/components/menu/menu-item.svelte +5 -1
  39. package/dist/components/menu/menu-popover.svelte +21 -10
  40. package/dist/components/menu/menu-trigger.svelte +22 -9
  41. package/dist/components/menu/menu.svelte +98 -87
  42. package/dist/components/number-box/docs/llms.md +10 -6
  43. package/dist/components/persona/docs/llms.md +19 -14
  44. package/dist/components/progress-bar/docs/llms.md +18 -10
  45. package/dist/components/progress-ring/docs/llms.md +19 -12
  46. package/dist/components/radio-button/docs/llms.md +13 -8
  47. package/dist/components/skeleton/docs/llms.md +13 -8
  48. package/dist/components/slider/docs/llms.md +33 -21
  49. package/dist/components/slider/slider.svelte +13 -11
  50. package/dist/components/table/docs/llms.md +531 -0
  51. package/dist/components/table/table-body.svelte +31 -0
  52. package/dist/components/table/table-body.svelte.d.ts +25 -0
  53. package/dist/components/table/table-cell-actions.svelte +48 -0
  54. package/dist/components/table/table-cell-actions.svelte.d.ts +17 -0
  55. package/dist/components/table/table-cell-layout.svelte +88 -0
  56. package/dist/components/table/table-cell-layout.svelte.d.ts +14 -0
  57. package/dist/components/table/table-cell.svelte +74 -0
  58. package/dist/components/table/table-cell.svelte.d.ts +25 -0
  59. package/dist/components/table/table-header-cell.svelte +257 -0
  60. package/dist/components/table/table-header-cell.svelte.d.ts +25 -0
  61. package/dist/components/table/table-header.svelte +28 -0
  62. package/dist/components/table/table-header.svelte.d.ts +25 -0
  63. package/dist/components/table/table-row.svelte +57 -0
  64. package/dist/components/table/table-row.svelte.d.ts +25 -0
  65. package/dist/components/table/table-selection-cell.svelte +89 -0
  66. package/dist/components/table/table-selection-cell.svelte.d.ts +25 -0
  67. package/dist/components/table/table.svelte +189 -0
  68. package/dist/components/table/table.svelte.d.ts +49 -0
  69. package/dist/components/table/table.svelte.js +138 -0
  70. package/dist/components/table/types.d.ts +287 -0
  71. package/dist/components/table/types.js +1 -0
  72. package/dist/components/text-box/docs/llms.md +23 -17
  73. package/dist/components/textarea/docs/llms.md +16 -7
  74. package/dist/components/time-picker/docs/llms.md +20 -10
  75. package/dist/components/toggle-switch/docs/llms.md +13 -9
  76. package/dist/components/tooltip/docs/llms.md +24 -10
  77. package/dist/components/tree-view/docs/llms.md +53 -35
  78. package/dist/components/tree-view/tree-view-item-content.svelte +1 -1
  79. package/dist/components/tree-view/tree-view-item.svelte +3 -3
  80. package/dist/components/tree-view/tree-view.svelte +89 -93
  81. package/dist/index.d.ts +9 -0
  82. package/dist/index.js +11 -0
  83. package/dist/providers/fluentui-svelte/fluentui-svelte.svelte +15 -15
  84. package/package.json +1 -1
@@ -0,0 +1,89 @@
1
+ <script lang="ts" generics="Tag extends 'td' | 'div' = 'td'">
2
+ import Checkbox from '../checkbox/checkbox.svelte';
3
+ import RadioButton from '../radio-button/radio-button.svelte';
4
+ import { colIndex, getTag, requireTableContext, selectOnSpace } from './table.svelte.js';
5
+ import type { TableDOM, TableSelectionCellProps } from './types.js';
6
+
7
+ let {
8
+ as,
9
+ ref = $bindable(),
10
+ type = 'checkbox',
11
+ checked = false,
12
+ subtle = false,
13
+ invisible = false,
14
+ checkboxIndicator,
15
+ radioIndicator,
16
+ class: classes,
17
+ onclick,
18
+ ...attributes
19
+ }: TableSelectionCellProps<Tag> = $props();
20
+
21
+ const context = requireTableContext();
22
+
23
+ const noNativeElements = $derived(context.config.noNativeElements);
24
+
25
+ const _as = $derived(getTag(as, 'td', noNativeElements));
26
+
27
+ /** A `grid` has no `cell`, only `gridcell`, so arrow navigation renames what the cell reports. */
28
+ const enableTabspot = $derived(context.config.enableTabspot);
29
+ </script>
30
+
31
+ <svelte:element
32
+ this={_as}
33
+ bind:this={ref as TableDOM[Tag]}
34
+ role={enableTabspot ? 'gridcell' : _as === 'div' ? 'cell' : undefined}
35
+ class={['fs-table-selection-cell', { flex: _as === 'div', subtle, invisible, checked: !!checked }, classes]}
36
+ {@attach enableTabspot ? colIndex() : undefined}
37
+ {@attach onclick ? selectOnSpace : undefined}
38
+ {onclick}
39
+ {...attributes}
40
+ >
41
+ <!--
42
+ The indicator answers the pointer itself, and the click still reaches the row underneath, so
43
+ hitting it selects the row rather than only ticking a box the consumer never hears about.
44
+ -->
45
+ {#if type === 'radio'}
46
+ <RadioButton checked={checked === true} {...radioIndicator} />
47
+ {:else}
48
+ <Checkbox wrapperAs="div" checked={checked === true} indeterminate={checked === 'mixed'} {...checkboxIndicator} />
49
+ {/if}
50
+ </svelte:element>
51
+
52
+ <style>
53
+ .fs-table-selection-cell {
54
+ height: var(--fs-table-row-height);
55
+ padding: 0 var(--fs-table-cell-padding);
56
+ vertical-align: middle;
57
+ /* Shrink to the indicator: a native table hands the leftover width to the data columns. */
58
+ width: 1%;
59
+ white-space: nowrap;
60
+ & :global(.fs-checkbox),
61
+ & :global(.fs-radio-button) {
62
+ display: flex;
63
+ align-items: center;
64
+ }
65
+ &.subtle :global(.fs-checkbox),
66
+ &.subtle :global(.fs-radio-button) {
67
+ /* Set by the row, so the indicator surfaces on hover and while the focus is inside. */
68
+ opacity: var(--fs-table-row-active, 0);
69
+ transition: opacity var(--fs-fast-duration) var(--fs-point-to-point);
70
+ @media (prefers-reduced-motion: reduce) {
71
+ transition: none;
72
+ }
73
+ }
74
+ &.subtle.checked :global(.fs-checkbox),
75
+ &.subtle.checked :global(.fs-radio-button) {
76
+ opacity: 1;
77
+ }
78
+ &.invisible :global(.fs-checkbox),
79
+ &.invisible :global(.fs-radio-button) {
80
+ visibility: hidden;
81
+ }
82
+ &.flex {
83
+ display: flex;
84
+ align-items: center;
85
+ flex: 0 0 auto;
86
+ width: auto;
87
+ }
88
+ }
89
+ </style>
@@ -0,0 +1,25 @@
1
+ import type { TableSelectionCellProps } from './types.ts';
2
+ declare function $$render<Tag extends 'td' | 'div' = 'td'>(): {
3
+ props: TableSelectionCellProps<Tag>;
4
+ exports: {};
5
+ bindings: "ref";
6
+ slots: {};
7
+ events: {};
8
+ };
9
+ declare class __sveltets_Render<Tag extends 'td' | 'div' = 'td'> {
10
+ props(): ReturnType<typeof $$render<Tag>>['props'];
11
+ events(): ReturnType<typeof $$render<Tag>>['events'];
12
+ slots(): ReturnType<typeof $$render<Tag>>['slots'];
13
+ bindings(): "ref";
14
+ exports(): {};
15
+ }
16
+ interface $$IsomorphicComponent {
17
+ new <Tag extends 'td' | 'div' = 'td'>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Tag>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Tag>['props']>, ReturnType<__sveltets_Render<Tag>['events']>, ReturnType<__sveltets_Render<Tag>['slots']>> & {
18
+ $$bindings?: ReturnType<__sveltets_Render<Tag>['bindings']>;
19
+ } & ReturnType<__sveltets_Render<Tag>['exports']>;
20
+ <Tag extends 'td' | 'div' = 'td'>(internal: unknown, props: ReturnType<__sveltets_Render<Tag>['props']> & {}): ReturnType<__sveltets_Render<Tag>['exports']>;
21
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
22
+ }
23
+ declare const TableSelectionCell: $$IsomorphicComponent;
24
+ type TableSelectionCell<Tag extends 'td' | 'div' = 'td'> = InstanceType<typeof TableSelectionCell<Tag>>;
25
+ export default TableSelectionCell;
@@ -0,0 +1,189 @@
1
+ <script lang="ts" generics="Tag extends 'table' | 'div' = 'table'">
2
+ import { tick } from 'svelte';
3
+ import { getTabspotAttributes, tabspotVirtual } from 'tabspot';
4
+ import { getTag, setTableContext, TABSPOT_ITEMS, TABSPOT_SKIP } from './table.svelte.js';
5
+ import type { TableContext, TableDOM, TableProps } from './types.js';
6
+
7
+ let {
8
+ as,
9
+ ref = $bindable(),
10
+ size = 'medium',
11
+ noNativeElements = false,
12
+ sortable = false,
13
+ noRowBorders = false,
14
+ enableTabspot = false,
15
+ virtualizer,
16
+ resizableColumns = false,
17
+ columnWidths = $bindable([]),
18
+ columnSizing,
19
+ onColumnResize,
20
+ class: classes,
21
+ children,
22
+ ...attributes
23
+ }: TableProps<Tag> = $props();
24
+
25
+ /** Narrow enough that a column never collapses to nothing, wide enough to stay grabbable. */
26
+ const MIN_COLUMN_WIDTH = 40;
27
+
28
+ const context: TableContext = $state({
29
+ config: {
30
+ get size() {
31
+ return size;
32
+ },
33
+ get noNativeElements() {
34
+ return noNativeElements;
35
+ },
36
+ get sortable() {
37
+ return sortable;
38
+ },
39
+ get enableTabspot() {
40
+ return enableTabspot;
41
+ },
42
+ get resizableColumns() {
43
+ return resizableColumns;
44
+ }
45
+ },
46
+ state: {
47
+ get columnWidths() {
48
+ return columnWidths;
49
+ },
50
+ set columnWidths(v) {
51
+ columnWidths = v;
52
+ }
53
+ },
54
+ events: null,
55
+ methods: {
56
+ getColumnWidth: (index: number) => context.state.columnWidths[index] ?? columnSizing?.[index]?.defaultWidth,
57
+ resizeColumn: (e: Event, index: number, width: number) => {
58
+ const { state } = context;
59
+ const min = columnSizing?.[index]?.minWidth ?? MIN_COLUMN_WIDTH;
60
+ const next = Math.round(Math.max(min, width));
61
+ if (state.columnWidths[index] === next) return;
62
+
63
+ // A fresh array, so a consumer binding to it sees the change however they store it.
64
+ const widths = [...state.columnWidths];
65
+ widths[index] = next;
66
+ state.columnWidths = widths;
67
+ onColumnResize?.(e, { index, width: next });
68
+ }
69
+ }
70
+ });
71
+
72
+ setTableContext(context);
73
+
74
+ /**
75
+ * The tag the invariant rules over. The markup renders it through here, so it is checked during
76
+ * SSR and on every prop update rather than only once on mount.
77
+ */
78
+ const _as = $derived(getTag(as, 'table', noNativeElements));
79
+
80
+ /**
81
+ * A grid mover: the arrows walk the cells, rows are read off the DOM parent of each cell, and
82
+ * the whole table takes a single tab stop. `Escape` is left alone, a grid has nothing to exit to.
83
+ */
84
+ const tabspotAttributes = $derived(
85
+ enableTabspot &&
86
+ getTabspotAttributes({
87
+ root: { manageSpecialKeys: { Home: true, End: true, PageUp: true, PageDown: true } },
88
+ mover: { layout: 'grid', rows: { by: 'parent' }, items: TABSPOT_ITEMS, skip: TABSPOT_SKIP }
89
+ })
90
+ );
91
+
92
+ /**
93
+ * Tabspot navigates the DOM, and a windowed list only renders a slice of it. The adapter is what
94
+ * lets a move aim at a row that is not there yet: it scrolls the row in, then waits for Svelte to
95
+ * flush before looking for it.
96
+ */
97
+ $effect(() => {
98
+ if (!enableTabspot || !virtualizer || !ref) return;
99
+ return tabspotVirtual(ref as HTMLElement, {
100
+ scrollToIndex: (index) => virtualizer.scrollToIndex?.(index),
101
+ count: () => virtualizer.size,
102
+ tick
103
+ });
104
+ });
105
+ </script>
106
+
107
+ <!--
108
+ @component
109
+ A table displays sets of two dimensional data. Every part is a primitive, so sorting and
110
+ selection stay the responsibility of the consumer.
111
+
112
+ - Usage:
113
+ ```tsx
114
+ <script>
115
+ import { Table, TableHeader, TableHeaderCell, TableBody, TableRow, TableCell } from 'fluentui-svelte';
116
+ </script>
117
+
118
+ <Table aria-label="Files">
119
+ <TableHeader>
120
+ <TableRow>
121
+ <TableHeaderCell>File</TableHeaderCell>
122
+ </TableRow>
123
+ </TableHeader>
124
+ <TableBody>
125
+ <TableRow>
126
+ <TableCell>Meeting notes</TableCell>
127
+ </TableRow>
128
+ </TableBody>
129
+ </Table>
130
+ ```
131
+ -->
132
+ <svelte:element
133
+ this={_as}
134
+ bind:this={ref as TableDOM[Tag]}
135
+ role={enableTabspot ? 'grid' : _as === 'div' ? 'table' : undefined}
136
+ class={[
137
+ 'fs-table',
138
+ size,
139
+ { flex: _as === 'div', 'no-row-borders': noRowBorders, resizable: resizableColumns },
140
+ classes
141
+ ]}
142
+ {...tabspotAttributes}
143
+ {...attributes}
144
+ >
145
+ {@render children?.()}
146
+ </svelte:element>
147
+
148
+ <style>
149
+ .fs-table {
150
+ width: 100%;
151
+ border-collapse: collapse;
152
+ text-align: left;
153
+ color: var(--fs-text-primary);
154
+ font-family: var(--fs-font-family-base);
155
+ font-size: var(--fs-body2-font-size);
156
+ line-height: var(--fs-body2-line-height);
157
+ /* Every part reads these, so a size change ripples out of the root alone. */
158
+ --fs-table-row-height: 2.75rem;
159
+ --fs-table-cell-padding: 0.5rem;
160
+ --fs-table-row-border: 1px solid var(--fs-divider-stroke-default);
161
+ &.no-row-borders {
162
+ --fs-table-row-border: none;
163
+ }
164
+ /*
165
+ * The auto algorithm re-measures every column from its content, which undoes a drag. A fixed
166
+ * table also has to size to the sum of its columns: a percentage width inside a shrink-to-fit
167
+ * container is circular, and the browser resolves that circle into a runaway width.
168
+ */
169
+ &.resizable {
170
+ table-layout: fixed;
171
+ width: max-content;
172
+ max-width: 100%;
173
+ }
174
+ &.small {
175
+ --fs-table-row-height: 2.125rem;
176
+ }
177
+ &.extra-small {
178
+ --fs-table-row-height: 1.5rem;
179
+ --fs-table-cell-padding: 0.375rem;
180
+ font-size: var(--fs-caption-font-size);
181
+ line-height: var(--fs-caption-line-height);
182
+ }
183
+ /* A `div` table has no table layout to lean on, so the rows stack and the cells flex. */
184
+ &.flex {
185
+ display: flex;
186
+ flex-direction: column;
187
+ }
188
+ }
189
+ </style>
@@ -0,0 +1,49 @@
1
+ import type { TableProps } from './types.ts';
2
+ declare function $$render<Tag extends 'table' | 'div' = 'table'>(): {
3
+ props: TableProps<Tag>;
4
+ exports: {};
5
+ bindings: "ref" | "columnWidths";
6
+ slots: {};
7
+ events: {};
8
+ };
9
+ declare class __sveltets_Render<Tag extends 'table' | 'div' = 'table'> {
10
+ props(): ReturnType<typeof $$render<Tag>>['props'];
11
+ events(): ReturnType<typeof $$render<Tag>>['events'];
12
+ slots(): ReturnType<typeof $$render<Tag>>['slots'];
13
+ bindings(): "ref" | "columnWidths";
14
+ exports(): {};
15
+ }
16
+ interface $$IsomorphicComponent {
17
+ new <Tag extends 'table' | 'div' = 'table'>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Tag>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Tag>['props']>, ReturnType<__sveltets_Render<Tag>['events']>, ReturnType<__sveltets_Render<Tag>['slots']>> & {
18
+ $$bindings?: ReturnType<__sveltets_Render<Tag>['bindings']>;
19
+ } & ReturnType<__sveltets_Render<Tag>['exports']>;
20
+ <Tag extends 'table' | 'div' = 'table'>(internal: unknown, props: ReturnType<__sveltets_Render<Tag>['props']> & {}): ReturnType<__sveltets_Render<Tag>['exports']>;
21
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
22
+ }
23
+ /**
24
+ * A table displays sets of two dimensional data. Every part is a primitive, so sorting and
25
+ * selection stay the responsibility of the consumer.
26
+ *
27
+ * - Usage:
28
+ * ```tsx
29
+ * <script>
30
+ * import { Table, TableHeader, TableHeaderCell, TableBody, TableRow, TableCell } from 'fluentui-svelte';
31
+ * </script>
32
+ *
33
+ * <Table aria-label="Files">
34
+ * <TableHeader>
35
+ * <TableRow>
36
+ * <TableHeaderCell>File</TableHeaderCell>
37
+ * </TableRow>
38
+ * </TableHeader>
39
+ * <TableBody>
40
+ * <TableRow>
41
+ * <TableCell>Meeting notes</TableCell>
42
+ * </TableRow>
43
+ * </TableBody>
44
+ * </Table>
45
+ * ```
46
+ */
47
+ declare const Table: $$IsomorphicComponent;
48
+ type Table<Tag extends 'table' | 'div' = 'table'> = InstanceType<typeof Table<Tag>>;
49
+ export default Table;
@@ -0,0 +1,138 @@
1
+ import { PREFIX } from '../../internal/constants.js';
2
+ import { createFSContext } from '../../internal/index.js';
3
+ import { on } from 'svelte/events';
4
+ export const COMPONENT_NAME = `${PREFIX}-table-`;
5
+ export const [getTableContext, setTableContext] = createFSContext();
6
+ /** The cells a grid mover lands on. Every kind of cell takes a turn, header and selection included. */
7
+ export const TABSPOT_ITEMS = '.fs-table-header-cell, .fs-table-cell, .fs-table-selection-cell';
8
+ /** Cells the cursor passes over instead of landing on, which is what `focusMode: 'none'` asks for. */
9
+ export const TABSPOT_SKIP = '[data-fs-focus-mode="none"]';
10
+ /**
11
+ * Everything that would answer the Tab key on its own. A disabled control takes no focus, so
12
+ * holding it would be pointless and entering the cell would land on something that cannot be left.
13
+ */
14
+ const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex], [contenteditable="true"]';
15
+ /** Controls that spend `Space` themselves, so a row must not take it from under them. */
16
+ const SPACE_CONSUMERS = 'a[href], button, input, select, textarea, [contenteditable="true"]';
17
+ /**
18
+ * `Space` activates a row or a cell the consumer wired, the same way a pointer click does. The grid
19
+ * pattern asks for it: the arrow keys park the focus on a cell, and a cell answers no key by itself.
20
+ *
21
+ * Only a wired element claims the key — anywhere else `Space` stays the page scroll it always was.
22
+ */
23
+ export const selectOnSpace = (node) => {
24
+ const onkeydown = (e) => {
25
+ if (e.key !== ' ' || e.defaultPrevented)
26
+ return;
27
+ if (e.target.closest(SPACE_CONSUMERS))
28
+ return;
29
+ e.preventDefault();
30
+ node.click();
31
+ };
32
+ return on(node, 'keydown', onkeydown);
33
+ };
34
+ /**
35
+ * Resolves which column a cell sits in. A cell is always a direct child of its row, so its position
36
+ * among its siblings is the column — nothing else in the markup knows it.
37
+ *
38
+ * Tabspot reads the attribute to find a cell again once a virtualized row has remounted, and the
39
+ * reported index is what lets a cell look its own width up.
40
+ */
41
+ export const colIndex = (report) => (node) => {
42
+ const parent = node.parentElement;
43
+ if (!parent)
44
+ return;
45
+ const index = [...parent.children].indexOf(node);
46
+ node.dataset.colindex = String(index);
47
+ report?.(index);
48
+ };
49
+ /**
50
+ * `focusMode: 'group'`. The focusable content of a cell is held out of the tab order until the user
51
+ * asks for it: `Enter` steps in, `Escape` steps back out to the cell itself.
52
+ *
53
+ * Without it every button in every row is its own tab stop, which is exactly what the single tab
54
+ * stop of a grid exists to avoid. Tabspot cannot express this on its own — its grouper is driven by
55
+ * a direction, and a grid has already spent all four arrows — so the two keys are handled here.
56
+ */
57
+ export const focusGroup = (node) => {
58
+ /**
59
+ * The tabindex a held element carried before is parked on the element itself, so releasing
60
+ * restores it rather than guessing, and a row that remounts takes its own bookkeeping with it.
61
+ */
62
+ const hold = () => {
63
+ for (const el of node.querySelectorAll(FOCUSABLE)) {
64
+ if (el.dataset.fsHeldTabindex !== undefined)
65
+ continue;
66
+ el.dataset.fsHeldTabindex = el.getAttribute('tabindex') ?? '';
67
+ el.setAttribute('tabindex', '-1');
68
+ }
69
+ };
70
+ const release = () => {
71
+ for (const el of node.querySelectorAll('[data-fs-held-tabindex]')) {
72
+ const previous = el.dataset.fsHeldTabindex;
73
+ if (previous)
74
+ el.setAttribute('tabindex', previous);
75
+ else
76
+ el.removeAttribute('tabindex');
77
+ delete el.dataset.fsHeldTabindex;
78
+ }
79
+ };
80
+ const onkeydown = (e) => {
81
+ if (e.key === 'Enter' && e.target === node) {
82
+ release();
83
+ const first = node.querySelector(FOCUSABLE);
84
+ if (!first)
85
+ return hold();
86
+ e.preventDefault();
87
+ first.focus();
88
+ }
89
+ else if (e.key === 'Escape' && e.target !== node) {
90
+ // The root would otherwise read the same Escape as "leave the table".
91
+ e.preventDefault();
92
+ e.stopPropagation();
93
+ hold();
94
+ node.focus();
95
+ }
96
+ };
97
+ /** Focus moving out of the cell ends the visit, however it left. */
98
+ const onfocusout = (e) => {
99
+ if (!node.contains(e.relatedTarget))
100
+ hold();
101
+ };
102
+ hold();
103
+ // Content that arrives later — a menu opening, a row re-rendering — has to be held too.
104
+ const observer = new MutationObserver(() => {
105
+ // The node's own document, so a table inside an iframe reads the focus that belongs to it.
106
+ if (!node.contains(node.ownerDocument.activeElement))
107
+ hold();
108
+ });
109
+ observer.observe(node, { childList: true, subtree: true });
110
+ const offKeydown = on(node, 'keydown', onkeydown);
111
+ const offFocusout = on(node, 'focusout', onfocusout);
112
+ return () => {
113
+ observer.disconnect();
114
+ offKeydown();
115
+ offFocusout();
116
+ release();
117
+ };
118
+ };
119
+ /**
120
+ * The element a table part renders. An explicit `as` wins, otherwise the part falls back to its
121
+ * semantic tag, or to a `div` when the table opted out of native elements.
122
+ *
123
+ * Every part accepts exactly its semantic tag or a `div`, so the invariant is checked here rather
124
+ * than against a list each caller would have to carry.
125
+ */
126
+ export const getTag = (as, native, noNativeElements) => {
127
+ const tag = as ?? (noNativeElements ? 'div' : native);
128
+ if (tag !== native && tag !== 'div')
129
+ throw new Error(`Invalid tag: ${tag}. Must be one of ${native}, div`);
130
+ return tag;
131
+ };
132
+ /** Reads the table context, or fails loudly when a part is rendered outside a `Table`. */
133
+ export const requireTableContext = () => {
134
+ const context = getTableContext();
135
+ if (!context)
136
+ throw new Error(`No TableContext found for ${COMPONENT_NAME}.`);
137
+ return context;
138
+ };