@lion/ui 0.17.0 → 0.18.0-menu-system.1
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 +1 -0
- package/components/button/src/LionButton.js +4 -0
- package/components/button/test-suites/LionButton.suite.js +1 -0
- package/components/calendar/src/LionCalendar.js +2 -0
- package/components/collapsible/src/DisclosureMixin.js +313 -0
- package/components/collapsible/types/DisclosureMixinTypes.ts +48 -0
- package/components/combobox/test/lion-combobox.test.js +1 -0
- package/components/form-core/src/form-group/FormGroupMixin.js +7 -0
- package/components/form-core/src/registration/FormRegistrarMixin.js +11 -0
- package/components/form-core/test/FormControlMixin.test.js +1 -0
- package/components/form-core/test/validate/Validator.test.js +2 -0
- package/components/form-core/test-suites/ValidateMixin.suite.js +2 -0
- package/components/form-core/test-suites/ValidateMixinFeedbackPart.suite.js +1 -0
- package/components/input-datepicker/src/localizeNamespaceLoader.js +1 -0
- package/components/input-datepicker/test/lion-input-datepicker.test.js +5 -6
- package/components/localize/test/date/formatDate.test.js +1 -0
- package/components/menu/CHANGELOG.md +37 -0
- package/components/menu/README.md +335 -0
- package/components/menu/index.js +2 -0
- package/components/menu/lion-menu-overlay.js +3 -0
- package/components/menu/lion-menu.js +3 -0
- package/components/menu/lion-spinbutton.js +3 -0
- package/components/menu/lion-toolbar.js +3 -0
- package/components/menu/lion-tree.js +3 -0
- package/components/menu/package.json +46 -0
- package/components/menu/requirements.temp.md +12 -0
- package/components/menu/src/InteractiveListMixin.js +805 -0
- package/components/menu/src/LionMenu.js +250 -0
- package/components/menu/src/LionMenuHybrid.js +46 -0
- package/components/menu/src/LionMenuOverlay.js +155 -0
- package/components/menu/src/LionSpinbutton.js +213 -0
- package/components/menu/src/LionToolbar.js +94 -0
- package/components/menu/src/LionTree.js +52 -0
- package/components/menu/src/MoreButtonMenuMixin.js +297 -0
- package/components/menu/src/MultiLevelListMixin.js +274 -0
- package/components/menu/src/utils/isInView.js +26 -0
- package/components/menu/src/utils/listItemInteractions.js +104 -0
- package/components/menu/src/utils/uuid.js +3 -0
- package/components/menu/test/lion-menu.test.js +3 -0
- package/components/menu/test-suites/InteractiveListMixin.suite.js +142 -0
- package/components/menu/types/InteractiveListMixinTypes.d.ts +82 -0
- package/components/navigation-bar/README.md +1 -0
- package/components/navigation-bar/src/LionNavigationBar.js +439 -0
- package/components/navigation-bar/src/types.js +41 -0
- package/components/overlays/NEXT.md +49 -0
- package/components/overlays/src/OverlayController.js +244 -111
- package/components/overlays/src/OverlayMixin.js +58 -11
- package/components/overlays/src/OverlaysManager.js +2 -2
- package/components/overlays/src/utils/adopt-styles.js +3 -2
- package/components/overlays/src/utils/get-focusable-elements.js +4 -0
- package/components/overlays/src/utils/is-equal-config.js +10 -6
- package/components/overlays/test/OverlayController.test.js +5 -3
- 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 +16 -5
- package/components/select-rich/test/lion-select-rich.test.js +3 -0
- package/custom-elements.json +32144 -19531
- package/dist-types/components/button/src/LionButton.d.ts +1 -0
- 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/collapsible/src/DisclosureMixin.d.ts +3 -0
- package/dist-types/components/collapsible/src/DisclosureMixin.d.ts.map +1 -0
- package/dist-types/components/collapsible/types/DisclosureMixinTypes.d.ts +24 -0
- package/dist-types/components/collapsible/types/DisclosureMixinTypes.d.ts.map +1 -0
- package/dist-types/components/dialog/src/LionDialog.d.ts +3 -1
- 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/menu/index.d.ts +3 -0
- package/dist-types/components/menu/index.d.ts.map +1 -0
- package/dist-types/components/menu/lion-menu-overlay.d.ts +2 -0
- package/dist-types/components/menu/lion-menu-overlay.d.ts.map +1 -0
- package/dist-types/components/menu/lion-menu.d.ts +2 -0
- package/dist-types/components/menu/lion-menu.d.ts.map +1 -0
- package/dist-types/components/menu/lion-spinbutton.d.ts +2 -0
- package/dist-types/components/menu/lion-spinbutton.d.ts.map +1 -0
- package/dist-types/components/menu/lion-toolbar.d.ts +2 -0
- package/dist-types/components/menu/lion-toolbar.d.ts.map +1 -0
- package/dist-types/components/menu/lion-tree.d.ts +2 -0
- package/dist-types/components/menu/lion-tree.d.ts.map +1 -0
- package/dist-types/components/menu/src/InteractiveListMixin.d.ts +611 -0
- package/dist-types/components/menu/src/InteractiveListMixin.d.ts.map +1 -0
- package/dist-types/components/menu/src/LionMenu.d.ts +563 -0
- package/dist-types/components/menu/src/LionMenu.d.ts.map +1 -0
- package/dist-types/components/menu/src/LionMenuHybrid.d.ts +19 -0
- package/dist-types/components/menu/src/LionMenuHybrid.d.ts.map +1 -0
- package/dist-types/components/menu/src/LionMenuOverlay.d.ts +1007 -0
- package/dist-types/components/menu/src/LionMenuOverlay.d.ts.map +1 -0
- package/dist-types/components/menu/src/LionSpinbutton.d.ts +58 -0
- package/dist-types/components/menu/src/LionSpinbutton.d.ts.map +1 -0
- package/dist-types/components/menu/src/LionToolbar.d.ts +8 -0
- package/dist-types/components/menu/src/LionToolbar.d.ts.map +1 -0
- package/dist-types/components/menu/src/LionTree.d.ts +445 -0
- package/dist-types/components/menu/src/LionTree.d.ts.map +1 -0
- 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 +493 -0
- package/dist-types/components/menu/src/MultiLevelListMixin.d.ts.map +1 -0
- package/dist-types/components/menu/src/utils/isInView.d.ts +9 -0
- package/dist-types/components/menu/src/utils/isInView.d.ts.map +1 -0
- package/dist-types/components/menu/src/utils/listItemInteractions.d.ts +42 -0
- package/dist-types/components/menu/src/utils/listItemInteractions.d.ts.map +1 -0
- package/dist-types/components/menu/src/utils/uuid.d.ts +2 -0
- package/dist-types/components/menu/src/utils/uuid.d.ts.map +1 -0
- package/dist-types/components/menu/test/lion-menu.test.d.ts +2 -0
- package/dist-types/components/menu/test/lion-menu.test.d.ts.map +1 -0
- package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts +13 -0
- package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts.map +1 -0
- package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts +238 -0
- package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts.map +1 -0
- package/dist-types/components/navigation-bar/src/types.d.ts +85 -0
- package/dist-types/components/navigation-bar/src/types.d.ts.map +1 -0
- package/dist-types/components/overlays/src/OverlayController.d.ts +9 -13
- 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/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/OverlayConfig.d.ts +10 -2
- package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
- package/dist-types/components/select-rich/src/LionSelectRich.d.ts +8 -2
- package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
- package/dist-types/exports/define/lion-item.d.ts +2 -0
- package/dist-types/exports/define/lion-item.d.ts.map +1 -0
- package/dist-types/exports/define/lion-menu-overlay.d.ts +2 -0
- package/dist-types/exports/define/lion-menu-overlay.d.ts.map +1 -0
- package/dist-types/exports/define/lion-menu.d.ts +2 -0
- package/dist-types/exports/define/lion-menu.d.ts.map +1 -0
- package/dist-types/exports/define/lion-navigation-bar.d.ts +2 -0
- package/dist-types/exports/define/lion-navigation-bar.d.ts.map +1 -0
- package/dist-types/exports/define/lion-toolbar.d.ts +2 -0
- package/dist-types/exports/define/lion-toolbar.d.ts.map +1 -0
- package/dist-types/exports/define/lion-tree.d.ts +2 -0
- package/dist-types/exports/define/lion-tree.d.ts.map +1 -0
- package/dist-types/exports/menu.d.ts +7 -0
- package/dist-types/exports/menu.d.ts.map +1 -0
- package/dist-types/exports/navigation-bar.d.ts +2 -0
- package/dist-types/exports/navigation-bar.d.ts.map +1 -0
- package/dist-types/exports/overlays.d.ts +3 -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/api-table.md +21 -8
- package/docs/components/combobox/overview.md +1 -1
- package/docs/components/combobox/use-cases.md +1 -1
- package/docs/components/dialog/api-table.md +31 -6
- 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 +21 -4
- 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 +450 -0
- package/docs/components/menu/applyMenuDemoStyles.js +173 -0
- package/docs/components/menu/extensions.md +235 -0
- package/docs/components/menu/index.md +13 -0
- package/docs/components/menu/overview.md +422 -0
- 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 +21 -5
- package/docs/components/select-rich/use-cases.md +2 -2
- package/docs/components/tabs/examples.md +1 -1
- package/docs/components/tooltip/api-table.md +36 -11
- 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/collapsible.js +1 -0
- package/exports/define/lion-item.js +3 -0
- package/exports/define/lion-menu-overlay.js +3 -0
- package/exports/define/lion-menu.js +3 -0
- package/exports/define/lion-navigation-bar.js +3 -0
- package/exports/define/lion-toolbar.js +3 -0
- package/exports/define/lion-tree.js +3 -0
- package/exports/menu.js +7 -0
- package/exports/navigation-bar.js +1 -0
- package/exports/overlays.js +6 -0
- package/menu.js +7 -0
- package/navigation-bar.js +1 -0
- package/overlays.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/* eslint-disable import/no-extraneous-dependencies */
|
|
2
|
+
import { html } from 'lit';
|
|
3
|
+
import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
|
4
|
+
// import { DisclosureMixin } from '@lion/ui/collapsible.js';
|
|
5
|
+
import { InteractiveListMixin } from './InteractiveListMixin.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {import('./InteractiveListMixin.js').LionItem} LionItem
|
|
9
|
+
* @typedef {import('../types/InteractiveListMixinTypes.js').InteractiveListHost} InteractiveList
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* All logic that is needed for interactive lists that are allowed to have multiple nested, collapsible levels
|
|
14
|
+
* Applies to [role=menu] and [role=tree]
|
|
15
|
+
*/
|
|
16
|
+
// @ts-ignore - mixin implementation
|
|
17
|
+
const MultiLevelListMixinImplementation = superclass =>
|
|
18
|
+
// @ts-ignore - mixin class extension
|
|
19
|
+
class extends InteractiveListMixin(superclass) {
|
|
20
|
+
render() {
|
|
21
|
+
return html`
|
|
22
|
+
<slot name="invoker"></slot>
|
|
23
|
+
<slot name="list"></slot>
|
|
24
|
+
<slot id="list-items-outlet"></slot>
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static get properties() {
|
|
29
|
+
return {
|
|
30
|
+
invokerNode: Object,
|
|
31
|
+
behaveAsAccordion: { type: Boolean, attribute: 'behave-as-accordion' },
|
|
32
|
+
level: { type: Number, reflect: true },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @configure DisclosureMixin
|
|
38
|
+
*/
|
|
39
|
+
get _invokerNode() {
|
|
40
|
+
// @ts-ignore - accessing static method and super property
|
|
41
|
+
return this.constructor._getFocusableInvokerEl(this.invokerNode) || super._invokerNode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @configure DisclosureMixin
|
|
46
|
+
* This will be compatible with DisclosureMixin and OverlayMixin. It contains the invoked
|
|
47
|
+
* InteractiveList widget (like menu|tree).
|
|
48
|
+
*/
|
|
49
|
+
get _contentNode() {
|
|
50
|
+
return this._listNode;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
constructor() {
|
|
54
|
+
super();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* When an invokerNode is supplied (usually by a parent level menu), it will take precedence
|
|
58
|
+
* over [slot=invoker] and previousElementSibling with [data-invoker] attribute
|
|
59
|
+
* @type {HTMLElement}
|
|
60
|
+
*/
|
|
61
|
+
// @ts-ignore - can be undefined initially
|
|
62
|
+
this.invokerNode = undefined;
|
|
63
|
+
/**
|
|
64
|
+
* @configure DisclosureMixin
|
|
65
|
+
*/
|
|
66
|
+
this.handleFocus = true;
|
|
67
|
+
/**
|
|
68
|
+
* The parent list if level > 1
|
|
69
|
+
* @type {InteractiveList}
|
|
70
|
+
*/
|
|
71
|
+
// @ts-ignore - can be undefined initially
|
|
72
|
+
this.parentList = undefined;
|
|
73
|
+
/**
|
|
74
|
+
* When true, will have a maximum of one submenu open at a time
|
|
75
|
+
*/
|
|
76
|
+
this.behaveAsAccordion = false;
|
|
77
|
+
/**
|
|
78
|
+
* The level of nested menus. Will be reflected as attribute for styling purposes
|
|
79
|
+
*/
|
|
80
|
+
this.level = 1;
|
|
81
|
+
/**
|
|
82
|
+
* @configure InteractiveListMixin
|
|
83
|
+
*/
|
|
84
|
+
this.noPreselect = true;
|
|
85
|
+
/**
|
|
86
|
+
* @configure InteractiveListMixin
|
|
87
|
+
*/
|
|
88
|
+
this._activeMode = 'roving-tabindex';
|
|
89
|
+
/**
|
|
90
|
+
* @type {Map<HTMLElement|LionItem,InteractiveList>}
|
|
91
|
+
*/
|
|
92
|
+
this._subListMap = new Map();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// /**
|
|
96
|
+
// * @param {import('lit-element').PropertyValues } changedProperties
|
|
97
|
+
// */
|
|
98
|
+
// updated(changedProperties) {
|
|
99
|
+
// super.updated(changedProperties);
|
|
100
|
+
|
|
101
|
+
// if (changedProperties.has('invokerNode')) {
|
|
102
|
+
// // TODO: update openableCtrl invokerNode once we make this a ctrl as well
|
|
103
|
+
// }
|
|
104
|
+
// }
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Gets the child InteractiveList, based on item
|
|
108
|
+
* @param {LionItem|HTMLElement} item can be sibling of or parent of InteractiveList
|
|
109
|
+
*/
|
|
110
|
+
static _getSubInteractiveList(item) {
|
|
111
|
+
/**
|
|
112
|
+
* Note that, according to W3C spec, the sub level element with [role=menu|menubar|tree]
|
|
113
|
+
* needs to be a sibling of the invoker element with
|
|
114
|
+
* [role=menuitem|menuitemchecbox|menuitemradio|treeitem] (and not the parent)
|
|
115
|
+
*
|
|
116
|
+
* In the example below <lion-menu> is considered as item
|
|
117
|
+
* @example
|
|
118
|
+
* <lion-menuitem>
|
|
119
|
+
* <button slot="invoker"></button>
|
|
120
|
+
* <lion-menu> ... </lion-menu>
|
|
121
|
+
* </lion-menuitem>
|
|
122
|
+
*/
|
|
123
|
+
// @ts-ignore - isInteractiveList property check
|
|
124
|
+
const siblingOfInvoker = item.nextElementSibling?.isInteractiveList;
|
|
125
|
+
if (siblingOfInvoker) {
|
|
126
|
+
return item.nextElementSibling;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* In the example below div[role=listitem] | [data-item] is considered as item
|
|
130
|
+
* @example
|
|
131
|
+
* <div role="listitem" data-item>
|
|
132
|
+
* <button slot="invoker"></button>
|
|
133
|
+
* <lion-menu> ... </lion-menu>
|
|
134
|
+
* </div>
|
|
135
|
+
*/
|
|
136
|
+
const childOfInvoker =
|
|
137
|
+
item.getAttribute?.('role') === 'listitem' || item.hasAttribute?.('data-item');
|
|
138
|
+
if (childOfInvoker) {
|
|
139
|
+
// @ts-ignore - isInteractiveList check
|
|
140
|
+
return Array.from(item.children).find(child => child.isInteractiveList);
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {*} newItems
|
|
147
|
+
*/
|
|
148
|
+
_initListItems(newItems) {
|
|
149
|
+
super._initListItems(newItems);
|
|
150
|
+
|
|
151
|
+
const ctor = this.constructor;
|
|
152
|
+
// @ts-ignore - forEach parameter types
|
|
153
|
+
newItems.forEach(item => {
|
|
154
|
+
// @ts-ignore - static method access
|
|
155
|
+
const subList = ctor._getSubInteractiveList(item);
|
|
156
|
+
if (subList) {
|
|
157
|
+
this._subListMap.set(item, subList);
|
|
158
|
+
subList.level = this.level + 1;
|
|
159
|
+
subList.parentList = this;
|
|
160
|
+
subList.invokerNode = item;
|
|
161
|
+
// @ts-ignore - invokerInteraction property
|
|
162
|
+
subList.invokerInteraction = this.invokerInteraction;
|
|
163
|
+
subList._activeMode = this._activeMode;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @enhance DisclosureMixin
|
|
170
|
+
* @param {Event} [ev]
|
|
171
|
+
*/
|
|
172
|
+
toggle(ev) {
|
|
173
|
+
// @ts-ignore - target property
|
|
174
|
+
if (ev && this._listNode.contains(ev.target)) {
|
|
175
|
+
// prevent nested menus (inside invokers) from triggering invoker
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// @ts-ignore - super.toggle method
|
|
179
|
+
super.toggle();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @enhance InteractiveListMixin
|
|
184
|
+
* @param {*} ev
|
|
185
|
+
*/
|
|
186
|
+
_onListKeyDown(ev) {
|
|
187
|
+
const targetFromsubList = Array.from(this._subListMap).find(
|
|
188
|
+
// @ts-ignore - contains method on InteractiveListHost
|
|
189
|
+
([, s]) => s === ev.target || s.contains(ev.target),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (targetFromsubList) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
super._onListKeyDown(ev);
|
|
197
|
+
|
|
198
|
+
const subListOfActiveItem = this._subListMap.get(this.activeItem);
|
|
199
|
+
const parentListOfActiveItem = this.parentList;
|
|
200
|
+
const { key } = ev;
|
|
201
|
+
|
|
202
|
+
if (key === 'Escape' && parentListOfActiveItem) {
|
|
203
|
+
// We need to stop here, or else we affect parent menu (handled by OverlayController)
|
|
204
|
+
ev.stopPropagation();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
switch (key) {
|
|
208
|
+
case 'Enter':
|
|
209
|
+
case ' ':
|
|
210
|
+
// make it work like a button
|
|
211
|
+
this.activeItem?.click();
|
|
212
|
+
break;
|
|
213
|
+
case 'ArrowDown':
|
|
214
|
+
if (
|
|
215
|
+
this.orientation === 'horizontal' &&
|
|
216
|
+
subListOfActiveItem &&
|
|
217
|
+
this._activeMode !== 'disclosure'
|
|
218
|
+
) {
|
|
219
|
+
this.activeItem.click();
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
case 'ArrowRight':
|
|
223
|
+
if (this.orientation === 'vertical' && subListOfActiveItem) {
|
|
224
|
+
this.activeItem.click();
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
case 'ArrowUp':
|
|
228
|
+
if (this.orientation === 'horizontal' && parentListOfActiveItem) {
|
|
229
|
+
// @ts-ignore - close method
|
|
230
|
+
this.close();
|
|
231
|
+
// @ts-ignore - _onOverlayShow method
|
|
232
|
+
parentListOfActiveItem._onOverlayShow();
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
case 'ArrowLeft':
|
|
236
|
+
if (this.orientation === 'vertical' && parentListOfActiveItem) {
|
|
237
|
+
// @ts-ignore - close method
|
|
238
|
+
this.close();
|
|
239
|
+
// @ts-ignore - _onOverlayShow method
|
|
240
|
+
parentListOfActiveItem._onOverlayShow();
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
/* no default */
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// /**
|
|
248
|
+
// * @enhance DisclosureMixin
|
|
249
|
+
// */
|
|
250
|
+
// _setupDisclosure() {
|
|
251
|
+
// if (!this._invokerNode) return;
|
|
252
|
+
|
|
253
|
+
// super._setupDisclosure();
|
|
254
|
+
// }
|
|
255
|
+
|
|
256
|
+
// /**
|
|
257
|
+
// * @enhance DisclosureMixin
|
|
258
|
+
// */
|
|
259
|
+
// _teardownDisclosure() {
|
|
260
|
+
// if (!this._invokerNode) return;
|
|
261
|
+
|
|
262
|
+
// super._teardownDisclosure();
|
|
263
|
+
// }
|
|
264
|
+
|
|
265
|
+
// /**
|
|
266
|
+
// * @enhance DisclosureMixin
|
|
267
|
+
// */
|
|
268
|
+
// async _onOpenedChanged() {
|
|
269
|
+
// if (!this._invokerNode) return;
|
|
270
|
+
|
|
271
|
+
// await super._onOpenedChanged();
|
|
272
|
+
// }
|
|
273
|
+
};
|
|
274
|
+
export const MultiLevelListMixin = dedupeMixin(MultiLevelListMixinImplementation);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determines whether element is visible within container
|
|
3
|
+
*
|
|
4
|
+
* @param {HTMLElement} container
|
|
5
|
+
* @param {HTMLElement} element
|
|
6
|
+
* @param {Boolean} [partial]
|
|
7
|
+
*/
|
|
8
|
+
export function isInView(container, element, partial = false) {
|
|
9
|
+
const cTop = container.scrollTop;
|
|
10
|
+
const cBottom = cTop + container.clientHeight;
|
|
11
|
+
const eTop = element.offsetTop;
|
|
12
|
+
const eBottom = eTop + element.clientHeight;
|
|
13
|
+
const isTotal = eTop >= cTop && eBottom <= cBottom;
|
|
14
|
+
let isPartial;
|
|
15
|
+
|
|
16
|
+
if (partial === true) {
|
|
17
|
+
isPartial = (eTop < cTop && eBottom > cTop) || (eBottom > cBottom && eTop < cBottom);
|
|
18
|
+
} else if (typeof partial === 'number') {
|
|
19
|
+
if (eTop < cTop && eBottom > cTop) {
|
|
20
|
+
isPartial = ((eBottom - cTop) * 100) / element.clientHeight > partial;
|
|
21
|
+
} else if (eBottom > cBottom && eTop < cBottom) {
|
|
22
|
+
isPartial = ((cBottom - eTop) * 100) / element.clientHeight > partial;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return isTotal || isPartial;
|
|
26
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Set of universal interactive list item state methods that are both compatible with
|
|
3
|
+
* 'smart' (LionOption) and 'regular' items
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {HTMLElement} item
|
|
8
|
+
*/
|
|
9
|
+
export function isDisabled(item) {
|
|
10
|
+
return item.hasAttribute('disabled') || item.getAttribute('aria-disabled') === 'true';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {HTMLElement} item
|
|
15
|
+
*/
|
|
16
|
+
export function setDisabled(item, unset = false) {
|
|
17
|
+
if (unset) {
|
|
18
|
+
item.removeAttribute('disabled');
|
|
19
|
+
item.setAttribute('aria-disabled', 'false');
|
|
20
|
+
} else {
|
|
21
|
+
item.setAttribute('disabled', '');
|
|
22
|
+
item.setAttribute('aria-disabled', 'true');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {HTMLElement} item
|
|
28
|
+
*/
|
|
29
|
+
export function toggleDisabled(item) {
|
|
30
|
+
setDisabled(item, isDisabled(item));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {HTMLElement} item
|
|
35
|
+
*/
|
|
36
|
+
export function isChecked(item) {
|
|
37
|
+
return item.hasAttribute('checked') || item.getAttribute('aria-selected') === 'true';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {HTMLElement} item
|
|
42
|
+
* @param {boolean} [shouldSet=false]
|
|
43
|
+
*/
|
|
44
|
+
export function setChecked(item, shouldSet = true) {
|
|
45
|
+
const ariaAttr = item.getAttribute('role') === 'option' ? 'selected' : 'checked';
|
|
46
|
+
if (shouldSet) {
|
|
47
|
+
item.setAttribute('checked', '');
|
|
48
|
+
item.setAttribute(`aria-${ariaAttr}`, 'true');
|
|
49
|
+
} else {
|
|
50
|
+
item.removeAttribute('checked');
|
|
51
|
+
item.setAttribute(`aria-${ariaAttr}`, 'false');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {HTMLElement} item
|
|
57
|
+
*/
|
|
58
|
+
export function toggleChecked(item) {
|
|
59
|
+
setChecked(item, !isChecked(item));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {HTMLElement} item
|
|
64
|
+
*/
|
|
65
|
+
export function isActive(item) {
|
|
66
|
+
return item.hasAttribute('active');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {HTMLElement} item
|
|
71
|
+
*/
|
|
72
|
+
export function setActive(item, unset = false) {
|
|
73
|
+
if (unset) {
|
|
74
|
+
item.removeAttribute('active');
|
|
75
|
+
} else {
|
|
76
|
+
item.setAttribute('active', '');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {HTMLElement} item
|
|
82
|
+
*/
|
|
83
|
+
function getDirectTextContent(item) {
|
|
84
|
+
return Array.from(item.childNodes)
|
|
85
|
+
.filter(n => n.nodeName === '#text')
|
|
86
|
+
.map(n => n.textContent)
|
|
87
|
+
.join('')
|
|
88
|
+
.trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {HTMLElement} item
|
|
93
|
+
*/
|
|
94
|
+
export function getValue(item) {
|
|
95
|
+
return (
|
|
96
|
+
// @ts-ignore - choiceValue is a custom property
|
|
97
|
+
item.choiceValue ||
|
|
98
|
+
// @ts-ignore - value might not exist on all HTMLElements
|
|
99
|
+
item.value ||
|
|
100
|
+
item.getAttribute('value') ||
|
|
101
|
+
item.getAttribute('data-value') ||
|
|
102
|
+
getDirectTextContent(item)
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import { expect, html, fixture, unsafeStatic, defineCE } from '@open-wc/testing';
|
|
3
|
+
import { InteractiveListMixin } from '../src/InteractiveListMixin.js';
|
|
4
|
+
|
|
5
|
+
class InteractiveListClass extends InteractiveListMixin(LitElement) {}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {import('../types/InteractiveListMixinTypes.js').InteractiveListItemRole} InteractiveListItemRole
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** @type {InteractiveListItemRole[]} */
|
|
12
|
+
const supportedRoles = [
|
|
13
|
+
'menuitem',
|
|
14
|
+
'menuitemcheckbox',
|
|
15
|
+
'menuitemradio',
|
|
16
|
+
'option',
|
|
17
|
+
'treeitem',
|
|
18
|
+
'radio',
|
|
19
|
+
'checkbox',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} TestConfig
|
|
24
|
+
* @property {string} tagString
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {TestConfig} [customConfig]
|
|
29
|
+
*/
|
|
30
|
+
export function runInteractiveListMixinSuite(customConfig) {
|
|
31
|
+
const cfg = {
|
|
32
|
+
tagString: customConfig?.tagString || defineCE(InteractiveListClass),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const { tagString } = cfg;
|
|
36
|
+
const tag = unsafeStatic(tagString);
|
|
37
|
+
|
|
38
|
+
describe('InteractiveListMixin', () => {
|
|
39
|
+
it(`supports items with role="${supportedRoles.join('|')}"`, async () => {
|
|
40
|
+
const el = await fixture(html`
|
|
41
|
+
<${tag} name="foo">
|
|
42
|
+
${supportedRoles.map(role => html` <div role="${role}"></div> `)}
|
|
43
|
+
</${tag}>
|
|
44
|
+
`);
|
|
45
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
46
|
+
expect(el.listItems.length).to.equal(supportedRoles.length);
|
|
47
|
+
supportedRoles.forEach((role, i) => {
|
|
48
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
49
|
+
expect(el.listItems[i].getAttribute('role')).to.equal(role);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('moves elements to a private [slot=list] for optimal DX and a11y', async () => {
|
|
54
|
+
const el = await fixture(html`
|
|
55
|
+
<${tag} name="foo">
|
|
56
|
+
<div role="menuitem" id="item1">Item 1</div>
|
|
57
|
+
<div role="menuitem" id="item2">Item 2</div>
|
|
58
|
+
</${tag}>
|
|
59
|
+
`);
|
|
60
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
61
|
+
const listSlotNode = Array.from(el.children).find(c => c.slot === 'list');
|
|
62
|
+
// @ts-ignore - accessing children property
|
|
63
|
+
const listItemEls = Array.from(listSlotNode.children);
|
|
64
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
65
|
+
expect(listItemEls.includes(el.listItems[0])).to.be.true;
|
|
66
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
67
|
+
expect(listItemEls.includes(el.listItems[1])).to.be.true;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('inherits from DisclosureMixin', async () => {});
|
|
71
|
+
|
|
72
|
+
describe('Active mode', async () => {
|
|
73
|
+
it('supports "activedescendant" pattern', async () => {
|
|
74
|
+
const el = await fixture(html`
|
|
75
|
+
<${tag} name="foo" ._activeMode="${'activedescendant'}">
|
|
76
|
+
<div role="menuitem" id="item1">Item 1</div>
|
|
77
|
+
<div role="menuitem" id="item2">Item 2</div>
|
|
78
|
+
</${tag}>
|
|
79
|
+
`);
|
|
80
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
81
|
+
el.checkedIndex = 0;
|
|
82
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
83
|
+
const listSlotNode = Array.from(el.children).find(c => c.slot === 'list');
|
|
84
|
+
|
|
85
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
86
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
87
|
+
expect(el.listItems[0].hasAttribute('id')).to.be.true;
|
|
88
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
89
|
+
expect(el.listItems[1].hasAttribute('id')).to.be.true;
|
|
90
|
+
expect(listSlotNode?.hasAttribute('aria-activedescendant')).to.be.false;
|
|
91
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
92
|
+
el.activeIndex = 1;
|
|
93
|
+
expect(listSlotNode?.hasAttribute('aria-activedescendant')).to.be.true;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('supports "roving-tabindex" pattern', async () => {
|
|
97
|
+
const el = await fixture(html`
|
|
98
|
+
<${tag} name="foo" ._activeMode="${'roving-tabindex'}">
|
|
99
|
+
<div role="menuitem" id="item1">Item 1</div>
|
|
100
|
+
<div role="menuitem" id="item2">Item 2</div>
|
|
101
|
+
</${tag}>
|
|
102
|
+
`);
|
|
103
|
+
|
|
104
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
105
|
+
el.activeIndex = 0;
|
|
106
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
107
|
+
expect(el.listItems[0].getAttribute('tabindex')).to.equal('0');
|
|
108
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
109
|
+
expect(el.listItems[1].getAttribute('tabindex')).to.equal('-1');
|
|
110
|
+
|
|
111
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
112
|
+
el.activeIndex = 1;
|
|
113
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
114
|
+
expect(el.listItems[0].getAttribute('tabindex')).to.equal('-1');
|
|
115
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
116
|
+
expect(el.listItems[1].getAttribute('tabindex')).to.equal('0');
|
|
117
|
+
|
|
118
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
119
|
+
el.activeIndex = 1;
|
|
120
|
+
// @ts-ignore - test element with InteractiveListMixin properties
|
|
121
|
+
expect(document.activeElement).to.equal(el.listItems[1]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Copy tests from ListboxMixin. Later, make this mixin a fundament of the ListboxMixin
|
|
126
|
+
|
|
127
|
+
// orientation
|
|
128
|
+
// multipleChoice
|
|
129
|
+
// selectionFollowsFocus
|
|
130
|
+
// rotateKeyboardNavigation
|
|
131
|
+
// noPreselect
|
|
132
|
+
// _scrollTargetNode
|
|
133
|
+
|
|
134
|
+
describe('Widget extensions', () => {
|
|
135
|
+
it('can be extended to [role=listbox]', async () => {});
|
|
136
|
+
it('can be extended to [role=menu]', async () => {});
|
|
137
|
+
it('can be extended to [role=menubar]', async () => {});
|
|
138
|
+
it('can be extended to [role=toolbar]', async () => {});
|
|
139
|
+
it('can be extended to [role=tree]', async () => {});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Constructor } from '@open-wc/dedupe-mixin';
|
|
2
|
+
import { LitElement } from 'lit';
|
|
3
|
+
import { SlotHost } from '../../core/types/SlotMixinTypes.js';
|
|
4
|
+
import { DisabledHost } from '../../core/types/DisabledMixinTypes.js';
|
|
5
|
+
|
|
6
|
+
export type InteractiveListItemRole =
|
|
7
|
+
| 'menuitem'
|
|
8
|
+
| 'menuitemcheckbox'
|
|
9
|
+
| 'menuitemradio'
|
|
10
|
+
| 'option'
|
|
11
|
+
| 'treeitem'
|
|
12
|
+
| 'radio'
|
|
13
|
+
| 'checkbox';
|
|
14
|
+
|
|
15
|
+
export declare class InteractiveListHost {
|
|
16
|
+
/**
|
|
17
|
+
* When true, will synchronize activedescendant and selected element on
|
|
18
|
+
* arrow key navigation.
|
|
19
|
+
* This behavior can usually be seen on <select> on the Windows platform.
|
|
20
|
+
* Note that this behavior cannot be used when multiple-choice is true.
|
|
21
|
+
* See: https://www.w3.org/TR/wai-aria-practices/#kbd_selection_follows_focus
|
|
22
|
+
*/
|
|
23
|
+
public selectionFollowsFocus: Boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Will give first option active state when navigated to the next option from
|
|
26
|
+
* last option.
|
|
27
|
+
*/
|
|
28
|
+
public rotateKeyboardNavigation: Boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Informs screen reader and affects keyboard navigation.
|
|
31
|
+
* By default 'vertical'
|
|
32
|
+
*/
|
|
33
|
+
public orientation: 'vertical' | 'horizontal';
|
|
34
|
+
|
|
35
|
+
public hasNoDefaultSelected: boolean;
|
|
36
|
+
|
|
37
|
+
public singleOption: boolean;
|
|
38
|
+
|
|
39
|
+
public checkedIndex: number | number[];
|
|
40
|
+
|
|
41
|
+
public activeIndex: number;
|
|
42
|
+
|
|
43
|
+
public listItems: HTMLElement[];
|
|
44
|
+
|
|
45
|
+
public setCheckedIndex(index: number): void;
|
|
46
|
+
|
|
47
|
+
protected _scrollTargetNode: HTMLElement;
|
|
48
|
+
|
|
49
|
+
protected _listNode: HTMLElement;
|
|
50
|
+
|
|
51
|
+
// private __setupListboxNode(): void;
|
|
52
|
+
|
|
53
|
+
protected _getPreviousEnabledOption(currentIndex: number, offset?: number): number;
|
|
54
|
+
|
|
55
|
+
protected _getNextEnabledOption(currentIndex: number, offset?: number): number;
|
|
56
|
+
|
|
57
|
+
protected _onListKeyDown(ev: KeyboardEvent): void;
|
|
58
|
+
|
|
59
|
+
protected _onListKeyUp(ev: KeyboardEvent): void;
|
|
60
|
+
|
|
61
|
+
// protected _setupListboxNode(): void;
|
|
62
|
+
|
|
63
|
+
// protected _teardownListboxNode(): void;
|
|
64
|
+
|
|
65
|
+
protected _onListClick(ev: MouseEvent): void;
|
|
66
|
+
|
|
67
|
+
// protected _setupListboxInteractions(): void;
|
|
68
|
+
|
|
69
|
+
// protected _onChildActiveChanged(ev: Event): void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export declare function InteractiveListHostImplementation<T extends Constructor<LitElement>>(
|
|
73
|
+
superclass: T,
|
|
74
|
+
): T &
|
|
75
|
+
Constructor<InteractiveListHost> &
|
|
76
|
+
InteractiveListHost &
|
|
77
|
+
Constructor<DisabledHost> &
|
|
78
|
+
typeof DisabledHost &
|
|
79
|
+
Constructor<SlotHost> &
|
|
80
|
+
typeof SlotHost;
|
|
81
|
+
|
|
82
|
+
export type InteractiveListMixin = typeof InteractiveListHostImplementation;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
N.B. api is very very beta
|