@zeyos/zx 5.0.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 +164 -0
- package/README.md +2 -1
- package/dist/types/components/account-menu/account-menu.d.ts +8 -0
- package/dist/types/components/activity-list/activity-list.d.ts +9 -1
- package/dist/types/components/app-icon/app-icon.d.ts +14 -2
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +288 -14
- 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/context-menu/context-menu.d.ts +6 -2
- package/dist/types/components/data-filter/data-filter.d.ts +4 -4
- package/dist/types/components/dropdown/dropdown.d.ts +22 -7
- package/dist/types/components/field/field.d.ts +30 -0
- package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
- package/dist/types/components/fieldset/fieldset.d.ts +7 -0
- 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/launcher/launcher.d.ts +8 -0
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +15 -6
- package/dist/types/components/menu-button/menu-button.d.ts +6 -0
- package/dist/types/components/message/message.d.ts +14 -1
- package/dist/types/components/modal/modal.d.ts +30 -0
- 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/select.d.ts +6 -0
- package/dist/types/components/sheet/sheet.d.ts +15 -0
- package/dist/types/components/table/table.d.ts +265 -17
- 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/component.d.ts +12 -2
- package/dist/types/core/i18n.d.ts +27 -3
- package/dist/types/core/keyboard.d.ts +98 -1
- package/dist/types/core/messages.d.ts +382 -0
- package/dist/types/core/overlay-host.d.ts +28 -1
- package/dist/types/core/overlay-stack.d.ts +260 -0
- package/dist/types/index.d.ts +5 -2
- package/dist/types/internal/app-rail.d.ts +38 -0
- package/dist/types/internal/menu-items.d.ts +71 -3
- package/dist/types/zeyos/index.d.ts +1 -0
- 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 +31 -5
- package/dist/types/zeyos/table.d.ts +245 -8
- package/dist/types/zeyos/view.d.ts +16 -2
- package/dist/zx-zeyos.esm.js +4422 -430
- package/dist/zx-zeyos.esm.min.js +5 -5
- package/dist/zx.css +1417 -134
- package/dist/zx.esm.js +6986 -830
- package/dist/zx.esm.min.js +8 -8
- package/dist/zx.global.js +6986 -830
- package/dist/zx.global.min.js +8 -8
- package/dist/zx.min.css +1 -1
- package/docs/api.json +794 -17
- package/docs/llms.md +503 -63
- package/docs/llms.txt +6 -2
- package/package.json +1 -1
- package/src/components/account-menu/account-menu.js +12 -0
- package/src/components/activity-list/activity-list.js +27 -1
- package/src/components/app-icon/app-icon.css +13 -2
- package/src/components/app-icon/app-icon.js +11 -1
- package/src/components/app-rail/app-rail.css +19 -0
- package/src/components/app-sidebar/app-sidebar.css +204 -3
- package/src/components/app-sidebar/app-sidebar.js +646 -40
- package/src/components/breadcrumb/breadcrumb.css +1 -1
- package/src/components/calendar/calendar.css +1 -1
- package/src/components/calendar/calendar.js +26 -52
- package/src/components/card/card.css +2 -2
- package/src/components/card-view/card-view.css +5 -5
- package/src/components/card-view/card-view.js +13 -9
- package/src/components/card-view/record-card.js +14 -3
- package/src/components/code-editor/code-editor.css +1 -1
- package/src/components/code-editor/field-adapter.js +9 -2
- package/src/components/context-menu/context-menu.js +8 -2
- package/src/components/copy/copy-input.css +1 -1
- package/src/components/data-filter/data-filter.js +2 -2
- package/src/components/date-picker/time-picker.css +2 -2
- package/src/components/dock/dock.css +5 -4
- package/src/components/dropdown/dropdown.js +157 -72
- package/src/components/field/field.js +33 -3
- package/src/components/fieldset/external-adapter.js +363 -0
- package/src/components/fieldset/fieldset.js +10 -59
- package/src/components/file-list/file-list.js +144 -21
- package/src/components/filter/filter.css +86 -2
- package/src/components/filter/filter.js +300 -8
- package/src/components/filter-panel/filter-panel.js +149 -32
- package/src/components/finder/finder.css +1 -1
- package/src/components/kanban-view/kanban-view.css +3 -3
- package/src/components/kanban-view/kanban-view.js +5 -2
- package/src/components/launcher/launcher.css +1 -1
- package/src/components/launcher/launcher.js +29 -3
- package/src/components/list-toolbar/list-toolbar.js +39 -13
- package/src/components/master-panel/master-panel.js +0 -1
- package/src/components/menu-button/menu-button.css +63 -0
- package/src/components/menu-button/menu-button.js +97 -11
- package/src/components/message/message.js +60 -20
- package/src/components/modal/modal.css +1 -1
- package/src/components/modal/modal.js +66 -13
- 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/number-field/field-adapter.js +2 -1
- package/src/components/pagination/pagination.css +5 -5
- package/src/components/pagination/pagination.js +20 -4
- package/src/components/panel/panel.css +3 -2
- package/src/components/questionnaire/questionnaire.css +4 -4
- package/src/components/rating/field-adapter.js +2 -1
- package/src/components/rating/rating.css +1 -1
- package/src/components/search/search.css +4 -1
- package/src/components/select/select.js +9 -0
- package/src/components/sheet/sheet.css +1 -1
- package/src/components/sheet/sheet.js +74 -32
- package/src/components/slider/field-adapter.js +6 -1
- package/src/components/slider/slider.css +2 -2
- package/src/components/split-view/split-view.css +1 -1
- package/src/components/stat-tile/stat-tile.css +1 -1
- package/src/components/stepper/stepper.css +1 -1
- package/src/components/tabbox/tabbox.css +9 -5
- package/src/components/table/table.css +86 -11
- package/src/components/table/table.js +628 -57
- package/src/components/tag-picker/field-adapter.js +2 -1
- package/src/components/tooltip/tooltip.js +35 -17
- package/src/components/tree/tree.css +1 -1
- 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/component.js +88 -17
- package/src/core/fontawesome.js +5 -1
- package/src/core/i18n.js +47 -4
- package/src/core/icons.js +12 -2
- package/src/core/keyboard.js +206 -11
- package/src/core/messages.js +427 -0
- package/src/core/overlay-host.js +64 -9
- package/src/core/overlay-stack.js +487 -0
- package/src/core/position.js +22 -3
- package/src/index.js +5 -2
- package/src/internal/app-rail.js +134 -53
- package/src/internal/menu-items.js +61 -3
- package/src/zeyos/form.js +5 -3
- package/src/zeyos/index.js +3 -0
- package/src/zeyos/launcher.js +8 -3
- package/src/zeyos/module-bar.js +181 -0
- package/src/zeyos/query.js +44 -13
- package/src/zeyos/table.js +220 -80
- package/src/zeyos/view.js +43 -93
- package/styles/base.css +16 -1
- package/styles/print.css +1 -1
- package/styles/tokens/dark.css +4 -0
- package/styles/tokens/semantic.css +56 -28
- package/styles/zx.css +3 -0
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { Component } from '../../core/component.js';
|
|
2
2
|
import { h, resolveElement } from '../../core/dom.js';
|
|
3
|
-
import { connectedOverlayHost, overlayHost } from '../../core/overlay-host.js';
|
|
3
|
+
import { connectedOverlayHost, openDialogOf, overlayHost, overlayScope } from '../../core/overlay-host.js';
|
|
4
|
+
import { pushOverlay } from '../../core/overlay-stack.js';
|
|
4
5
|
import { position } from '../../core/position.js';
|
|
5
6
|
import { uid } from '../../core/util.js';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
const
|
|
8
|
+
/** What Tab may land on inside a panel: the controls a reader reaches in sequence. */
|
|
9
|
+
const TABBABLE_SELECTOR = [
|
|
10
|
+
'a[href]', 'button:not([disabled])', 'input:not([disabled]):not([type="hidden"])',
|
|
11
|
+
'select:not([disabled])', 'textarea:not([disabled])', 'iframe', '[contenteditable="true"]',
|
|
12
|
+
'[tabindex]'
|
|
13
|
+
].map((selector) => `${selector}:not([tabindex^="-"])`).join(',');
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
16
|
* @typedef {Object} DropdownOptions
|
|
@@ -22,6 +27,16 @@ const HANDLE_ESCAPE_DISMISS = Symbol('zxDropdownEscapeDismiss');
|
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
29
|
* Generic anchored floating panel using the browser popover top layer.
|
|
30
|
+
*
|
|
31
|
+
* Open, the panel is one entry on the overlay stack (`core/overlay-stack.js`): Escape closes it
|
|
32
|
+
* only while it is the topmost layer that takes Escape, a press outside it and every overlay opened
|
|
33
|
+
* from it closes it, and it closes with the overlay its anchor sits in. Inside an open dialog the
|
|
34
|
+
* panel is hosted by that dialog, so `showModal()` does not make it inert.
|
|
35
|
+
*
|
|
36
|
+
* The panel follows its trigger in the Tab order: Tab on the anchor of an open panel moves into
|
|
37
|
+
* the panel's first focusable control (or onto the panel itself when it carries a `tabindex`),
|
|
38
|
+
* Tab past the panel's last control continues after the anchor, and Shift+Tab from the first goes
|
|
39
|
+
* back to the anchor. Closing while focus is inside returns it to the anchor.
|
|
25
40
|
* @fires Dropdown#open
|
|
26
41
|
* @fires Dropdown#close
|
|
27
42
|
* @extends {Component<DropdownOptions>}
|
|
@@ -46,6 +61,12 @@ export class Dropdown extends Component {
|
|
|
46
61
|
#position = null;
|
|
47
62
|
/** @type {Map<string, string|null>} */
|
|
48
63
|
#anchorAttributes = new Map();
|
|
64
|
+
/** @type {import('../../core/overlay-stack.js').OverlayHandle|null} */
|
|
65
|
+
#overlay = null;
|
|
66
|
+
/** @type {((event: KeyboardEvent) => void)|null} Escape behaviour a composing component supplies. */
|
|
67
|
+
#escapeHandler = null;
|
|
68
|
+
/** Whether the panel joins the Tab order after its anchor; menus and rail flyouts opt out. */
|
|
69
|
+
#tabReach = true;
|
|
49
70
|
#destroyed = false;
|
|
50
71
|
|
|
51
72
|
/**
|
|
@@ -59,8 +80,9 @@ export class Dropdown extends Component {
|
|
|
59
80
|
super(null, options);
|
|
60
81
|
this.#anchor = resolved;
|
|
61
82
|
// An anchor that is not in the document yet has no theme scope to read; the panel then waits
|
|
62
|
-
// detached and finds its host on the first open, instead of settling on <body>.
|
|
63
|
-
|
|
83
|
+
// detached and finds its host on the first open, instead of settling on <body>. A closed panel
|
|
84
|
+
// rests in the theme scope even when the anchor is in a dialog; it moves in only while open.
|
|
85
|
+
connectedOverlayHost(this.options.scope ?? resolved, { dialog: false })?.append(this.el);
|
|
64
86
|
this.#rememberAnchorAttribute('aria-expanded');
|
|
65
87
|
this.#rememberAnchorAttribute('aria-controls');
|
|
66
88
|
this.#anchor.setAttribute('aria-expanded', 'false');
|
|
@@ -73,6 +95,8 @@ export class Dropdown extends Component {
|
|
|
73
95
|
this.listen(this.el, 'click', () => {
|
|
74
96
|
if (this.options.closeOnSelect) this.close();
|
|
75
97
|
});
|
|
98
|
+
this.listen(this.#anchor, 'keydown', (event) => this.#onAnchorKeydown(/** @type {KeyboardEvent} */ (event)));
|
|
99
|
+
this.listen(this.el, 'keydown', (event) => this.#onPanelKeydown(/** @type {KeyboardEvent} */ (event)));
|
|
76
100
|
}
|
|
77
101
|
|
|
78
102
|
/**
|
|
@@ -95,7 +119,7 @@ export class Dropdown extends Component {
|
|
|
95
119
|
*/
|
|
96
120
|
open() {
|
|
97
121
|
if (this.isOpen() || this.#destroyed) return this;
|
|
98
|
-
const host =
|
|
122
|
+
const host = this.#host();
|
|
99
123
|
if (this.el.parentElement !== host) host.append(this.el);
|
|
100
124
|
this.#position = position(this.#anchor, this.el, {
|
|
101
125
|
placement: this.options.placement,
|
|
@@ -103,9 +127,18 @@ export class Dropdown extends Component {
|
|
|
103
127
|
matchWidth: Boolean(this.options.matchWidth)
|
|
104
128
|
});
|
|
105
129
|
this.el.dataset.state = 'open';
|
|
106
|
-
this
|
|
130
|
+
this.#overlay = pushOverlay({
|
|
131
|
+
kind: 'popup',
|
|
132
|
+
root: this.el,
|
|
133
|
+
owner: this,
|
|
134
|
+
anchor: [this.#anchor, resolveElement(this.options.scope)],
|
|
135
|
+
inside: [this.#anchor],
|
|
136
|
+
onEscape: (event) => this.#onEscape(event),
|
|
137
|
+
onOutsidePointer: () => this.close(),
|
|
138
|
+
onParentClose: () => this.close()
|
|
139
|
+
});
|
|
140
|
+
this.el.dataset.zxOverlayOrder = String(this.#overlay.order);
|
|
107
141
|
this.#anchor.setAttribute('aria-expanded', 'true');
|
|
108
|
-
ensureDismissManager();
|
|
109
142
|
this.emit('open');
|
|
110
143
|
return this;
|
|
111
144
|
}
|
|
@@ -117,13 +150,24 @@ export class Dropdown extends Component {
|
|
|
117
150
|
*/
|
|
118
151
|
close() {
|
|
119
152
|
if (!this.isOpen()) return this;
|
|
153
|
+
const hadFocus = this.el.contains(document.activeElement);
|
|
120
154
|
this.#position?.destroy();
|
|
121
155
|
this.#position = null;
|
|
122
156
|
this.el.dataset.state = 'closed';
|
|
123
157
|
delete this.el.dataset.zxOverlayOrder;
|
|
158
|
+
const overlay = this.#overlay;
|
|
159
|
+
this.#overlay = null;
|
|
124
160
|
this.#anchor.setAttribute('aria-expanded', 'false');
|
|
161
|
+
// A panel that took the Tab order hands focus back rather than dropping it on <body>. Menus and
|
|
162
|
+
// flyouts decide where their own focus goes.
|
|
163
|
+
if (hadFocus && this.#tabReach && this.#anchor instanceof HTMLElement) this.#anchor.focus();
|
|
164
|
+
// Back to the theme scope: a closed panel left inside a dialog would sit in that dialog's
|
|
165
|
+
// focus search and go with it when the dialog is removed.
|
|
166
|
+
if (openDialogOf(this.el.parentElement) && this.el.parentElement?.isConnected) {
|
|
167
|
+
overlayScope(this.options.scope ?? this.#anchor).append(this.el);
|
|
168
|
+
}
|
|
169
|
+
overlay?.release();
|
|
125
170
|
this.emit('close');
|
|
126
|
-
destroyUnusedDismissManager();
|
|
127
171
|
return this;
|
|
128
172
|
}
|
|
129
173
|
|
|
@@ -174,50 +218,78 @@ export class Dropdown extends Component {
|
|
|
174
218
|
super.destroy();
|
|
175
219
|
}
|
|
176
220
|
|
|
177
|
-
/**
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Internal seam for components built on a Dropdown (MenuButton, ContextMenu, the app rail):
|
|
223
|
+
* replaces what Escape does when the overlay stack routes it to this panel, and takes the panel
|
|
224
|
+
* out of the anchor's Tab order — a menu is reached with the arrow keys, not Tab.
|
|
225
|
+
* @param {{onEscape?: ((event: KeyboardEvent) => void)|null, tabReach?: boolean}} settings
|
|
226
|
+
* @returns {this}
|
|
227
|
+
* @internal
|
|
228
|
+
*/
|
|
229
|
+
_configure(settings) {
|
|
230
|
+
if ('onEscape' in settings) this.#escapeHandler = settings.onEscape ?? null;
|
|
231
|
+
if ('tabReach' in settings) this.#tabReach = Boolean(settings.tabReach);
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** @returns {Element} Where the panel lives while open. */
|
|
236
|
+
#host() {
|
|
237
|
+
const dialog = openDialogOf(this.#anchor) ?? openDialogOf(resolveElement(this.options.scope));
|
|
238
|
+
return dialog ?? overlayHost(this.options.scope ?? this.#anchor);
|
|
183
239
|
}
|
|
184
240
|
|
|
185
241
|
/** @param {KeyboardEvent} event @returns {void} */
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
242
|
+
#onEscape(event) {
|
|
243
|
+
if (this.#escapeHandler) {
|
|
244
|
+
this.#escapeHandler(event);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
189
247
|
const restoreFocus = this.el.contains(document.activeElement);
|
|
190
248
|
this.close();
|
|
191
249
|
if (restoreFocus && this.#anchor instanceof HTMLElement) this.#anchor.focus();
|
|
192
250
|
}
|
|
193
251
|
|
|
194
|
-
/** @param {
|
|
195
|
-
#
|
|
196
|
-
this.#
|
|
252
|
+
/** @param {KeyboardEvent} event @returns {void} */
|
|
253
|
+
#onAnchorKeydown(event) {
|
|
254
|
+
if (event.key !== 'Tab' || event.shiftKey || event.defaultPrevented || !this.#tabReach || !this.isOpen()) return;
|
|
255
|
+
const target = tabbables(this.el)[0] ?? (this.el.hasAttribute('tabindex') ? this.el : null);
|
|
256
|
+
if (!(target instanceof HTMLElement)) return;
|
|
257
|
+
event.preventDefault();
|
|
258
|
+
target.focus();
|
|
197
259
|
}
|
|
198
|
-
}
|
|
199
260
|
|
|
200
|
-
/**
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
261
|
+
/** @param {KeyboardEvent} event @returns {void} */
|
|
262
|
+
#onPanelKeydown(event) {
|
|
263
|
+
if (event.key !== 'Tab' || event.defaultPrevented || !this.#tabReach || !this.isOpen()) return;
|
|
264
|
+
const items = tabbables(this.el);
|
|
265
|
+
const active = document.activeElement;
|
|
266
|
+
/*
|
|
267
|
+
* Judged by position, not identity: focus may rest on something Tab itself would never stop
|
|
268
|
+
* at — a composite's `tabindex="-1"` part, a roving item — and what matters is whether any
|
|
269
|
+
* real stop is left in the panel on the side Tab is moving to.
|
|
270
|
+
*/
|
|
271
|
+
const ahead = active && active !== this.el
|
|
272
|
+
? items.filter((item) => !item.contains(active) && (event.shiftKey
|
|
273
|
+
? Boolean(active.compareDocumentPosition(item) & Node.DOCUMENT_POSITION_PRECEDING)
|
|
274
|
+
: Boolean(active.compareDocumentPosition(item) & Node.DOCUMENT_POSITION_FOLLOWING)
|
|
275
|
+
&& !active.contains(item)))
|
|
276
|
+
: (event.shiftKey ? [] : items);
|
|
277
|
+
if (ahead.length > 0) return;
|
|
278
|
+
if (event.shiftKey) {
|
|
279
|
+
if (!(this.#anchor instanceof HTMLElement)) return;
|
|
280
|
+
event.preventDefault();
|
|
281
|
+
this.#anchor.focus();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const next = followingTabbable(this.#anchor, this.el);
|
|
285
|
+
if (!next) return;
|
|
286
|
+
event.preventDefault();
|
|
287
|
+
next.focus();
|
|
214
288
|
}
|
|
215
289
|
|
|
216
|
-
/** @returns {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
document.body.append(marker);
|
|
220
|
-
return marker;
|
|
290
|
+
/** @param {string} name @returns {void} */
|
|
291
|
+
#rememberAnchorAttribute(name) {
|
|
292
|
+
this.#anchorAttributes.set(name, this.#anchor.getAttribute(name));
|
|
221
293
|
}
|
|
222
294
|
}
|
|
223
295
|
|
|
@@ -251,41 +323,54 @@ function replaceContent(target, content) {
|
|
|
251
323
|
}
|
|
252
324
|
}
|
|
253
325
|
|
|
254
|
-
/**
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
topmost = candidate;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
return topmost ? Component.from(topmost) : null;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/** @returns {number} */
|
|
269
|
-
function nextOverlayOrder() {
|
|
270
|
-
const orders = Array.from(document.querySelectorAll('[data-zx-overlay-order]'), (element) =>
|
|
271
|
-
Number(element.getAttribute('data-zx-overlay-order')) || 0
|
|
272
|
-
);
|
|
273
|
-
return Math.max(0, ...orders) + 1;
|
|
326
|
+
/**
|
|
327
|
+
* The controls inside a container Tab would visit, in document order.
|
|
328
|
+
* @param {Element} container
|
|
329
|
+
* @returns {HTMLElement[]}
|
|
330
|
+
*/
|
|
331
|
+
function tabbables(container) {
|
|
332
|
+
return /** @type {HTMLElement[]} */ (Array.from(container.querySelectorAll(TABBABLE_SELECTOR))
|
|
333
|
+
.filter((element) => isTabbable(element)));
|
|
274
334
|
}
|
|
275
335
|
|
|
276
|
-
/**
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
336
|
+
/**
|
|
337
|
+
* The control Tab reaches after `anchor`, skipping the panel that follows it.
|
|
338
|
+
* @param {Element} anchor
|
|
339
|
+
* @param {Element} panel
|
|
340
|
+
* @returns {HTMLElement|null}
|
|
341
|
+
*/
|
|
342
|
+
function followingTabbable(anchor, panel) {
|
|
343
|
+
// Inside a modal dialog nothing beyond it can take focus, so the search stays within it.
|
|
344
|
+
const scope = openDialogOf(anchor) ?? anchor.ownerDocument.body ?? anchor.ownerDocument.documentElement;
|
|
345
|
+
const all = tabbables(scope).filter((element) => !panel.contains(element));
|
|
346
|
+
const index = all.findIndex((element) => element === anchor || anchor.contains(element));
|
|
347
|
+
if (index < 0) return null;
|
|
348
|
+
let next = index + 1;
|
|
349
|
+
while (next < all.length && anchor.contains(all[next])) next += 1;
|
|
350
|
+
return all[next] ?? null;
|
|
281
351
|
}
|
|
282
352
|
|
|
283
|
-
/**
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
353
|
+
/**
|
|
354
|
+
* Whether sequential navigation stops at an element: focusable, a non-negative tab index, not
|
|
355
|
+
* disabled, hidden, inert or `aria-hidden`, rendered, and — for a radio — the one stop its group
|
|
356
|
+
* offers (the checked radio, or the first when none is checked).
|
|
357
|
+
* @param {Element} element
|
|
358
|
+
* @returns {boolean}
|
|
359
|
+
*/
|
|
360
|
+
function isTabbable(element) {
|
|
361
|
+
const html = /** @type {HTMLElement} */ (element);
|
|
362
|
+
if (typeof html.focus !== 'function' || html.tabIndex < 0) return false;
|
|
363
|
+
if (/** @type {HTMLButtonElement} */ (html).disabled) return false;
|
|
364
|
+
if (element.closest('[inert], [hidden], [aria-hidden="true"], fieldset[disabled]')) return false;
|
|
365
|
+
if (html instanceof HTMLInputElement && html.type === 'radio' && html.name && !html.checked) {
|
|
366
|
+
const scope = html.form ?? html.getRootNode();
|
|
367
|
+
const group = /** @type {HTMLInputElement[]} */ (Array.from(
|
|
368
|
+
/** @type {ParentNode} */ (scope).querySelectorAll?.('input[type="radio"]') ?? []
|
|
369
|
+
)).filter((radio) => radio.name === html.name && (radio.form ?? radio.getRootNode()) === scope);
|
|
370
|
+
if (group.some((radio) => radio.checked) || group[0] !== html) return false;
|
|
371
|
+
}
|
|
372
|
+
if (typeof html.checkVisibility === 'function') return html.checkVisibility({ visibilityProperty: true });
|
|
373
|
+
return html.getClientRects().length > 0;
|
|
289
374
|
}
|
|
290
375
|
|
|
291
376
|
/** @param {Element} element @param {string} name @param {string|null} value @returns {void} */
|
|
@@ -15,6 +15,9 @@ import { uid } from '../../core/util.js';
|
|
|
15
15
|
* @property {string} [placeholder=''] Control placeholder.
|
|
16
16
|
* @property {boolean} [required=false] Whether a value is required.
|
|
17
17
|
* @property {boolean} [disabled=false] Whether the control is disabled.
|
|
18
|
+
* @property {boolean} [readonly=false] Whether the value is read-only: shown and focusable, not
|
|
19
|
+
* editable. Types whose control has no read-only state (`checkbox`, `select`, `optionlist`,
|
|
20
|
+
* `zxselect`, `date`, `time`, …) ignore it.
|
|
18
21
|
* @property {string[]|Record<string, string>|null} [options=null] Select or option-list values.
|
|
19
22
|
* @property {'stack'|'inline'} [layout='stack'] Label/control layout.
|
|
20
23
|
* @property {Record<string, unknown>} [props={}] Adapter-specific control properties.
|
|
@@ -30,6 +33,8 @@ import { uid } from '../../core/util.js';
|
|
|
30
33
|
* @property {(value: unknown, options?: {silent?: boolean}) => void} set Set the current value.
|
|
31
34
|
* @property {() => void} focus Focus the control.
|
|
32
35
|
* @property {(disabled: boolean) => void} setDisabled Set disabled state.
|
|
36
|
+
* @property {(readonly: boolean) => void} [setReadonly] Set read-only state; left out when the
|
|
37
|
+
* control has none.
|
|
33
38
|
* @property {() => void} [destroy] Release adapter-owned resources.
|
|
34
39
|
* @property {Element} [labelTarget] The element the field's label, description, and validity
|
|
35
40
|
* belong to when it is not `el` — the text input inside a composite control. Left out, a
|
|
@@ -42,6 +47,7 @@ import { uid } from '../../core/util.js';
|
|
|
42
47
|
/**
|
|
43
48
|
* One labelled form control backed by a registered adapter.
|
|
44
49
|
* @fires Field#change
|
|
50
|
+
* @fires Field#input
|
|
45
51
|
* @fires Field#invalid
|
|
46
52
|
* @extends {Component<FieldOptions>}
|
|
47
53
|
*/
|
|
@@ -59,6 +65,7 @@ export class Field extends Component {
|
|
|
59
65
|
placeholder: '',
|
|
60
66
|
required: false,
|
|
61
67
|
disabled: false,
|
|
68
|
+
readonly: false,
|
|
62
69
|
options: null,
|
|
63
70
|
layout: 'stack',
|
|
64
71
|
props: {},
|
|
@@ -108,7 +115,7 @@ export class Field extends Component {
|
|
|
108
115
|
const layout = this.options.layout === 'inline' ? 'inline' : 'stack';
|
|
109
116
|
this._ownedComponents = new Set();
|
|
110
117
|
this._ownedNodes = [];
|
|
111
|
-
this._rootAttributes = rememberAttributes(root, ['data-layout', 'data-field-type', 'data-disabled', 'data-state']);
|
|
118
|
+
this._rootAttributes = rememberAttributes(root, ['data-layout', 'data-field-type', 'data-disabled', 'data-readonly', 'data-state']);
|
|
112
119
|
root.dataset.layout = layout;
|
|
113
120
|
root.dataset.fieldType = type;
|
|
114
121
|
|
|
@@ -175,6 +182,8 @@ export class Field extends Component {
|
|
|
175
182
|
this.initialValue = initialValue(type, this.options.value);
|
|
176
183
|
this.adapter.set(this.initialValue, { silent: true });
|
|
177
184
|
this.setDisabled(Boolean(this.options.disabled));
|
|
185
|
+
if (this.options.readonly) this.setReadonly(true);
|
|
186
|
+
// Adapters whose widget reports its own commits stop the native `change` before this sees it.
|
|
178
187
|
this.listen(control, 'change', () => {
|
|
179
188
|
this.emit('change', { value: this.getValue() });
|
|
180
189
|
});
|
|
@@ -233,6 +242,19 @@ export class Field extends Component {
|
|
|
233
242
|
return this;
|
|
234
243
|
}
|
|
235
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Makes the value read-only or editable again, where the control has a read-only state (see
|
|
247
|
+
* the `readonly` option); reflected as `data-readonly` on the root either way.
|
|
248
|
+
* @param {boolean} readonly Read-only state.
|
|
249
|
+
* @returns {this}
|
|
250
|
+
*/
|
|
251
|
+
setReadonly(readonly) {
|
|
252
|
+
const value = Boolean(readonly);
|
|
253
|
+
this.adapter.setReadonly?.(value);
|
|
254
|
+
this.el.dataset.readonly = String(value);
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
|
|
236
258
|
/**
|
|
237
259
|
* Displays a validation or status message.
|
|
238
260
|
* @param {string} message Message text.
|
|
@@ -470,7 +492,7 @@ function textAdapter(tag, type) {
|
|
|
470
492
|
required: options.required,
|
|
471
493
|
disabled: options.disabled
|
|
472
494
|
}));
|
|
473
|
-
return elementValueAdapter(el);
|
|
495
|
+
return { ...elementValueAdapter(el), setReadonly: (readonly) => { el.readOnly = readonly; } };
|
|
474
496
|
};
|
|
475
497
|
}
|
|
476
498
|
|
|
@@ -507,7 +529,8 @@ function numericAdapter(kind) {
|
|
|
507
529
|
});
|
|
508
530
|
return {
|
|
509
531
|
...elementValueAdapter(input),
|
|
510
|
-
get: () => coerce(input.value).value
|
|
532
|
+
get: () => coerce(input.value).value,
|
|
533
|
+
setReadonly: (readonly) => { input.readOnly = readonly; }
|
|
511
534
|
};
|
|
512
535
|
};
|
|
513
536
|
}
|
|
@@ -642,6 +665,13 @@ Field.register('custom', customAdapter);
|
|
|
642
665
|
* @type {CustomEvent<{value: unknown}>}
|
|
643
666
|
*/
|
|
644
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Live edit before it is committed, from the types whose widget tells the two apart (`code`,
|
|
670
|
+
* `slider`): every keystroke or drag step, where `change` follows only on commit.
|
|
671
|
+
* @event Field#input
|
|
672
|
+
* @type {CustomEvent<{value: unknown}>}
|
|
673
|
+
*/
|
|
674
|
+
|
|
645
675
|
/**
|
|
646
676
|
* Field validation failure event.
|
|
647
677
|
* @event Field#invalid
|