@lion/ui 0.18.0-menu-system.6 → 0.18.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/collapsible.js +0 -1
- package/components/button/src/LionButton.js +0 -4
- package/components/button/test-suites/LionButton.suite.js +0 -1
- package/components/calendar/src/LionCalendar.js +1 -3
- package/components/combobox/test/lion-combobox.test.js +0 -1
- package/components/form-core/src/form-group/FormGroupMixin.js +0 -7
- package/components/form-core/src/registration/FormRegistrarMixin.js +0 -11
- package/components/form-core/test/FormControlMixin.test.js +0 -1
- package/components/form-core/test/validate/Validator.test.js +0 -2
- package/components/form-core/test-suites/ValidateMixin.suite.js +0 -2
- package/components/form-core/test-suites/ValidateMixinFeedbackPart.suite.js +0 -1
- package/components/input-datepicker/src/localizeNamespaceLoader.js +0 -1
- package/components/input-datepicker/test/lion-input-datepicker.test.js +6 -5
- package/components/listbox/test-suites/ListboxMixin.suite.js +1 -7
- package/components/localize/src/date/utils/normalizeIntlDate.js +2 -3
- package/components/localize/test/date/formatDate.test.js +2 -3
- package/components/overlays/src/OverlayController.js +110 -278
- package/components/overlays/src/OverlayMixin.js +11 -58
- package/components/overlays/src/OverlaysManager.js +2 -2
- package/components/overlays/src/configurations/withDropdownConfig.js +2 -22
- package/components/overlays/src/configurations/withPopoverConfig.js +32 -0
- package/components/overlays/src/utils/adopt-styles.js +5 -4
- package/components/overlays/src/utils/get-focusable-elements.js +0 -4
- package/components/overlays/src/utils/is-equal-config.js +6 -10
- package/components/overlays/test/OverlayController.test.js +21 -128
- 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/ArrowMixinTypes.ts +1 -3
- package/components/overlays/types/OverlayConfig.ts +5 -20
- package/components/select-rich/test/lion-select-rich.test.js +0 -3
- package/components/switch/src/LionSwitchButton.js +8 -3
- package/components/switch/test/lion-switch-button.test.js +38 -0
- package/custom-elements.json +17566 -30468
- package/dist-types/components/button/src/LionButton.d.ts +0 -1
- package/dist-types/components/button/src/LionButton.d.ts.map +1 -1
- package/dist-types/components/button/test-suites/LionButton.suite.d.ts.map +1 -1
- package/dist-types/components/calendar/src/LionCalendar.d.ts.map +1 -1
- package/dist-types/components/dialog/src/LionDialog.d.ts +1 -5
- package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
- package/dist-types/components/form-core/src/form-group/FormGroupMixin.d.ts.map +1 -1
- package/dist-types/components/form-core/src/registration/FormRegistrarMixin.d.ts.map +1 -1
- package/dist-types/components/form-core/test-suites/ValidateMixin.suite.d.ts.map +1 -1
- package/dist-types/components/form-core/test-suites/ValidateMixinFeedbackPart.suite.d.ts.map +1 -1
- package/dist-types/components/input-datepicker/src/localizeNamespaceLoader.d.ts.map +1 -1
- package/dist-types/components/listbox/test-suites/ListboxMixin.suite.d.ts.map +1 -1
- package/dist-types/components/localize/src/date/utils/normalizeIntlDate.d.ts.map +1 -1
- package/dist-types/components/overlays/src/OverlayController.d.ts +12 -24
- 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/configurations/withDropdownConfig.d.ts +0 -1
- package/dist-types/components/overlays/src/configurations/withDropdownConfig.d.ts.map +1 -1
- package/dist-types/components/overlays/src/configurations/withPopoverConfig.d.ts +6 -0
- package/dist-types/components/overlays/src/configurations/withPopoverConfig.d.ts.map +1 -0
- package/dist-types/components/overlays/src/utils/get-focusable-elements.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/ArrowMixinTypes.d.ts.map +1 -1
- package/dist-types/components/overlays/types/OverlayConfig.d.ts +2 -12
- package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
- package/dist-types/components/select-rich/src/LionSelectRich.d.ts +2 -10
- package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
- package/dist-types/components/switch/src/LionSwitchButton.d.ts +6 -0
- package/dist-types/components/switch/src/LionSwitchButton.d.ts.map +1 -1
- package/dist-types/exports/overlays.d.ts +1 -3
- 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/api-table.md +8 -21
- package/docs/components/combobox/overview.md +1 -1
- package/docs/components/combobox/use-cases.md +1 -1
- package/docs/components/dialog/api-table.md +6 -31
- 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/api-table.md +4 -21
- 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/radio-group/overview.md +3 -3
- package/docs/components/select/use-cases.md +1 -1
- package/docs/components/select-rich/api-table.md +5 -21
- package/docs/components/select-rich/use-cases.md +2 -2
- package/docs/components/switch/api-table.md +2 -0
- package/docs/components/tabs/examples.md +1 -1
- package/docs/components/tooltip/api-table.md +11 -36
- 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 -36
- 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/collapsible.js +0 -1
- package/exports/overlays.js +1 -6
- package/overlays.js +1 -6
- package/package.json +1 -1
- package/components/collapsible/src/DisclosureMixin.js +0 -313
- package/components/collapsible/test-suites/DisclosureMixin.suite.js +0 -30
- package/components/collapsible/types/DisclosureMixinTypes.ts +0 -48
- package/components/menu/CHANGELOG.md +0 -37
- package/components/menu/README.md +0 -335
- package/components/menu/index.js +0 -2
- package/components/menu/lion-menu-overlay.js +0 -3
- package/components/menu/lion-menu.js +0 -3
- package/components/menu/lion-spinbutton.js +0 -3
- package/components/menu/lion-toolbar.js +0 -3
- package/components/menu/lion-tree.js +0 -3
- package/components/menu/package.json +0 -46
- package/components/menu/requirements.temp.md +0 -12
- package/components/menu/src/InteractiveListMixin.js +0 -841
- package/components/menu/src/LionMenu.js +0 -250
- package/components/menu/src/LionMenuHybrid.js +0 -46
- package/components/menu/src/LionMenuOverlay.js +0 -155
- package/components/menu/src/LionSpinbutton.js +0 -213
- package/components/menu/src/LionToolbar.js +0 -94
- package/components/menu/src/LionTree.js +0 -52
- package/components/menu/src/MoreButtonMenuMixin.js +0 -278
- package/components/menu/src/MultiLevelListMixin.js +0 -274
- package/components/menu/src/utils/isInView.js +0 -26
- package/components/menu/src/utils/listItemInteractions.js +0 -124
- package/components/menu/src/utils/uuid.js +0 -3
- package/components/menu/test/lion-menu.test.js +0 -6
- package/components/menu/test-suites/InteractiveListMixin.suite.js +0 -642
- package/components/menu/test-suites/LionMenuHybrid.suite.js +0 -583
- package/components/menu/types/InteractiveListMixinTypes.d.ts +0 -82
- package/components/navigation-bar/README.md +0 -1
- package/components/navigation-bar/src/LionNavigationBar.js +0 -439
- package/components/navigation-bar/src/types.js +0 -41
- package/components/overlays/NEXT.md +0 -49
- package/dist-types/components/collapsible/src/DisclosureMixin.d.ts +0 -3
- package/dist-types/components/collapsible/src/DisclosureMixin.d.ts.map +0 -1
- package/dist-types/components/collapsible/test-suites/DisclosureMixin.suite.d.ts +0 -2
- package/dist-types/components/collapsible/test-suites/DisclosureMixin.suite.d.ts.map +0 -1
- package/dist-types/components/collapsible/types/DisclosureMixinTypes.d.ts +0 -24
- package/dist-types/components/collapsible/types/DisclosureMixinTypes.d.ts.map +0 -1
- package/dist-types/components/menu/index.d.ts +0 -3
- package/dist-types/components/menu/index.d.ts.map +0 -1
- package/dist-types/components/menu/lion-menu-overlay.d.ts +0 -2
- package/dist-types/components/menu/lion-menu-overlay.d.ts.map +0 -1
- package/dist-types/components/menu/lion-menu.d.ts +0 -2
- package/dist-types/components/menu/lion-menu.d.ts.map +0 -1
- package/dist-types/components/menu/lion-spinbutton.d.ts +0 -2
- package/dist-types/components/menu/lion-spinbutton.d.ts.map +0 -1
- package/dist-types/components/menu/lion-toolbar.d.ts +0 -2
- package/dist-types/components/menu/lion-toolbar.d.ts.map +0 -1
- package/dist-types/components/menu/lion-tree.d.ts +0 -2
- package/dist-types/components/menu/lion-tree.d.ts.map +0 -1
- package/dist-types/components/menu/src/InteractiveListMixin.d.ts +0 -617
- package/dist-types/components/menu/src/InteractiveListMixin.d.ts.map +0 -1
- package/dist-types/components/menu/src/LionMenu.d.ts +0 -565
- package/dist-types/components/menu/src/LionMenu.d.ts.map +0 -1
- package/dist-types/components/menu/src/LionMenuHybrid.d.ts +0 -19
- package/dist-types/components/menu/src/LionMenuHybrid.d.ts.map +0 -1
- package/dist-types/components/menu/src/LionMenuOverlay.d.ts +0 -1013
- package/dist-types/components/menu/src/LionMenuOverlay.d.ts.map +0 -1
- package/dist-types/components/menu/src/LionSpinbutton.d.ts +0 -58
- package/dist-types/components/menu/src/LionSpinbutton.d.ts.map +0 -1
- package/dist-types/components/menu/src/LionToolbar.d.ts +0 -8
- package/dist-types/components/menu/src/LionToolbar.d.ts.map +0 -1
- package/dist-types/components/menu/src/LionTree.d.ts +0 -447
- package/dist-types/components/menu/src/LionTree.d.ts.map +0 -1
- package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts +0 -39
- package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts.map +0 -1
- package/dist-types/components/menu/src/MultiLevelListMixin.d.ts +0 -495
- package/dist-types/components/menu/src/MultiLevelListMixin.d.ts.map +0 -1
- package/dist-types/components/menu/src/utils/isInView.d.ts +0 -9
- package/dist-types/components/menu/src/utils/isInView.d.ts.map +0 -1
- package/dist-types/components/menu/src/utils/listItemInteractions.d.ts +0 -48
- package/dist-types/components/menu/src/utils/listItemInteractions.d.ts.map +0 -1
- package/dist-types/components/menu/src/utils/uuid.d.ts +0 -2
- package/dist-types/components/menu/src/utils/uuid.d.ts.map +0 -1
- package/dist-types/components/menu/test/lion-menu.test.d.ts +0 -2
- package/dist-types/components/menu/test/lion-menu.test.d.ts.map +0 -1
- package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts +0 -14
- package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts.map +0 -1
- package/dist-types/components/menu/test-suites/LionMenuHybrid.suite.d.ts +0 -5
- package/dist-types/components/menu/test-suites/LionMenuHybrid.suite.d.ts.map +0 -1
- package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts +0 -238
- package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts.map +0 -1
- package/dist-types/components/navigation-bar/src/types.d.ts +0 -85
- package/dist-types/components/navigation-bar/src/types.d.ts.map +0 -1
- package/dist-types/exports/define/lion-item.d.ts +0 -2
- package/dist-types/exports/define/lion-item.d.ts.map +0 -1
- package/dist-types/exports/define/lion-menu-overlay.d.ts +0 -2
- package/dist-types/exports/define/lion-menu-overlay.d.ts.map +0 -1
- package/dist-types/exports/define/lion-menu.d.ts +0 -2
- package/dist-types/exports/define/lion-menu.d.ts.map +0 -1
- package/dist-types/exports/define/lion-navigation-bar.d.ts +0 -2
- package/dist-types/exports/define/lion-navigation-bar.d.ts.map +0 -1
- package/dist-types/exports/define/lion-toolbar.d.ts +0 -2
- package/dist-types/exports/define/lion-toolbar.d.ts.map +0 -1
- package/dist-types/exports/define/lion-tree.d.ts +0 -2
- package/dist-types/exports/define/lion-tree.d.ts.map +0 -1
- package/dist-types/exports/menu-test-suites.d.ts +0 -2
- package/dist-types/exports/menu-test-suites.d.ts.map +0 -1
- package/dist-types/exports/menu.d.ts +0 -7
- package/dist-types/exports/menu.d.ts.map +0 -1
- package/dist-types/exports/navigation-bar.d.ts +0 -2
- package/dist-types/exports/navigation-bar.d.ts.map +0 -1
- package/docs/components/menu/api-table.md +0 -455
- package/docs/components/menu/applyMenuDemoStyles.js +0 -173
- package/docs/components/menu/extensions.md +0 -262
- package/docs/components/menu/index.md +0 -13
- package/docs/components/menu/overview.md +0 -422
- package/exports/define/lion-item.js +0 -3
- package/exports/define/lion-menu-overlay.js +0 -3
- package/exports/define/lion-menu.js +0 -3
- package/exports/define/lion-navigation-bar.js +0 -3
- package/exports/define/lion-toolbar.js +0 -3
- package/exports/define/lion-tree.js +0 -3
- package/exports/menu-test-suites.js +0 -2
- package/exports/menu.js +0 -7
- package/exports/navigation-bar.js +0 -1
- package/menu-test-suites.js +0 -2
- package/menu.js +0 -7
- package/navigation-bar.js +0 -1
|
@@ -1,841 +0,0 @@
|
|
|
1
|
-
/* eslint-disable max-classes-per-file */
|
|
2
|
-
/* eslint-disable import/no-extraneous-dependencies */
|
|
3
|
-
import { html, css, LitElement } from 'lit';
|
|
4
|
-
import { SlotMixin, DisabledMixin } from '@lion/ui/core.js';
|
|
5
|
-
import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
|
6
|
-
import { MoreButtonMenuMixin } from './MoreButtonMenuMixin.js';
|
|
7
|
-
import { uuid } from './utils/uuid.js';
|
|
8
|
-
import { isInView } from './utils/isInView.js';
|
|
9
|
-
import {
|
|
10
|
-
isDisabled,
|
|
11
|
-
isChecked,
|
|
12
|
-
setChecked,
|
|
13
|
-
toggleChecked,
|
|
14
|
-
isActive,
|
|
15
|
-
setActive,
|
|
16
|
-
getValue,
|
|
17
|
-
} from './utils/listItemInteractions.js';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @typedef {import('../types/InteractiveListMixinTypes.js').InteractiveListItemRole} InteractiveListItemRole
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
// TODO: consider renaming to FocusGroupMixin
|
|
24
|
-
|
|
25
|
-
// TODO: make all available in controller/directive (same logic with an elegant prop-to-host-mapping)
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @param {Element} potentialFocusable
|
|
29
|
-
* @returns {Element|null}
|
|
30
|
-
*/
|
|
31
|
-
function isFocusableElement(potentialFocusable) {
|
|
32
|
-
// @ts-ignore - returns Element or falsy value
|
|
33
|
-
return (
|
|
34
|
-
potentialFocusable &&
|
|
35
|
-
(potentialFocusable.hasAttribute('tabindex') ||
|
|
36
|
-
['BUTTON', 'A'].includes(potentialFocusable.tagName))
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Lightweight component supporting InteractiveListItemRole as a type.
|
|
42
|
-
*
|
|
43
|
-
* Supports:
|
|
44
|
-
* - 1. menu item
|
|
45
|
-
* <lion-item> Item label </lion-item>
|
|
46
|
-
* - 2. sub menu
|
|
47
|
-
* <lion-item>
|
|
48
|
-
* <button slot="invoker"> Item label </button>
|
|
49
|
-
* <lion-menu slot="subitems"> ... </lion-menu>
|
|
50
|
-
* </lion-item>
|
|
51
|
-
*
|
|
52
|
-
* Sets the role property on the right element. For case 1, this would be the item itself,
|
|
53
|
-
* for case 2 it would be the [slot=invoker].
|
|
54
|
-
*/
|
|
55
|
-
export class LionItem extends LitElement {
|
|
56
|
-
/** @type {any} */
|
|
57
|
-
static get properties() {
|
|
58
|
-
return {
|
|
59
|
-
type: { type: String },
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
get _invokerNode() {
|
|
64
|
-
return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
|
|
65
|
-
child => child.slot === 'invoker',
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
static styles = [
|
|
70
|
-
css`
|
|
71
|
-
:host {
|
|
72
|
-
display: block;
|
|
73
|
-
}
|
|
74
|
-
`,
|
|
75
|
-
];
|
|
76
|
-
|
|
77
|
-
constructor() {
|
|
78
|
-
super();
|
|
79
|
-
/** @type {InteractiveListItemRole |''} */
|
|
80
|
-
this.type = '';
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
connectedCallback() {
|
|
84
|
-
super.connectedCallback();
|
|
85
|
-
|
|
86
|
-
if (!this._computedType) {
|
|
87
|
-
this._computedType = this.type || 'listitem';
|
|
88
|
-
}
|
|
89
|
-
if (this._invokerNode) {
|
|
90
|
-
this._invokerNode.setAttribute('role', this._computedType);
|
|
91
|
-
} else {
|
|
92
|
-
this.setAttribute('role', this._computedType);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
render() {
|
|
97
|
-
return html`
|
|
98
|
-
<slot name="invoker"></slot>
|
|
99
|
-
<slot></slot>
|
|
100
|
-
`;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export class LionMenuitem extends LionItem {
|
|
105
|
-
connectedCallback() {
|
|
106
|
-
this._computedType = `menuitem${this.type}`;
|
|
107
|
-
super.connectedCallback();
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
customElements.define('lion-menuitem', LionMenuitem);
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Handles keyboard interaction plus bookkeeping for active and selected states.
|
|
114
|
-
* Can be used as a fundament for:
|
|
115
|
-
* - [role="menu|menubar"] having children with [role="menuitem|menuitemcheckbox|menuitemradio"]
|
|
116
|
-
* - [role="listbox"] having children with [role="option"]
|
|
117
|
-
* - [role="toolbar"] having children with [role="radiogroup|radio|group|checkbox|spinbutton"]
|
|
118
|
-
* - [role="tree"] having children with [role="group|treeitem"]
|
|
119
|
-
*
|
|
120
|
-
* An interactive list accepts two types of items:
|
|
121
|
-
* - 'smart' items: responsible for registering themselves and reflecting accessibility properties,
|
|
122
|
-
* for example LionOption
|
|
123
|
-
* - 'regular' items with a [role="menuitem|menuitemcheckbox|menuitemradio|option|radio|checkbox|treeitem|listitem]
|
|
124
|
-
*
|
|
125
|
-
* Features:
|
|
126
|
-
* - keyboard navigation according to [aria specs](https://www.w3.org/TR/wai-aria-practices-1.1) for role
|
|
127
|
-
* - types
|
|
128
|
-
*
|
|
129
|
-
* @param {import('@open-wc/dedupe-mixin').Constructor<import('lit').LitElement>} superclass
|
|
130
|
-
*/
|
|
131
|
-
const InteractiveListMixinImplementation = superclass =>
|
|
132
|
-
// @ts-ignore https://github.com/microsoft/TypeScript/issues/36821#issuecomment-588375051
|
|
133
|
-
class InteractiveListMixin extends DisabledMixin(SlotMixin(MoreButtonMenuMixin(superclass))) {
|
|
134
|
-
/** @type {any} */
|
|
135
|
-
static styles = [
|
|
136
|
-
css`
|
|
137
|
-
:host {
|
|
138
|
-
display: block;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
:host([hidden]) {
|
|
142
|
-
display: none;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
:host([disabled]) {
|
|
146
|
-
color: #adadad;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
:host([orientation='horizontal']) ::slotted([slot='list']) {
|
|
150
|
-
display: flex;
|
|
151
|
-
}
|
|
152
|
-
`,
|
|
153
|
-
];
|
|
154
|
-
|
|
155
|
-
render() {
|
|
156
|
-
return html`
|
|
157
|
-
<slot name="list"></slot>
|
|
158
|
-
<slot></slot>
|
|
159
|
-
`;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
static get properties() {
|
|
163
|
-
return {
|
|
164
|
-
// TODO: align with name of open-ui => "inline|block" in focusgroup
|
|
165
|
-
orientation: { type: String, reflect: true },
|
|
166
|
-
// TODO: align with name of platform => ("multiple" in select, selection not part of focusgroup)
|
|
167
|
-
multipleChoice: { type: Boolean, attribute: 'multiple-choice' },
|
|
168
|
-
// TODO: align with name of open-ui
|
|
169
|
-
selectionFollowsFocus: { type: Boolean, attribute: 'selection-follows-focus' },
|
|
170
|
-
// TODO: align with name of open-ui => "wrap" in focsugroup
|
|
171
|
-
rotateKeyboardNavigation: { type: Boolean, attribute: 'rotate-keyboard-navigation' },
|
|
172
|
-
// TODO: align with name of open-ui
|
|
173
|
-
noPreselect: { type: Boolean, attribute: 'no-preselect' },
|
|
174
|
-
|
|
175
|
-
// TODO: implement, for now we start with only more-menu. See instructions in code about more menu.
|
|
176
|
-
// Values:
|
|
177
|
-
// - 'more-menu'
|
|
178
|
-
// - 'scroll-buttons' (think of those material design tab bars)
|
|
179
|
-
// - 'scroll-bar' (puts overflow: scroll on it) (think of tab bars, tables etc.)
|
|
180
|
-
itemWrap: { type: Boolean, attribute: 'item-wrap' },
|
|
181
|
-
|
|
182
|
-
_activateOnTypedChars: Boolean,
|
|
183
|
-
_hasSmartListItems: Boolean,
|
|
184
|
-
_activeMode: String,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
get slots() {
|
|
189
|
-
return {
|
|
190
|
-
...super.slots,
|
|
191
|
-
list: () => document.createElement('div'),
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* This is the (usually focusable, unless combobox) element that will contain [aria-activedescendant]
|
|
197
|
-
* Depending on the children, role will be "listbox|menu|menubar|tree|toolbar"
|
|
198
|
-
* @type {HTMLElement}
|
|
199
|
-
*/
|
|
200
|
-
get _listNode() {
|
|
201
|
-
return /** @type {HTMLElement} */ (Array.from(this.children).find(c => c.slot === 'list'));
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
get _scrollTargetNode() {
|
|
205
|
-
return this;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Place in the template that contains children with ._childrenRoles
|
|
210
|
-
* @type {HTMLSlotElement}
|
|
211
|
-
*/
|
|
212
|
-
get _listItemsSlot() {
|
|
213
|
-
const slots = Array.from(
|
|
214
|
-
/** @type {ShadowRoot} */ (this.shadowRoot).querySelectorAll('slot'),
|
|
215
|
-
);
|
|
216
|
-
return /** @type {HTMLSlotElement} */ (slots.find(s => !s.name));
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* When used with FormRegistrarMixin, children will register themselves. Otherwise,
|
|
221
|
-
* __listItems are gathered via a slotchange listener
|
|
222
|
-
* @type {HTMLElement[]}
|
|
223
|
-
*/
|
|
224
|
-
get listItems() {
|
|
225
|
-
// @ts-ignore
|
|
226
|
-
return this.__listItems || this.formElements;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* The roles to look for when children are registered to listItems
|
|
231
|
-
* @overridable
|
|
232
|
-
* @type {InteractiveListItemRole[]}
|
|
233
|
-
*/
|
|
234
|
-
// eslint-disable-next-line class-methods-use-this
|
|
235
|
-
get _childrenRoles() {
|
|
236
|
-
// @ts-ignore - string type in array of InteractiveListItemRole
|
|
237
|
-
return [...this._interactiveChildrenRoles, 'listitem', 'group'];
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// eslint-disable-next-line class-methods-use-this
|
|
241
|
-
get _interactiveChildrenRoles() {
|
|
242
|
-
return ['menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'treeitem'];
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
get activeIndex() {
|
|
246
|
-
return this.listItems.findIndex(el => isActive(el));
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
set activeIndex(index) {
|
|
250
|
-
const activeItem = this.listItems[index];
|
|
251
|
-
|
|
252
|
-
if (!activeItem) return;
|
|
253
|
-
|
|
254
|
-
const prevActiveEl = this.activeItem;
|
|
255
|
-
// This can be a focusable element or a wrapper thereof
|
|
256
|
-
const el = this.listItems[index];
|
|
257
|
-
|
|
258
|
-
// Unset siblings
|
|
259
|
-
this.listItems.forEach(item => {
|
|
260
|
-
setActive(item, true);
|
|
261
|
-
});
|
|
262
|
-
setActive(el);
|
|
263
|
-
|
|
264
|
-
// Determine focusableEl and prevFocusableEl
|
|
265
|
-
|
|
266
|
-
let focusableEl;
|
|
267
|
-
let prevFocusableEl;
|
|
268
|
-
|
|
269
|
-
if (this._activeMode === 'activedescendant') {
|
|
270
|
-
focusableEl = el;
|
|
271
|
-
prevFocusableEl = prevActiveEl;
|
|
272
|
-
}
|
|
273
|
-
// activeMode is 'roving-tabindex' or 'tabbable-disclosure'
|
|
274
|
-
else {
|
|
275
|
-
// are we a focusable element or a wrapper thereof?
|
|
276
|
-
focusableEl = isFocusableElement(el) ? el : el.firstElementChild;
|
|
277
|
-
prevFocusableEl =
|
|
278
|
-
prevActiveEl &&
|
|
279
|
-
(isFocusableElement(prevActiveEl) ? prevActiveEl : prevActiveEl.firstElementChild);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (!focusableEl) return;
|
|
283
|
-
|
|
284
|
-
// Update 'active mode'
|
|
285
|
-
if (this._activeMode === 'activedescendant') {
|
|
286
|
-
this._listNode.setAttribute('aria-activedescendant', focusableEl.id);
|
|
287
|
-
} else if (
|
|
288
|
-
this._activeMode === 'roving-tabindex' ||
|
|
289
|
-
this._activeMode === 'tabbable-disclosure'
|
|
290
|
-
) {
|
|
291
|
-
// @ts-ignore - Element extends HTMLElement for focus
|
|
292
|
-
focusableEl.focus();
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// @ts-ignore - focusableEl is safely used as HTMLElement here
|
|
296
|
-
if (this.orientation === 'vertical' && !isInView(this._scrollTargetNode, focusableEl)) {
|
|
297
|
-
focusableEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (this._activeMode === 'roving-tabindex') {
|
|
301
|
-
if (prevFocusableEl) {
|
|
302
|
-
prevFocusableEl.setAttribute('tabindex', '-1');
|
|
303
|
-
}
|
|
304
|
-
focusableEl.setAttribute('tabindex', '0');
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
get activeItem() {
|
|
309
|
-
return this.listItems[this.activeIndex];
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* @type {number | number[]}
|
|
314
|
-
*/
|
|
315
|
-
get checkedIndex() {
|
|
316
|
-
if (!this.multipleChoice) {
|
|
317
|
-
return this.listItems.findIndex(o => isChecked(o));
|
|
318
|
-
}
|
|
319
|
-
return this.listItems.filter(o => isChecked(o)).map(o => this.listItems.indexOf(o));
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* @deprecated
|
|
324
|
-
* This setter exists for backwards compatibility of single choice groups.
|
|
325
|
-
* A setter api would be confusing for a multipleChoice group. Use `setCheckedIndex` instead.
|
|
326
|
-
* @param {number} index
|
|
327
|
-
*/
|
|
328
|
-
set checkedIndex(index) {
|
|
329
|
-
this.setCheckedIndex(index);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* When `multipleChoice` is false, will toggle, else will check provided index
|
|
334
|
-
* @param {Number} index
|
|
335
|
-
*/
|
|
336
|
-
setCheckedIndex(index) {
|
|
337
|
-
const item = this.listItems[index];
|
|
338
|
-
if (!item) return;
|
|
339
|
-
if (!this.multipleChoice) {
|
|
340
|
-
// Uncheck all
|
|
341
|
-
this.listItems.forEach(listItem => {
|
|
342
|
-
setChecked(listItem, true);
|
|
343
|
-
});
|
|
344
|
-
setChecked(this.listItems[index]);
|
|
345
|
-
} else {
|
|
346
|
-
toggleChecked(this.listItems[index]);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Since we cannot check instanceof with mixins, use this getter instead.
|
|
352
|
-
* This way, the outside world can confirm the type of an element
|
|
353
|
-
*/
|
|
354
|
-
// eslint-disable-next-line class-methods-use-this
|
|
355
|
-
get isInteractiveList() {
|
|
356
|
-
return true;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
constructor() {
|
|
360
|
-
super();
|
|
361
|
-
|
|
362
|
-
// TODO: copy descriptions from ListboxMixin and ChoiceGroupMixin to here
|
|
363
|
-
this.orientation = 'vertical';
|
|
364
|
-
this.multipleChoice = false;
|
|
365
|
-
this.selectionFollowsFocus = false;
|
|
366
|
-
this.rotateKeyboardNavigation = false;
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* By default, checkedIndex is set to 0. When noPreselect is true,
|
|
370
|
-
* checkedIndex will not be set
|
|
371
|
-
* @type {boolean}
|
|
372
|
-
*/
|
|
373
|
-
this.noPreselect = false;
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Whenever possible (for [role="listbox|menu"], usually with one level) we use an
|
|
377
|
-
* [active descendant](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_focus_activedescendant).
|
|
378
|
-
* For [role=toolbar], we use a [roving tabindex](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_roving_tabindex)
|
|
379
|
-
* Other examples:
|
|
380
|
-
* - [role="menubar"](https://www.w3.org/TR/wai-aria-practices-1.1/examples/menubar/menubar-1/menubar-1.html)
|
|
381
|
-
* - [role="tree"](https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-1/treeview-1b.html)
|
|
382
|
-
* For disclosure menus, we need keyboard navigation, but treat every list item
|
|
383
|
-
* (more precisely its anchor child) as a tab stop.
|
|
384
|
-
* @type {'activedescendant'|'roving-tabindex'|'tabbable-disclosure'|'none'}
|
|
385
|
-
*/
|
|
386
|
-
this._activeMode = 'activedescendant';
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* When children are 'smart' (like LionOption), checked and active states will be set
|
|
390
|
-
* to them and they are responsible for setting [aria-selected].
|
|
391
|
-
* When false, `get listItems()` needs to be configured to point to the list items.
|
|
392
|
-
* @type {boolean}
|
|
393
|
-
*/
|
|
394
|
-
this._hasSmartChildren = false;
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* The role that will be put on _listNode
|
|
398
|
-
* @type {'menu'|'menubar'|'listbox'|'tree'|'toolbar'}
|
|
399
|
-
*/
|
|
400
|
-
this._listRole = 'menu';
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* @type {HTMLElement[]}
|
|
404
|
-
*/
|
|
405
|
-
this.__listItems = [];
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* By default, a typed character (or a sequence thereof) will be matched against
|
|
409
|
-
* the values of list items. The first and closest match, becomes active
|
|
410
|
-
* (focused or activedescendant depending on _matchMode)
|
|
411
|
-
* @type {boolean}
|
|
412
|
-
*/
|
|
413
|
-
this._activateOnTypedChars = false;
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* The pending char sequence that will set active list item
|
|
417
|
-
* @type {number}
|
|
418
|
-
*/
|
|
419
|
-
this._activateOnCharTimeout = 2000;
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* @type {string[]}
|
|
423
|
-
*/
|
|
424
|
-
this.__typedChars = [];
|
|
425
|
-
|
|
426
|
-
this._onListClick = this._onListClick.bind(this);
|
|
427
|
-
this._onListKeyUp = this._onListKeyUp.bind(this);
|
|
428
|
-
this._onListKeyDown = this._onListKeyDown.bind(this);
|
|
429
|
-
this._onListFocusIn = this._onListFocusIn.bind(this);
|
|
430
|
-
}
|
|
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
|
-
|
|
442
|
-
/**
|
|
443
|
-
* @param {import('lit').PropertyValues } changedProperties
|
|
444
|
-
*/
|
|
445
|
-
firstUpdated(changedProperties) {
|
|
446
|
-
super.firstUpdated(changedProperties);
|
|
447
|
-
this._setupList();
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* @param {Node[]} nodes
|
|
452
|
-
*/
|
|
453
|
-
#identifyNewItemsAndInitListItems(nodes) {
|
|
454
|
-
/**
|
|
455
|
-
*
|
|
456
|
-
* @param {HTMLElement} node
|
|
457
|
-
* @param {{ newItems: HTMLElement[], level?: number }} opts
|
|
458
|
-
* @returns
|
|
459
|
-
*/
|
|
460
|
-
const handleInteractiveListAdditionLevel = (node, { newItems, level = 1 }) => {
|
|
461
|
-
// Move list item to element with aria-activedescendant ([role="listbox|menu|menubar"])
|
|
462
|
-
if (level === 1) {
|
|
463
|
-
this._listNode.appendChild(node);
|
|
464
|
-
if (this._hasSmartChildren || !(node instanceof Element)) {
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* When [role=group], we can have a grouped set within a level, like multiple
|
|
470
|
-
* menuitemradios or -checkboxes.
|
|
471
|
-
* When no role at all, we might deal with a wrapped node (for instance triggering a
|
|
472
|
-
* submenu)
|
|
473
|
-
* Alternatively, when _activeMode is ['tabbable-disclosure'](https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-navigation.html),
|
|
474
|
-
* we search for a[href]
|
|
475
|
-
*/
|
|
476
|
-
if (node.getAttribute('role') === 'group') {
|
|
477
|
-
// @ts-ignore - HTMLCollection is iterable
|
|
478
|
-
for (const child of node.children) {
|
|
479
|
-
// @ts-ignore
|
|
480
|
-
handleInteractiveListAdditionLevel(child, { newItems, level: level + 1 });
|
|
481
|
-
}
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
// Also support scenarios like these: https://adrianroselli.com/2019/06/link-disclosure-widget-navigation.html
|
|
485
|
-
// TODO: For now, we only allow direct children. Should we consider deeper nesting?
|
|
486
|
-
const focusableChildren = Array.from(node.children).filter(c => isFocusableElement(c));
|
|
487
|
-
if (focusableChildren.length) {
|
|
488
|
-
for (const focusableChild of focusableChildren) {
|
|
489
|
-
// @ts-ignore - focusableChild is HTMLElement
|
|
490
|
-
newItems.push(focusableChild);
|
|
491
|
-
}
|
|
492
|
-
} else {
|
|
493
|
-
// Then we must be focusable (either via activedescendant, tabindex)
|
|
494
|
-
newItems.push(node);
|
|
495
|
-
}
|
|
496
|
-
};
|
|
497
|
-
|
|
498
|
-
/** @type {HTMLElement[]} */
|
|
499
|
-
const newItems = [];
|
|
500
|
-
|
|
501
|
-
// Move items to _listNode and add interactive node references (with [role]) to listItems
|
|
502
|
-
nodes.forEach(node => {
|
|
503
|
-
// @ts-expect-error - node type compatibility
|
|
504
|
-
handleInteractiveListAdditionLevel(node, { newItems });
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
// @ts-expect-error
|
|
508
|
-
if (this.itemWrap && nodes.length) {
|
|
509
|
-
// @ts-expect-error
|
|
510
|
-
this._initMoreButtonMenu();
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
newItems.forEach(item => {
|
|
514
|
-
if (!this.__listItems.includes(item)) {
|
|
515
|
-
this.__listItems.push(item);
|
|
516
|
-
}
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
this._initListItems(newItems);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
_setupList() {
|
|
523
|
-
// /**
|
|
524
|
-
// * @param {HTMLElement} node
|
|
525
|
-
// */
|
|
526
|
-
// const shouldAddItem = node => {
|
|
527
|
-
// const role = /** @type {InteractiveListItemRole|'group'} */ (node.getAttribute('role'));
|
|
528
|
-
// return (
|
|
529
|
-
// (role && this._interactiveChildrenRoles.includes(role)) || node.hasAttribute('data-item')
|
|
530
|
-
// );
|
|
531
|
-
// };
|
|
532
|
-
|
|
533
|
-
this._listItemsSlot.addEventListener('slotchange', () => {
|
|
534
|
-
// N.B. we do not use `this._listItemsSlot.assignedNodes()`, as it omits comment nodes.
|
|
535
|
-
// Comment nodes are really important for lit-html)
|
|
536
|
-
const nodes = Array.from(
|
|
537
|
-
(this._listItemsSlot.assignedSlot || this).childNodes || [],
|
|
538
|
-
// @ts-expect-error
|
|
539
|
-
).filter(n => !n.slot);
|
|
540
|
-
this.#identifyNewItemsAndInitListItems(nodes);
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
this._listNode.setAttribute('role', this._listRole);
|
|
544
|
-
this._listNode.setAttribute(
|
|
545
|
-
'tabindex',
|
|
546
|
-
this._activeMode === 'tabbable-disclosure' ? '-1' : '0',
|
|
547
|
-
);
|
|
548
|
-
this._listNode.addEventListener('click', this._onListClick);
|
|
549
|
-
this._listNode.addEventListener('keyup', this._onListKeyUp);
|
|
550
|
-
this._listNode.addEventListener('keydown', this._onListKeyDown);
|
|
551
|
-
this._listNode.addEventListener('focusin', this._onListFocusIn);
|
|
552
|
-
|
|
553
|
-
// TODO: add "more menu" functionality here... when itemWrap is set. We only support "more-menu" (for now):
|
|
554
|
-
// 1. we can measure the width (if orientation is horizontal) of this._listNode
|
|
555
|
-
// and see how its children fit, moving them to an overlayController
|
|
556
|
-
// having hideVisually as hide mechanism (opening on focus, closing on blur).
|
|
557
|
-
// N.B. this._listNode has `display:flex`. This means we need to add `text-wrap: nowrap;`
|
|
558
|
-
// to avoid elems going over two lines, or give them a max-height based on measuring the height of one item.
|
|
559
|
-
// 2. N.B. hideVisually needs to be implementd in OverlayController.
|
|
560
|
-
// 3. when the more menu opens a next level, MultiLevelListMixin will be applied to the host as well.
|
|
561
|
-
// That means it knows when a child menu is open. If _subListMap of one of the items is open, we should hide more menu like this:
|
|
562
|
-
// https://jsfiddle.net/a4ocktfp/19/
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* When _listItemsSlot receives new items, they are initialized via this method
|
|
567
|
-
* @param {LionItem[]|HTMLElement[]} newItems the newly added items needing initialization
|
|
568
|
-
*/
|
|
569
|
-
_initListItems(newItems) {
|
|
570
|
-
if (this._activeMode === 'roving-tabindex') {
|
|
571
|
-
// Make item focusable, but not part of tab sequence
|
|
572
|
-
newItems.forEach(item => item.setAttribute?.('tabindex', '-1'));
|
|
573
|
-
} else if (this._activeMode === 'activedescendant') {
|
|
574
|
-
newItems.forEach(item => {
|
|
575
|
-
// eslint-disable-next-line no-param-reassign
|
|
576
|
-
item.id = item.id || `${this.localName}-item-${uuid()}`;
|
|
577
|
-
});
|
|
578
|
-
} // for 'tabbable-disclosure', we assume items are already focusable and part of tab sequence
|
|
579
|
-
|
|
580
|
-
if (!this.noPreselect && this.checkedIndex === -1) {
|
|
581
|
-
this.checkedIndex = 0;
|
|
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);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* Handles various keyboard controls
|
|
596
|
-
* @param {KeyboardEvent} ev - the keydown event object
|
|
597
|
-
*/
|
|
598
|
-
_onListKeyDown(ev) {
|
|
599
|
-
if (isDisabled(this)) {
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const charHandled = this.__onKeydownActivateTypedChars(ev);
|
|
604
|
-
if (charHandled) {
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const { key } = ev;
|
|
609
|
-
|
|
610
|
-
switch (key) {
|
|
611
|
-
case 'Enter':
|
|
612
|
-
case ' ':
|
|
613
|
-
ev.preventDefault();
|
|
614
|
-
// ev.stopPropagation(); // prevent nested menus from conflicting
|
|
615
|
-
this.setCheckedIndex(this.activeIndex);
|
|
616
|
-
break;
|
|
617
|
-
case 'ArrowUp':
|
|
618
|
-
ev.preventDefault();
|
|
619
|
-
if (this.orientation === 'vertical' || this._activeMode === 'tabbable-disclosure') {
|
|
620
|
-
this.activeIndex = this._getPreviousEnabledOption(this.activeIndex);
|
|
621
|
-
}
|
|
622
|
-
break;
|
|
623
|
-
case 'ArrowLeft':
|
|
624
|
-
if (this.orientation === 'horizontal' || this._activeMode === 'tabbable-disclosure') {
|
|
625
|
-
this.activeIndex = this._getPreviousEnabledOption(this.activeIndex);
|
|
626
|
-
}
|
|
627
|
-
break;
|
|
628
|
-
case 'ArrowDown':
|
|
629
|
-
ev.preventDefault();
|
|
630
|
-
if (this.orientation === 'vertical' || this._activeMode === 'tabbable-disclosure') {
|
|
631
|
-
this.activeIndex = this._getNextEnabledOption(this.activeIndex);
|
|
632
|
-
}
|
|
633
|
-
break;
|
|
634
|
-
case 'ArrowRight':
|
|
635
|
-
if (this.orientation === 'horizontal' || this._activeMode === 'tabbable-disclosure') {
|
|
636
|
-
this.activeIndex = this._getNextEnabledOption(this.activeIndex);
|
|
637
|
-
}
|
|
638
|
-
break;
|
|
639
|
-
case 'Home':
|
|
640
|
-
ev.preventDefault();
|
|
641
|
-
this.activeIndex = this._getNextEnabledOption(0, 0);
|
|
642
|
-
break;
|
|
643
|
-
case 'End':
|
|
644
|
-
ev.preventDefault();
|
|
645
|
-
this.activeIndex = this._getPreviousEnabledOption(this.listItems.length - 1, 0);
|
|
646
|
-
break;
|
|
647
|
-
/* no default */
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
const keys = ['ArrowUp', 'ArrowDown', 'Home', 'End'];
|
|
651
|
-
if (keys.includes(key) && this.selectionFollowsFocus && !this.multipleChoice) {
|
|
652
|
-
this.setCheckedIndex(this.activeIndex);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* @param {KeyboardEvent} ev - the keydown event object
|
|
658
|
-
* @returns {boolean} whether a character key or a number was typed
|
|
659
|
-
*/
|
|
660
|
-
__onKeydownActivateTypedChars(ev) {
|
|
661
|
-
if (!this._activateOnTypedChars) {
|
|
662
|
-
return false;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
const { key, code } = ev;
|
|
666
|
-
|
|
667
|
-
if (code.startsWith('Key') || code.startsWith('Digit') || code.startsWith('Numpad')) {
|
|
668
|
-
this.__typedChars.push(key);
|
|
669
|
-
// TODO: schedule in updated loop for perf
|
|
670
|
-
const chars = this.__typedChars.join('');
|
|
671
|
-
const matchItem = this.listItems.find(item =>
|
|
672
|
-
this._matchCharsAgainstItem({ chars: [chars], item }),
|
|
673
|
-
);
|
|
674
|
-
// @ts-ignore - matchItem might be undefined
|
|
675
|
-
this.activeIndex = this.listItems.indexOf(matchItem);
|
|
676
|
-
|
|
677
|
-
// @ts-ignore - __clearCharTimeout can be a function
|
|
678
|
-
if (typeof this.__clearCharTimeout === 'function') {
|
|
679
|
-
// @ts-ignore
|
|
680
|
-
this.__clearCharTimeout();
|
|
681
|
-
}
|
|
682
|
-
this.__clearCharTimeout = setTimeout(() => {
|
|
683
|
-
this.__typedChars = [];
|
|
684
|
-
}, this._activateOnCharTimeout);
|
|
685
|
-
|
|
686
|
-
return true;
|
|
687
|
-
}
|
|
688
|
-
return false;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
/**
|
|
692
|
-
* @overridable
|
|
693
|
-
* @param {{chars: string[], item: HTMLElement}} config
|
|
694
|
-
*/
|
|
695
|
-
// eslint-disable-next-line class-methods-use-this
|
|
696
|
-
_matchCharsAgainstItem({ chars, item }) {
|
|
697
|
-
const value = getValue(item).toLowerCase();
|
|
698
|
-
return value.startsWith(chars);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
/**
|
|
702
|
-
* @param {KeyboardEvent} ev
|
|
703
|
-
*/
|
|
704
|
-
__preventScrollingWithArrowKeys(ev) {
|
|
705
|
-
if (this.disabled) {
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
const { key } = ev;
|
|
709
|
-
switch (key) {
|
|
710
|
-
case 'ArrowUp':
|
|
711
|
-
case 'ArrowDown':
|
|
712
|
-
case 'Home':
|
|
713
|
-
case 'End':
|
|
714
|
-
ev.preventDefault();
|
|
715
|
-
/* no default */
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* @overridable
|
|
721
|
-
* @param {FocusEvent} ev
|
|
722
|
-
*/
|
|
723
|
-
_onListFocusIn(ev) {
|
|
724
|
-
/** @type {HTMLElement} */
|
|
725
|
-
// @ts-ignore - target is EventTarget
|
|
726
|
-
const { target } = ev;
|
|
727
|
-
// const item = /** @type {HTMLElement} */ (target.closest('[role]'));
|
|
728
|
-
// @ts-ignore - target is HTMLElement
|
|
729
|
-
const foundIndex = this.listItems.indexOf(target);
|
|
730
|
-
if (foundIndex > -1 && foundIndex !== this.activeIndex) {
|
|
731
|
-
this.activeIndex = foundIndex;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
* @overridable
|
|
737
|
-
* @param {MouseEvent} ev
|
|
738
|
-
*/
|
|
739
|
-
// eslint-disable-next-line class-methods-use-this, no-unused-vars
|
|
740
|
-
_onListClick(ev) {
|
|
741
|
-
/** @type {HTMLElement} */
|
|
742
|
-
// @ts-ignore - target is EventTarget
|
|
743
|
-
const { target } = ev;
|
|
744
|
-
const item = /** @type {HTMLElement} */ (target.closest('[role]')); // TODO: we might need a more specific contract
|
|
745
|
-
const foundIndex = this.listItems.indexOf(item);
|
|
746
|
-
if (foundIndex > -1) {
|
|
747
|
-
this.activeIndex = foundIndex;
|
|
748
|
-
this.setCheckedIndex(foundIndex);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
/**
|
|
753
|
-
* @overridable
|
|
754
|
-
* @param {KeyboardEvent} ev
|
|
755
|
-
*/
|
|
756
|
-
// eslint-disable-next-line class-methods-use-this, no-unused-vars
|
|
757
|
-
_onListKeyUp(ev) {
|
|
758
|
-
if (this.disabled) {
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
const { key } = ev;
|
|
762
|
-
// eslint-disable-next-line default-case
|
|
763
|
-
switch (key) {
|
|
764
|
-
case 'ArrowUp':
|
|
765
|
-
case 'ArrowDown':
|
|
766
|
-
case 'Home':
|
|
767
|
-
case 'End':
|
|
768
|
-
case ' ':
|
|
769
|
-
case 'Enter':
|
|
770
|
-
ev.preventDefault();
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
/**
|
|
775
|
-
* @param {number} currentIndex
|
|
776
|
-
* @param {number} offset
|
|
777
|
-
*/
|
|
778
|
-
__getNextOption(currentIndex, offset) {
|
|
779
|
-
/**
|
|
780
|
-
* @param {number} i
|
|
781
|
-
*/
|
|
782
|
-
const until = i => (offset === 1 ? i < this.listItems.length : i >= 0);
|
|
783
|
-
|
|
784
|
-
for (let i = currentIndex + offset; until(i); i += offset) {
|
|
785
|
-
if (this.listItems[i] && !isDisabled(this.listItems[i])) {
|
|
786
|
-
return i;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
if (this.rotateKeyboardNavigation) {
|
|
791
|
-
const startIndex = offset === -1 ? this.listItems.length - 1 : 0;
|
|
792
|
-
for (let i = startIndex; until(i); i += 1) {
|
|
793
|
-
if (this.listItems[i] && !isDisabled(this.listItems[i])) {
|
|
794
|
-
return i;
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
return currentIndex;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
/**
|
|
802
|
-
* @param {number} currentIndex
|
|
803
|
-
* @param {number} [offset=1]
|
|
804
|
-
*/
|
|
805
|
-
_getNextEnabledOption(currentIndex, offset = 1) {
|
|
806
|
-
return this.__getNextOption(currentIndex, offset);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
/**
|
|
810
|
-
* @param {number} currentIndex
|
|
811
|
-
* @param {number} [offset=-1]
|
|
812
|
-
*/
|
|
813
|
-
_getPreviousEnabledOption(currentIndex, offset = -1) {
|
|
814
|
-
return this.__getNextOption(currentIndex, offset);
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
/**
|
|
818
|
-
* @param {Location} location
|
|
819
|
-
*/
|
|
820
|
-
__syncCurrentPageWithLocationHref(location) {
|
|
821
|
-
this.listItems.forEach(item => {
|
|
822
|
-
if (/** @type {HTMLAnchorElement} */ (item).href) {
|
|
823
|
-
const pathname = location.pathname.endsWith('/')
|
|
824
|
-
? location.pathname.substring(0, location.pathname.length - 1)
|
|
825
|
-
: location.pathname;
|
|
826
|
-
if (pathname === /** @type {HTMLAnchorElement} */ (item).href) {
|
|
827
|
-
setChecked(item);
|
|
828
|
-
} else {
|
|
829
|
-
setChecked(item, true);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
/** @protected */
|
|
836
|
-
_onPopState = () => {
|
|
837
|
-
this.__syncCurrentPageWithLocationHref(document.location);
|
|
838
|
-
};
|
|
839
|
-
};
|
|
840
|
-
// @ts-ignore
|
|
841
|
-
export const InteractiveListMixin = dedupeMixin(InteractiveListMixinImplementation);
|