@zeyos/zx 3.0.0 → 4.1.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 +95 -0
- package/DESIGN-SYSTEM.md +87 -71
- package/README.md +16 -37
- package/dist/types/components/app-icon/app-icon.d.ts +88 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +106 -32
- package/dist/types/components/card/card.d.ts +128 -0
- package/dist/types/components/chart/chart.d.ts +1 -0
- package/dist/types/components/filter/filter-model.d.ts +87 -0
- package/dist/types/components/filter/filter.d.ts +331 -0
- package/dist/types/components/launcher/launcher.d.ts +65 -6
- package/dist/types/components/select/select.d.ts +97 -4
- package/dist/types/components/table/table.d.ts +107 -3
- package/dist/types/components/tag-picker/tag-picker.d.ts +16 -0
- package/dist/types/core/dom.d.ts +9 -1
- package/dist/types/core/util.d.ts +9 -0
- package/dist/types/index.d.ts +6 -2
- package/dist/types/{components/app-rail → internal}/app-rail.d.ts +34 -19
- package/dist/types/zeyos/icons.d.ts +24 -2
- package/dist/types/zeyos/index.d.ts +3 -2
- package/dist/types/zeyos/launcher.d.ts +147 -0
- package/dist/types/zeyos/select.d.ts +41 -0
- package/dist/zx-zeyos.esm.js +2018 -113
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +815 -16
- package/dist/zx.esm.js +2661 -338
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +2661 -338
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +602 -110
- package/docs/llms.md +181 -123
- package/docs/llms.txt +24 -21
- package/package.json +8 -17
- package/src/components/app-icon/app-icon.css +105 -0
- package/src/components/app-icon/app-icon.js +134 -0
- package/src/components/app-sidebar/app-sidebar.css +12 -0
- package/src/components/app-sidebar/app-sidebar.js +310 -110
- package/src/components/card/card.css +149 -0
- package/src/components/card/card.js +285 -0
- package/src/components/chart/chart.js +8 -3
- package/src/components/filter/filter-model.js +226 -0
- package/src/components/filter/filter.css +147 -0
- package/src/components/filter/filter.js +554 -0
- package/src/components/grid/grid.css +1 -1
- package/src/components/grid/grid.js +14 -4
- package/src/components/launcher/launcher.css +113 -3
- package/src/components/launcher/launcher.js +234 -26
- package/src/components/message/message.css +36 -3
- package/src/components/select/field-adapter.js +9 -4
- package/src/components/select/select.css +161 -10
- package/src/components/select/select.js +361 -48
- package/src/components/table/table.css +85 -0
- package/src/components/table/table.js +426 -31
- package/src/components/tag-picker/tag-picker.css +19 -0
- package/src/components/tag-picker/tag-picker.js +42 -5
- package/src/core/dom.js +21 -1
- package/src/core/util.js +20 -0
- package/src/index.js +9 -2
- package/src/{components/app-rail → internal}/app-rail.js +80 -30
- package/src/zeyos/icons.js +38 -20
- package/src/zeyos/index.js +8 -2
- package/src/zeyos/launcher.js +338 -0
- package/src/zeyos/modules.js +1 -0
- package/src/zeyos/select.js +116 -1
- package/styles/tokens/global.css +1 -1
- package/styles/tokens/modules.css +1 -0
- package/styles/tokens/semantic.css +9 -0
- package/styles/tokens/themes.css +2 -2
- package/styles/zx.css +3 -0
- package/MIGRATION.md +0 -749
- package/dist/types/compat/base.d.ts +0 -156
- package/dist/types/compat/globals.d.ts +0 -20
- package/dist/types/compat/index.d.ts +0 -128
- package/dist/types/compat/map/basic.d.ts +0 -175
- package/dist/types/compat/map/data.d.ts +0 -103
- package/dist/types/compat/map/date-time.d.ts +0 -80
- package/dist/types/compat/map/factory.d.ts +0 -9
- package/dist/types/compat/map/form.d.ts +0 -98
- package/dist/types/compat/map/helpers.d.ts +0 -12
- package/dist/types/compat/map/http.d.ts +0 -36
- package/dist/types/compat/map/layout.d.ts +0 -106
- package/dist/types/compat/map/options.d.ts +0 -80
- package/dist/types/compat/map/select.d.ts +0 -95
- package/dist/types/compat/map/stubs.d.ts +0 -32
- package/dist/types/compat/map/utilities.d.ts +0 -37
- package/dist/types/compat-entry.d.ts +0 -5
- package/dist/zx-compat.esm.js +0 -17848
- package/dist/zx-compat.esm.min.js +0 -1
- package/dist/zx-compat.global.js +0 -29008
- package/dist/zx-compat.global.min.js +0 -3
- package/src/compat/base.js +0 -291
- package/src/compat/globals.js +0 -167
- package/src/compat/index.js +0 -84
- package/src/compat/map/basic.js +0 -464
- package/src/compat/map/data.js +0 -254
- package/src/compat/map/date-time.js +0 -177
- package/src/compat/map/factory.js +0 -45
- package/src/compat/map/form.js +0 -330
- package/src/compat/map/helpers.js +0 -46
- package/src/compat/map/http.js +0 -117
- package/src/compat/map/layout.js +0 -256
- package/src/compat/map/options.js +0 -267
- package/src/compat/map/select.js +0 -198
- package/src/compat/map/stubs.js +0 -26
- package/src/compat/map/utilities.js +0 -113
- package/src/compat-entry.js +0 -11
|
@@ -2,32 +2,42 @@ import { Component } from '../../core/component.js';
|
|
|
2
2
|
import { h } from '../../core/dom.js';
|
|
3
3
|
import { icon } from '../../core/icons.js';
|
|
4
4
|
import { uid } from '../../core/util.js';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
AppRailPresenter as MinimizedPresenter, appNavIcon, normalizeAppItems
|
|
7
|
+
} from '../../internal/app-rail.js';
|
|
6
8
|
|
|
7
|
-
/** @typedef {import('
|
|
8
|
-
/** @typedef {import('
|
|
9
|
+
/** @typedef {import('../../internal/app-rail.js').AppNavItem} AppNavItem */
|
|
10
|
+
/** @typedef {import('../../internal/app-rail.js').AppNavSelectDetail} AppNavSelectDetail */
|
|
9
11
|
/**
|
|
10
12
|
* @typedef {Object} AppSidebarOptions
|
|
11
13
|
* @property {AppNavItem[]} [items=[]] Application navigation tree.
|
|
12
14
|
* @property {string|number|null} [active=null] Active item id.
|
|
13
15
|
* @property {Array<string|number>} [expanded=[]] Initially expanded branch ids.
|
|
14
|
-
* @property {boolean} [collapsed=false] Whether the minimized
|
|
15
|
-
* @property {
|
|
16
|
+
* @property {boolean} [collapsed=false] Whether the minimized presentation is shown.
|
|
17
|
+
* @property {boolean} [collapsible=true] Whether vertical navigation exposes a mode toggle.
|
|
18
|
+
* @property {'vertical'|'horizontal'} [orientation='vertical'] Navigation flow; horizontal is always minimized.
|
|
19
|
+
* @property {'left'|'right'|'top'|'bottom'} [side='left'] Workspace edge occupied by navigation.
|
|
16
20
|
* @property {string} [label='Applications'] Navigation accessible label.
|
|
17
21
|
* @property {Node|string|number|{toElement: () => Node|null}|null} [header=null] Expanded header content.
|
|
18
22
|
* @property {Node|string|number|{toElement: () => Node|null}|null} [footer=null] Expanded sticky footer content.
|
|
19
|
-
* @property {Node|string|number|{toElement: () => Node|null}|null} [railHeader=null] Minimized
|
|
20
|
-
* @property {Node|string|number|{toElement: () => Node|null}|null} [railFooter=null] Minimized
|
|
23
|
+
* @property {Node|string|number|{toElement: () => Node|null}|null} [railHeader=null] Minimized leading content.
|
|
24
|
+
* @property {Node|string|number|{toElement: () => Node|null}|null} [railFooter=null] Minimized trailing content.
|
|
25
|
+
* @property {number} [openDelay=80] Minimized flyout hover-open delay.
|
|
26
|
+
* @property {number} [closeDelay=160] Minimized flyout crossing grace.
|
|
27
|
+
* @property {(item: AppNavItem, context: Record<string, any>) => Node|null} [renderIcon] Optional host icon renderer.
|
|
21
28
|
* @property {(event: CustomEvent<AppNavSelectDetail>) => void} [onselect] Preventable action listener.
|
|
22
|
-
* @property {(event: CustomEvent<{item: AppNavItem|null, id: string|number|null, expanded: boolean|null, ids: Array<string|number>}>) => void} [onbranchchange] Branch-state listener
|
|
29
|
+
* @property {(event: CustomEvent<{item: AppNavItem|null, id: string|number|null, expanded: boolean|null, ids: Array<string|number>}>) => void} [onbranchchange] Branch-state listener.
|
|
23
30
|
* @property {(event: CustomEvent<{collapsed: boolean}>) => void} [oncollapsechange] Presentation-state listener.
|
|
31
|
+
* @property {(event: CustomEvent<{item: AppNavItem, id: string|number, open: boolean}>) => void} [onflyoutchange] Minimized flyout-state listener.
|
|
24
32
|
*/
|
|
25
33
|
|
|
26
34
|
/**
|
|
27
|
-
*
|
|
35
|
+
* One application navigation component. Vertical expanded descendants render inline; minimized
|
|
36
|
+
* vertical and horizontal descendants use pointer-, focus-, and keyboard-accessible flyouts.
|
|
28
37
|
* @fires AppSidebar#select
|
|
29
38
|
* @fires AppSidebar#branchchange
|
|
30
39
|
* @fires AppSidebar#collapsechange
|
|
40
|
+
* @fires AppSidebar#flyoutchange
|
|
31
41
|
* @extends {Component<AppSidebarOptions>}
|
|
32
42
|
*/
|
|
33
43
|
export class AppSidebar extends Component {
|
|
@@ -39,48 +49,29 @@ export class AppSidebar extends Component {
|
|
|
39
49
|
active: null,
|
|
40
50
|
expanded: [],
|
|
41
51
|
collapsed: false,
|
|
52
|
+
collapsible: true,
|
|
53
|
+
orientation: 'vertical',
|
|
42
54
|
side: 'left',
|
|
43
55
|
label: 'Applications',
|
|
44
56
|
header: null,
|
|
45
57
|
footer: null,
|
|
46
58
|
railHeader: null,
|
|
47
|
-
railFooter: null
|
|
59
|
+
railFooter: null,
|
|
60
|
+
openDelay: 80,
|
|
61
|
+
closeDelay: 160,
|
|
62
|
+
renderIcon: null
|
|
48
63
|
};
|
|
49
64
|
|
|
50
65
|
/**
|
|
51
|
-
* Creates or enhances an application-
|
|
52
|
-
* @param {Element|string|null} target
|
|
53
|
-
* @param {AppSidebarOptions} [options={}]
|
|
66
|
+
* Creates or enhances an application-navigation host.
|
|
67
|
+
* @param {Element|string|null} [target=null] Navigation target.
|
|
68
|
+
* @param {AppSidebarOptions} [options={}] Navigation options.
|
|
54
69
|
*/
|
|
55
70
|
constructor(target = null, options = {}) {
|
|
56
71
|
super(target, options);
|
|
57
|
-
this.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
type: 'button',
|
|
61
|
-
ariaLabel: 'Expand application sidebar'
|
|
62
|
-
}, icon(this.el.dataset.side === 'right' ? 'chevron-left' : 'chevron-right'));
|
|
63
|
-
this._railExpandButton = expandButton;
|
|
64
|
-
const railHeader = h('div', { class: 'zx-app-sidebar__rail-header' }, expandButton);
|
|
65
|
-
appendSlot(railHeader, this.options.railHeader);
|
|
66
|
-
this._rail = new AppRail(this.refs.rail, {
|
|
67
|
-
items: this._items,
|
|
68
|
-
active: this._active,
|
|
69
|
-
orientation: 'vertical',
|
|
70
|
-
side: this.el.dataset.side,
|
|
71
|
-
label: this.options.label,
|
|
72
|
-
header: railHeader,
|
|
73
|
-
footer: this.options.railFooter,
|
|
74
|
-
onselect: (event) => {
|
|
75
|
-
const selected = this.emit('select', event.detail);
|
|
76
|
-
if (selected.defaultPrevented) event.preventDefault();
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
this.listen(expandButton, 'click', () => {
|
|
80
|
-
this.expand();
|
|
81
|
-
});
|
|
82
|
-
this._renderTree();
|
|
83
|
-
this.setCollapsed(Boolean(this.options.collapsed), { silent: true });
|
|
72
|
+
this._rail = null;
|
|
73
|
+
this._railExpandButton = null;
|
|
74
|
+
this._renderMode();
|
|
84
75
|
}
|
|
85
76
|
|
|
86
77
|
/** @returns {HTMLElement} */
|
|
@@ -94,49 +85,38 @@ export class AppSidebar extends Component {
|
|
|
94
85
|
};
|
|
95
86
|
this._items = normalizeAppItems(this.options.items);
|
|
96
87
|
this._active = this.options.active;
|
|
97
|
-
this._expanded = new Set(Array.isArray(this.options.expanded)
|
|
88
|
+
this._expanded = new Set(Array.isArray(this.options.expanded)
|
|
89
|
+
? this.options.expanded.map(String) : []);
|
|
98
90
|
this._collapsed = Boolean(this.options.collapsed);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const collapseButton = h('button', {
|
|
104
|
-
ref: 'collapse',
|
|
105
|
-
class: 'zx-app-sidebar__collapse zx-icon-btn',
|
|
106
|
-
type: 'button',
|
|
107
|
-
ariaLabel: 'Minimize application sidebar'
|
|
108
|
-
}, icon(root.dataset.side === 'right' ? 'chevron-right' : 'chevron-left'));
|
|
109
|
-
const footer = h('footer', { class: 'zx-app-sidebar__footer' });
|
|
110
|
-
appendSlot(footer, this.options.footer);
|
|
111
|
-
footer.hidden = !footer.hasChildNodes();
|
|
112
|
-
const expanded = h('div', { ref: 'expanded', class: 'zx-app-sidebar__expanded' },
|
|
113
|
-
h('header', { class: 'zx-app-sidebar__header' }, headerContent, collapseButton),
|
|
114
|
-
h('nav', { class: 'zx-app-sidebar__nav', ariaLabel: this.options.label },
|
|
115
|
-
h('ul', { ref: 'list', class: 'zx-app-sidebar__list' })),
|
|
116
|
-
footer);
|
|
117
|
-
const rail = h('div', { ref: 'rail', class: 'zx-app-sidebar__rail' });
|
|
118
|
-
root.replaceChildren(expanded, rail);
|
|
119
|
-
this.listen(collapseButton, 'click', () => {
|
|
120
|
-
this.collapse();
|
|
121
|
-
});
|
|
122
|
-
this.listen(this.refs.list, 'click', (event) => this._onTreeClick(event));
|
|
91
|
+
this._orientation = this.options.orientation === 'horizontal' ? 'horizontal' : 'vertical';
|
|
92
|
+
this._side = normalizeSide(this._orientation, this.options.side);
|
|
93
|
+
this._modeAbort = null;
|
|
94
|
+
root.replaceChildren();
|
|
123
95
|
return root;
|
|
124
96
|
}
|
|
125
97
|
|
|
126
|
-
/** Replaces the navigation tree in
|
|
98
|
+
/** Replaces the navigation tree in the active presentation. @param {AppNavItem[]} items @returns {this} */
|
|
127
99
|
setItems(items) {
|
|
128
100
|
this._items = normalizeAppItems(items);
|
|
129
101
|
this._reconcileExpanded();
|
|
130
|
-
this.
|
|
131
|
-
this.
|
|
102
|
+
if (this._rail) this._rail.setItems(this._items);
|
|
103
|
+
else this._renderTree();
|
|
132
104
|
return this;
|
|
133
105
|
}
|
|
134
106
|
|
|
135
|
-
/**
|
|
107
|
+
/** Returns a shallow copy of normalized items. @returns {AppNavItem[]} */
|
|
108
|
+
getItems() {
|
|
109
|
+
return this._items.map((item) => ({
|
|
110
|
+
...item,
|
|
111
|
+
children: item.children?.map((child) => ({ ...child }))
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Sets the active id without routing. @param {string|number|null} id @returns {this} */
|
|
136
116
|
setActive(id) {
|
|
137
117
|
this._active = id;
|
|
138
|
-
this.
|
|
139
|
-
this.
|
|
118
|
+
if (this._rail) this._rail.setActive(id);
|
|
119
|
+
else this._syncActive();
|
|
140
120
|
return this;
|
|
141
121
|
}
|
|
142
122
|
|
|
@@ -144,7 +124,7 @@ export class AppSidebar extends Component {
|
|
|
144
124
|
setExpanded(ids, { silent = false } = {}) {
|
|
145
125
|
this._expanded = new Set(Array.isArray(ids) ? ids.map(String) : []);
|
|
146
126
|
this._reconcileExpanded();
|
|
147
|
-
this._renderTree();
|
|
127
|
+
if (!this._rail) this._renderTree();
|
|
148
128
|
if (!silent) this.emit('branchchange', {
|
|
149
129
|
item: null,
|
|
150
130
|
id: null,
|
|
@@ -171,53 +151,122 @@ export class AppSidebar extends Component {
|
|
|
171
151
|
const expanded = !this._expanded.has(key);
|
|
172
152
|
if (expanded) this._expanded.add(key);
|
|
173
153
|
else this._expanded.delete(key);
|
|
174
|
-
this._renderTree();
|
|
175
|
-
if (!silent) this.emit('branchchange', {
|
|
154
|
+
if (!this._rail) this._renderTree();
|
|
155
|
+
if (!silent) this.emit('branchchange', {
|
|
156
|
+
item,
|
|
157
|
+
id: item.id,
|
|
158
|
+
expanded,
|
|
159
|
+
ids: this.getExpanded()
|
|
160
|
+
});
|
|
176
161
|
return this;
|
|
177
162
|
}
|
|
178
163
|
|
|
179
|
-
/** Shows the expanded
|
|
164
|
+
/** Shows the expanded vertical presentation. @param {{silent?: boolean}} [options={}] @returns {this} */
|
|
180
165
|
expand(options = {}) {
|
|
181
166
|
return this.setCollapsed(false, options);
|
|
182
167
|
}
|
|
183
168
|
|
|
184
|
-
/** Shows the minimized
|
|
169
|
+
/** Shows the minimized presentation. @param {{silent?: boolean}} [options={}] @returns {this} */
|
|
185
170
|
collapse(options = {}) {
|
|
186
171
|
return this.setCollapsed(true, options);
|
|
187
172
|
}
|
|
188
173
|
|
|
189
|
-
/** Sets expanded/minimized
|
|
174
|
+
/** Sets the saved vertical expanded/minimized state. @param {boolean} collapsed @param {{silent?: boolean}} [options={}] @returns {this} */
|
|
190
175
|
setCollapsed(collapsed, { silent = false } = {}) {
|
|
191
|
-
const
|
|
192
|
-
const changed = next !== this._collapsed;
|
|
176
|
+
const previous = this.isCollapsed();
|
|
193
177
|
const active = document.activeElement;
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
178
|
+
const outgoingOwnedFocus = Boolean(active
|
|
179
|
+
&& (this.el.contains(active) || this._rail?._containsNode(active)));
|
|
180
|
+
this._collapsed = Boolean(collapsed);
|
|
181
|
+
const next = this.isCollapsed();
|
|
182
|
+
if (previous !== next) {
|
|
183
|
+
this._renderMode();
|
|
184
|
+
if (outgoingOwnedFocus) {
|
|
185
|
+
if (next) this._railExpandButton?.focus();
|
|
186
|
+
else this.refs.collapse?.focus();
|
|
187
|
+
}
|
|
188
|
+
if (!silent) this.emit('collapsechange', { collapsed: next });
|
|
189
|
+
}
|
|
204
190
|
return this;
|
|
205
191
|
}
|
|
206
192
|
|
|
207
|
-
/** Reports
|
|
193
|
+
/** Reports the effective presentation state; horizontal navigation is always minimized. @returns {boolean} */
|
|
208
194
|
isCollapsed() {
|
|
209
|
-
return this._collapsed;
|
|
195
|
+
return this._orientation === 'horizontal' || this._collapsed;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Changes navigation flow/edge without replacing state.
|
|
200
|
+
* @param {{orientation?: 'vertical'|'horizontal', side?: 'left'|'right'|'top'|'bottom'}} layout
|
|
201
|
+
* @param {{silent?: boolean}} [options]
|
|
202
|
+
* @returns {this}
|
|
203
|
+
*/
|
|
204
|
+
setLayout(layout = {}, { silent = false } = {}) {
|
|
205
|
+
const orientation = layout.orientation === 'horizontal' ? 'horizontal'
|
|
206
|
+
: layout.orientation === 'vertical' ? 'vertical' : this._orientation;
|
|
207
|
+
const side = normalizeSide(orientation, layout.side ?? this._side);
|
|
208
|
+
if (orientation === this._orientation && side === this._side) return this;
|
|
209
|
+
const previous = this.isCollapsed();
|
|
210
|
+
const active = document.activeElement;
|
|
211
|
+
const outgoingOwnedFocus = Boolean(active
|
|
212
|
+
&& (this.el.contains(active) || this._rail?._containsNode(active)));
|
|
213
|
+
const focusedId = outgoingOwnedFocus && active instanceof Element
|
|
214
|
+
? active.closest('[data-app-nav-id]')?.getAttribute('data-app-nav-id') ?? null : null;
|
|
215
|
+
this._orientation = orientation;
|
|
216
|
+
this._side = side;
|
|
217
|
+
this._renderMode();
|
|
218
|
+
const next = this.isCollapsed();
|
|
219
|
+
if (outgoingOwnedFocus) {
|
|
220
|
+
const replacement = focusedId === null ? null
|
|
221
|
+
: [...this.el.querySelectorAll('[data-app-nav-id]')]
|
|
222
|
+
.find((control) => control.getAttribute('data-app-nav-id') === focusedId) ?? null;
|
|
223
|
+
if (replacement instanceof HTMLElement) replacement.focus();
|
|
224
|
+
else if (next) this.focus();
|
|
225
|
+
else if (this.refs.collapse instanceof HTMLElement && !this.refs.collapse.hidden) {
|
|
226
|
+
this.refs.collapse.focus();
|
|
227
|
+
} else visibleTreeControls(this.refs.list)[0]?.focus();
|
|
228
|
+
}
|
|
229
|
+
if (previous !== next && !silent) this.emit('collapsechange', { collapsed: next });
|
|
230
|
+
return this;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Opens a minimized descendant flyout. @param {string|number} id @param {{focus?: boolean}} [options={}] @returns {this} */
|
|
234
|
+
openFlyout(id, options = {}) {
|
|
235
|
+
this._rail?.openFlyout(id, options);
|
|
236
|
+
return this;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Closes a minimized descendant flyout. @param {string|number} id @returns {this} */
|
|
240
|
+
closeFlyout(id) {
|
|
241
|
+
this._rail?.closeFlyout(id);
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Closes every minimized flyout. @returns {this} */
|
|
246
|
+
closeAllFlyouts() {
|
|
247
|
+
this._rail?.closeAllFlyouts();
|
|
248
|
+
return this;
|
|
210
249
|
}
|
|
211
250
|
|
|
212
|
-
/**
|
|
213
|
-
|
|
214
|
-
return this._rail;
|
|
251
|
+
/** Reports whether a minimized flyout is open. @param {string|number} id @returns {boolean} */
|
|
252
|
+
isFlyoutOpen(id) {
|
|
253
|
+
return this._rail?.isFlyoutOpen(id) ?? false;
|
|
215
254
|
}
|
|
216
255
|
|
|
217
|
-
/**
|
|
256
|
+
/** Focuses the first navigation action. @returns {this} */
|
|
257
|
+
focus() {
|
|
258
|
+
if (this._rail) this._rail.focus();
|
|
259
|
+
else visibleTreeControls(this.refs.list)[0]?.focus();
|
|
260
|
+
return this;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Destroys flyouts and restores/removes the host. @returns {void} */
|
|
218
264
|
destroy() {
|
|
219
265
|
const original = this._original;
|
|
220
|
-
this.
|
|
266
|
+
this._modeAbort?.abort();
|
|
267
|
+
this._modeAbort = null;
|
|
268
|
+
this._rail?.destroy();
|
|
269
|
+
this._rail = null;
|
|
221
270
|
super.destroy();
|
|
222
271
|
if (!this._createdRoot && original) {
|
|
223
272
|
for (const attribute of Array.from(this.el.attributes)) this.el.removeAttribute(attribute.name);
|
|
@@ -226,10 +275,100 @@ export class AppSidebar extends Component {
|
|
|
226
275
|
}
|
|
227
276
|
}
|
|
228
277
|
|
|
278
|
+
/** Mounts exactly one expanded tree or minimized flyout presentation. @returns {void} */
|
|
279
|
+
_renderMode() {
|
|
280
|
+
this._modeAbort?.abort();
|
|
281
|
+
this._modeAbort = new AbortController();
|
|
282
|
+
this._rail?.destroy();
|
|
283
|
+
this._rail = null;
|
|
284
|
+
this._railExpandButton = null;
|
|
285
|
+
for (const key of ['expanded', 'list', 'collapse', 'rail']) delete this.refs[key];
|
|
286
|
+
this.el.replaceChildren();
|
|
287
|
+
this.el.dataset.orientation = this._orientation;
|
|
288
|
+
this.el.dataset.side = this._side;
|
|
289
|
+
this.el.dataset.collapsed = String(this.isCollapsed());
|
|
290
|
+
if (this.isCollapsed()) this._renderRail();
|
|
291
|
+
else this._renderExpanded();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** @returns {void} */
|
|
295
|
+
_renderExpanded() {
|
|
296
|
+
const headerContent = h('div', { class: 'zx-app-sidebar__header-content' });
|
|
297
|
+
appendSlot(headerContent, this.options.header);
|
|
298
|
+
const collapseButton = h('button', {
|
|
299
|
+
class: 'zx-app-sidebar__collapse zx-icon-btn',
|
|
300
|
+
type: 'button',
|
|
301
|
+
ariaLabel: 'Minimize application sidebar'
|
|
302
|
+
}, icon(this._side === 'right' ? 'chevron-right' : 'chevron-left'));
|
|
303
|
+
collapseButton.hidden = !this.options.collapsible;
|
|
304
|
+
const footer = h('footer', { class: 'zx-app-sidebar__footer' });
|
|
305
|
+
appendSlot(footer, this.options.footer);
|
|
306
|
+
footer.hidden = !footer.hasChildNodes();
|
|
307
|
+
const list = h('ul', { class: 'zx-app-sidebar__list' });
|
|
308
|
+
const expanded = h('div', { class: 'zx-app-sidebar__expanded' },
|
|
309
|
+
h('header', { class: 'zx-app-sidebar__header' }, headerContent, collapseButton),
|
|
310
|
+
h('nav', { class: 'zx-app-sidebar__nav', ariaLabel: this.options.label }, list),
|
|
311
|
+
footer);
|
|
312
|
+
this.refs.expanded = expanded;
|
|
313
|
+
this.refs.list = list;
|
|
314
|
+
this.refs.collapse = collapseButton;
|
|
315
|
+
this.el.append(expanded);
|
|
316
|
+
this._listenMode(collapseButton, 'click', () => this.collapse());
|
|
317
|
+
this._listenMode(list, 'click', (event) => this._onTreeClick(event));
|
|
318
|
+
this._listenMode(list, 'keydown', (event) => this._onTreeKeydown(/** @type {KeyboardEvent} */ (event)));
|
|
319
|
+
this._renderTree();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** @returns {void} */
|
|
323
|
+
_renderRail() {
|
|
324
|
+
const railHost = h('nav', { class: 'zx-app-sidebar__rail' });
|
|
325
|
+
this.el.append(railHost);
|
|
326
|
+
this.refs.rail = railHost;
|
|
327
|
+
const showToggle = this._orientation === 'vertical' && Boolean(this.options.collapsible);
|
|
328
|
+
const railHeader = h('div', { class: 'zx-app-sidebar__rail-header' });
|
|
329
|
+
if (showToggle) {
|
|
330
|
+
const expandButton = h('button', {
|
|
331
|
+
class: 'zx-app-sidebar__rail-toggle zx-icon-btn',
|
|
332
|
+
type: 'button',
|
|
333
|
+
ariaLabel: 'Expand application sidebar'
|
|
334
|
+
}, icon(this._side === 'right' ? 'chevron-left' : 'chevron-right'));
|
|
335
|
+
railHeader.append(expandButton);
|
|
336
|
+
this._railExpandButton = expandButton;
|
|
337
|
+
this._listenMode(expandButton, 'click', () => this.expand());
|
|
338
|
+
}
|
|
339
|
+
appendSlot(railHeader, this.options.railHeader);
|
|
340
|
+
this._rail = new MinimizedPresenter(railHost, {
|
|
341
|
+
items: this._items,
|
|
342
|
+
active: this._active,
|
|
343
|
+
orientation: this._orientation,
|
|
344
|
+
side: this._side,
|
|
345
|
+
label: this.options.label,
|
|
346
|
+
header: railHeader.hasChildNodes() ? railHeader : null,
|
|
347
|
+
footer: this.options.railFooter,
|
|
348
|
+
openDelay: this.options.openDelay,
|
|
349
|
+
closeDelay: this.options.closeDelay,
|
|
350
|
+
renderIcon: this.options.renderIcon,
|
|
351
|
+
onselect: (event) => {
|
|
352
|
+
const selected = this.emit('select', event.detail);
|
|
353
|
+
if (selected.defaultPrevented) event.preventDefault();
|
|
354
|
+
},
|
|
355
|
+
onflyoutchange: (event) => this.emit('flyoutchange', event.detail)
|
|
356
|
+
});
|
|
357
|
+
this._listenMode(railHost, 'zx-select', (event) => event.stopPropagation());
|
|
358
|
+
this._listenMode(railHost, 'zx-flyoutchange', (event) => event.stopPropagation());
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** @param {EventTarget} target @param {string} type @param {EventListener} listener @returns {void} */
|
|
362
|
+
_listenMode(target, type, listener) {
|
|
363
|
+
target.addEventListener(type, listener, { signal: this._modeAbort.signal });
|
|
364
|
+
}
|
|
365
|
+
|
|
229
366
|
/** @returns {void} */
|
|
230
367
|
_renderTree() {
|
|
368
|
+
if (!this.refs.list) return;
|
|
231
369
|
const focused = this.refs.list.contains(document.activeElement)
|
|
232
|
-
? /** @type {Element} */ (document.activeElement).closest('[data-app-nav-id]')
|
|
370
|
+
? /** @type {Element} */ (document.activeElement).closest('[data-app-nav-id]')
|
|
371
|
+
?.getAttribute('data-app-nav-id')
|
|
233
372
|
: null;
|
|
234
373
|
this.refs.list.replaceChildren(...this._items.map((item) => this._treeRow(item, 1)));
|
|
235
374
|
this._syncActive();
|
|
@@ -247,7 +386,14 @@ export class AppSidebar extends Component {
|
|
|
247
386
|
const key = String(item.id);
|
|
248
387
|
const expanded = hasChildren && this._expanded.has(key);
|
|
249
388
|
const childId = hasChildren ? uid('zx-app-sidebar-branch') : null;
|
|
250
|
-
const
|
|
389
|
+
const visual = appNavIcon(item, this.options.renderIcon, {
|
|
390
|
+
orientation: this._orientation,
|
|
391
|
+
collapsed: false,
|
|
392
|
+
side: this._side,
|
|
393
|
+
level,
|
|
394
|
+
location: level === 1 ? 'root' : 'inline'
|
|
395
|
+
});
|
|
396
|
+
const control = navControl(item, hasChildren, expanded, childId, visual);
|
|
251
397
|
const row = h('li', {
|
|
252
398
|
class: 'zx-app-sidebar__row',
|
|
253
399
|
dataset: { level }
|
|
@@ -262,7 +408,7 @@ export class AppSidebar extends Component {
|
|
|
262
408
|
return row;
|
|
263
409
|
}
|
|
264
410
|
|
|
265
|
-
/**
|
|
411
|
+
/** @param {Event} event @returns {void} */
|
|
266
412
|
_onTreeClick(event) {
|
|
267
413
|
const control = /** @type {Element|null} */ (event.target)?.closest?.('[data-app-nav-id]');
|
|
268
414
|
if (!control || !this.refs.list.contains(control)) return;
|
|
@@ -275,6 +421,43 @@ export class AppSidebar extends Component {
|
|
|
275
421
|
else this._select(item, event);
|
|
276
422
|
}
|
|
277
423
|
|
|
424
|
+
/** @param {KeyboardEvent} event @returns {void} */
|
|
425
|
+
_onTreeKeydown(event) {
|
|
426
|
+
const current = /** @type {Element|null} */ (event.target)?.closest?.('[data-app-nav-id]');
|
|
427
|
+
if (!(current instanceof HTMLElement)) return;
|
|
428
|
+
const item = findItem(this._items, current.getAttribute('data-app-nav-id'));
|
|
429
|
+
const controls = visibleTreeControls(this.refs.list);
|
|
430
|
+
const index = controls.indexOf(current);
|
|
431
|
+
if (event.key === 'ArrowRight' && item?.children?.length) {
|
|
432
|
+
event.preventDefault();
|
|
433
|
+
if (!this._expanded.has(String(item.id))) this.toggleBranch(item.id);
|
|
434
|
+
else visibleTreeControls(current.parentElement?.querySelector('.zx-app-sidebar__sublist'))[0]?.focus();
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (event.key === 'ArrowLeft') {
|
|
438
|
+
if (item?.children?.length && this._expanded.has(String(item.id))) {
|
|
439
|
+
event.preventDefault();
|
|
440
|
+
this.toggleBranch(item.id);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const parentList = current.closest('.zx-app-sidebar__sublist');
|
|
444
|
+
const parentControl = parentList?.parentElement?.querySelector(':scope > [data-app-nav-id]');
|
|
445
|
+
if (parentControl instanceof HTMLElement) {
|
|
446
|
+
event.preventDefault();
|
|
447
|
+
parentControl.focus();
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
let nextIndex;
|
|
452
|
+
if (event.key === 'Home') nextIndex = 0;
|
|
453
|
+
else if (event.key === 'End') nextIndex = controls.length - 1;
|
|
454
|
+
else if (event.key === 'ArrowDown') nextIndex = Math.min(controls.length - 1, index + 1);
|
|
455
|
+
else if (event.key === 'ArrowUp') nextIndex = Math.max(0, index - 1);
|
|
456
|
+
else return;
|
|
457
|
+
event.preventDefault();
|
|
458
|
+
controls[nextIndex]?.focus();
|
|
459
|
+
}
|
|
460
|
+
|
|
278
461
|
/** @param {AppNavItem} item @param {Event} event @returns {void} */
|
|
279
462
|
_select(item, event) {
|
|
280
463
|
if (item.disabled) {
|
|
@@ -297,15 +480,15 @@ export class AppSidebar extends Component {
|
|
|
297
480
|
|
|
298
481
|
/** @returns {void} */
|
|
299
482
|
_syncActive() {
|
|
483
|
+
if (!this.refs.list) return;
|
|
300
484
|
const activeId = this._active == null ? null : String(this._active);
|
|
301
485
|
for (const control of this.refs.list.querySelectorAll('[data-app-nav-id]')) {
|
|
302
486
|
const item = findItem(this._items, control.getAttribute('data-app-nav-id'));
|
|
303
487
|
const direct = activeId !== null && control.getAttribute('data-app-nav-id') === activeId;
|
|
304
488
|
const descendant = activeId !== null && item
|
|
305
489
|
? containsItemId(item.children ?? [], activeId) : false;
|
|
306
|
-
if (direct)
|
|
307
|
-
|
|
308
|
-
} else control.removeAttribute('aria-current');
|
|
490
|
+
if (direct) control.setAttribute('aria-current', 'page');
|
|
491
|
+
else control.removeAttribute('aria-current');
|
|
309
492
|
if (descendant) control.dataset.activeDescendant = 'true';
|
|
310
493
|
else delete control.dataset.activeDescendant;
|
|
311
494
|
}
|
|
@@ -314,13 +497,15 @@ export class AppSidebar extends Component {
|
|
|
314
497
|
/** @returns {void} */
|
|
315
498
|
_reconcileExpanded() {
|
|
316
499
|
const branchIds = new Set();
|
|
317
|
-
walkItems(this._items, (item) => {
|
|
500
|
+
walkItems(this._items, (item) => {
|
|
501
|
+
if (item.children?.length) branchIds.add(String(item.id));
|
|
502
|
+
});
|
|
318
503
|
for (const id of this._expanded) if (!branchIds.has(id)) this._expanded.delete(id);
|
|
319
504
|
}
|
|
320
505
|
}
|
|
321
506
|
|
|
322
|
-
/** @param {AppNavItem} item @param {boolean} disclosure @param {boolean} expanded @param {string|null} controls @returns {HTMLElement} */
|
|
323
|
-
function navControl(item, disclosure, expanded, controls) {
|
|
507
|
+
/** @param {AppNavItem} item @param {boolean} disclosure @param {boolean} expanded @param {string|null} controls @param {Node|null} visual @returns {HTMLElement} */
|
|
508
|
+
function navControl(item, disclosure, expanded, controls, visual) {
|
|
324
509
|
const tag = item.href && !disclosure ? 'a' : 'button';
|
|
325
510
|
return /** @type {HTMLElement} */ (h(tag, {
|
|
326
511
|
class: 'zx-app-sidebar__item',
|
|
@@ -332,14 +517,21 @@ function navControl(item, disclosure, expanded, controls) {
|
|
|
332
517
|
ariaControls: controls,
|
|
333
518
|
dataset: { appNavId: String(item.id) }
|
|
334
519
|
},
|
|
335
|
-
|
|
520
|
+
visual ? h('span', { class: 'zx-app-sidebar__item-icon', ariaHidden: 'true' }, visual) : null,
|
|
336
521
|
h('span', { class: 'zx-app-sidebar__item-label' }, item.label),
|
|
337
522
|
item.badge != null ? h('span', { class: 'zx-app-sidebar__item-badge' }, String(item.badge)) : null,
|
|
338
523
|
disclosure ? h('span', { class: 'zx-app-sidebar__item-disclosure', ariaHidden: 'true' },
|
|
339
524
|
icon(expanded ? 'chevron-down' : 'chevron-right')) : null));
|
|
340
525
|
}
|
|
341
526
|
|
|
342
|
-
/**
|
|
527
|
+
/** @param {Element|null|undefined} root @returns {HTMLElement[]} */
|
|
528
|
+
function visibleTreeControls(root) {
|
|
529
|
+
if (!root) return [];
|
|
530
|
+
return /** @type {HTMLElement[]} */ ([...root.querySelectorAll('[data-app-nav-id]')]
|
|
531
|
+
.filter((control) => !control.closest('.zx-app-sidebar__sublist[hidden]')));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Returns a control or its nearest owning disclosure when inside a collapsed branch. @param {Element|null} control @returns {HTMLElement|null} */
|
|
343
535
|
function visibleTreeControl(control) {
|
|
344
536
|
let candidate = /** @type {HTMLElement|null} */ (control);
|
|
345
537
|
while (candidate) {
|
|
@@ -350,7 +542,7 @@ function visibleTreeControl(control) {
|
|
|
350
542
|
return null;
|
|
351
543
|
}
|
|
352
544
|
|
|
353
|
-
/** @param {AppNavItem[]} items @param {string|number} id @returns {AppNavItem|null} */
|
|
545
|
+
/** @param {AppNavItem[]} items @param {string|number|null} id @returns {AppNavItem|null} */
|
|
354
546
|
function findItem(items, id) {
|
|
355
547
|
for (const item of items) {
|
|
356
548
|
if (String(item.id) === String(id)) return item;
|
|
@@ -376,13 +568,21 @@ function containsItemId(items, id) {
|
|
|
376
568
|
/** @param {Element} target @param {unknown} content @returns {void} */
|
|
377
569
|
function appendSlot(target, content) {
|
|
378
570
|
if (content == null) return;
|
|
379
|
-
if (typeof content === 'string' || typeof content === 'number')
|
|
380
|
-
|
|
571
|
+
if (typeof content === 'string' || typeof content === 'number') {
|
|
572
|
+
target.append(document.createTextNode(String(content)));
|
|
573
|
+
} else if (typeof content === 'object' && typeof content.toElement === 'function') {
|
|
381
574
|
const element = content.toElement();
|
|
382
575
|
if (element) target.append(element);
|
|
383
576
|
} else if (typeof content === 'object' && typeof content.nodeType === 'number') target.append(content);
|
|
384
577
|
}
|
|
385
578
|
|
|
579
|
+
/** @param {'vertical'|'horizontal'} orientation @param {unknown} side @returns {'left'|'right'|'top'|'bottom'} */
|
|
580
|
+
function normalizeSide(orientation, side) {
|
|
581
|
+
if (orientation === 'horizontal') return side === 'bottom' ? 'bottom' : 'top';
|
|
582
|
+
return side === 'right' ? 'right' : 'left';
|
|
583
|
+
}
|
|
584
|
+
|
|
386
585
|
/** Fired when a destination/action is activated. @event AppSidebar#select @type {CustomEvent<AppNavSelectDetail>} */
|
|
387
586
|
/** Fired when an inline branch changes. @event AppSidebar#branchchange @type {CustomEvent<{item: AppNavItem|null, id: string|number|null, expanded: boolean|null, ids: Array<string|number>}>} */
|
|
388
587
|
/** Fired when expanded/minimized presentation changes. @event AppSidebar#collapsechange @type {CustomEvent<{collapsed: boolean}>} */
|
|
588
|
+
/** Fired when a minimized flyout changes. @event AppSidebar#flyoutchange @type {CustomEvent<{item: AppNavItem, id: string|number, open: boolean}>} */
|