@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
|
@@ -101,6 +101,9 @@ export class MenuButton extends Component {
|
|
|
101
101
|
content: null
|
|
102
102
|
});
|
|
103
103
|
this.#panel = this.#dropdown.getPanel();
|
|
104
|
+
// Escape reaches the menu through the overlay stack, which offers it only to the topmost layer;
|
|
105
|
+
// the menu decides where focus goes. Items are reached with the arrow keys, not Tab.
|
|
106
|
+
this.#dropdown._configure({ onEscape: () => this.#dismiss(), tabReach: false });
|
|
104
107
|
this.#panel.classList.add('zx-menu-button__menu');
|
|
105
108
|
this.#panel.setAttribute('role', 'menu');
|
|
106
109
|
// The menu is named by its trigger, as the APG menu-button pattern has it. An id given here to
|
|
@@ -126,7 +129,7 @@ export class MenuButton extends Component {
|
|
|
126
129
|
if (this.isOpen()) this.close();
|
|
127
130
|
else {
|
|
128
131
|
this.open();
|
|
129
|
-
this.#
|
|
132
|
+
this.#focusFirstItem();
|
|
130
133
|
}
|
|
131
134
|
});
|
|
132
135
|
this.listen(this.el, 'keydown', (event) => this.#onTriggerKeydown(event));
|
|
@@ -135,6 +138,9 @@ export class MenuButton extends Component {
|
|
|
135
138
|
if (item && this.#panel.contains(item)) this.#selectElement(item, event);
|
|
136
139
|
});
|
|
137
140
|
this.listen(this.#panel, 'keydown', (event) => this.#onMenuKeydown(event));
|
|
141
|
+
// A choice row runs across, not down: Left and Right move within it, Up and Down leave it.
|
|
142
|
+
// Capture, so the menu's own roving focus never sees the keys this takes.
|
|
143
|
+
this.listen(this.#panel, 'keydown', (event) => this.#onChoiceKeydown(event), { capture: true });
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
/**
|
|
@@ -158,7 +164,7 @@ export class MenuButton extends Component {
|
|
|
158
164
|
*/
|
|
159
165
|
setItems(items) {
|
|
160
166
|
this.#items = Array.isArray(items) ? items.slice() : [];
|
|
161
|
-
this.#panel.replaceChildren(...renderMenuEntries(this.#items, 'menu-button'));
|
|
167
|
+
this.#panel.replaceChildren(...renderMenuEntries(this.#items, 'menu-button', { choices: true }));
|
|
162
168
|
return this;
|
|
163
169
|
}
|
|
164
170
|
|
|
@@ -183,6 +189,15 @@ export class MenuButton extends Component {
|
|
|
183
189
|
return this;
|
|
184
190
|
}
|
|
185
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Opens the menu when it is closed, and closes it when it is open. Opening this way moves no
|
|
194
|
+
* focus; `focusFirst()` opens with focus on the first item.
|
|
195
|
+
* @returns {this}
|
|
196
|
+
*/
|
|
197
|
+
toggle() {
|
|
198
|
+
return this.isOpen() ? this.close() : this.open();
|
|
199
|
+
}
|
|
200
|
+
|
|
186
201
|
/**
|
|
187
202
|
* Reports whether the menu is open.
|
|
188
203
|
* @returns {boolean}
|
|
@@ -208,7 +223,7 @@ export class MenuButton extends Component {
|
|
|
208
223
|
*/
|
|
209
224
|
focusFirst(options = {}) {
|
|
210
225
|
this.open(options);
|
|
211
|
-
this.#
|
|
226
|
+
this.#focusFirstItem();
|
|
212
227
|
return this;
|
|
213
228
|
}
|
|
214
229
|
|
|
@@ -256,18 +271,11 @@ export class MenuButton extends Component {
|
|
|
256
271
|
event.preventDefault();
|
|
257
272
|
this.open();
|
|
258
273
|
if (event.key === 'ArrowUp') this.#roving.focusLast();
|
|
259
|
-
else this.#
|
|
274
|
+
else this.#focusFirstItem();
|
|
260
275
|
}
|
|
261
276
|
|
|
262
277
|
/** @param {KeyboardEvent} event @returns {void} */
|
|
263
278
|
#onMenuKeydown(event) {
|
|
264
|
-
if (event.key === 'Escape') {
|
|
265
|
-
event.preventDefault();
|
|
266
|
-
const target = this.#returnTarget();
|
|
267
|
-
this.close();
|
|
268
|
-
target?.focus();
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
279
|
if (event.key === 'Tab') {
|
|
272
280
|
this.close();
|
|
273
281
|
return;
|
|
@@ -283,10 +291,88 @@ export class MenuButton extends Component {
|
|
|
283
291
|
this.#typeahead(event);
|
|
284
292
|
}
|
|
285
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Escape, as the overlay stack routes it here: closes the menu, and returns focus when the menu
|
|
296
|
+
* held it (or it had already fallen to the body).
|
|
297
|
+
* @returns {void}
|
|
298
|
+
*/
|
|
299
|
+
#dismiss() {
|
|
300
|
+
const active = document.activeElement;
|
|
301
|
+
const reclaim = !active || active === document.body || this.#panel.contains(active);
|
|
302
|
+
const target = this.#returnTarget();
|
|
303
|
+
this.close();
|
|
304
|
+
if (reclaim) target?.focus();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Focuses the first item, or, when that is a choice row, its checked option. @returns {void} */
|
|
308
|
+
#focusFirstItem() {
|
|
309
|
+
this.#roving.focusFirst();
|
|
310
|
+
const first = this.#menuItems()[0];
|
|
311
|
+
const checked = first?.hasAttribute('data-menu-option')
|
|
312
|
+
? first.parentElement?.querySelector('[aria-checked="true"]') : null;
|
|
313
|
+
if (checked instanceof HTMLElement) checked.focus();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** @param {KeyboardEvent} event @returns {void} */
|
|
317
|
+
#onChoiceKeydown(event) {
|
|
318
|
+
const option = /** @type {Element|null} */ (event.target)?.closest?.('[data-menu-option]');
|
|
319
|
+
if (!(option instanceof HTMLElement) || !this.#panel.contains(option)) {
|
|
320
|
+
// Arriving at a choice row from another item lands on its checked option, not its nearest.
|
|
321
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
|
|
322
|
+
const items = this.#menuItems();
|
|
323
|
+
const index = items.indexOf(/** @type {HTMLElement} */ (event.target));
|
|
324
|
+
if (index < 0) return;
|
|
325
|
+
const next = items[(index + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length];
|
|
326
|
+
const row = next?.hasAttribute('data-menu-option') ? next.parentElement : null;
|
|
327
|
+
if (!row) return;
|
|
328
|
+
event.preventDefault();
|
|
329
|
+
event.stopPropagation();
|
|
330
|
+
/** @type {HTMLElement} */ (row.querySelector('[aria-checked="true"]') ?? next).focus();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const row = /** @type {HTMLElement[]} */ ([...(option.parentElement?.children ?? [])]);
|
|
334
|
+
const items = this.#menuItems();
|
|
335
|
+
let target = null;
|
|
336
|
+
if (event.key === 'ArrowLeft') target = row[(row.indexOf(option) - 1 + row.length) % row.length];
|
|
337
|
+
else if (event.key === 'ArrowRight') target = row[(row.indexOf(option) + 1) % row.length];
|
|
338
|
+
else if (event.key === 'ArrowDown') target = items[(items.indexOf(row.at(-1)) + 1) % items.length];
|
|
339
|
+
else if (event.key === 'ArrowUp') target = items[(items.indexOf(row[0]) - 1 + items.length) % items.length];
|
|
340
|
+
else return;
|
|
341
|
+
event.preventDefault();
|
|
342
|
+
event.stopPropagation();
|
|
343
|
+
// Into another choice row, focus lands on its checked option.
|
|
344
|
+
const checked = target?.closest('[role="group"]')?.querySelector('[data-menu-option][aria-checked="true"]');
|
|
345
|
+
const next = event.key === 'ArrowDown' || event.key === 'ArrowUp' ? /** @type {HTMLElement} */ (checked ?? target) : target;
|
|
346
|
+
next?.focus();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** @param {HTMLElement} element @param {Record<string, any>} entry @param {Event|null} event @returns {void} */
|
|
350
|
+
#selectChoice(element, entry, event) {
|
|
351
|
+
const option = entry.options?.[Number(element.getAttribute('data-menu-option'))];
|
|
352
|
+
if (!option || option.disabled) {
|
|
353
|
+
event?.preventDefault();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
// A setting changes in place and the menu stays open, so its effect shows.
|
|
357
|
+
for (const sibling of element.parentElement?.children ?? []) {
|
|
358
|
+
sibling.setAttribute('aria-checked', String(sibling === element));
|
|
359
|
+
}
|
|
360
|
+
element.focus();
|
|
361
|
+
entry.onselect?.(option.value, entry, this);
|
|
362
|
+
// A handler that closed the menu leaves focus on a hidden option: it goes where a close sends it.
|
|
363
|
+
if (!this.isOpen() && (element.contains(document.activeElement) || document.activeElement === document.body)) {
|
|
364
|
+
this.#returnTarget()?.focus();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
286
368
|
/** @param {Element} element @param {Event|null} [event=null] @returns {void} */
|
|
287
369
|
#selectElement(element, event = null) {
|
|
288
370
|
const index = Number(element.getAttribute('data-menu-item'));
|
|
289
371
|
const item = this.#items[index];
|
|
372
|
+
if (item && typeof item === 'object' && item.type === 'choice') {
|
|
373
|
+
this.#selectChoice(/** @type {HTMLElement} */ (element), item, event);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
290
376
|
if (!isMenuItem(item) || item.disabled) {
|
|
291
377
|
event?.preventDefault();
|
|
292
378
|
return;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Component } from '../../core/component.js';
|
|
2
2
|
import { h, resolveElement } from '../../core/dom.js';
|
|
3
3
|
import { icon } from '../../core/icons.js';
|
|
4
|
-
import {
|
|
4
|
+
import { overlayScope } from '../../core/overlay-host.js';
|
|
5
|
+
import { overlayStack } from '../../core/overlay-stack.js';
|
|
5
6
|
import { button } from '../button/button.js';
|
|
6
7
|
|
|
7
8
|
/** @typedef {'info'|'success'|'warning'|'error'} MessageKind */
|
|
@@ -160,9 +161,6 @@ export class Message extends Component {
|
|
|
160
161
|
maxVisible: 5
|
|
161
162
|
};
|
|
162
163
|
|
|
163
|
-
/** @type {WeakMap<Element, Message>} Floating region per overlay host. */
|
|
164
|
-
static #floating = new WeakMap();
|
|
165
|
-
|
|
166
164
|
/** @returns {HTMLElement} */
|
|
167
165
|
render() {
|
|
168
166
|
const created = !this.el;
|
|
@@ -170,8 +168,14 @@ export class Message extends Component {
|
|
|
170
168
|
this._createdRoot = created;
|
|
171
169
|
this._original = created ? null : snapshot(root);
|
|
172
170
|
this._entries = new Set();
|
|
173
|
-
/** @type {Element|null} The
|
|
171
|
+
/** @type {Element|null} The theme scope this instance is the floating region of; null when inline. */
|
|
174
172
|
this._floatingHost = null;
|
|
173
|
+
/** @type {Element|null} Where the floating region was last put: its scope, or the topmost modal. */
|
|
174
|
+
this._placedIn = null;
|
|
175
|
+
/** @type {import('../../core/overlay-stack.js').OverlayHandle|null} The region's toast entry. */
|
|
176
|
+
this._overlay = null;
|
|
177
|
+
/** @type {(() => void)|null} Stops following the overlay stack. */
|
|
178
|
+
this._unfollow = null;
|
|
175
179
|
this._queue = new MessageQueue(normalizeLimit(this.options.maxVisible), (entry) => {
|
|
176
180
|
this._activate(entry);
|
|
177
181
|
});
|
|
@@ -276,10 +280,8 @@ export class Message extends Component {
|
|
|
276
280
|
if (scope == null) return false;
|
|
277
281
|
const context = resolveElement(scope);
|
|
278
282
|
if (!context) return false;
|
|
279
|
-
const
|
|
280
|
-
const region = Message.#floating.get(host);
|
|
283
|
+
const region = Message.#regionFor(overlayScope(context));
|
|
281
284
|
if (!region) return false;
|
|
282
|
-
Message.#floating.delete(host);
|
|
283
285
|
region._disposeFloating();
|
|
284
286
|
return true;
|
|
285
287
|
}
|
|
@@ -288,7 +290,13 @@ export class Message extends Component {
|
|
|
288
290
|
destroy() {
|
|
289
291
|
if (this._cleaned) return;
|
|
290
292
|
this._cleaned = true;
|
|
293
|
+
this._unfollow?.();
|
|
294
|
+
this._unfollow = null;
|
|
295
|
+
const overlay = this._overlay;
|
|
296
|
+
this._overlay = null;
|
|
297
|
+
overlay?.release();
|
|
291
298
|
this._floatingHost = null;
|
|
299
|
+
this._placedIn = null;
|
|
292
300
|
for (const entry of this._entries) {
|
|
293
301
|
clearEntryTimers(entry);
|
|
294
302
|
entry.node.remove();
|
|
@@ -525,6 +533,23 @@ export class Message extends Component {
|
|
|
525
533
|
Message.#release(this);
|
|
526
534
|
}
|
|
527
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Puts a floating region where it can be seen and used: inside the topmost open modal dialog
|
|
538
|
+
* while one is open — outside it, `showModal()` would leave every toast unclickable — and in its
|
|
539
|
+
* own theme scope otherwise. Re-showing it after a move puts it back on top of the top layer.
|
|
540
|
+
* @returns {void}
|
|
541
|
+
*/
|
|
542
|
+
_rehome() {
|
|
543
|
+
const host = this._floatingHost;
|
|
544
|
+
if (!host) return;
|
|
545
|
+
const target = overlayStack.topmostModal() ?? host;
|
|
546
|
+
if (this.el.parentElement === target && this._placedIn === target) return;
|
|
547
|
+
target.append(this.el);
|
|
548
|
+
this._placedIn = target;
|
|
549
|
+
const region = /** @type {HTMLElement} */ (this.el);
|
|
550
|
+
if (typeof region.showPopover === 'function' && !region.matches(':popover-open')) region.showPopover();
|
|
551
|
+
}
|
|
552
|
+
|
|
528
553
|
/**
|
|
529
554
|
* Destroys a floating region and removes its element.
|
|
530
555
|
* @returns {void}
|
|
@@ -554,33 +579,48 @@ export class Message extends Component {
|
|
|
554
579
|
* @returns {Message}
|
|
555
580
|
*/
|
|
556
581
|
static #floatingRegion(scope) {
|
|
557
|
-
let host =
|
|
582
|
+
let host = overlayScope(scope ?? null);
|
|
558
583
|
// A scope that has itself left the document cannot show anything; the toast still must be seen.
|
|
559
|
-
if (!host.isConnected) host =
|
|
560
|
-
const existing = this.#
|
|
561
|
-
if (existing && existing.el.isConnected && existing.el.parentElement ===
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
existing._disposeFloating();
|
|
584
|
+
if (!host.isConnected) host = overlayScope(null);
|
|
585
|
+
const existing = this.#regionFor(host);
|
|
586
|
+
if (existing && existing.el.isConnected && existing.el.parentElement === existing._placedIn) {
|
|
587
|
+
existing._rehome();
|
|
588
|
+
return existing;
|
|
565
589
|
}
|
|
590
|
+
existing?._disposeFloating();
|
|
566
591
|
const region = h('div', {
|
|
567
592
|
class: 'zx-message-region',
|
|
568
593
|
popover: 'manual',
|
|
569
594
|
role: 'status',
|
|
570
595
|
ariaLive: 'polite'
|
|
571
596
|
});
|
|
572
|
-
host.append(region);
|
|
573
597
|
const floating = new Message(region);
|
|
574
598
|
floating._floatingHost = host;
|
|
575
|
-
|
|
576
|
-
|
|
599
|
+
// A toast entry on the overlay stack: the registry of regions, and the reason a press on a
|
|
600
|
+
// toast dismisses no drawer or menu. Following the stack moves the region into a modal dialog
|
|
601
|
+
// when one opens and back out when it closes.
|
|
602
|
+
floating._overlay = overlayStack.push({ kind: 'toast', root: region, owner: floating });
|
|
603
|
+
floating._unfollow = overlayStack.subscribe(() => floating._rehome());
|
|
604
|
+
floating._rehome();
|
|
577
605
|
return floating;
|
|
578
606
|
}
|
|
579
607
|
|
|
608
|
+
/**
|
|
609
|
+
* The live floating region of a theme scope, from the overlay stack's toast entries.
|
|
610
|
+
* @param {Element} host Theme scope.
|
|
611
|
+
* @returns {Message|null}
|
|
612
|
+
*/
|
|
613
|
+
static #regionFor(host) {
|
|
614
|
+
for (const entry of overlayStack.entries()) {
|
|
615
|
+
if (entry.kind === 'toast' && entry.owner instanceof Message && entry.owner._floatingHost === host) {
|
|
616
|
+
return entry.owner;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
|
|
580
622
|
/** @param {Message} region Floating region whose last toast closed. @returns {void} */
|
|
581
623
|
static #release(region) {
|
|
582
|
-
const host = region._floatingHost;
|
|
583
|
-
if (host && Message.#floating.get(host) === region) Message.#floating.delete(host);
|
|
584
624
|
region._disposeFloating();
|
|
585
625
|
}
|
|
586
626
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Component } from '../../core/component.js';
|
|
2
2
|
import { h } from '../../core/dom.js';
|
|
3
|
-
import { connectedOverlayHost,
|
|
3
|
+
import { connectedOverlayHost, overlayScope } from '../../core/overlay-host.js';
|
|
4
|
+
import { pushOverlay } from '../../core/overlay-stack.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Controls initial focus may be placed on: what a reader reaches with Tab, rather than everything
|
|
@@ -62,6 +63,8 @@ export class Modal extends Component {
|
|
|
62
63
|
#nativeClosingPresentations = [];
|
|
63
64
|
/** @type {Element|null} Element that had focus when the overlay was opened. */
|
|
64
65
|
#opener = null;
|
|
66
|
+
/** @type {import('../../core/overlay-stack.js').OverlayHandle|null} Entry on the overlay stack while shown. */
|
|
67
|
+
#overlay = null;
|
|
65
68
|
|
|
66
69
|
/**
|
|
67
70
|
* Creates a modal in the configured theme scope, or at document level when none is present.
|
|
@@ -103,7 +106,9 @@ export class Modal extends Component {
|
|
|
103
106
|
* @returns {Element}
|
|
104
107
|
*/
|
|
105
108
|
mountTarget() {
|
|
106
|
-
|
|
109
|
+
// The theme scope, never an open dialog: a dialog opened from inside another stacks above it in
|
|
110
|
+
// the top layer wherever it lives, and nesting it would tie its rendering to the outer one.
|
|
111
|
+
return overlayScope(this.options.scope);
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
/**
|
|
@@ -159,6 +164,61 @@ export class Modal extends Component {
|
|
|
159
164
|
else if (this.el.getAttribute('aria-modal') === 'true') this.el.removeAttribute('aria-modal');
|
|
160
165
|
}
|
|
161
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Describes the current presentation for the overlay stack, or returns null to stay off it.
|
|
169
|
+
*
|
|
170
|
+
* The kind is read from the platform like `aria-modal` is: a `showModal()` dialog is `modal`,
|
|
171
|
+
* and its Escape and backdrop stay the browser's; anything else is `non-modal`, and a subclass
|
|
172
|
+
* presenting that way adds the Escape and outside-press handling it wants.
|
|
173
|
+
* @returns {import('../../core/overlay-stack.js').OverlayEntryOptions|null}
|
|
174
|
+
* @protected
|
|
175
|
+
*/
|
|
176
|
+
_overlayEntry() {
|
|
177
|
+
return {
|
|
178
|
+
kind: this.el.matches(':modal') ? 'modal' : 'non-modal',
|
|
179
|
+
root: this.el,
|
|
180
|
+
owner: this,
|
|
181
|
+
anchor: this.#opener
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Re-registers the current presentation on the overlay stack: called after every presentation,
|
|
187
|
+
* including a subclass's change of address that bypasses `open()`.
|
|
188
|
+
* @returns {void}
|
|
189
|
+
* @protected
|
|
190
|
+
*/
|
|
191
|
+
_syncOverlay() {
|
|
192
|
+
this._releaseOverlay();
|
|
193
|
+
if (!this._isShown()) return;
|
|
194
|
+
const entry = this._overlayEntry();
|
|
195
|
+
if (!entry) return;
|
|
196
|
+
this.#overlay = pushOverlay(entry);
|
|
197
|
+
this.el.dataset.zxOverlayOrder = String(this.#overlay.order);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Takes the overlay off the stack. Safe to call repeatedly.
|
|
202
|
+
* @returns {void}
|
|
203
|
+
* @protected
|
|
204
|
+
*/
|
|
205
|
+
_releaseOverlay() {
|
|
206
|
+
const overlay = this.#overlay;
|
|
207
|
+
this.#overlay = null;
|
|
208
|
+
delete (/** @type {HTMLElement} */ (this.el)).dataset.zxOverlayOrder;
|
|
209
|
+
overlay?.release();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Whether a node is inside this overlay or anything opened from it, as the overlay stack sees it.
|
|
214
|
+
* @param {Node|null|undefined} node Candidate.
|
|
215
|
+
* @returns {boolean}
|
|
216
|
+
* @protected
|
|
217
|
+
*/
|
|
218
|
+
_overlayContains(node) {
|
|
219
|
+
return this.#overlay ? this.#overlay.contains(node) : this.el.contains(node ?? null);
|
|
220
|
+
}
|
|
221
|
+
|
|
162
222
|
/**
|
|
163
223
|
* Hides the current presentation and records which generation its queued close event belongs to.
|
|
164
224
|
* @param {string} [result=''] Native string return value.
|
|
@@ -202,7 +262,7 @@ export class Modal extends Component {
|
|
|
202
262
|
return false;
|
|
203
263
|
}
|
|
204
264
|
this.el.dataset.state = 'closed';
|
|
205
|
-
|
|
265
|
+
this._releaseOverlay();
|
|
206
266
|
/*
|
|
207
267
|
* Only the flag this class raised. A subclass presenting non-modally writes
|
|
208
268
|
* `aria-modal="false"` and owns that value across its own presentations.
|
|
@@ -300,16 +360,15 @@ export class Modal extends Component {
|
|
|
300
360
|
this.#opener = document.activeElement instanceof Element ? document.activeElement : null;
|
|
301
361
|
const host = this.mountTarget();
|
|
302
362
|
if (this.el.parentElement !== host) host.append(this.el);
|
|
303
|
-
this.el.dataset.zxOverlayOrder = String(nextOverlayOrder());
|
|
304
363
|
this.el.returnValue = '';
|
|
305
364
|
try {
|
|
306
365
|
this._present();
|
|
307
366
|
} catch (error) {
|
|
308
|
-
delete this.el.dataset.zxOverlayOrder;
|
|
309
367
|
// Nothing was presented, so nothing will close; a capture left behind would outlive its open.
|
|
310
368
|
this.#opener = null;
|
|
311
369
|
throw error;
|
|
312
370
|
}
|
|
371
|
+
this._syncOverlay();
|
|
313
372
|
this.el.dataset.state = 'open';
|
|
314
373
|
this.emit('open');
|
|
315
374
|
// Deferred by one microtask: the platform's own focusing step runs as part of presenting, and
|
|
@@ -362,6 +421,8 @@ export class Modal extends Component {
|
|
|
362
421
|
if (this.#destroying) return;
|
|
363
422
|
this.#destroying = true;
|
|
364
423
|
if (this.isOpen()) this._dismiss();
|
|
424
|
+
// The queued close that would settle it never arrives once listeners are aborted.
|
|
425
|
+
this._releaseOverlay();
|
|
365
426
|
super.destroy();
|
|
366
427
|
}
|
|
367
428
|
|
|
@@ -458,11 +519,3 @@ function isBackdropClick(dialog, event) {
|
|
|
458
519
|
return event.clientX < rect.left || event.clientX > rect.right ||
|
|
459
520
|
event.clientY < rect.top || event.clientY > rect.bottom;
|
|
460
521
|
}
|
|
461
|
-
|
|
462
|
-
/** @returns {number} */
|
|
463
|
-
function nextOverlayOrder() {
|
|
464
|
-
const orders = Array.from(document.querySelectorAll('[data-zx-overlay-order]'), (element) =>
|
|
465
|
-
Number(element.getAttribute('data-zx-overlay-order')) || 0
|
|
466
|
-
);
|
|
467
|
-
return Math.max(0, ...orders) + 1;
|
|
468
|
-
}
|