@lion/ui 0.18.0-menu-system.2 → 0.18.0-menu-system.5
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/collapsible/test-suites/DisclosureMixin.suite.js +30 -0
- package/components/menu/src/InteractiveListMixin.js +57 -24
- package/components/menu/src/LionMenu.js +1 -1
- package/components/menu/src/LionToolbar.js +1 -1
- package/components/menu/src/MoreButtonMenuMixin.js +79 -92
- package/components/menu/src/MultiLevelListMixin.js +1 -1
- package/components/menu/src/utils/listItemInteractions.js +25 -6
- package/components/menu/test/lion-menu.test.js +2 -1
- package/components/menu/test-suites/InteractiveListMixin.suite.js +164 -2
- package/components/navigation-bar/src/LionNavigationBar.js +2 -2
- package/custom-elements.json +354 -54
- package/dist-types/components/collapsible/test-suites/DisclosureMixin.suite.d.ts +2 -0
- package/dist-types/components/collapsible/test-suites/DisclosureMixin.suite.d.ts.map +1 -0
- package/dist-types/components/menu/src/InteractiveListMixin.d.ts +11 -5
- package/dist-types/components/menu/src/InteractiveListMixin.d.ts.map +1 -1
- package/dist-types/components/menu/src/LionMenu.d.ts +4 -2
- package/dist-types/components/menu/src/LionMenu.d.ts.map +1 -1
- package/dist-types/components/menu/src/LionMenuOverlay.d.ts +8 -4
- package/dist-types/components/menu/src/LionMenuOverlay.d.ts.map +1 -1
- package/dist-types/components/menu/src/LionTree.d.ts +4 -2
- package/dist-types/components/menu/src/LionTree.d.ts.map +1 -1
- package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts +11 -2
- package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts.map +1 -1
- package/dist-types/components/menu/src/MultiLevelListMixin.d.ts +4 -2
- package/dist-types/components/menu/src/MultiLevelListMixin.d.ts.map +1 -1
- package/dist-types/components/menu/src/utils/listItemInteractions.d.ts +8 -2
- package/dist-types/components/menu/src/utils/listItemInteractions.d.ts.map +1 -1
- package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts +6 -5
- package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts.map +1 -1
- package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts +4 -4
- package/dist-types/exports/menu-test-suites.d.ts +2 -0
- package/dist-types/exports/menu-test-suites.d.ts.map +1 -0
- 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/api-table.md +197 -192
- package/docs/components/menu/extensions.md +27 -0
- package/docs/components/menu/overview.md +39 -39
- 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/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/menu-test-suites.js +2 -0
- package/menu-test-suites.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// import { LitElement } from 'lit';
|
|
2
|
+
// import { expect, html, fixture as _fixture, unsafeStatic, defineCE } from '@open-wc/testing';
|
|
3
|
+
// import { DisclosureMixin } from '../src/DisclosureMixin.js';
|
|
4
|
+
|
|
5
|
+
// import '@lion/ui/define/lion-menu.js';
|
|
6
|
+
// import '@lion/ui/define/lion-item.js';
|
|
7
|
+
|
|
8
|
+
// class DisclosureClass extends DisclosureMixin(LitElement) {}
|
|
9
|
+
|
|
10
|
+
// /**
|
|
11
|
+
// * @typedef {import('lit').TemplateResult} TemplateResult
|
|
12
|
+
// */
|
|
13
|
+
|
|
14
|
+
// const fixture = /** @type {(arg: TemplateResult) => Promise<LionMenu>} */ (_fixture);
|
|
15
|
+
|
|
16
|
+
// /**
|
|
17
|
+
// * @param { {tagString?:string, itemTagString?:string} } [customConfig]
|
|
18
|
+
// */
|
|
19
|
+
// export function runDisclosureMixinSuite(customConfig = {}) {
|
|
20
|
+
// const cfg = {
|
|
21
|
+
// tagString: 'lion-menu',
|
|
22
|
+
// ...customConfig,
|
|
23
|
+
// };
|
|
24
|
+
|
|
25
|
+
// const tagString = cfg.tagString || defineCE(DisclosureClass);
|
|
26
|
+
// const tag = unsafeStatic(tagString);
|
|
27
|
+
|
|
28
|
+
// describe('DisclosureMixin', () => {
|
|
29
|
+
// });
|
|
30
|
+
// }
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
|
|
23
23
|
// TODO: consider renaming to FocusGroupMixin
|
|
24
24
|
|
|
25
|
-
// TODO: make all available in controller/
|
|
25
|
+
// TODO: make all available in controller/directive (same logic with an elegant prop-to-host-mapping)
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* @param {Element} potentialFocusable
|
|
@@ -55,13 +55,25 @@ function isFocusableElement(potentialFocusable) {
|
|
|
55
55
|
export class LionItem extends LitElement {
|
|
56
56
|
/** @type {any} */
|
|
57
57
|
static get properties() {
|
|
58
|
-
return {
|
|
58
|
+
return {
|
|
59
|
+
type: { type: String },
|
|
60
|
+
};
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
get _invokerNode() {
|
|
62
|
-
return Array.from(this.children).find(
|
|
64
|
+
return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
|
|
65
|
+
child => child.slot === 'invoker',
|
|
66
|
+
);
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
static styles = [
|
|
70
|
+
css`
|
|
71
|
+
:host {
|
|
72
|
+
display: block;
|
|
73
|
+
}
|
|
74
|
+
`,
|
|
75
|
+
];
|
|
76
|
+
|
|
65
77
|
constructor() {
|
|
66
78
|
super();
|
|
67
79
|
/** @type {InteractiveListItemRole |''} */
|
|
@@ -81,14 +93,6 @@ export class LionItem extends LitElement {
|
|
|
81
93
|
}
|
|
82
94
|
}
|
|
83
95
|
|
|
84
|
-
static styles = [
|
|
85
|
-
css`
|
|
86
|
-
:host {
|
|
87
|
-
display: block;
|
|
88
|
-
}
|
|
89
|
-
`,
|
|
90
|
-
];
|
|
91
|
-
|
|
92
96
|
render() {
|
|
93
97
|
return html`
|
|
94
98
|
<slot name="invoker"></slot>
|
|
@@ -332,11 +336,10 @@ const InteractiveListMixinImplementation = superclass =>
|
|
|
332
336
|
setCheckedIndex(index) {
|
|
333
337
|
const item = this.listItems[index];
|
|
334
338
|
if (!item) return;
|
|
335
|
-
|
|
336
339
|
if (!this.multipleChoice) {
|
|
337
340
|
// Uncheck all
|
|
338
341
|
this.listItems.forEach(listItem => {
|
|
339
|
-
setChecked(listItem,
|
|
342
|
+
setChecked(listItem, true);
|
|
340
343
|
});
|
|
341
344
|
setChecked(this.listItems[index]);
|
|
342
345
|
} else {
|
|
@@ -377,7 +380,7 @@ const InteractiveListMixinImplementation = superclass =>
|
|
|
377
380
|
* - [role="menubar"](https://www.w3.org/TR/wai-aria-practices-1.1/examples/menubar/menubar-1/menubar-1.html)
|
|
378
381
|
* - [role="tree"](https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-1/treeview-1b.html)
|
|
379
382
|
* For disclosure menus, we need keyboard navigation, but treat every list item
|
|
380
|
-
* (more precisely its anchor child) as a tab stop.
|
|
383
|
+
* (more precisely its anchor child) as a tab stop.
|
|
381
384
|
* @type {'activedescendant'|'roving-tabindex'|'tabbable-disclosure'|'none'}
|
|
382
385
|
*/
|
|
383
386
|
this._activeMode = 'activedescendant';
|
|
@@ -426,6 +429,16 @@ const InteractiveListMixinImplementation = superclass =>
|
|
|
426
429
|
this._onListFocusIn = this._onListFocusIn.bind(this);
|
|
427
430
|
}
|
|
428
431
|
|
|
432
|
+
connectedCallback() {
|
|
433
|
+
super.connectedCallback();
|
|
434
|
+
window.addEventListener('popstate', this._onPopState);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
disconnectedCallback() {
|
|
438
|
+
super.disconnectedCallback();
|
|
439
|
+
window.removeEventListener('popstate', this._onPopState);
|
|
440
|
+
}
|
|
441
|
+
|
|
429
442
|
/**
|
|
430
443
|
* @param {import('lit').PropertyValues } changedProperties
|
|
431
444
|
*/
|
|
@@ -457,11 +470,10 @@ const InteractiveListMixinImplementation = superclass =>
|
|
|
457
470
|
* menuitemradios or -checkboxes.
|
|
458
471
|
* When no role at all, we might deal with a wrapped node (for instance triggering a
|
|
459
472
|
* submenu)
|
|
460
|
-
* Alternatively, when _activeMode is ['disclosure'](https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-navigation.html),
|
|
473
|
+
* Alternatively, when _activeMode is ['tabbable-disclosure'](https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-navigation.html),
|
|
461
474
|
* we search for a[href]
|
|
462
475
|
*/
|
|
463
476
|
if (node.getAttribute('role') === 'group') {
|
|
464
|
-
// console.debug('checking children of', node, node.children);
|
|
465
477
|
// @ts-ignore - HTMLCollection is iterable
|
|
466
478
|
for (const child of node.children) {
|
|
467
479
|
// @ts-ignore
|
|
@@ -568,6 +580,15 @@ const InteractiveListMixinImplementation = superclass =>
|
|
|
568
580
|
if (!this.noPreselect && this.checkedIndex === -1) {
|
|
569
581
|
this.checkedIndex = 0;
|
|
570
582
|
}
|
|
583
|
+
newItems.forEach(item => {
|
|
584
|
+
if (!item.hasAttribute('checked')) {
|
|
585
|
+
setChecked(item, true);
|
|
586
|
+
} else {
|
|
587
|
+
setChecked(item);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
this.__syncCurrentPageWithLocationHref(window.location);
|
|
571
592
|
}
|
|
572
593
|
|
|
573
594
|
/**
|
|
@@ -706,16 +727,8 @@ const InteractiveListMixinImplementation = superclass =>
|
|
|
706
727
|
// const item = /** @type {HTMLElement} */ (target.closest('[role]'));
|
|
707
728
|
// @ts-ignore - target is HTMLElement
|
|
708
729
|
const foundIndex = this.listItems.indexOf(target);
|
|
709
|
-
// console.debug('focusin', target, foundIndex, this.listItems);
|
|
710
|
-
const previousActiveIndex = this.activeIndex;
|
|
711
730
|
if (foundIndex > -1 && foundIndex !== this.activeIndex) {
|
|
712
731
|
this.activeIndex = foundIndex;
|
|
713
|
-
if (this._activeMode === 'tabbable-disclosure') {
|
|
714
|
-
const previousActiveItem = this.listItems[previousActiveIndex];
|
|
715
|
-
previousActiveItem?.removeAttribute('aria-current');
|
|
716
|
-
const activeItem = this.listItems[this.activeIndex];
|
|
717
|
-
activeItem.setAttribute('aria-current', 'page');
|
|
718
|
-
}
|
|
719
732
|
}
|
|
720
733
|
}
|
|
721
734
|
|
|
@@ -800,6 +813,26 @@ const InteractiveListMixinImplementation = superclass =>
|
|
|
800
813
|
_getPreviousEnabledOption(currentIndex, offset = -1) {
|
|
801
814
|
return this.__getNextOption(currentIndex, offset);
|
|
802
815
|
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* @param {Location} location
|
|
819
|
+
*/
|
|
820
|
+
__syncCurrentPageWithLocationHref(location) {
|
|
821
|
+
this.listItems.forEach(item => {
|
|
822
|
+
if (/** @type {HTMLAnchorElement} */ (item).href) {
|
|
823
|
+
if (location.href.includes(/** @type {HTMLAnchorElement} */ (item).href)) {
|
|
824
|
+
setChecked(item);
|
|
825
|
+
} else {
|
|
826
|
+
setChecked(item, true);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** @protected */
|
|
833
|
+
_onPopState = () => {
|
|
834
|
+
this.__syncCurrentPageWithLocationHref(document.location);
|
|
835
|
+
};
|
|
803
836
|
};
|
|
804
837
|
// @ts-ignore
|
|
805
838
|
export const InteractiveListMixin = dedupeMixin(InteractiveListMixinImplementation);
|
|
@@ -189,7 +189,7 @@ export class LionMenu extends MultiLevelListMixin(LitElement) {
|
|
|
189
189
|
if (!multiple) {
|
|
190
190
|
// Uncheck all
|
|
191
191
|
listItemsWithinGroup.forEach(listItem => {
|
|
192
|
-
setChecked(listItem,
|
|
192
|
+
setChecked(listItem, true);
|
|
193
193
|
});
|
|
194
194
|
setChecked(this.listItems[index]);
|
|
195
195
|
} else {
|
|
@@ -1,62 +1,31 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
`;
|
|
1
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
2
|
+
import { css } from 'lit';
|
|
3
|
+
|
|
4
|
+
const styles = css`
|
|
5
|
+
/** More button is hidden by default */
|
|
6
|
+
[data-more-button-wrapper] {
|
|
7
|
+
display: none;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
[data-more-button-menu][data-open]:not(:has([role='listitem'] [aria-expanded='true'])) {
|
|
11
|
+
position: absolute;
|
|
12
|
+
overflow: visible;
|
|
13
|
+
height: auto;
|
|
14
|
+
width: auto;
|
|
15
|
+
top: 100%;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** N.B. this is a simplified version of sr-only. when we move to overlayctrl, we should build this into overlayctrl */
|
|
19
|
+
[data-more-button-menu] {
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
height: 1px;
|
|
22
|
+
width: 1px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
[role='list']:has(> [data-more-button-wrapper]) > [role='listitem'] > :is(button, a) {
|
|
26
|
+
white-space: nowrap;
|
|
27
|
+
}
|
|
28
|
+
`;
|
|
60
29
|
|
|
61
30
|
// @ts-ignore - JS mixin typing
|
|
62
31
|
export const MoreButtonMenuMixin = superclass =>
|
|
@@ -85,7 +54,7 @@ export const MoreButtonMenuMixin = superclass =>
|
|
|
85
54
|
// eslint-disable-next-line class-methods-use-this
|
|
86
55
|
_getDeepActiveElement() {
|
|
87
56
|
let host = document.activeElement || document.body;
|
|
88
|
-
while (host
|
|
57
|
+
while (host?.shadowRoot?.activeElement) {
|
|
89
58
|
host = host.shadowRoot.activeElement;
|
|
90
59
|
}
|
|
91
60
|
return host;
|
|
@@ -94,7 +63,7 @@ export const MoreButtonMenuMixin = superclass =>
|
|
|
94
63
|
_createMoreButtonWrapper() {
|
|
95
64
|
const moreButtonWrapper = document.createElement('div');
|
|
96
65
|
const style = document.createElement('style');
|
|
97
|
-
style.textContent = styles;
|
|
66
|
+
style.textContent = styles.cssText;
|
|
98
67
|
moreButtonWrapper.appendChild(style);
|
|
99
68
|
moreButtonWrapper.setAttribute('data-more-button-wrapper', '');
|
|
100
69
|
[...this.getMoreButtonSlotProjection().childNodes].forEach(node =>
|
|
@@ -107,32 +76,38 @@ export const MoreButtonMenuMixin = superclass =>
|
|
|
107
76
|
const moreButtonMenu = document.createElement('div');
|
|
108
77
|
moreButtonMenu.setAttribute('data-more-button-menu', '');
|
|
109
78
|
moreButtonWrapper.appendChild(moreButtonMenu);
|
|
110
|
-
moreButtonMenu.setAttribute('role', 'none');
|
|
111
79
|
this._listNode.appendChild(moreButtonWrapper);
|
|
112
80
|
|
|
113
|
-
const isElementDirectFocusableItemUnderMoreButtonMenu = element =>
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
element?.parentElement?.parentElement === moreButtonMenu &&
|
|
117
|
-
(targetTagName === 'BUTTON' || targetTagName === 'A')
|
|
118
|
-
);
|
|
119
|
-
};
|
|
81
|
+
const isElementDirectFocusableItemUnderMoreButtonMenu = (/** @type {Element} */ element) =>
|
|
82
|
+
this.listItems.includes(element) && moreButtonMenu.contains(element);
|
|
120
83
|
|
|
121
|
-
moreButtonMenu.addEventListener(
|
|
122
|
-
|
|
123
|
-
|
|
84
|
+
moreButtonMenu.addEventListener(
|
|
85
|
+
'focusin',
|
|
86
|
+
/** @type {FocusEvent} */ event => {
|
|
87
|
+
const target = /** @type {HTMLElement} */ (event?.target);
|
|
88
|
+
if (!isElementDirectFocusableItemUnderMoreButtonMenu(target)) return;
|
|
89
|
+
|
|
90
|
+
// A user did `Shift + Tab` from L2 menu to More menu
|
|
124
91
|
if (target?.getAttribute('aria-expanded') === 'true') {
|
|
92
|
+
// close L2 menu
|
|
125
93
|
target?.click();
|
|
126
94
|
}
|
|
127
95
|
moreButton?.setAttribute('aria-expanded', 'true');
|
|
128
|
-
|
|
129
|
-
|
|
96
|
+
moreButtonMenu.setAttribute('data-open', '');
|
|
97
|
+
},
|
|
98
|
+
);
|
|
130
99
|
|
|
131
|
-
moreButtonMenu.addEventListener('focusout',
|
|
132
|
-
|
|
133
|
-
|
|
100
|
+
moreButtonMenu.addEventListener('focusout', () => {
|
|
101
|
+
// We need this timeout for Safari/WebKit.
|
|
102
|
+
// Unfortunately, an in progress click gets canceled when overflow:hidden is applied.
|
|
103
|
+
// Although mousedown would work instead, the consuming developer should have the freedom to apply all possible interaction pattterns.
|
|
104
|
+
// Especially, we should be compatible with 'withClickInteraction'
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
if (isElementDirectFocusableItemUnderMoreButtonMenu(this._getDeepActiveElement())) return;
|
|
134
107
|
moreButton?.setAttribute('aria-expanded', 'false');
|
|
135
|
-
|
|
108
|
+
|
|
109
|
+
moreButtonMenu.removeAttribute('data-open');
|
|
110
|
+
}, 100);
|
|
136
111
|
});
|
|
137
112
|
|
|
138
113
|
this.hasMoreButtonMenuAnyFocusedFirstLevelItems = false;
|
|
@@ -140,22 +115,26 @@ export const MoreButtonMenuMixin = superclass =>
|
|
|
140
115
|
moreButton?.addEventListener('mousedown', () => {
|
|
141
116
|
this.hasMoreButtonMenuAnyFocusedFirstLevelItems = false;
|
|
142
117
|
const focusableFirstLevelItems = moreButtonMenu.querySelectorAll(
|
|
143
|
-
':scope > [role="listitem"] > a,
|
|
118
|
+
':scope > [role="listitem"] > :is(a, button)',
|
|
144
119
|
);
|
|
145
120
|
|
|
146
|
-
[
|
|
121
|
+
// @ts-ignore Type 'NodeListOf<Element>' must have a '[Symbol.iterator]()' method that returns an iterator.
|
|
122
|
+
for (const focusableFirstLevelItem of focusableFirstLevelItems) {
|
|
147
123
|
if (this._getDeepActiveElement() === focusableFirstLevelItem) {
|
|
148
124
|
this.hasMoreButtonMenuAnyFocusedFirstLevelItems = true;
|
|
149
125
|
}
|
|
150
|
-
}
|
|
126
|
+
}
|
|
151
127
|
});
|
|
152
128
|
|
|
153
129
|
moreButton?.addEventListener('click', () => {
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
130
|
+
if (this.hasMoreButtonMenuAnyFocusedFirstLevelItems) return;
|
|
131
|
+
/** @type {HTMLElement | null} */
|
|
132
|
+
const firstInteractiveItem = moreButtonMenu.querySelector(
|
|
133
|
+
':scope > [role="listitem"] > :is(a, button)',
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
firstInteractiveItem?.focus();
|
|
137
|
+
moreButtonMenu.setAttribute('data-open', '');
|
|
159
138
|
});
|
|
160
139
|
}
|
|
161
140
|
|
|
@@ -232,13 +211,13 @@ export const MoreButtonMenuMixin = superclass =>
|
|
|
232
211
|
return fragment;
|
|
233
212
|
};
|
|
234
213
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
214
|
+
/**
|
|
215
|
+
* @param {{ moreButtonMenuElement: HTMLButtonElement, listItem: HTMLElement }} opts
|
|
216
|
+
*/
|
|
217
|
+
moveItemToMoreButtonMenuFromMainMenu({ moreButtonMenuElement, listItem }) {
|
|
240
218
|
const fragment = this.getListItemWithAdjacentNodes(listItem);
|
|
241
219
|
moreButtonMenuElement.prepend(fragment);
|
|
220
|
+
// eslint-disable-next-line no-param-reassign
|
|
242
221
|
listItem.style.display = '';
|
|
243
222
|
this.displayMoreButton();
|
|
244
223
|
}
|
|
@@ -268,9 +247,15 @@ export const MoreButtonMenuMixin = superclass =>
|
|
|
268
247
|
return hiddenItemsCount;
|
|
269
248
|
}
|
|
270
249
|
|
|
250
|
+
/**
|
|
251
|
+
* @param {number} hiddenItemsCount
|
|
252
|
+
*/
|
|
271
253
|
moveHiddenItemsFromMainMenuToMoreButtonMenu(hiddenItemsCount) {
|
|
254
|
+
const moreButtonMenuElement = this.getMoreButtonMenu();
|
|
255
|
+
const listItems = this._listNode.querySelectorAll(':scope > [role="listitem"]');
|
|
272
256
|
for (let i = 0; i < hiddenItemsCount; i += 1) {
|
|
273
|
-
|
|
257
|
+
const listItem = listItems[listItems.length - 1 - i];
|
|
258
|
+
this.moveItemToMoreButtonMenuFromMainMenu({ moreButtonMenuElement, listItem });
|
|
274
259
|
}
|
|
275
260
|
}
|
|
276
261
|
|
|
@@ -279,6 +264,8 @@ export const MoreButtonMenuMixin = superclass =>
|
|
|
279
264
|
return;
|
|
280
265
|
}
|
|
281
266
|
|
|
267
|
+
this.style.visibility = 'hidden';
|
|
268
|
+
|
|
282
269
|
this.__resizeTimeout = setTimeout(() => {
|
|
283
270
|
this.__resizeTimeout = null;
|
|
284
271
|
|
|
@@ -214,7 +214,7 @@ const MultiLevelListMixinImplementation = superclass =>
|
|
|
214
214
|
if (
|
|
215
215
|
this.orientation === 'horizontal' &&
|
|
216
216
|
subListOfActiveItem &&
|
|
217
|
-
this._activeMode !== 'disclosure'
|
|
217
|
+
this._activeMode !== 'tabbable-disclosure'
|
|
218
218
|
) {
|
|
219
219
|
this.activeItem.click();
|
|
220
220
|
}
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* 'smart' (LionOption) and 'regular' items
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {import('@lion/ui/checkbox-group.js').LionCheckboxIndeterminate} LionCheckboxIndeterminate
|
|
8
|
+
*/
|
|
9
|
+
|
|
6
10
|
/**
|
|
7
11
|
* @param {HTMLElement} item
|
|
8
12
|
*/
|
|
@@ -34,18 +38,33 @@ export function toggleDisabled(item) {
|
|
|
34
38
|
* @param {HTMLElement} item
|
|
35
39
|
*/
|
|
36
40
|
export function isChecked(item) {
|
|
37
|
-
return item.hasAttribute('checked')
|
|
41
|
+
return item.hasAttribute('checked');
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
/**
|
|
41
45
|
* @param {HTMLElement} item
|
|
42
|
-
* @param {
|
|
46
|
+
* @param {Object} [options]
|
|
47
|
+
* @param {boolean} [options.unset=false]
|
|
48
|
+
* @param {'page'|'mixed'} [options.text] - optional text to set for aria-checked/aria-selected/aria-current (e.g. "mixed")
|
|
43
49
|
*/
|
|
44
|
-
export function setChecked(item,
|
|
45
|
-
const
|
|
46
|
-
|
|
50
|
+
export function setChecked(item, unset = false) {
|
|
51
|
+
const role = item.getAttribute('role');
|
|
52
|
+
let ariaAttr = 'current';
|
|
53
|
+
if (role?.endsWith('checkbox') || role?.endsWith('radio')) {
|
|
54
|
+
ariaAttr = 'checked';
|
|
55
|
+
} else if (role === 'option' || role === 'treeitem') {
|
|
56
|
+
ariaAttr = 'selected';
|
|
57
|
+
}
|
|
58
|
+
let text;
|
|
59
|
+
if (/** @type {HTMLAnchorElement} */ (item).href) {
|
|
60
|
+
text = 'page';
|
|
61
|
+
}
|
|
62
|
+
if (/** @type {LionCheckboxIndeterminate} */ (item).mixedState) {
|
|
63
|
+
text = 'mixed';
|
|
64
|
+
}
|
|
65
|
+
if (!unset) {
|
|
47
66
|
item.setAttribute('checked', '');
|
|
48
|
-
item.setAttribute(`aria-${ariaAttr}`, 'true');
|
|
67
|
+
item.setAttribute(`aria-${ariaAttr}`, text || 'true');
|
|
49
68
|
} else {
|
|
50
69
|
item.removeAttribute('checked');
|
|
51
70
|
item.setAttribute(`aria-${ariaAttr}`, 'false');
|