@lion/ui 0.17.0-menu-system.0 → 0.18.0-menu-system.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/components/menu/README.md +1 -1
- package/components/menu/src/InteractiveListMixin.js +14 -5
- package/components/menu/src/MoreButtonMenuMixin.js +296 -0
- package/components/menu/src/MultiLevelListMixin.js +0 -1
- package/components/navigation-bar/src/LionNavigationBar.js +97 -107
- package/components/overlays/src/OverlayController.js +87 -38
- package/components/overlays/src/OverlayMixin.js +1 -0
- package/components/overlays/src/utils/adopt-styles.js +1 -1
- package/components/overlays/src/utils/is-equal-config.js +9 -8
- package/components/overlays/test/OverlayController.test.js +3 -1
- package/components/overlays/test/utils-tests/is-equal-config.test.js +1 -1
- package/components/overlays/test-suites/OverlayMixin.suite.js +8 -8
- package/components/overlays/types/OverlayConfig.ts +6 -0
- package/custom-elements.json +751 -83
- package/dist-types/components/dialog/src/LionDialog.d.ts +1 -0
- package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
- package/dist-types/components/menu/src/InteractiveListMixin.d.ts.map +1 -1
- package/dist-types/components/menu/src/LionMenuOverlay.d.ts +1 -0
- package/dist-types/components/menu/src/LionMenuOverlay.d.ts.map +1 -1
- package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts +31 -0
- package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts.map +1 -0
- package/dist-types/components/menu/src/MultiLevelListMixin.d.ts.map +1 -1
- package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts +47 -35
- package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts.map +1 -1
- package/dist-types/components/overlays/src/OverlayController.d.ts +3 -7
- package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
- package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
- package/dist-types/components/overlays/src/utils/is-equal-config.d.ts.map +1 -1
- package/dist-types/components/overlays/types/OverlayConfig.d.ts +5 -0
- package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
- package/dist-types/components/select-rich/src/LionSelectRich.d.ts +1 -0
- package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
- package/dist-types/exports/overlays.d.ts +1 -0
- package/dist-types/exports/overlays.d.ts.map +1 -1
- package/dist-types/tsconfig.tsbuildinfo +1 -1
- package/docs/components/checkbox-group/overview.md +2 -2
- package/docs/components/collapsible/overview.md +1 -1
- package/docs/components/combobox/overview.md +1 -1
- package/docs/components/combobox/use-cases.md +1 -1
- package/docs/components/dialog/overview.md +1 -1
- package/docs/components/fieldset/overview.md +5 -5
- package/docs/components/form/overview.md +3 -3
- package/docs/components/form/use-cases.md +2 -2
- package/docs/components/icon/use-cases.md +1 -1
- package/docs/components/input/overview.md +1 -1
- package/docs/components/input-amount/overview.md +3 -3
- package/docs/components/input-date/overview.md +3 -3
- package/docs/components/input-datepicker/overview.md +5 -5
- package/docs/components/input-email/overview.md +2 -2
- package/docs/components/input-file/overview.md +1 -1
- package/docs/components/input-iban/overview.md +2 -2
- package/docs/components/input-range/overview.md +2 -2
- package/docs/components/input-stepper/overview.md +1 -1
- package/docs/components/input-tel/overview.md +1 -1
- package/docs/components/input-tel/use-cases.md +6 -6
- package/docs/components/input-tel-dropdown/overview.md +1 -1
- package/docs/components/listbox/use-cases.md +1 -1
- package/docs/components/menu/extensions.md +19 -0
- package/docs/components/radio-group/overview.md +3 -3
- package/docs/components/select/use-cases.md +1 -1
- package/docs/components/select-rich/use-cases.md +2 -2
- package/docs/components/tabs/examples.md +1 -1
- package/docs/fundamentals/index.md +4 -4
- package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
- package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
- package/docs/fundamentals/rationales/accessibility.md +1 -1
- package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
- package/docs/fundamentals/systems/core/overview.md +1 -1
- package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
- package/docs/fundamentals/systems/form/model-value.md +1 -1
- package/docs/fundamentals/systems/form/overview.md +28 -28
- package/docs/fundamentals/systems/form/validate.md +2 -2
- package/docs/fundamentals/systems/localize/overview.md +4 -4
- package/docs/fundamentals/systems/overlays/api-table.md +34 -34
- package/docs/fundamentals/systems/overlays/configuration.md +2 -2
- package/docs/fundamentals/systems/overlays/overview.md +5 -5
- package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
- package/docs/fundamentals/tools/helpers/overview.md +1 -1
- package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
- package/docs/guides/how-to/create-a-custom-field.md +5 -5
- package/docs/guides/how-to/create-a-form.md +4 -4
- package/docs/guides/how-to/extend-a-native-input.md +2 -2
- package/docs/guides/how-to/index.md +1 -1
- package/docs/guides/index.md +8 -8
- package/docs/guides/principles/index.md +5 -5
- package/docs/guides/principles/styling.md +1 -1
- package/exports/overlays.js +4 -0
- package/overlays.js +4 -0
- package/package.json +1 -1
|
@@ -147,7 +147,7 @@ export const tree = () => html`
|
|
|
147
147
|
|
|
148
148
|
## Disclosure menu
|
|
149
149
|
|
|
150
|
-
See https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-navigation.html
|
|
150
|
+
See [disclosure navigation](https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-navigation.html)
|
|
151
151
|
|
|
152
152
|
```js preview-story
|
|
153
153
|
export const disclosureMenu = () => html`
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { html, css, LitElement } from 'lit';
|
|
4
4
|
import { SlotMixin, DisabledMixin } from '@lion/ui/core.js';
|
|
5
5
|
import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
|
6
|
+
import { MoreButtonMenuMixin } from './MoreButtonMenuMixin.js';
|
|
6
7
|
import { uuid } from './utils/uuid.js';
|
|
7
8
|
import { isInView } from './utils/isInView.js';
|
|
8
9
|
import {
|
|
@@ -125,7 +126,7 @@ customElements.define('lion-menuitem', LionMenuitem);
|
|
|
125
126
|
*/
|
|
126
127
|
const InteractiveListMixinImplementation = superclass =>
|
|
127
128
|
// @ts-ignore https://github.com/microsoft/TypeScript/issues/36821#issuecomment-588375051
|
|
128
|
-
class InteractiveListMixin extends DisabledMixin(SlotMixin(superclass)) {
|
|
129
|
+
class InteractiveListMixin extends DisabledMixin(SlotMixin(MoreButtonMenuMixin(superclass))) {
|
|
129
130
|
/** @type {any} */
|
|
130
131
|
static styles = [
|
|
131
132
|
css`
|
|
@@ -156,13 +157,13 @@ const InteractiveListMixinImplementation = superclass =>
|
|
|
156
157
|
|
|
157
158
|
static get properties() {
|
|
158
159
|
return {
|
|
159
|
-
// TODO: align with name of open-ui
|
|
160
|
+
// TODO: align with name of open-ui => "inline|block" in focusgroup
|
|
160
161
|
orientation: { type: String, reflect: true },
|
|
161
|
-
// TODO: align with name of platform
|
|
162
|
+
// TODO: align with name of platform => ("multiple" in select, selection not part of focusgroup)
|
|
162
163
|
multipleChoice: { type: Boolean, attribute: 'multiple-choice' },
|
|
163
164
|
// TODO: align with name of open-ui
|
|
164
165
|
selectionFollowsFocus: { type: Boolean, attribute: 'selection-follows-focus' },
|
|
165
|
-
// TODO: align with name of open-ui
|
|
166
|
+
// TODO: align with name of open-ui => "wrap" in focsugroup
|
|
166
167
|
rotateKeyboardNavigation: { type: Boolean, attribute: 'rotate-keyboard-navigation' },
|
|
167
168
|
// TODO: align with name of open-ui
|
|
168
169
|
noPreselect: { type: Boolean, attribute: 'no-preselect' },
|
|
@@ -463,6 +464,7 @@ const InteractiveListMixinImplementation = superclass =>
|
|
|
463
464
|
// console.debug('checking children of', node, node.children);
|
|
464
465
|
// @ts-ignore - HTMLCollection is iterable
|
|
465
466
|
for (const child of node.children) {
|
|
467
|
+
// @ts-ignore
|
|
466
468
|
handleInteractiveListAdditionLevel(child, { newItems, level: level + 1 });
|
|
467
469
|
}
|
|
468
470
|
return;
|
|
@@ -486,10 +488,16 @@ const InteractiveListMixinImplementation = superclass =>
|
|
|
486
488
|
|
|
487
489
|
// Move items to _listNode and add interactive node references (with [role]) to listItems
|
|
488
490
|
nodes.forEach(node => {
|
|
489
|
-
// @ts-
|
|
491
|
+
// @ts-expect-error - node type compatibility
|
|
490
492
|
handleInteractiveListAdditionLevel(node, { newItems });
|
|
491
493
|
});
|
|
492
494
|
|
|
495
|
+
// @ts-expect-error
|
|
496
|
+
if (this.itemWrap && nodes.length) {
|
|
497
|
+
// @ts-expect-error
|
|
498
|
+
this._initMoreButtonMenu();
|
|
499
|
+
}
|
|
500
|
+
|
|
493
501
|
newItems.forEach(item => {
|
|
494
502
|
if (!this.__listItems.includes(item)) {
|
|
495
503
|
this.__listItems.push(item);
|
|
@@ -793,4 +801,5 @@ const InteractiveListMixinImplementation = superclass =>
|
|
|
793
801
|
return this.__getNextOption(currentIndex, offset);
|
|
794
802
|
}
|
|
795
803
|
};
|
|
804
|
+
// @ts-ignore
|
|
796
805
|
export const InteractiveListMixin = dedupeMixin(InteractiveListMixinImplementation);
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* @param {any} superclass
|
|
4
|
+
*/
|
|
5
|
+
/** @type {any} */
|
|
6
|
+
|
|
7
|
+
const styles = `
|
|
8
|
+
<style>
|
|
9
|
+
/** More button is hidden by default */
|
|
10
|
+
[data-more-button-wrapper] {
|
|
11
|
+
display: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
[data-more-button-menu]:has([role='listitem'] [active]:focus):not(
|
|
15
|
+
:has([role='listitem'] [active][aria-expanded='true'])
|
|
16
|
+
) {
|
|
17
|
+
overflow: visible;
|
|
18
|
+
width: auto;
|
|
19
|
+
height: auto;
|
|
20
|
+
position: absolute;
|
|
21
|
+
top: 100%;
|
|
22
|
+
background-color: var(--oj2_bg_default);
|
|
23
|
+
}
|
|
24
|
+
[data-more-button-menu],
|
|
25
|
+
[data-more-button-menu]:has([role='listitem'] [active][aria-expanded='true']) {
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
width: 1px;
|
|
28
|
+
height: 1px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
:host([responsive-mode='desktop']) [level='1'] > [slot='list'] > [role='listitem'] > button,
|
|
32
|
+
:host([responsive-mode='desktop']) [level='1'] > [slot='list'] > [role='listitem'] > a,
|
|
33
|
+
:host([responsive-mode='desktop'])
|
|
34
|
+
[level='1']
|
|
35
|
+
> [slot='list']
|
|
36
|
+
> [data-more-button-wrapper]
|
|
37
|
+
> [role='listitem']
|
|
38
|
+
> button,
|
|
39
|
+
:host([responsive-mode='desktop'])
|
|
40
|
+
[level='1']
|
|
41
|
+
> [slot='list']
|
|
42
|
+
> [data-more-button-wrapper]
|
|
43
|
+
> [role='listitem']
|
|
44
|
+
> a {
|
|
45
|
+
white-space: nowrap;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* TODO listitem rigth margin/gap is not taken at the account ATM
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* More button relies on the fact that the menu items do not break the line
|
|
52
|
+
* TODO is it optimal?
|
|
53
|
+
*/
|
|
54
|
+
:host([responsive-mode='desktop']) > [role='list'] > [role='listitem'] > button,
|
|
55
|
+
:host([responsive-mode='desktop']) > [role='list'] > [role='listitem'] > a {
|
|
56
|
+
white-space: nowrap;
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
// @ts-ignore - JS mixin typing
|
|
62
|
+
export const MoreButtonMenuMixin = superclass =>
|
|
63
|
+
class MoreButtonMenuMixinClass extends superclass {
|
|
64
|
+
constructor() {
|
|
65
|
+
super();
|
|
66
|
+
this.__resizeTimeout = null;
|
|
67
|
+
this.isMoreButtonShown = null;
|
|
68
|
+
this.handleResize = this.handleResize.bind(this);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
disconnectedCallback() {
|
|
72
|
+
window.removeEventListener('resize', this.handleResize);
|
|
73
|
+
if (this.__resizeTimeout) {
|
|
74
|
+
clearTimeout(this.__resizeTimeout);
|
|
75
|
+
}
|
|
76
|
+
super.disconnectedCallback?.();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_initMoreButtonMenu() {
|
|
80
|
+
this._createMoreButtonWrapper();
|
|
81
|
+
window.addEventListener('resize', this.handleResize);
|
|
82
|
+
this.handleResize();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// eslint-disable-next-line class-methods-use-this
|
|
86
|
+
_getDeepActiveElement() {
|
|
87
|
+
let host = document.activeElement || document.body;
|
|
88
|
+
while (host && host.shadowRoot && host.shadowRoot.activeElement) {
|
|
89
|
+
host = host.shadowRoot.activeElement;
|
|
90
|
+
}
|
|
91
|
+
return host;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_createMoreButtonWrapper() {
|
|
95
|
+
const moreButtonWrapper = document.createElement('div');
|
|
96
|
+
const style = document.createElement('style');
|
|
97
|
+
style.textContent = styles;
|
|
98
|
+
moreButtonWrapper.appendChild(style);
|
|
99
|
+
moreButtonWrapper.setAttribute('data-more-button-wrapper', '');
|
|
100
|
+
[...this.getMoreButtonSlotProjection().childNodes].forEach(node =>
|
|
101
|
+
moreButtonWrapper.appendChild(node),
|
|
102
|
+
);
|
|
103
|
+
const moreButton = moreButtonWrapper.querySelector('button');
|
|
104
|
+
moreButton?.setAttribute('data-more-button', '');
|
|
105
|
+
moreButton?.setAttribute('tabindex', '-1');
|
|
106
|
+
moreButton?.setAttribute('aria-expanded', 'false');
|
|
107
|
+
const moreButtonMenu = document.createElement('div');
|
|
108
|
+
moreButtonMenu.setAttribute('data-more-button-menu', '');
|
|
109
|
+
moreButtonWrapper.appendChild(moreButtonMenu);
|
|
110
|
+
moreButtonMenu.setAttribute('role', 'none');
|
|
111
|
+
this._listNode.appendChild(moreButtonWrapper);
|
|
112
|
+
|
|
113
|
+
const isElementDirectFocusableItemUnderMoreButtonMenu = element => {
|
|
114
|
+
const targetTagName = element?.tagName;
|
|
115
|
+
return (
|
|
116
|
+
element?.parentElement?.parentElement === moreButtonMenu &&
|
|
117
|
+
(targetTagName === 'BUTTON' || targetTagName === 'A')
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
moreButtonMenu.addEventListener('focusin', event => {
|
|
122
|
+
const target = event?.target;
|
|
123
|
+
if (isElementDirectFocusableItemUnderMoreButtonMenu(target)) {
|
|
124
|
+
if (target?.getAttribute('aria-expanded') === 'true') {
|
|
125
|
+
target?.click();
|
|
126
|
+
}
|
|
127
|
+
moreButton?.setAttribute('aria-expanded', 'true');
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
moreButtonMenu.addEventListener('focusout', event => {
|
|
132
|
+
const target = event?.target;
|
|
133
|
+
if (isElementDirectFocusableItemUnderMoreButtonMenu(target)) {
|
|
134
|
+
moreButton?.setAttribute('aria-expanded', 'false');
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
this.hasMoreButtonMenuAnyFocusedFirstLevelItems = false;
|
|
139
|
+
|
|
140
|
+
moreButton?.addEventListener('mousedown', () => {
|
|
141
|
+
this.hasMoreButtonMenuAnyFocusedFirstLevelItems = false;
|
|
142
|
+
const focusableFirstLevelItems = moreButtonMenu.querySelectorAll(
|
|
143
|
+
':scope > [role="listitem"] > a, :scope > [role="listitem"] > button',
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
[...focusableFirstLevelItems]?.forEach(focusableFirstLevelItem => {
|
|
147
|
+
if (this._getDeepActiveElement() === focusableFirstLevelItem) {
|
|
148
|
+
this.hasMoreButtonMenuAnyFocusedFirstLevelItems = true;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
moreButton?.addEventListener('click', () => {
|
|
154
|
+
if (!this.hasMoreButtonMenuAnyFocusedFirstLevelItems) {
|
|
155
|
+
moreButtonMenu
|
|
156
|
+
.querySelector(':scope > [role="listitem"] > a, :scope > [role="listitem"] > button')
|
|
157
|
+
?.focus();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getMoreButtonSlotProjection() {
|
|
163
|
+
return this.querySelector('[slot="more-button"]');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
getMoreButtonMenu() {
|
|
167
|
+
return this.querySelector('[data-more-button-menu]');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
getMoreButtonMenuWrapper() {
|
|
171
|
+
return this.querySelector('[data-more-button-wrapper]');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
displayMoreButton() {
|
|
175
|
+
this.getMoreButtonMenuWrapper().style.display = 'block';
|
|
176
|
+
this.isMoreButtonShown = true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
hideMoreButton() {
|
|
180
|
+
this.getMoreButtonMenuWrapper().style.display = 'none';
|
|
181
|
+
this.isMoreButtonShown = false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
doItemsFit() {
|
|
185
|
+
return this._listNode.scrollWidth - this._listNode.clientWidth === 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
getListItems() {
|
|
189
|
+
const listNode = this._listNode;
|
|
190
|
+
return Array.from(listNode.querySelectorAll(':scope > [role="listitem"]'));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {Node} listItem
|
|
195
|
+
*/
|
|
196
|
+
getListItemWithAdjacentNodes = listItem => {
|
|
197
|
+
const markerNodeType = this.nodeType && Node.COMMENT_NODE;
|
|
198
|
+
/** @param {Node | null | undefined} node */
|
|
199
|
+
const isMarkerComment = node => node?.nodeType === markerNodeType && node?.nodeValue === '';
|
|
200
|
+
|
|
201
|
+
const previousNodes = [];
|
|
202
|
+
let { previousSibling } = listItem;
|
|
203
|
+
while (previousSibling) {
|
|
204
|
+
previousNodes.unshift(previousSibling);
|
|
205
|
+
if (isMarkerComment(previousSibling)) {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
({ previousSibling } = previousSibling);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const nextNodes = [];
|
|
212
|
+
let { nextSibling } = listItem;
|
|
213
|
+
let includeOneMoreAfterComment = false;
|
|
214
|
+
while (nextSibling) {
|
|
215
|
+
nextNodes.push(nextSibling);
|
|
216
|
+
|
|
217
|
+
if (includeOneMoreAfterComment) {
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (isMarkerComment(nextSibling)) {
|
|
222
|
+
includeOneMoreAfterComment = true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
({ nextSibling } = nextSibling);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const fragment = document.createDocumentFragment();
|
|
229
|
+
const nodes = [...previousNodes, listItem, ...nextNodes];
|
|
230
|
+
nodes.forEach(node => fragment.appendChild(node));
|
|
231
|
+
return fragment;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
moveItemToMoreButtonMenuFromMainMenu() {
|
|
235
|
+
const moreButtonMenuElement = this.getMoreButtonMenu();
|
|
236
|
+
const listItems = this._listNode.querySelectorAll(':scope > [role="listitem"]');
|
|
237
|
+
const listItem = listItems[listItems.length - 1];
|
|
238
|
+
|
|
239
|
+
const fragment = this.getListItemWithAdjacentNodes(listItem);
|
|
240
|
+
moreButtonMenuElement.prepend(fragment);
|
|
241
|
+
listItem.style.display = '';
|
|
242
|
+
this.displayMoreButton();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
moveAllItemsToMainMenuFromMoreButtonMenu() {
|
|
246
|
+
const moreButtonMenu = this.getMoreButtonMenu();
|
|
247
|
+
const moreButtonMenuWrapper = this.getMoreButtonMenuWrapper();
|
|
248
|
+
if (moreButtonMenu.childNodes.length === 0) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
[...moreButtonMenu.childNodes].forEach(node => {
|
|
252
|
+
moreButtonMenuWrapper.before(node);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
renderAllItemsInMainMenu() {
|
|
257
|
+
this.moveAllItemsToMainMenuFromMoreButtonMenu();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
hideItemsOneByOneInMainMenuUntilTheyFit() {
|
|
261
|
+
let hiddenItemsCount = 0;
|
|
262
|
+
const mainMenuItems = this.getListItems();
|
|
263
|
+
for (let i = mainMenuItems.length - 1; i >= 0 && !this.doItemsFit(); i -= 1) {
|
|
264
|
+
mainMenuItems[i].style.display = 'none';
|
|
265
|
+
hiddenItemsCount += 1;
|
|
266
|
+
}
|
|
267
|
+
return hiddenItemsCount;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
moveHiddenItemsFromMainMenuToMoreButtonMenu(hiddenItemsCount) {
|
|
271
|
+
for (let i = 0; i < hiddenItemsCount; i += 1) {
|
|
272
|
+
this.moveItemToMoreButtonMenuFromMainMenu();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
handleResize() {
|
|
277
|
+
if (this.__resizeTimeout) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
this.__resizeTimeout = setTimeout(() => {
|
|
282
|
+
this.__resizeTimeout = null;
|
|
283
|
+
|
|
284
|
+
this.renderAllItemsInMainMenu();
|
|
285
|
+
this.hideMoreButton();
|
|
286
|
+
|
|
287
|
+
if (this.doItemsFit()) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
this.displayMoreButton();
|
|
292
|
+
const hiddenItemsCount = this.hideItemsOneByOneInMainMenuUntilTheyFit();
|
|
293
|
+
this.moveHiddenItemsFromMainMenuToMoreButtonMenu(hiddenItemsCount);
|
|
294
|
+
}, 50);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/* eslint-disable class-methods-use-this */
|
|
3
|
+
import { LitElement, html, css, nothing } from 'lit';
|
|
2
4
|
import { LionMenuHybrid } from '@lion/ui/menu.js';
|
|
3
5
|
import { ScopedElementsMixin } from '../../core/src/ScopedElementsMixin.js';
|
|
4
6
|
|
|
@@ -102,6 +104,17 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
|
|
|
102
104
|
padding-bottom: 200%;
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
/**
|
|
108
|
+
* TODO remove it. Use it now for testing More button feature
|
|
109
|
+
*/
|
|
110
|
+
:host([responsive-mode='desktop']) [level='1'] > [slot='list'] > [role='listitem'],
|
|
111
|
+
:host([responsive-mode='desktop'])
|
|
112
|
+
[level='1']
|
|
113
|
+
> [slot='list']
|
|
114
|
+
> [data-more-button-wrapper] {
|
|
115
|
+
margin-right: 180px;
|
|
116
|
+
}
|
|
117
|
+
|
|
105
118
|
:host([responsive-mode='mobile']) nav {
|
|
106
119
|
display: flex;
|
|
107
120
|
flex-direction: column;
|
|
@@ -157,101 +170,25 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
|
|
|
157
170
|
|
|
158
171
|
this.prefilledSuggestion = '';
|
|
159
172
|
|
|
160
|
-
// Assign styles as a property, so that subclasses can augment it
|
|
161
|
-
// this.styles = styles;
|
|
162
|
-
// this.menuOpened = false;
|
|
163
|
-
|
|
164
173
|
const mql = window.matchMedia(`(width <= ${this.breakpointMin}px)`);
|
|
165
174
|
/** @type {NavBarResponsiveMode} */
|
|
166
175
|
this.responsiveMode = mql.matches ? 'mobile' : 'desktop';
|
|
167
|
-
this._levelCfg =
|
|
168
|
-
this._levelSecondaryCfg = this
|
|
176
|
+
this._levelCfg = this._getLevelCfg(this.responsiveMode);
|
|
177
|
+
this._levelSecondaryCfg = this._getSecondaryLevelCfg(this.responsiveMode);
|
|
169
178
|
|
|
170
179
|
mql.addEventListener('change', () => {
|
|
171
180
|
this.responsiveMode = mql.matches ? 'mobile' : 'desktop';
|
|
172
|
-
this._levelCfg =
|
|
173
|
-
this._levelSecondaryCfg = this
|
|
181
|
+
this._levelCfg = this._getLevelCfg(this.responsiveMode);
|
|
182
|
+
this._levelSecondaryCfg = this._getSecondaryLevelCfg(this.responsiveMode);
|
|
174
183
|
});
|
|
175
184
|
|
|
176
185
|
this.#storeLatestFocusedElementId();
|
|
177
|
-
this.#delegateAnalyticsEvents();
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
#getSecondaryLevelCfg() {
|
|
181
|
-
if (this.responsiveMode === 'mobile') {
|
|
182
|
-
return {
|
|
183
|
-
l1: {
|
|
184
|
-
isBar: true,
|
|
185
|
-
},
|
|
186
|
-
l2: {
|
|
187
|
-
isBar: false,
|
|
188
|
-
},
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
l1: {
|
|
194
|
-
isBar: true,
|
|
195
|
-
},
|
|
196
|
-
l2: {
|
|
197
|
-
isBar: false,
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** @type {string | null} */
|
|
203
|
-
#latestFocusedElementId = null;
|
|
204
|
-
|
|
205
|
-
#storeLatestFocusedElementId() {
|
|
206
|
-
this.addEventListener('focusin', ({ target }) => {
|
|
207
|
-
// @ts-ignore
|
|
208
|
-
this.#latestFocusedElementId = target.id;
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
#syncLatestFocusedElementForNewResponsiveMode() {
|
|
213
|
-
// We might show/hide or rerender some elements (with same ids) in different positions in the DOM.
|
|
214
|
-
// As these are physically different elements, we must move focus (as otherwise it would be reset to body)
|
|
215
|
-
if (!this.#latestFocusedElementId) return;
|
|
216
|
-
const newFocusedElement = this.shadowRoot?.querySelector(`#${this.#latestFocusedElementId}`);
|
|
217
|
-
if (newFocusedElement instanceof HTMLElement) {
|
|
218
|
-
newFocusedElement.focus();
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
#delegateAnalyticsEvents() {
|
|
223
|
-
this.addEventListener('click', ({ target }) => {
|
|
224
|
-
if (!target || !(target instanceof HTMLElement)) return;
|
|
225
|
-
|
|
226
|
-
// @ts-expect-error
|
|
227
|
-
const isButton = target.tagName === 'BUTTON' || target.constructor._$isLionButton$;
|
|
228
|
-
const isAnchor = target.tagName === 'A';
|
|
229
|
-
if (!isButton && !isAnchor) return;
|
|
230
|
-
|
|
231
|
-
if (!target.id) return;
|
|
232
|
-
|
|
233
|
-
// TODO: do in extension layer
|
|
234
|
-
this.dispatchEvent(
|
|
235
|
-
new CustomEvent('nav-item-click', {
|
|
236
|
-
// N.B. we can also retrive the item
|
|
237
|
-
detail: {
|
|
238
|
-
item: {
|
|
239
|
-
id: target.id,
|
|
240
|
-
label: target.textContent?.trim() || '',
|
|
241
|
-
href: isAnchor ? target.getAttribute('href') : undefined,
|
|
242
|
-
},
|
|
243
|
-
},
|
|
244
|
-
// bubbles: true,
|
|
245
|
-
// composed: true,
|
|
246
|
-
}),
|
|
247
|
-
);
|
|
248
|
-
});
|
|
249
186
|
}
|
|
250
187
|
|
|
251
188
|
/**
|
|
252
189
|
* @param {NavBarResponsiveMode} responsiveMode
|
|
253
190
|
*/
|
|
254
|
-
|
|
191
|
+
_getLevelCfg(responsiveMode) {
|
|
255
192
|
// N.B. we're fighting some of the overlay configs provided by LionMenuOverlay here,
|
|
256
193
|
// the idea is to take over all of that when depending on LionMenu, after removing LonMenuOverlay and LionMenuHybrid components altogether.
|
|
257
194
|
|
|
@@ -322,12 +259,66 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
|
|
|
322
259
|
};
|
|
323
260
|
}
|
|
324
261
|
|
|
262
|
+
_getSecondaryLevelCfg(responsiveMode) {
|
|
263
|
+
if (responsiveMode === 'mobile') {
|
|
264
|
+
return {
|
|
265
|
+
l1: {
|
|
266
|
+
isBar: true,
|
|
267
|
+
},
|
|
268
|
+
l2: {
|
|
269
|
+
isBar: false,
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
l1: {
|
|
276
|
+
isBar: true,
|
|
277
|
+
},
|
|
278
|
+
l2: {
|
|
279
|
+
isBar: false,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** @type {string | null} */
|
|
285
|
+
#latestFocusedElementId = null;
|
|
286
|
+
|
|
287
|
+
#storeLatestFocusedElementId() {
|
|
288
|
+
this.addEventListener('focusin', ({ target }) => {
|
|
289
|
+
// @ts-ignore
|
|
290
|
+
this.#latestFocusedElementId = target.id;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#syncLatestFocusedElementForNewResponsiveMode() {
|
|
295
|
+
// We might show/hide or rerender some elements (with same ids) in different positions in the DOM.
|
|
296
|
+
// As these are physically different elements, we must move focus (as otherwise it would be reset to body)
|
|
297
|
+
if (!this.#latestFocusedElementId) return;
|
|
298
|
+
const newFocusedElement = this.shadowRoot?.querySelector(`#${this.#latestFocusedElementId}`);
|
|
299
|
+
if (newFocusedElement instanceof HTMLElement) {
|
|
300
|
+
newFocusedElement.focus();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* @param {import("./types.js").MenuItem}
|
|
306
|
+
* @param {number} level
|
|
307
|
+
*/
|
|
308
|
+
_getId(menuItem, level) {
|
|
309
|
+
if (menuItem.id) return menuItem.id;
|
|
310
|
+
const title = menuItem.title || menuItem.label || 'item';
|
|
311
|
+
return `${
|
|
312
|
+
Number.isInteger(level) ? `l${level}-${title}` : title
|
|
313
|
+
}-${Math.random().toString(16).slice(2)}`;
|
|
314
|
+
}
|
|
315
|
+
|
|
325
316
|
/**
|
|
326
317
|
* @param {import('lit').PropertyValues} changedProperties
|
|
327
318
|
*/
|
|
328
319
|
updated(changedProperties) {
|
|
329
320
|
if (changedProperties.has('responsiveMode')) {
|
|
330
|
-
this._levelCfg =
|
|
321
|
+
this._levelCfg = this._getLevelCfg(this.responsiveMode);
|
|
331
322
|
this.#syncLatestFocusedElementForNewResponsiveMode();
|
|
332
323
|
}
|
|
333
324
|
}
|
|
@@ -344,18 +335,18 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
|
|
|
344
335
|
? html`<button data-close data-level="1">✖️</button>`
|
|
345
336
|
: ''}
|
|
346
337
|
</div>
|
|
347
|
-
${
|
|
338
|
+
${this._searchTemplate()}
|
|
348
339
|
</div>
|
|
349
340
|
<div class="nav-body">
|
|
350
|
-
${this._menulevelTemplate(this.menuSupportItems, 1,
|
|
341
|
+
${this._menulevelTemplate(this.menuSupportItems, 1, '')}
|
|
351
342
|
${this.responsiveMode === 'desktop'
|
|
352
|
-
?
|
|
343
|
+
? this._ctasTemplate(this.ctaPrimary, this.ctaSecondary)
|
|
353
344
|
: ''}
|
|
354
|
-
${this._menulevelTemplate(this.menuItems, 1,
|
|
345
|
+
${this._menulevelTemplate(this.menuItems, 1, '', true)}
|
|
355
346
|
</div>
|
|
356
347
|
${this.responsiveMode === 'mobile'
|
|
357
348
|
? html`<div class="nav-footer">
|
|
358
|
-
${
|
|
349
|
+
${this._ctasTemplate(this.ctaPrimary, this.ctaSecondary)}
|
|
359
350
|
</div>`
|
|
360
351
|
: ''}
|
|
361
352
|
</nav>
|
|
@@ -366,7 +357,7 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
|
|
|
366
357
|
* @param {string[]} suggestions
|
|
367
358
|
* @returns {import('lit').TemplateResult}
|
|
368
359
|
*/
|
|
369
|
-
|
|
360
|
+
_searchTemplate(suggestions = this.suggestions, prefilledSuggestion = this.prefilledSuggestion) {
|
|
370
361
|
return html` <form role="search">
|
|
371
362
|
<input
|
|
372
363
|
type="search"
|
|
@@ -386,16 +377,18 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
|
|
|
386
377
|
* @param {CtaLink} ctaSecondary
|
|
387
378
|
* @returns {import('lit').TemplateResult}
|
|
388
379
|
*/
|
|
389
|
-
|
|
380
|
+
_ctasTemplate(ctaPrimary, ctaSecondary) {
|
|
390
381
|
return html`<div class="cta-items">
|
|
391
382
|
${ctaSecondary?.href
|
|
392
383
|
? html`<div class="cta-secondary">
|
|
393
|
-
<a id="${ctaSecondary
|
|
384
|
+
<a id="${this._getId(ctaSecondary)}" href="${ctaSecondary.href}"
|
|
385
|
+
>${ctaSecondary.label}
|
|
386
|
+
</a>
|
|
394
387
|
</div>`
|
|
395
388
|
: ''}
|
|
396
389
|
${ctaPrimary?.href
|
|
397
390
|
? html`<div class="cta-primary">
|
|
398
|
-
<a id="${ctaPrimary
|
|
391
|
+
<a id="${this._getId(ctaPrimary)}" href="${ctaPrimary.href}"> ${ctaPrimary.label}</a>
|
|
399
392
|
</div>`
|
|
400
393
|
: ''}
|
|
401
394
|
</div>`;
|
|
@@ -404,17 +397,14 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
|
|
|
404
397
|
/**
|
|
405
398
|
* @param {MenuItem[]} menuItemsForLevel
|
|
406
399
|
* @param {number} level
|
|
407
|
-
* @param {LevelConfig} cfgForLevel
|
|
408
400
|
* @returns {import('lit').TemplateResult}
|
|
409
401
|
*/
|
|
410
|
-
_menulevelTemplate(menuItemsForLevel, level,
|
|
402
|
+
_menulevelTemplate(menuItemsForLevel, level, prevText = '', itemWrap = false) {
|
|
411
403
|
// @ts-ignore
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
const getId = (/** @type {import("./types.js").MenuItem} */ menuItem) =>
|
|
415
|
-
menuItem.id || `l${level}-${menuItem.title}`;
|
|
404
|
+
const cfgForLevel = this._levelCfg[`l${level}`];
|
|
416
405
|
|
|
417
406
|
return html`<lion-menu-hybrid
|
|
407
|
+
?item-wrap=${itemWrap}
|
|
418
408
|
.config="${cfgForLevel.openableConfig || {}}"
|
|
419
409
|
.bar="${cfgForLevel.isBar}"
|
|
420
410
|
?data-has-full-width-flyout="${cfgForLevel.hasFullWidthFlyout}"
|
|
@@ -429,21 +419,21 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
|
|
|
429
419
|
menuItem => html`
|
|
430
420
|
<div role="listitem">
|
|
431
421
|
${menuItem.sub
|
|
432
|
-
? html` <button id="${
|
|
422
|
+
? html` <button id="${this._getId(menuItem)}" data-invoker>${menuItem.title}</button>
|
|
433
423
|
${menuItem.sub
|
|
434
|
-
? this._menulevelTemplate(
|
|
435
|
-
menuItem.sub,
|
|
436
|
-
level + 1,
|
|
437
|
-
cfgForNextLevel,
|
|
438
|
-
menuItem.title,
|
|
439
|
-
)
|
|
424
|
+
? this._menulevelTemplate(menuItem.sub, level + 1, menuItem.title)
|
|
440
425
|
: ''}`
|
|
441
|
-
: html` <a id="${
|
|
426
|
+
: html` <a id="${this._getId(menuItem)}" href="${menuItem.link || ''}"
|
|
442
427
|
>${menuItem.title}</a
|
|
443
428
|
>`}
|
|
444
429
|
</div>
|
|
445
430
|
`,
|
|
446
431
|
)}
|
|
447
|
-
|
|
432
|
+
${itemWrap
|
|
433
|
+
? html`<div slot="more-button">
|
|
434
|
+
<button>More</button>
|
|
435
|
+
</div>`
|
|
436
|
+
: nothing}
|
|
437
|
+
</lion-menu-hybrid> `;
|
|
448
438
|
}
|
|
449
439
|
}
|