@zeyos/zx 4.4.0 → 5.2.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.
- package/CHANGELOG.md +447 -0
- package/README.md +44 -11
- package/dist/types/components/account-menu/account-menu.d.ts +8 -0
- package/dist/types/components/activity-list/activity-list.d.ts +31 -1
- package/dist/types/components/app-icon/app-icon.d.ts +14 -2
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +293 -17
- package/dist/types/components/avatar/avatar.d.ts +46 -7
- package/dist/types/components/badge/badge.d.ts +21 -1
- package/dist/types/components/button/button.d.ts +15 -0
- package/dist/types/components/calendar/calendar.d.ts +67 -5
- package/dist/types/components/card-view/card-view.d.ts +7 -4
- package/dist/types/components/card-view/record-card.d.ts +11 -0
- package/dist/types/components/chart/chart.d.ts +10 -0
- package/dist/types/components/checklist/checklist.d.ts +9 -0
- package/dist/types/components/code-editor/code-editor.d.ts +6 -17
- package/dist/types/components/context-menu/context-menu.d.ts +6 -2
- package/dist/types/components/data-filter/data-filter.d.ts +4 -4
- package/dist/types/components/datebox/date-range-box.d.ts +5 -1
- package/dist/types/components/datebox/datebox.d.ts +7 -2
- package/dist/types/components/dialog/dialog.d.ts +46 -6
- package/dist/types/components/dropdown/dropdown.d.ts +24 -9
- package/dist/types/components/entity-ref/entity-ref.d.ts +2 -2
- package/dist/types/components/field/field.d.ts +54 -2
- package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
- package/dist/types/components/fieldset/fieldset.d.ts +26 -1
- package/dist/types/components/file-item/file-item.d.ts +34 -6
- package/dist/types/components/file-list/file-list.d.ts +35 -3
- package/dist/types/components/filter/filter.d.ts +127 -3
- package/dist/types/components/filter-panel/filter-panel.d.ts +36 -7
- package/dist/types/components/form/form.d.ts +12 -1
- package/dist/types/components/kanban-view/kanban-card.d.ts +8 -0
- package/dist/types/components/launcher/launcher.d.ts +268 -15
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +16 -7
- package/dist/types/components/menu-button/menu-button.d.ts +39 -5
- package/dist/types/components/message/message.d.ts +132 -17
- package/dist/types/components/modal/modal.d.ts +43 -2
- package/dist/types/components/module-bar/module-bar.d.ts +395 -0
- package/dist/types/components/pagination/pagination.d.ts +17 -3
- package/dist/types/components/select/color.d.ts +12 -0
- package/dist/types/components/select/glyphs.d.ts +124 -0
- package/dist/types/components/select/select.d.ts +17 -3
- package/dist/types/components/sheet/sheet.d.ts +15 -0
- package/dist/types/components/stat-tile/stat-tile.d.ts +16 -5
- package/dist/types/components/tabbox/tabbox.d.ts +9 -0
- package/dist/types/components/table/table.d.ts +311 -18
- package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
- package/dist/types/components/tree/tree.d.ts +13 -0
- package/dist/types/components/view/record-view.d.ts +59 -2
- package/dist/types/components/view-settings/view-settings.d.ts +879 -0
- package/dist/types/components/workspace-switcher/workspace-switcher.d.ts +327 -0
- package/dist/types/core/breakpoint.d.ts +1 -1
- package/dist/types/core/component.d.ts +12 -2
- package/dist/types/core/i18n.d.ts +27 -3
- package/dist/types/core/icons.d.ts +23 -25
- package/dist/types/core/keyboard.d.ts +98 -1
- package/dist/types/core/messages.d.ts +382 -0
- package/dist/types/core/motion.d.ts +59 -0
- package/dist/types/core/overlay-host.d.ts +42 -1
- package/dist/types/core/overlay-stack.d.ts +260 -0
- package/dist/types/index.d.ts +10 -5
- package/dist/types/internal/app-rail.d.ts +38 -0
- package/dist/types/internal/menu-items.d.ts +71 -3
- package/dist/types/zeyos/connect.d.ts +6 -0
- package/dist/types/zeyos/form.d.ts +50 -9
- package/dist/types/zeyos/index.d.ts +2 -1
- package/dist/types/zeyos/launcher.d.ts +9 -0
- package/dist/types/zeyos/module-bar.d.ts +176 -0
- package/dist/types/zeyos/query.d.ts +110 -5
- package/dist/types/zeyos/schema.d.ts +24 -1
- package/dist/types/zeyos/table.d.ts +345 -22
- package/dist/types/zeyos/view.d.ts +26 -2
- package/dist/zx-zeyos.esm.js +6178 -1008
- package/dist/zx-zeyos.esm.min.js +53 -5
- package/dist/zx.css +2861 -827
- package/dist/zx.esm.js +8909 -1480
- package/dist/zx.esm.min.js +55 -7
- package/dist/zx.global.js +8911 -1482
- package/dist/zx.global.min.js +55 -7
- package/dist/zx.min.css +1 -1
- package/docs/api.json +1079 -117
- package/docs/llms.md +956 -154
- package/docs/llms.txt +9 -5
- package/package.json +1 -1
- package/src/components/account-menu/account-menu.css +35 -10
- package/src/components/account-menu/account-menu.js +12 -0
- package/src/components/activity-item/activity-item.css +1 -1
- package/src/components/activity-item/activity-item.js +3 -0
- package/src/components/activity-list/activity-list.css +38 -3
- package/src/components/activity-list/activity-list.js +61 -1
- package/src/components/app-icon/app-icon.css +14 -3
- package/src/components/app-icon/app-icon.js +11 -1
- package/src/components/app-rail/app-rail.css +59 -79
- package/src/components/app-sidebar/app-sidebar.css +304 -168
- package/src/components/app-sidebar/app-sidebar.js +667 -46
- package/src/components/aurora/aurora.css +5 -3
- package/src/components/avatar/avatar.css +32 -7
- package/src/components/avatar/avatar.js +72 -8
- package/src/components/badge/badge.css +54 -2
- package/src/components/badge/badge.js +29 -2
- package/src/components/breadcrumb/breadcrumb.css +1 -1
- package/src/components/button/button.css +67 -9
- package/src/components/button/button.js +38 -0
- package/src/components/calendar/calendar.css +47 -47
- package/src/components/calendar/calendar.js +26 -52
- package/src/components/card/card.css +4 -4
- package/src/components/card-view/card-view.css +14 -12
- package/src/components/card-view/card-view.js +13 -9
- package/src/components/card-view/record-card.js +14 -3
- package/src/components/chart/chart.css +4 -1
- package/src/components/chart/chart.js +12 -0
- package/src/components/check-button/check-button.css +10 -2
- package/src/components/checklist/checklist.css +9 -2
- package/src/components/checklist/checklist.js +11 -5
- package/src/components/checklist/field-adapter.js +3 -1
- package/src/components/code-editor/code-editor.css +7 -6
- package/src/components/code-editor/code-editor.js +19 -1
- package/src/components/code-editor/field-adapter.js +9 -2
- package/src/components/context-menu/context-menu.css +9 -4
- package/src/components/context-menu/context-menu.js +8 -2
- package/src/components/copy/copy-button.css +6 -0
- package/src/components/copy/copy-input.css +9 -4
- package/src/components/data-filter/data-filter.css +5 -4
- package/src/components/data-filter/data-filter.js +2 -2
- package/src/components/date-picker/date-picker.css +6 -6
- package/src/components/date-picker/date-range-picker.css +3 -3
- package/src/components/date-picker/time-picker.css +3 -3
- package/src/components/datebox/date-range-box.css +79 -2
- package/src/components/datebox/date-range-box.js +13 -2
- package/src/components/datebox/datebox.css +79 -2
- package/src/components/datebox/datebox.js +13 -2
- package/src/components/dialog/dialog.css +26 -0
- package/src/components/dialog/dialog.js +99 -17
- package/src/components/dock/dock.css +5 -4
- package/src/components/dropdown/dropdown.css +1 -1
- package/src/components/dropdown/dropdown.js +159 -72
- package/src/components/empty-state/empty-state.css +2 -2
- package/src/components/entity-ref/entity-ref.css +1 -1
- package/src/components/entity-ref/entity-ref.js +1 -1
- package/src/components/field/field.css +28 -9
- package/src/components/field/field.js +101 -29
- package/src/components/field-upload/field-upload.css +1 -1
- package/src/components/fieldset/external-adapter.js +363 -0
- package/src/components/fieldset/fieldset.css +5 -3
- package/src/components/fieldset/fieldset.js +38 -64
- package/src/components/file-item/file-item.js +144 -10
- package/src/components/file-list/file-list.js +144 -21
- package/src/components/filter/filter.css +86 -2
- package/src/components/filter/filter.js +307 -11
- package/src/components/filter-panel/filter-panel.css +1 -1
- package/src/components/filter-panel/filter-panel.js +149 -32
- package/src/components/finder/finder.css +16 -14
- package/src/components/form/form.css +5 -4
- package/src/components/form/form.js +11 -2
- package/src/components/grid/grid.css +1 -1
- package/src/components/groupbox/groupbox.css +1 -1
- package/src/components/kanban-view/kanban-card.js +11 -3
- package/src/components/kanban-view/kanban-view.css +42 -18
- package/src/components/kanban-view/kanban-view.js +9 -6
- package/src/components/launcher/launcher.css +108 -26
- package/src/components/launcher/launcher.js +530 -42
- package/src/components/list-toolbar/list-toolbar.css +13 -8
- package/src/components/list-toolbar/list-toolbar.js +40 -14
- package/src/components/master-panel/master-panel.css +2 -0
- package/src/components/master-panel/master-panel.js +0 -1
- package/src/components/menu-button/menu-button.css +72 -4
- package/src/components/menu-button/menu-button.js +160 -18
- package/src/components/message/message.css +33 -3
- package/src/components/message/message.js +264 -22
- package/src/components/modal/modal.css +1 -1
- package/src/components/modal/modal.js +83 -15
- package/src/components/module-bar/module-bar.css +486 -0
- package/src/components/module-bar/module-bar.js +1074 -0
- package/src/components/multi-value-editor/multi-value-editor.css +1 -1
- package/src/components/navigation-bar/navigation-bar.css +18 -9
- package/src/components/number-field/field-adapter.js +3 -1
- package/src/components/number-field/number-field.css +70 -0
- package/src/components/number-field/number-field.js +2 -0
- package/src/components/pagination/pagination.css +13 -11
- package/src/components/pagination/pagination.js +20 -4
- package/src/components/panel/panel.css +4 -3
- package/src/components/questionnaire/questionnaire.css +4 -4
- package/src/components/rating/field-adapter.js +2 -1
- package/src/components/rating/rating.css +8 -1
- package/src/components/search/search.css +4 -1
- package/src/components/select/color.js +22 -0
- package/src/components/select/field-adapter.js +2 -0
- package/src/components/select/glyphs.js +165 -0
- package/src/components/select/select.css +138 -45
- package/src/components/select/select.js +92 -47
- package/src/components/sheet/sheet.css +12 -9
- package/src/components/sheet/sheet.js +74 -32
- package/src/components/slider/field-adapter.js +6 -1
- package/src/components/slider/slider.css +18 -4
- package/src/components/slider/slider.js +1 -1
- package/src/components/sort-control/sort-control.css +4 -1
- package/src/components/split-view/split-view.css +1 -1
- package/src/components/stat-tile/stat-tile.css +42 -47
- package/src/components/stat-tile/stat-tile.js +17 -6
- package/src/components/stepper/stepper.css +1 -1
- package/src/components/tabbox/tabbox.css +41 -32
- package/src/components/tabbox/tabbox.js +5 -1
- package/src/components/table/table.css +187 -37
- package/src/components/table/table.js +718 -73
- package/src/components/tag-picker/field-adapter.js +2 -1
- package/src/components/tag-picker/tag-picker.css +83 -5
- package/src/components/tag-picker/tag-picker.js +25 -8
- package/src/components/timebox/timebox.css +22 -2
- package/src/components/toggle/toggle.css +12 -1
- package/src/components/toolbar/toolbar.js +2 -2
- package/src/components/tooltip/tooltip.css +4 -0
- package/src/components/tooltip/tooltip.js +44 -19
- package/src/components/tree/tree.css +26 -17
- package/src/components/tree/tree.js +31 -0
- package/src/components/value-list/value-list.css +3 -3
- package/src/components/view/record-view.css +1 -1
- package/src/components/view/record-view.js +178 -18
- package/src/components/view-settings/view-settings.css +285 -0
- package/src/components/view-settings/view-settings.js +1830 -0
- package/src/components/workspace-switcher/workspace-switcher.css +188 -0
- package/src/components/workspace-switcher/workspace-switcher.js +463 -0
- package/src/core/breakpoint.js +1 -1
- package/src/core/component.js +88 -17
- package/src/core/fontawesome.js +5 -1
- package/src/core/i18n.js +47 -4
- package/src/core/icons.js +204 -61
- package/src/core/keyboard.js +206 -11
- package/src/core/messages.js +427 -0
- package/src/core/motion.js +147 -0
- package/src/core/overlay-host.js +77 -3
- package/src/core/overlay-stack.js +487 -0
- package/src/core/position.js +22 -3
- package/src/index.js +10 -5
- package/src/internal/app-rail.js +135 -55
- package/src/internal/menu-items.js +61 -3
- package/src/zeyos/connect.js +6 -2
- package/src/zeyos/form.js +69 -27
- package/src/zeyos/index.js +4 -1
- package/src/zeyos/launcher.js +8 -3
- package/src/zeyos/module-bar.js +181 -0
- package/src/zeyos/query.js +197 -13
- package/src/zeyos/schema.js +144 -27
- package/src/zeyos/table.js +287 -84
- package/src/zeyos/view.js +49 -93
- package/styles/base.css +72 -1
- package/styles/icons.css +11 -1
- package/styles/print.css +1 -1
- package/styles/tokens/dark.css +117 -37
- package/styles/tokens/global.css +121 -27
- package/styles/tokens/semantic.css +140 -43
- package/styles/tokens/themes.css +7 -6
- package/styles/zx.css +3 -1
- package/styles/tokens/density.css +0 -9
package/src/core/keyboard.js
CHANGED
|
@@ -5,10 +5,18 @@ const FOCUSABLE_SELECTOR = [
|
|
|
5
5
|
'[contenteditable="true"]', '[tabindex]:not([tabindex="-1"])'
|
|
6
6
|
].join(',');
|
|
7
7
|
|
|
8
|
+
/** Attributes that change whether a roving item is available; nothing else is observed. */
|
|
9
|
+
const AVAILABILITY_ATTRIBUTES = ['hidden', 'disabled', 'aria-disabled', 'aria-hidden'];
|
|
10
|
+
/** `<input>` types without a caret, which a roving group may move focus away from freely. */
|
|
11
|
+
const CARETLESS_INPUT_TYPES = new Set(['button', 'checkbox', 'color', 'file', 'image', 'reset', 'submit']);
|
|
12
|
+
|
|
8
13
|
/**
|
|
9
14
|
* @typedef {Object} RovingOptions
|
|
10
15
|
* @property {'vertical'|'horizontal'} [orientation='vertical'] Arrow-key axis.
|
|
11
16
|
* @property {boolean} [wrap=true] Whether navigation wraps at either end.
|
|
17
|
+
* @property {'auto'|boolean} [rtl='auto'] Whether ArrowLeft and ArrowRight swap meaning in a
|
|
18
|
+
* horizontal group. `'auto'` reads the container's computed `direction` on every key press, so
|
|
19
|
+
* a group inside `dir="rtl"` moves to the visually next item; `true`/`false` force it.
|
|
12
20
|
*/
|
|
13
21
|
/**
|
|
14
22
|
* @typedef {Object} FocusTrapController
|
|
@@ -73,12 +81,23 @@ export function focusTrap(container) {
|
|
|
73
81
|
|
|
74
82
|
/**
|
|
75
83
|
* Applies the ARIA roving-tabindex keyboard pattern to matching descendants.
|
|
84
|
+
*
|
|
85
|
+
* Editable items keep the keys they need, as the APG toolbar pattern asks: Home and End always
|
|
86
|
+
* stay with a text field, select, or contenteditable region, and so do the arrow keys — except
|
|
87
|
+
* that a text field whose caret already sits at the edge the arrow points past hands focus on to
|
|
88
|
+
* the neighbouring item, so a reader is never trapped in it. Under `dir="rtl"` the horizontal
|
|
89
|
+
* arrows follow the visual order.
|
|
90
|
+
*
|
|
91
|
+
* The group re-reads its items when children are added or removed and when an availability
|
|
92
|
+
* attribute (`hidden`, `disabled`, `aria-disabled`, `aria-hidden`) changes on an item or on an
|
|
93
|
+
* element inside the container that holds items — a `<fieldset disabled>` toggled around them; its
|
|
94
|
+
* own `tabindex` writes and unrelated attribute churn are not observed.
|
|
76
95
|
* @param {Element} container Event and query container.
|
|
77
96
|
* @param {string} itemSelector Selector for roving items.
|
|
78
97
|
* @param {RovingOptions} [options={}] Behavior options.
|
|
79
98
|
* @returns {RovingController}
|
|
80
99
|
*/
|
|
81
|
-
export function rovingTabindex(container, itemSelector, { orientation = 'vertical', wrap = true } = {}) {
|
|
100
|
+
export function rovingTabindex(container, itemSelector, { orientation = 'vertical', wrap = true, rtl = 'auto' } = {}) {
|
|
82
101
|
const controller = new AbortController();
|
|
83
102
|
const originals = new Map();
|
|
84
103
|
let destroyed = false;
|
|
@@ -117,14 +136,13 @@ export function rovingTabindex(container, itemSelector, { orientation = 'vertica
|
|
|
117
136
|
const index = available.indexOf(current);
|
|
118
137
|
if (index < 0) return;
|
|
119
138
|
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
if (event.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (nextIndex === null || available.length === 0) return;
|
|
139
|
+
const reversed = orientation === 'horizontal' && isRightToLeft(container, rtl);
|
|
140
|
+
const step = rovingStep(event.key, orientation, reversed);
|
|
141
|
+
if (step === null) return;
|
|
142
|
+
if (!editableAllowsRoving(/** @type {Element|null} */ (event.target), step)) return;
|
|
143
|
+
let nextIndex = step === 'first' ? 0 : step === 'last' ? available.length - 1
|
|
144
|
+
: step === 'previous' ? index - 1 : index + 1;
|
|
145
|
+
if (available.length === 0) return;
|
|
128
146
|
|
|
129
147
|
if (wrap) nextIndex = (nextIndex + available.length) % available.length;
|
|
130
148
|
else nextIndex = Math.max(0, Math.min(available.length - 1, nextIndex));
|
|
@@ -143,10 +161,38 @@ export function rovingTabindex(container, itemSelector, { orientation = 'vertica
|
|
|
143
161
|
|
|
144
162
|
container.addEventListener('keydown', onKeydown, { signal: controller.signal });
|
|
145
163
|
container.addEventListener('focusin', onFocus, { signal: controller.signal });
|
|
146
|
-
const observer = typeof MutationObserver === 'function'
|
|
147
|
-
|
|
164
|
+
const observer = typeof MutationObserver === 'function'
|
|
165
|
+
? new MutationObserver((records) => { if (records.some(concernsItems)) update(); })
|
|
166
|
+
: null;
|
|
167
|
+
observer?.observe(container, {
|
|
168
|
+
childList: true, subtree: true, attributes: true, attributeFilter: AVAILABILITY_ATTRIBUTES
|
|
169
|
+
});
|
|
148
170
|
update();
|
|
149
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Whether a mutation can change the item set: an item's availability attribute, or an added or
|
|
174
|
+
* removed subtree that is, holds, or held an item. Content churn inside an item is ignored.
|
|
175
|
+
* @param {MutationRecord} record Observed mutation.
|
|
176
|
+
* @returns {boolean}
|
|
177
|
+
*/
|
|
178
|
+
function concernsItems(record) {
|
|
179
|
+
if (record.type === 'attributes') {
|
|
180
|
+
// An item's own attribute, or one on an element that holds items: a `<fieldset disabled>`
|
|
181
|
+
// disables every control inside it without touching their attributes.
|
|
182
|
+
return record.target instanceof Element
|
|
183
|
+
&& (record.target.matches(itemSelector) || record.target.querySelector(itemSelector) !== null);
|
|
184
|
+
}
|
|
185
|
+
for (const node of record.addedNodes) {
|
|
186
|
+
if (node instanceof Element && (node.matches(itemSelector) || node.querySelector(itemSelector))) return true;
|
|
187
|
+
}
|
|
188
|
+
for (const node of record.removedNodes) {
|
|
189
|
+
if (!(node instanceof Element)) continue;
|
|
190
|
+
if (originals.has(/** @type {HTMLElement} */ (node))) return true;
|
|
191
|
+
for (const item of originals.keys()) if (node.contains(item)) return true;
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
150
196
|
return {
|
|
151
197
|
/** Focuses the first available item. @returns {void} */
|
|
152
198
|
focusFirst() {
|
|
@@ -182,6 +228,77 @@ export function rovingTabindex(container, itemSelector, { orientation = 'vertica
|
|
|
182
228
|
};
|
|
183
229
|
}
|
|
184
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Where focus sat inside one keyed item of a collection, recorded before the collection re-renders.
|
|
233
|
+
* @typedef {Object} ItemFocusMemo
|
|
234
|
+
* @property {unknown} key The item's key.
|
|
235
|
+
* @property {boolean} self Whether the item element itself held focus.
|
|
236
|
+
* @property {number} index Position of the focused control among the item's focusable controls.
|
|
237
|
+
* @property {string} signature Tag, data attributes, and name of the focused control, which
|
|
238
|
+
* identify its replacement after a rebuild.
|
|
239
|
+
*/
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Records which control of which keyed item holds focus, so a collection that rebuilds an item
|
|
243
|
+
* can put focus back on the equivalent control afterwards. Returns null when focus is elsewhere.
|
|
244
|
+
* @param {Element} root Collection root.
|
|
245
|
+
* @param {(element: Element) => ({key: unknown, element: Element}|null)} locate Finds the keyed
|
|
246
|
+
* item an element belongs to.
|
|
247
|
+
* @returns {ItemFocusMemo|null}
|
|
248
|
+
*/
|
|
249
|
+
export function captureItemFocus(root, locate) {
|
|
250
|
+
const active = root?.ownerDocument?.activeElement ?? null;
|
|
251
|
+
if (!active || active === root || !root.contains(active)) return null;
|
|
252
|
+
const item = locate(active);
|
|
253
|
+
if (!item) return null;
|
|
254
|
+
const controls = itemControls(item.element);
|
|
255
|
+
return {
|
|
256
|
+
key: item.key,
|
|
257
|
+
self: active === item.element,
|
|
258
|
+
index: controls.indexOf(/** @type {HTMLElement} */ (active)),
|
|
259
|
+
signature: controlSignature(active)
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Puts focus back on the control a {@link captureItemFocus} memo describes, unless focus is still
|
|
265
|
+
* on a live element inside the collection (nothing was lost). The replacement is the control with
|
|
266
|
+
* the same signature, else the one at the same position, else the item's last control, else the
|
|
267
|
+
* item itself when it is focusable. Scrolling is suppressed: the reader did not move.
|
|
268
|
+
* @param {Element} root Collection root.
|
|
269
|
+
* @param {ItemFocusMemo|null} memo Recorded focus.
|
|
270
|
+
* @param {(key: unknown) => Element|null} resolve Finds the item element for a key after the update.
|
|
271
|
+
* @returns {boolean} Whether focus was moved.
|
|
272
|
+
*/
|
|
273
|
+
export function restoreItemFocus(root, memo, resolve) {
|
|
274
|
+
if (!memo || !root) return false;
|
|
275
|
+
const active = root.ownerDocument?.activeElement ?? null;
|
|
276
|
+
if (active && active !== root.ownerDocument?.body && active.isConnected && root.contains(active)) return false;
|
|
277
|
+
const element = resolve(memo.key);
|
|
278
|
+
if (!element) return false;
|
|
279
|
+
const controls = itemControls(element);
|
|
280
|
+
const target = memo.self ? /** @type {HTMLElement} */ (element)
|
|
281
|
+
: controls.find((control) => controlSignature(control) === memo.signature)
|
|
282
|
+
?? controls[memo.index] ?? controls.at(-1) ?? null;
|
|
283
|
+
if (!target || typeof target.focus !== 'function') return false;
|
|
284
|
+
target.focus({ preventScroll: true });
|
|
285
|
+
return root.ownerDocument?.activeElement === target;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** @param {Element} element @returns {HTMLElement[]} Focusable, enabled controls inside an item. */
|
|
289
|
+
function itemControls(element) {
|
|
290
|
+
return /** @type {HTMLElement[]} */ (Array.from(element.querySelectorAll(`${FOCUSABLE_SELECTOR},[tabindex="-1"]`)))
|
|
291
|
+
.filter((control) => isAvailable(control));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** @param {Element} element @returns {string} A rebuild-stable description of a control. */
|
|
295
|
+
function controlSignature(element) {
|
|
296
|
+
const data = Object.entries(/** @type {HTMLElement} */ (element).dataset ?? {})
|
|
297
|
+
.map(([name, value]) => `${name}=${value}`).sort().join('&');
|
|
298
|
+
const name = element.getAttribute('aria-label') ?? element.textContent?.trim() ?? '';
|
|
299
|
+
return `${element.tagName}|${data}|${name}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
185
302
|
/**
|
|
186
303
|
* Creates a 500 ms buffered, case-insensitive first-letter typeahead handler.
|
|
187
304
|
* @template T
|
|
@@ -212,6 +329,84 @@ export function typeahead(getItems, onMatch) {
|
|
|
212
329
|
};
|
|
213
330
|
}
|
|
214
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Maps a key to a roving step, or null for a key the group does not handle.
|
|
334
|
+
* @param {string} key `KeyboardEvent.key`.
|
|
335
|
+
* @param {'vertical'|'horizontal'} orientation Arrow-key axis.
|
|
336
|
+
* @param {boolean} reversed Whether the horizontal arrows are swapped (right-to-left).
|
|
337
|
+
* @returns {'first'|'last'|'previous'|'next'|null}
|
|
338
|
+
*/
|
|
339
|
+
export function rovingStep(key, orientation, reversed = false) {
|
|
340
|
+
if (key === 'Home') return 'first';
|
|
341
|
+
if (key === 'End') return 'last';
|
|
342
|
+
if (orientation === 'horizontal') {
|
|
343
|
+
if (key === 'ArrowLeft') return reversed ? 'next' : 'previous';
|
|
344
|
+
if (key === 'ArrowRight') return reversed ? 'previous' : 'next';
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
if (key === 'ArrowUp') return 'previous';
|
|
348
|
+
if (key === 'ArrowDown') return 'next';
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Whether an element takes keyboard text input, so that arrow keys and Home/End move its caret
|
|
354
|
+
* or its value: a text-like `<input>`, `<textarea>`, `<select>`, or a contenteditable region.
|
|
355
|
+
* @param {Element|null} element Candidate, normally a key event's target.
|
|
356
|
+
* @returns {boolean}
|
|
357
|
+
*/
|
|
358
|
+
export function isEditableTarget(element) {
|
|
359
|
+
if (!element || typeof element !== 'object') return false;
|
|
360
|
+
const tag = String(element.tagName ?? '').toUpperCase();
|
|
361
|
+
if (tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
|
362
|
+
if (tag === 'INPUT') {
|
|
363
|
+
return !CARETLESS_INPUT_TYPES.has(String(/** @type {HTMLInputElement} */ (element).type ?? 'text').toLowerCase());
|
|
364
|
+
}
|
|
365
|
+
return Boolean(/** @type {HTMLElement} */ (element).isContentEditable);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Whether a roving step may leave the event target. Non-editable targets always may. An editable
|
|
370
|
+
* one keeps Home/End, and keeps the arrows unless it is a single-line text field whose collapsed
|
|
371
|
+
* caret already sits at the edge the step points past: position 0 for `previous`, the end of the
|
|
372
|
+
* value for `next`. The steps are logical, so under right-to-left the caret check still matches
|
|
373
|
+
* the direction the caret would have moved.
|
|
374
|
+
* @param {Element|null} target Key event target.
|
|
375
|
+
* @param {'first'|'last'|'previous'|'next'} step Roving step.
|
|
376
|
+
* @returns {boolean}
|
|
377
|
+
*/
|
|
378
|
+
export function editableAllowsRoving(target, step) {
|
|
379
|
+
if (!isEditableTarget(target)) return true;
|
|
380
|
+
if (step === 'first' || step === 'last') return false;
|
|
381
|
+
if (String(target?.tagName ?? '').toUpperCase() !== 'INPUT') return false;
|
|
382
|
+
const input = /** @type {HTMLInputElement} */ (target);
|
|
383
|
+
let start;
|
|
384
|
+
let end;
|
|
385
|
+
try {
|
|
386
|
+
start = input.selectionStart;
|
|
387
|
+
end = input.selectionEnd;
|
|
388
|
+
} catch {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
// Types without a selection API (number, date, range …) own their arrows outright.
|
|
392
|
+
if (typeof start !== 'number' || typeof end !== 'number' || start !== end) return false;
|
|
393
|
+
return step === 'previous' ? start === 0 : end === String(input.value ?? '').length;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* @param {Element} container Group container.
|
|
398
|
+
* @param {'auto'|boolean} rtl Configured direction handling.
|
|
399
|
+
* @returns {boolean}
|
|
400
|
+
*/
|
|
401
|
+
function isRightToLeft(container, rtl) {
|
|
402
|
+
if (rtl === true || rtl === false) return rtl;
|
|
403
|
+
try {
|
|
404
|
+
return globalThis.getComputedStyle?.(container)?.direction === 'rtl';
|
|
405
|
+
} catch {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
215
410
|
/** @param {Element} container @returns {HTMLElement[]} */
|
|
216
411
|
function focusableItems(container) {
|
|
217
412
|
return /** @type {HTMLElement[]} */ (Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)))
|