@zeyos/zx 2.2.1 → 3.0.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 +197 -0
- package/DESIGN-SYSTEM.md +136 -0
- package/MIGRATION.md +155 -4
- package/README.md +51 -14
- package/dist/types/compat/map/data.d.ts +3 -2
- package/dist/types/components/account-menu/account-menu.d.ts +144 -0
- package/dist/types/components/app-rail/app-rail.d.ts +250 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +169 -0
- package/dist/types/components/avatar/avatar.d.ts +100 -0
- package/dist/types/components/chart/chart.d.ts +321 -0
- package/dist/types/components/date-picker/month-picker.d.ts +6 -0
- package/dist/types/components/dialog/dialog.d.ts +9 -0
- package/dist/types/components/dock/dock.d.ts +495 -0
- package/dist/types/components/dropdown/dropdown.d.ts +2 -2
- package/dist/types/components/field/field.d.ts +14 -0
- package/dist/types/components/field-upload/field-upload.d.ts +37 -4
- package/dist/types/components/form/form.d.ts +25 -3
- package/dist/types/components/grid/grid.d.ts +172 -0
- package/dist/types/components/launcher/launcher.d.ts +300 -0
- package/dist/types/components/layout/layout.d.ts +4 -4
- package/dist/types/components/menu-button/menu-button.d.ts +10 -2
- package/dist/types/components/modal/modal.d.ts +25 -0
- package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +74 -9
- package/dist/types/components/questionnaire/items.d.ts +327 -0
- package/dist/types/components/questionnaire/questionnaire.d.ts +449 -0
- package/dist/types/components/select/select.d.ts +36 -1
- package/dist/types/components/sheet/sheet-stack.d.ts +167 -0
- package/dist/types/components/sheet/sheet.d.ts +294 -0
- package/dist/types/components/split-view/split-view.d.ts +2 -21
- package/dist/types/components/tabbox/tabbox.d.ts +12 -3
- package/dist/types/components/table/table.d.ts +219 -12
- package/dist/types/components/tooltip/tooltip.d.ts +2 -2
- package/dist/types/components/value-list/value-list.d.ts +24 -3
- package/dist/types/core/dom.d.ts +114 -0
- package/dist/types/core/drag-axis.d.ts +111 -0
- package/dist/types/core/position.d.ts +4 -2
- package/dist/types/index.d.ts +12 -2
- package/dist/zx-compat.esm.js +943 -311
- package/dist/zx-compat.esm.min.js +1 -1
- package/dist/zx-compat.global.js +7893 -2252
- package/dist/zx-compat.global.min.js +3 -3
- package/dist/zx-zeyos.esm.js +826 -114
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +1720 -243
- package/dist/zx.esm.js +8227 -2599
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +8227 -2599
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +2597 -255
- package/docs/llms.md +475 -37
- package/docs/llms.txt +21 -9
- package/package.json +4 -2
- package/src/compat/globals.js +3 -1
- package/src/compat/map/data.js +31 -9
- package/src/components/account-menu/account-menu.css +70 -0
- package/src/components/account-menu/account-menu.js +208 -0
- package/src/components/app-rail/app-rail.css +107 -0
- package/src/components/app-rail/app-rail.js +668 -0
- package/src/components/app-sidebar/app-sidebar.css +68 -0
- package/src/components/app-sidebar/app-sidebar.js +388 -0
- package/src/components/avatar/avatar.css +61 -0
- package/src/components/avatar/avatar.js +222 -0
- package/src/components/button/button.css +0 -1
- package/src/components/chart/chart.css +61 -0
- package/src/components/chart/chart.js +518 -0
- package/src/components/date-picker/date-range-picker.css +7 -0
- package/src/components/date-picker/month-picker.js +9 -0
- package/src/components/dialog/dialog.js +11 -4
- package/src/components/dock/dock.css +228 -0
- package/src/components/dock/dock.js +977 -0
- package/src/components/dropdown/dropdown.js +1 -1
- package/src/components/field/field.js +2 -0
- package/src/components/field-upload/field-upload.js +9 -4
- package/src/components/fieldset/fieldset.css +7 -0
- package/src/components/form/form.css +7 -0
- package/src/components/form/form.js +6 -3
- package/src/components/grid/grid.css +6 -0
- package/src/components/grid/grid.js +213 -0
- package/src/components/launcher/launcher.css +100 -0
- package/src/components/launcher/launcher.js +557 -0
- package/src/components/layout/layout.js +2 -2
- package/src/components/menu-button/menu-button.js +27 -2
- package/src/components/modal/modal.css +5 -1
- package/src/components/modal/modal.js +37 -2
- package/src/components/multi-value-editor/multi-value-editor.css +99 -15
- package/src/components/multi-value-editor/multi-value-editor.js +237 -19
- package/src/components/navigation-bar/navigation-bar.css +7 -0
- package/src/components/questionnaire/items.js +370 -0
- package/src/components/questionnaire/questionnaire.css +387 -0
- package/src/components/questionnaire/questionnaire.js +1033 -0
- package/src/components/search/search.css +10 -2
- package/src/components/select/select.css +6 -1
- package/src/components/select/select.js +99 -11
- package/src/components/sheet/sheet-stack.js +304 -0
- package/src/components/sheet/sheet.css +351 -0
- package/src/components/sheet/sheet.js +551 -0
- package/src/components/split-view/split-view.js +76 -161
- package/src/components/stepper/stepper.css +12 -2
- package/src/components/tabbox/tabbox.css +13 -3
- package/src/components/tabbox/tabbox.js +34 -10
- package/src/components/table/table.css +43 -0
- package/src/components/table/table.js +523 -45
- package/src/components/tooltip/tooltip.js +5 -2
- package/src/components/value-list/value-list.js +6 -3
- package/src/core/dom.js +302 -0
- package/src/core/drag-axis.js +203 -0
- package/src/core/position.js +68 -22
- package/src/index.js +19 -2
- package/styles/tokens/dark.css +15 -13
- package/styles/tokens/global.css +17 -5
- package/styles/tokens/semantic.css +14 -8
- package/styles/tokens/themes.css +72 -0
- package/styles/zx.css +11 -1
- package/dist/types/components/permission/permission.d.ts +0 -93
- package/src/components/permission/permission.css +0 -47
- package/src/components/permission/permission.js +0 -198
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
import { Component } from '../../core/component.js';
|
|
2
|
+
import { h } from '../../core/dom.js';
|
|
3
|
+
import { icon } from '../../core/icons.js';
|
|
4
|
+
import { Dropdown } from '../dropdown/dropdown.js';
|
|
5
|
+
import { Tooltip } from '../tooltip/tooltip.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} AppNavItem
|
|
9
|
+
* @property {string|number} id Stable item identifier.
|
|
10
|
+
* @property {string} label Visible/accessibility label.
|
|
11
|
+
* @property {string} [icon] Kernel icon name.
|
|
12
|
+
* @property {string|number} [badge] Count or status badge.
|
|
13
|
+
* @property {unknown} [value] Emitted value; defaults to `id`.
|
|
14
|
+
* @property {string} [href] Native navigation target.
|
|
15
|
+
* @property {string} [target] Native link browsing context.
|
|
16
|
+
* @property {() => void} [invoke] Application-owned non-link action.
|
|
17
|
+
* @property {boolean} [disabled] Whether activation is unavailable.
|
|
18
|
+
* @property {AppNavItem[]} [children] Child destinations/actions.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} AppRailOptions
|
|
22
|
+
* @property {AppNavItem[]} [items=[]] Application navigation tree.
|
|
23
|
+
* @property {string|number|null} [active=null] Active item id.
|
|
24
|
+
* @property {'vertical'|'horizontal'} [orientation='vertical'] Rail flow.
|
|
25
|
+
* @property {'left'|'right'|'top'|'bottom'} [side='left'] Workspace edge occupied by the rail.
|
|
26
|
+
* @property {string} [label='Applications'] Navigation accessible label.
|
|
27
|
+
* @property {Node|string|number|{toElement: () => Node|null}|null} [header=null] Leading rail content.
|
|
28
|
+
* @property {Node|string|number|{toElement: () => Node|null}|null} [footer=null] Trailing rail content.
|
|
29
|
+
* @property {number} [openDelay=80] Hover-open delay in milliseconds.
|
|
30
|
+
* @property {number} [closeDelay=160] Pointer/focus crossing grace in milliseconds.
|
|
31
|
+
* @property {(event: CustomEvent<AppNavSelectDetail>) => void} [onselect] Preventable action listener.
|
|
32
|
+
* @property {(event: CustomEvent<{item: AppNavItem, id: string|number, open: boolean}>) => void} [onflyoutchange] Flyout-state listener.
|
|
33
|
+
*/
|
|
34
|
+
/** @typedef {{item: AppNavItem, id: string|number, value: unknown, href: string|null, event: Event}} AppNavSelectDetail */
|
|
35
|
+
/** @typedef {{key: string, parentKey: string|null, item: AppNavItem, trigger: HTMLElement, panel: HTMLElement, dropdown: Dropdown}} RailFlyout */
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Minimized application navigation; descendants always open in hover/focus flyouts, never inline.
|
|
39
|
+
* @fires AppRail#select
|
|
40
|
+
* @fires AppRail#flyoutchange
|
|
41
|
+
* @extends {Component<AppRailOptions>}
|
|
42
|
+
*/
|
|
43
|
+
export class AppRail extends Component {
|
|
44
|
+
static cssName = 'app-rail';
|
|
45
|
+
|
|
46
|
+
/** @type {Readonly<AppRailOptions>} */
|
|
47
|
+
static defaults = {
|
|
48
|
+
items: [],
|
|
49
|
+
active: null,
|
|
50
|
+
orientation: 'vertical',
|
|
51
|
+
side: 'left',
|
|
52
|
+
label: 'Applications',
|
|
53
|
+
header: null,
|
|
54
|
+
footer: null,
|
|
55
|
+
openDelay: 80,
|
|
56
|
+
closeDelay: 160
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates or enhances a rail host.
|
|
61
|
+
* @param {Element|string|null} target Rail target.
|
|
62
|
+
* @param {AppRailOptions} [options={}] Rail options.
|
|
63
|
+
*/
|
|
64
|
+
constructor(target = null, options = {}) {
|
|
65
|
+
super(target, options);
|
|
66
|
+
this._rebuild();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** @returns {HTMLElement} */
|
|
70
|
+
render() {
|
|
71
|
+
this._createdRoot = this.el === null;
|
|
72
|
+
const root = /** @type {HTMLElement} */ (this.el ?? h('nav'));
|
|
73
|
+
this.el = root;
|
|
74
|
+
this._original = this._createdRoot ? null : {
|
|
75
|
+
attributes: Array.from(root.attributes, (attribute) => [attribute.name, attribute.value]),
|
|
76
|
+
children: Array.from(root.childNodes)
|
|
77
|
+
};
|
|
78
|
+
this._items = normalizeAppItems(this.options.items);
|
|
79
|
+
this._active = this.options.active;
|
|
80
|
+
this._flyouts = new Map();
|
|
81
|
+
this._tooltips = [];
|
|
82
|
+
this._tooltipById = new Map();
|
|
83
|
+
this._interaction = null;
|
|
84
|
+
this._openTimers = new Map();
|
|
85
|
+
this._closeTimers = new Map();
|
|
86
|
+
this._pointerFocus = new WeakSet();
|
|
87
|
+
this._silenceFlyoutEvents = false;
|
|
88
|
+
root.setAttribute('aria-label', String(this.options.label));
|
|
89
|
+
if (root.tagName !== 'NAV') root.setAttribute('role', 'navigation');
|
|
90
|
+
root.dataset.orientation = this.options.orientation === 'horizontal' ? 'horizontal' : 'vertical';
|
|
91
|
+
const sides = root.dataset.orientation === 'horizontal' ? ['top', 'bottom'] : ['left', 'right'];
|
|
92
|
+
root.dataset.side = sides.includes(this.options.side) ? this.options.side : sides[0];
|
|
93
|
+
const header = h('div', { class: 'zx-app-rail__header' });
|
|
94
|
+
const footer = h('div', { class: 'zx-app-rail__footer' });
|
|
95
|
+
appendSlot(header, this.options.header);
|
|
96
|
+
appendSlot(footer, this.options.footer);
|
|
97
|
+
header.hidden = !header.hasChildNodes();
|
|
98
|
+
footer.hidden = !footer.hasChildNodes();
|
|
99
|
+
root.replaceChildren(header, h('ul', { ref: 'list', class: 'zx-app-rail__list' }), footer);
|
|
100
|
+
this.listen(root, 'keydown', (event) => this._onRailKeydown(/** @type {KeyboardEvent} */ (event)));
|
|
101
|
+
return root;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Replaces navigation items and closes removed flyouts. @param {AppNavItem[]} items @returns {this} */
|
|
105
|
+
setItems(items) {
|
|
106
|
+
this._items = normalizeAppItems(items);
|
|
107
|
+
this._rebuild();
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Sets the active item without routing. @param {string|number|null} id @returns {this} */
|
|
112
|
+
setActive(id) {
|
|
113
|
+
this._active = id;
|
|
114
|
+
this._syncActive();
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Returns a shallow copy of normalized items. @returns {AppNavItem[]} */
|
|
119
|
+
getItems() {
|
|
120
|
+
return this._items.map((item) => ({ ...item, children: item.children?.map((child) => ({ ...child })) }));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Opens one item's flyout, optionally focusing its first action. @param {string|number} id @param {{focus?: boolean}} [options={}] @returns {this} */
|
|
124
|
+
openFlyout(id, { focus = false } = {}) {
|
|
125
|
+
const flyout = this._resolveFlyout(id);
|
|
126
|
+
if (!flyout) return this;
|
|
127
|
+
const branch = [flyout];
|
|
128
|
+
let owner = flyout.parentKey === null ? null : this._flyouts.get(flyout.parentKey) ?? null;
|
|
129
|
+
while (owner) {
|
|
130
|
+
branch.unshift(owner);
|
|
131
|
+
owner = owner.parentKey === null ? null : this._flyouts.get(owner.parentKey) ?? null;
|
|
132
|
+
}
|
|
133
|
+
if (branch.some((member) => member.item.disabled)) return this;
|
|
134
|
+
this._cancelFlyoutTimers(flyout, true);
|
|
135
|
+
for (const [otherId, other] of this._flyouts) {
|
|
136
|
+
if (otherId !== flyout.key && other.dropdown.isOpen() && !this._isAncestor(other, flyout)) {
|
|
137
|
+
this.closeFlyout(otherId);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const member of branch) {
|
|
141
|
+
this._tooltipById.get(String(member.item.id))?.hide();
|
|
142
|
+
member.dropdown.open();
|
|
143
|
+
}
|
|
144
|
+
if (focus) firstAction(flyout.panel)?.focus();
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Closes one flyout. @param {string|number} id @returns {this} */
|
|
149
|
+
closeFlyout(id) {
|
|
150
|
+
const flyout = this._resolveFlyout(id);
|
|
151
|
+
if (!flyout) return this;
|
|
152
|
+
for (const descendant of [...this._flyouts.values()].reverse()) {
|
|
153
|
+
if (descendant !== flyout && this._isAncestor(flyout, descendant)) {
|
|
154
|
+
this._cancelTimers(descendant.key);
|
|
155
|
+
descendant.dropdown.close();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
this._cancelTimers(flyout.key);
|
|
159
|
+
flyout.dropdown.close();
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Closes every flyout and cancels pending hover/focus transitions. @returns {this} */
|
|
164
|
+
closeAllFlyouts() {
|
|
165
|
+
for (const flyout of this._flyouts.values()) {
|
|
166
|
+
if (flyout.parentKey === null) this.closeFlyout(flyout.key);
|
|
167
|
+
}
|
|
168
|
+
for (const flyout of [...this._flyouts.values()].reverse()) {
|
|
169
|
+
this._cancelTimers(flyout.key);
|
|
170
|
+
if (flyout.dropdown.isOpen()) flyout.dropdown.close();
|
|
171
|
+
}
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Reports whether an item's flyout is open. @param {string|number} id @returns {boolean} */
|
|
176
|
+
isFlyoutOpen(id) {
|
|
177
|
+
return this._resolveFlyout(id)?.dropdown.isOpen() ?? false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Focuses the first top-level rail item. @returns {this} */
|
|
181
|
+
focus() {
|
|
182
|
+
firstAction(this.refs.list)?.focus();
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Reports whether a node belongs to this rail, including detached flyout panels. @param {Node|null} node @returns {boolean} @private */
|
|
187
|
+
_containsNode(node) {
|
|
188
|
+
return Boolean(node && (this.el.contains(node)
|
|
189
|
+
|| [...this._flyouts.values()].some((flyout) => flyout.panel.contains(node))));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Destroys body popovers/tooltips and restores or removes the root. @returns {void} */
|
|
193
|
+
destroy() {
|
|
194
|
+
const original = this._original;
|
|
195
|
+
this._clearArtifacts();
|
|
196
|
+
super.destroy();
|
|
197
|
+
if (!this._createdRoot && original) {
|
|
198
|
+
for (const attribute of Array.from(this.el.attributes)) this.el.removeAttribute(attribute.name);
|
|
199
|
+
for (const [name, value] of original.attributes) this.el.setAttribute(name, value);
|
|
200
|
+
this.el.replaceChildren(...original.children);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** @returns {void} */
|
|
205
|
+
_rebuild() {
|
|
206
|
+
this._clearArtifacts();
|
|
207
|
+
this.refs.list.replaceChildren();
|
|
208
|
+
this._interaction = new RailListenerScope(/** @type {HTMLElement} */ (this.el));
|
|
209
|
+
this._items.forEach((item) => {
|
|
210
|
+
const hasChildren = Array.isArray(item.children) && item.children.length > 0;
|
|
211
|
+
const control = navControl(item, {
|
|
212
|
+
className: 'zx-app-rail__item',
|
|
213
|
+
index: String(item.id),
|
|
214
|
+
iconOnly: true,
|
|
215
|
+
disclosure: hasChildren,
|
|
216
|
+
disclosureIcon: railDisclosureIcon(this.el.dataset.side)
|
|
217
|
+
});
|
|
218
|
+
const row = h('li', { class: 'zx-app-rail__row' }, control);
|
|
219
|
+
this.refs.list.append(row);
|
|
220
|
+
const tooltip = new Tooltip(control, {
|
|
221
|
+
content: item.label,
|
|
222
|
+
placement: tooltipPlacement(this.el.dataset.side),
|
|
223
|
+
delay: 450
|
|
224
|
+
});
|
|
225
|
+
this._tooltips.push(tooltip);
|
|
226
|
+
this._tooltipById.set(String(item.id), tooltip);
|
|
227
|
+
if (hasChildren) this._buildFlyout(item, control, String(item.id), null);
|
|
228
|
+
else this._interaction.listen(control, 'click', (event) => {
|
|
229
|
+
const selected = this._select(item, event);
|
|
230
|
+
if (!selected.defaultPrevented) {
|
|
231
|
+
for (const flyout of this._flyouts.values()) {
|
|
232
|
+
if (flyout.parentKey === null && flyout.dropdown.isOpen()) this.closeFlyout(flyout.key);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
this._interaction.listen(document, 'pointerdown', (event) => {
|
|
238
|
+
const open = [...this._flyouts.values()].filter((flyout) => flyout.dropdown.isOpen());
|
|
239
|
+
if (open.length === 0) return;
|
|
240
|
+
const path = event.composedPath();
|
|
241
|
+
if (open.some((flyout) => path.includes(flyout.panel) || path.includes(flyout.trigger))) return;
|
|
242
|
+
for (const flyout of open) if (flyout.parentKey === null) this.closeFlyout(flyout.key);
|
|
243
|
+
});
|
|
244
|
+
this._syncActive();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** @param {AppNavItem} item @param {HTMLElement} trigger @param {string} key @param {string|null} parentKey @returns {void} */
|
|
248
|
+
_buildFlyout(item, trigger, key, parentKey) {
|
|
249
|
+
const list = h('ul', { class: 'zx-app-rail__flyout-list' });
|
|
250
|
+
for (const child of item.children ?? []) {
|
|
251
|
+
const childKey = `${key}/${child.id}`;
|
|
252
|
+
const hasChildren = Boolean(child.children?.length);
|
|
253
|
+
const control = navControl(child, {
|
|
254
|
+
className: 'zx-app-rail__flyout-item',
|
|
255
|
+
index: childKey,
|
|
256
|
+
iconOnly: false,
|
|
257
|
+
disclosure: hasChildren,
|
|
258
|
+
disclosureIcon: nestedDisclosureIcon(this.el.dataset.side)
|
|
259
|
+
});
|
|
260
|
+
list.append(h('li', {}, control));
|
|
261
|
+
if (hasChildren) this._buildFlyout(child, control, childKey, key);
|
|
262
|
+
else this._interaction.listen(control, 'click', (event) => {
|
|
263
|
+
const selected = this._select(child, event);
|
|
264
|
+
if (!selected.defaultPrevented) this._closeFlyoutChain(key);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
const panel = h('div', {
|
|
268
|
+
class: 'zx-app-rail__flyout'
|
|
269
|
+
}, h('div', { class: 'zx-app-rail__flyout-title' }, item.label), list);
|
|
270
|
+
const dropdown = new Dropdown(trigger, {
|
|
271
|
+
openOn: 'manual',
|
|
272
|
+
placement: parentKey === null ? flyoutPlacement(this.el.dataset.side)
|
|
273
|
+
: nestedFlyoutPlacement(this.el.dataset.side),
|
|
274
|
+
content: panel,
|
|
275
|
+
offset: 6
|
|
276
|
+
});
|
|
277
|
+
const actualPanel = dropdown.getPanel();
|
|
278
|
+
actualPanel.classList.add('zx-app-rail__popover');
|
|
279
|
+
actualPanel.setAttribute('role', 'region');
|
|
280
|
+
actualPanel.setAttribute('aria-label', `${item.label} sub-navigation`);
|
|
281
|
+
const flyout = { key, parentKey, item, trigger, panel: actualPanel, dropdown };
|
|
282
|
+
this._flyouts.set(key, flyout);
|
|
283
|
+
dropdown.on('open', () => {
|
|
284
|
+
if (!this._silenceFlyoutEvents) this.emit('flyoutchange', { item, id: item.id, open: true });
|
|
285
|
+
});
|
|
286
|
+
dropdown.on('close', () => {
|
|
287
|
+
if (!this._silenceFlyoutEvents) this.emit('flyoutchange', { item, id: item.id, open: false });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
this._interaction.listen(trigger, 'pointerdown', () => {
|
|
291
|
+
this._pointerFocus.add(trigger);
|
|
292
|
+
queueMicrotask(() => this._pointerFocus.delete(trigger));
|
|
293
|
+
});
|
|
294
|
+
this._interaction.listen(trigger, 'pointerenter', (event) => {
|
|
295
|
+
if (/** @type {PointerEvent} */ (event).pointerType === 'touch') return;
|
|
296
|
+
this._scheduleOpen(key);
|
|
297
|
+
});
|
|
298
|
+
this._interaction.listen(trigger, 'pointerleave', () => this._scheduleClose(key));
|
|
299
|
+
this._interaction.listen(trigger, 'focus', () => {
|
|
300
|
+
if (!this._pointerFocus.has(trigger)) this.openFlyout(key);
|
|
301
|
+
});
|
|
302
|
+
this._interaction.listen(trigger, 'focusout', (event) => {
|
|
303
|
+
if (!actualPanel.contains(/** @type {Node|null} */ (event.relatedTarget))) this._scheduleClose(key);
|
|
304
|
+
});
|
|
305
|
+
this._interaction.listen(trigger, 'click', (event) => {
|
|
306
|
+
event.preventDefault();
|
|
307
|
+
if (event instanceof MouseEvent && event.detail === 0) this.openFlyout(key, { focus: true });
|
|
308
|
+
else if (dropdown.isOpen()) this.closeFlyout(key);
|
|
309
|
+
else this.openFlyout(key);
|
|
310
|
+
});
|
|
311
|
+
this._interaction.listen(actualPanel, 'pointerenter', () => this._cancelFlyoutTimers(flyout, true));
|
|
312
|
+
this._interaction.listen(actualPanel, 'pointerleave', () => this._scheduleFlyoutClose(flyout, true));
|
|
313
|
+
this._interaction.listen(actualPanel, 'focusin', () => this._cancelFlyoutTimers(flyout, true));
|
|
314
|
+
this._interaction.listen(actualPanel, 'focusout', (event) => {
|
|
315
|
+
if (event.relatedTarget !== trigger && !actualPanel.contains(/** @type {Node|null} */ (event.relatedTarget))) {
|
|
316
|
+
this._scheduleFlyoutClose(flyout, true);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
this._interaction.listen(actualPanel, 'keydown', (event) => this._onFlyoutKeydown(
|
|
320
|
+
/** @type {KeyboardEvent} */ (event), flyout));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** @param {AppNavItem} item @param {Event} event @returns {Event} */
|
|
324
|
+
_select(item, event) {
|
|
325
|
+
if (item.disabled) {
|
|
326
|
+
event.preventDefault();
|
|
327
|
+
const canceled = new Event('select', { cancelable: true });
|
|
328
|
+
canceled.preventDefault();
|
|
329
|
+
return canceled;
|
|
330
|
+
}
|
|
331
|
+
const selected = this.emit('select', {
|
|
332
|
+
item,
|
|
333
|
+
id: item.id,
|
|
334
|
+
value: item.value ?? item.id,
|
|
335
|
+
href: item.href ?? null,
|
|
336
|
+
event
|
|
337
|
+
});
|
|
338
|
+
if (selected.defaultPrevented) event.preventDefault();
|
|
339
|
+
else if (!item.href) {
|
|
340
|
+
event.preventDefault();
|
|
341
|
+
item.invoke?.();
|
|
342
|
+
}
|
|
343
|
+
return selected;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** @param {KeyboardEvent} event @returns {void} */
|
|
347
|
+
_onRailKeydown(event) {
|
|
348
|
+
const controls = topLevelControls(this.refs.list);
|
|
349
|
+
const current = /** @type {Element|null} */ (event.target)?.closest?.('[data-app-nav-id]');
|
|
350
|
+
const index = controls.indexOf(current);
|
|
351
|
+
if (index < 0) return;
|
|
352
|
+
const horizontal = this.el.dataset.orientation === 'horizontal';
|
|
353
|
+
const previous = horizontal ? 'ArrowLeft' : 'ArrowUp';
|
|
354
|
+
const next = horizontal ? 'ArrowRight' : 'ArrowDown';
|
|
355
|
+
const into = ({ left: 'ArrowRight', right: 'ArrowLeft', top: 'ArrowDown', bottom: 'ArrowUp' })[this.el.dataset.side];
|
|
356
|
+
const id = current.getAttribute('data-app-nav-id');
|
|
357
|
+
if (event.key === into && this._flyouts.has(String(id))) {
|
|
358
|
+
event.preventDefault();
|
|
359
|
+
this.openFlyout(String(id), { focus: true });
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
let nextIndex;
|
|
363
|
+
if (event.key === 'Home') nextIndex = 0;
|
|
364
|
+
else if (event.key === 'End') nextIndex = controls.length - 1;
|
|
365
|
+
else if (event.key === previous) nextIndex = (index - 1 + controls.length) % controls.length;
|
|
366
|
+
else if (event.key === next) nextIndex = (index + 1) % controls.length;
|
|
367
|
+
else return;
|
|
368
|
+
event.preventDefault();
|
|
369
|
+
controls[nextIndex]?.focus();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** @param {KeyboardEvent} event @param {RailFlyout} flyout @returns {void} */
|
|
373
|
+
_onFlyoutKeydown(event, flyout) {
|
|
374
|
+
const controls = actions(flyout.panel);
|
|
375
|
+
const current = /** @type {Element|null} */ (event.target)?.closest?.('a, button');
|
|
376
|
+
const index = controls.indexOf(current);
|
|
377
|
+
if (event.key === 'Escape') {
|
|
378
|
+
event.preventDefault();
|
|
379
|
+
event.stopPropagation();
|
|
380
|
+
const nested = [...this._flyouts.values()].find((candidate) =>
|
|
381
|
+
candidate.trigger === current && candidate.dropdown.isOpen());
|
|
382
|
+
if (nested) {
|
|
383
|
+
this.closeFlyout(nested.key);
|
|
384
|
+
this._restoreFocus(nested.trigger);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
this.closeFlyout(flyout.key);
|
|
388
|
+
this._restoreFocus(flyout.trigger);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const nested = [...this._flyouts.values()].find((candidate) => candidate.trigger === current);
|
|
392
|
+
const into = this.el.dataset.side === 'right' ? 'ArrowLeft' : 'ArrowRight';
|
|
393
|
+
const out = into === 'ArrowRight' ? 'ArrowLeft' : 'ArrowRight';
|
|
394
|
+
if (nested && event.key === into) {
|
|
395
|
+
event.preventDefault();
|
|
396
|
+
event.stopPropagation();
|
|
397
|
+
this.openFlyout(nested.key, { focus: true });
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (flyout.parentKey !== null && event.key === out) {
|
|
401
|
+
event.preventDefault();
|
|
402
|
+
event.stopPropagation();
|
|
403
|
+
this.closeFlyout(flyout.key);
|
|
404
|
+
this._restoreFocus(flyout.trigger);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (controls.length === 0) return;
|
|
408
|
+
let nextIndex;
|
|
409
|
+
if (event.key === 'Home') nextIndex = 0;
|
|
410
|
+
else if (event.key === 'End') nextIndex = controls.length - 1;
|
|
411
|
+
else if (event.key === 'ArrowDown') nextIndex = (index + 1 + controls.length) % controls.length;
|
|
412
|
+
else if (event.key === 'ArrowUp') nextIndex = (index - 1 + controls.length) % controls.length;
|
|
413
|
+
else return;
|
|
414
|
+
event.preventDefault();
|
|
415
|
+
controls[nextIndex]?.focus();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** @param {string|number} id @returns {void} */
|
|
419
|
+
_scheduleOpen(id) {
|
|
420
|
+
const key = String(id);
|
|
421
|
+
this._cancelTimer(this._closeTimers, key);
|
|
422
|
+
this._cancelTimer(this._openTimers, key);
|
|
423
|
+
const delay = Math.max(0, Number(this.options.openDelay) || 0);
|
|
424
|
+
this._openTimers.set(key, setTimeout(() => {
|
|
425
|
+
this._openTimers.delete(key);
|
|
426
|
+
this.openFlyout(id);
|
|
427
|
+
}, delay));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** @param {string|number} id @returns {void} */
|
|
431
|
+
_scheduleClose(id) {
|
|
432
|
+
const key = String(id);
|
|
433
|
+
this._cancelTimer(this._openTimers, key);
|
|
434
|
+
this._cancelTimer(this._closeTimers, key);
|
|
435
|
+
const delay = Math.max(0, Number(this.options.closeDelay) || 0);
|
|
436
|
+
this._closeTimers.set(key, setTimeout(() => {
|
|
437
|
+
this._closeTimers.delete(key);
|
|
438
|
+
const flyout = this._flyouts.get(key);
|
|
439
|
+
if (!flyout) return;
|
|
440
|
+
if (this._branchInteracting(flyout)) return;
|
|
441
|
+
this.closeFlyout(key);
|
|
442
|
+
}, delay));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Schedules a flyout and, when leaving a detached panel, each owning flyout for re-evaluation. @param {RailFlyout} flyout @param {boolean} ancestors @returns {void} */
|
|
446
|
+
_scheduleFlyoutClose(flyout, ancestors) {
|
|
447
|
+
let current = flyout;
|
|
448
|
+
while (current) {
|
|
449
|
+
this._scheduleClose(current.key);
|
|
450
|
+
if (!ancestors || current.parentKey === null) break;
|
|
451
|
+
current = this._flyouts.get(current.parentKey) ?? null;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Resolves a public id or internal descendant path. @param {string|number} id @returns {RailFlyout|null} */
|
|
456
|
+
_resolveFlyout(id) {
|
|
457
|
+
const key = String(id);
|
|
458
|
+
return this._flyouts.get(key)
|
|
459
|
+
?? [...this._flyouts.values()].find((flyout) => String(flyout.item.id) === key)
|
|
460
|
+
?? null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Closes the root flyout that owns a selected descendant. @param {string} key @returns {void} */
|
|
464
|
+
_closeFlyoutChain(key) {
|
|
465
|
+
let flyout = this._flyouts.get(key) ?? null;
|
|
466
|
+
while (flyout?.parentKey !== null) flyout = this._flyouts.get(flyout.parentKey) ?? null;
|
|
467
|
+
if (flyout) this.closeFlyout(flyout.key);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Reports whether one flyout owns another through its parent chain. @param {RailFlyout} ancestor @param {RailFlyout} descendant @returns {boolean} */
|
|
471
|
+
_isAncestor(ancestor, descendant) {
|
|
472
|
+
let parentKey = descendant.parentKey;
|
|
473
|
+
while (parentKey !== null) {
|
|
474
|
+
if (parentKey === ancestor.key) return true;
|
|
475
|
+
parentKey = this._flyouts.get(parentKey)?.parentKey ?? null;
|
|
476
|
+
}
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Cancels crossing timers for a flyout and optionally every owning flyout. @param {RailFlyout} flyout @param {boolean} ancestors @returns {void} */
|
|
481
|
+
_cancelFlyoutTimers(flyout, ancestors) {
|
|
482
|
+
let current = flyout;
|
|
483
|
+
while (current) {
|
|
484
|
+
this._cancelTimers(current.key);
|
|
485
|
+
if (!ancestors || current.parentKey === null) break;
|
|
486
|
+
current = this._flyouts.get(current.parentKey) ?? null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Reports pointer/focus presence in a flyout branch, including detached descendant panels. @param {RailFlyout} flyout @returns {boolean} */
|
|
491
|
+
_branchInteracting(flyout) {
|
|
492
|
+
if (flyout.trigger.matches(':hover') || flyout.panel.matches(':hover')
|
|
493
|
+
|| flyout.trigger === document.activeElement || flyout.panel.contains(document.activeElement)) return true;
|
|
494
|
+
return [...this._flyouts.values()].some((candidate) => this._isAncestor(flyout, candidate)
|
|
495
|
+
&& candidate.dropdown.isOpen()
|
|
496
|
+
&& (candidate.trigger.matches(':hover') || candidate.panel.matches(':hover')
|
|
497
|
+
|| candidate.trigger === document.activeElement || candidate.panel.contains(document.activeElement)));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Restores trigger focus without immediately reopening the flyout through its focus handler. @param {HTMLElement} trigger @returns {void} */
|
|
501
|
+
_restoreFocus(trigger) {
|
|
502
|
+
this._pointerFocus.add(trigger);
|
|
503
|
+
trigger.focus();
|
|
504
|
+
queueMicrotask(() => this._pointerFocus.delete(trigger));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** @param {string} key @returns {void} */
|
|
508
|
+
_cancelTimers(key) {
|
|
509
|
+
this._cancelTimer(this._openTimers, key);
|
|
510
|
+
this._cancelTimer(this._closeTimers, key);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** @param {Map<string, ReturnType<typeof setTimeout>>} timers @param {string} key @returns {void} */
|
|
514
|
+
_cancelTimer(timers, key) {
|
|
515
|
+
const timer = timers.get(key);
|
|
516
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
517
|
+
timers.delete(key);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** @returns {void} */
|
|
521
|
+
_syncActive() {
|
|
522
|
+
const activeId = this._active == null ? null : String(this._active);
|
|
523
|
+
for (const control of this.refs.list.querySelectorAll('[data-app-nav-id]')) {
|
|
524
|
+
const direct = activeId !== null && control.getAttribute('data-app-nav-value-id') === activeId;
|
|
525
|
+
const flyout = this._flyouts.get(String(control.getAttribute('data-app-nav-id')));
|
|
526
|
+
const descendant = activeId !== null && flyout
|
|
527
|
+
? containsItemId(flyout.item.children ?? [], activeId) : false;
|
|
528
|
+
if (direct) control.setAttribute('aria-current', 'page');
|
|
529
|
+
else control.removeAttribute('aria-current');
|
|
530
|
+
if (descendant) control.dataset.activeDescendant = 'true';
|
|
531
|
+
else delete control.dataset.activeDescendant;
|
|
532
|
+
}
|
|
533
|
+
for (const flyout of this._flyouts.values()) {
|
|
534
|
+
const descendant = activeId !== null && containsItemId(flyout.item.children ?? [], activeId);
|
|
535
|
+
if (descendant) flyout.trigger.dataset.activeDescendant = 'true';
|
|
536
|
+
else delete flyout.trigger.dataset.activeDescendant;
|
|
537
|
+
for (const control of flyout.panel.querySelectorAll('[data-app-nav-value-id]')) {
|
|
538
|
+
if (activeId !== null && control.getAttribute('data-app-nav-value-id') === activeId) {
|
|
539
|
+
control.setAttribute('aria-current', 'page');
|
|
540
|
+
} else control.removeAttribute('aria-current');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** @returns {void} */
|
|
546
|
+
_clearArtifacts() {
|
|
547
|
+
for (const timer of this._openTimers ? this._openTimers.values() : []) clearTimeout(timer);
|
|
548
|
+
for (const timer of this._closeTimers ? this._closeTimers.values() : []) clearTimeout(timer);
|
|
549
|
+
this._openTimers?.clear?.();
|
|
550
|
+
this._closeTimers?.clear?.();
|
|
551
|
+
this._interaction?.destroy?.();
|
|
552
|
+
for (const tooltip of this._tooltips ?? []) tooltip.destroy();
|
|
553
|
+
this._silenceFlyoutEvents = true;
|
|
554
|
+
for (const flyout of this._flyouts?.values?.() ?? []) flyout.dropdown.destroy();
|
|
555
|
+
this._silenceFlyoutEvents = false;
|
|
556
|
+
this._tooltips = [];
|
|
557
|
+
this._tooltipById = new Map();
|
|
558
|
+
this._interaction = null;
|
|
559
|
+
this._flyouts = new Map();
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** Owns listeners attached to one dynamic rail render so `setItems()` can release them immediately. */
|
|
564
|
+
class RailListenerScope extends Component {
|
|
565
|
+
/** @param {HTMLElement} host Rail host. */
|
|
566
|
+
constructor(host) {
|
|
567
|
+
super(null);
|
|
568
|
+
host.append(this.el);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** @returns {HTMLElement} */
|
|
572
|
+
render() {
|
|
573
|
+
return h('span', { hidden: true, 'data-zx-app-rail-listener-scope': '' });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Normalizes an application navigation tree without mutating caller data.
|
|
579
|
+
* @param {unknown} items Candidate tree.
|
|
580
|
+
* @returns {AppNavItem[]}
|
|
581
|
+
*/
|
|
582
|
+
export function normalizeAppItems(items) {
|
|
583
|
+
if (!Array.isArray(items)) return [];
|
|
584
|
+
return items.filter((item) => item && typeof item === 'object' && item.id != null && item.label != null)
|
|
585
|
+
.map((item) => ({
|
|
586
|
+
...item,
|
|
587
|
+
label: String(item.label),
|
|
588
|
+
children: Array.isArray(item.children) ? normalizeAppItems(item.children) : []
|
|
589
|
+
}));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** @param {AppNavItem} item @param {{className: string, index: string, iconOnly: boolean, disclosure: boolean, disclosureIcon: string}} options @returns {HTMLElement} */
|
|
593
|
+
function navControl(item, { className, index, iconOnly, disclosure, disclosureIcon }) {
|
|
594
|
+
const tag = item.href && !disclosure ? 'a' : 'button';
|
|
595
|
+
return /** @type {HTMLElement} */ (h(tag, {
|
|
596
|
+
class: className,
|
|
597
|
+
type: tag === 'button' ? 'button' : null,
|
|
598
|
+
href: tag === 'a' ? item.href : null,
|
|
599
|
+
target: tag === 'a' ? item.target ?? null : null,
|
|
600
|
+
ariaLabel: iconOnly ? item.label : null,
|
|
601
|
+
ariaDisabled: item.disabled ? 'true' : null,
|
|
602
|
+
dataset: { appNavId: index, appNavValueId: String(item.id) }
|
|
603
|
+
},
|
|
604
|
+
item.icon ? h('span', { class: `${className}-icon`, ariaHidden: 'true' }, icon(item.icon)) : null,
|
|
605
|
+
h('span', { class: `${className}-label` }, item.label),
|
|
606
|
+
item.badge != null ? h('span', { class: `${className}-badge` }, String(item.badge)) : null,
|
|
607
|
+
disclosure ? h('span', { class: `${className}-disclosure`, ariaHidden: 'true' }, icon(disclosureIcon)) : null));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** @param {string} side @returns {string} */
|
|
611
|
+
function railDisclosureIcon(side) {
|
|
612
|
+
return { left: 'chevron-right', right: 'chevron-left', top: 'chevron-down', bottom: 'chevron-up' }[side]
|
|
613
|
+
?? 'chevron-right';
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** @param {string} side @returns {string} */
|
|
617
|
+
function nestedDisclosureIcon(side) {
|
|
618
|
+
return side === 'right' ? 'chevron-left' : 'chevron-right';
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** @param {string} side @returns {'right-start'|'left-start'|'bottom-start'|'top-start'} */
|
|
622
|
+
function flyoutPlacement(side) {
|
|
623
|
+
return /** @type {any} */ ({ left: 'right-start', right: 'left-start', top: 'bottom-start', bottom: 'top-start' }[side]
|
|
624
|
+
?? 'right-start');
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** @param {string} side @returns {'right-start'|'left-start'} */
|
|
628
|
+
function nestedFlyoutPlacement(side) {
|
|
629
|
+
return side === 'right' ? 'left-start' : 'right-start';
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** @param {string} side @returns {'right'|'left'|'bottom'|'top'} */
|
|
633
|
+
function tooltipPlacement(side) {
|
|
634
|
+
return /** @type {any} */ ({ left: 'right', right: 'left', top: 'bottom', bottom: 'top' }[side] ?? 'right');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** @param {Element} root @returns {HTMLElement[]} */
|
|
638
|
+
function actions(root) {
|
|
639
|
+
return /** @type {HTMLElement[]} */ ([...root.querySelectorAll('a:not([aria-disabled="true"]), button:not([aria-disabled="true"])')]);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** @param {Element} root @returns {HTMLElement|null} */
|
|
643
|
+
function firstAction(root) {
|
|
644
|
+
return actions(root)[0] ?? null;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** @param {Element} list @returns {HTMLElement[]} */
|
|
648
|
+
function topLevelControls(list) {
|
|
649
|
+
return /** @type {HTMLElement[]} */ ([...list.children].map((row) => row.querySelector('[data-app-nav-id]')).filter(Boolean));
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** @param {AppNavItem[]} items @param {string} id @returns {boolean} */
|
|
653
|
+
function containsItemId(items, id) {
|
|
654
|
+
return items.some((item) => String(item.id) === id || containsItemId(item.children ?? [], id));
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** @param {Element} target @param {unknown} content @returns {void} */
|
|
658
|
+
function appendSlot(target, content) {
|
|
659
|
+
if (content == null) return;
|
|
660
|
+
if (typeof content === 'string' || typeof content === 'number') target.append(document.createTextNode(String(content)));
|
|
661
|
+
else if (typeof content === 'object' && typeof content.toElement === 'function') {
|
|
662
|
+
const element = content.toElement();
|
|
663
|
+
if (element) target.append(element);
|
|
664
|
+
} else if (typeof content === 'object' && typeof content.nodeType === 'number') target.append(content);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** Fired when a destination/action is activated. @event AppRail#select @type {CustomEvent<AppNavSelectDetail>} */
|
|
668
|
+
/** Fired when a flyout opens or closes. @event AppRail#flyoutchange @type {CustomEvent<{item: AppNavItem, id: string|number, open: boolean}>} */
|