@lion/ui 0.18.0-menu-system.5 → 0.18.0-menu-system.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/menu/src/InteractiveListMixin.js +11 -9
- package/components/menu/src/LionMenu.js +1 -1
- package/components/menu/src/LionTree.js +1 -1
- package/components/menu/src/MoreButtonMenuMixin.js +18 -27
- package/components/menu/src/MultiLevelListMixin.js +46 -9
- package/components/menu/src/utils/listItemInteractions.js +2 -1
- package/components/menu/test/InteractiveListMixin.test.js +3 -0
- package/components/menu/test/MoreButtonMenuMixin.test.js +4 -0
- package/components/menu/test/MultiLevelListMixin.test.js +3 -0
- package/components/menu/test/lion-menu-hybrid.test.js +3 -0
- package/components/menu/test/lion-menu.test.js +7 -0
- package/components/menu/test/lion-toolbar.test.js +8 -0
- package/components/menu/test/lion-tree.test.js +7 -0
- package/components/menu/test-helpers/mimicKeyPress.js +9 -0
- package/components/menu/test-suites/InteractiveListMixin.suite.js +155 -5
- package/components/menu/test-suites/LionMenu.suite.js +113 -0
- package/components/menu/test-suites/LionMenuHybrid.suite.js +583 -0
- package/components/menu/test-suites/MoreButtonMenuMixin.suite.js +372 -0
- package/components/menu/test-suites/MultiLevelListMixin.suite.js +303 -0
- package/components/menu/types/InteractiveListMixinTypes.d.ts +2 -0
- package/components/overlays/src/OverlayController.js +1 -1
- package/components/overlays/types/OverlayMixinTypes.ts +2 -0
- package/custom-elements.json +245 -153
- package/dist-types/components/combobox/src/LionCombobox.d.ts +1 -1
- package/dist-types/components/dialog/src/LionDialog.d.ts +1 -1
- package/dist-types/components/input-datepicker/src/LionInputDatepicker.d.ts +1 -1
- package/dist-types/components/menu/src/InteractiveListMixin.d.ts +1 -1
- package/dist-types/components/menu/src/InteractiveListMixin.d.ts.map +1 -1
- package/dist-types/components/menu/src/LionMenu.d.ts +75 -5
- package/dist-types/components/menu/src/LionMenu.d.ts.map +1 -1
- package/dist-types/components/menu/src/LionMenuOverlay.d.ts +4 -2
- package/dist-types/components/menu/src/LionMenuOverlay.d.ts.map +1 -1
- package/dist-types/components/menu/src/LionTree.d.ts +2 -2
- package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts +0 -1
- package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts.map +1 -1
- package/dist-types/components/menu/src/MultiLevelListMixin.d.ts +10 -2
- package/dist-types/components/menu/src/MultiLevelListMixin.d.ts.map +1 -1
- package/dist-types/components/menu/src/utils/listItemInteractions.d.ts.map +1 -1
- package/dist-types/components/menu/test/InteractiveListMixin.test.d.ts +2 -0
- package/dist-types/components/menu/test/InteractiveListMixin.test.d.ts.map +1 -0
- package/dist-types/components/menu/test/MoreButtonMenuMixin.test.d.ts +2 -0
- package/dist-types/components/menu/test/MoreButtonMenuMixin.test.d.ts.map +1 -0
- package/dist-types/components/menu/test/MultiLevelListMixin.test.d.ts +2 -0
- package/dist-types/components/menu/test/MultiLevelListMixin.test.d.ts.map +1 -0
- package/dist-types/components/menu/test/lion-menu-hybrid.test.d.ts +2 -0
- package/dist-types/components/menu/test/lion-menu-hybrid.test.d.ts.map +1 -0
- package/dist-types/components/menu/test/lion-toolbar.test.d.ts +2 -0
- package/dist-types/components/menu/test/lion-toolbar.test.d.ts.map +1 -0
- package/dist-types/components/menu/test/lion-tree.test.d.ts +2 -0
- package/dist-types/components/menu/test/lion-tree.test.d.ts.map +1 -0
- package/dist-types/components/menu/test-helpers/mimicKeyPress.d.ts +7 -0
- package/dist-types/components/menu/test-helpers/mimicKeyPress.d.ts.map +1 -0
- package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts.map +1 -1
- package/dist-types/components/menu/test-suites/LionMenu.suite.d.ts +9 -0
- package/dist-types/components/menu/test-suites/LionMenu.suite.d.ts.map +1 -0
- package/dist-types/components/menu/test-suites/LionMenuHybrid.suite.d.ts +5 -0
- package/dist-types/components/menu/test-suites/LionMenuHybrid.suite.d.ts.map +1 -0
- package/dist-types/components/menu/test-suites/MoreButtonMenuMixin.suite.d.ts +14 -0
- package/dist-types/components/menu/test-suites/MoreButtonMenuMixin.suite.d.ts.map +1 -0
- package/dist-types/components/menu/test-suites/MultiLevelListMixin.suite.d.ts +11 -0
- package/dist-types/components/menu/test-suites/MultiLevelListMixin.suite.d.ts.map +1 -0
- package/dist-types/components/overlays/types/OverlayMixinTypes.d.ts +1 -0
- package/dist-types/components/overlays/types/OverlayMixinTypes.d.ts.map +1 -1
- package/dist-types/components/select-rich/src/LionSelectRich.d.ts +1 -1
- package/dist-types/components/tooltip/src/LionTooltip.d.ts +1 -1
- package/dist-types/exports/menu-test-suites.d.ts +2 -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 +44 -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
- package/components/menu/src/utils/uuid.js +0 -3
- 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
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import { expect, html, fixture as _fixture, unsafeStatic, defineCE } from '@open-wc/testing';
|
|
3
|
+
import { sendMouse, resetMouse } from '@web/test-runner-commands';
|
|
4
|
+
import sinon from 'sinon';
|
|
5
|
+
import { getDeepActiveElement } from '@lion/ui/overlays.js';
|
|
6
|
+
import { InteractiveListMixin } from '../src/InteractiveListMixin.js';
|
|
7
|
+
|
|
8
|
+
class InteractiveListClass extends InteractiveListMixin(LitElement) {}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('../types/InteractiveListMixinTypes.js').InteractiveListItemRole} InteractiveListItemRole
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {import('../src/LionMenu.js').LionMenu} LionMenu
|
|
16
|
+
* @typedef {import('lit').TemplateResult} TemplateResult
|
|
17
|
+
*
|
|
18
|
+
* * @typedef {Object} TestConfig
|
|
19
|
+
* @property {string} tagString
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fixture = /** @type {(arg: TemplateResult) => Promise<LionMenu>} */ (_fixture);
|
|
23
|
+
|
|
24
|
+
const l1Config = {
|
|
25
|
+
isBar: true,
|
|
26
|
+
hasFullWidthFlyout: true,
|
|
27
|
+
openableConfig: {
|
|
28
|
+
// We want disclosure behavior
|
|
29
|
+
placementMode: 'custom',
|
|
30
|
+
// N.B. we don't have an invoker...
|
|
31
|
+
isActivated: false,
|
|
32
|
+
inheritsReferenceWidth: 'none',
|
|
33
|
+
focusContentOnOpen: true,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {TestConfig} [customConfig]
|
|
39
|
+
*/
|
|
40
|
+
export function runMoreButtonMenuMixinSuite(customConfig) {
|
|
41
|
+
const cfg = {
|
|
42
|
+
tagString: customConfig?.tagString || defineCE(InteractiveListClass),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const { tagString } = cfg;
|
|
46
|
+
const tag = unsafeStatic(tagString);
|
|
47
|
+
|
|
48
|
+
describe('MoreButtonMenuMixin', () => {
|
|
49
|
+
describe('Overflow handling', () => {
|
|
50
|
+
/** @type {import('sinon').SinonFakeTimers | null} */
|
|
51
|
+
let clock = null;
|
|
52
|
+
|
|
53
|
+
/** @param {HTMLElement} el */
|
|
54
|
+
const isMoreButtonShown = el => {
|
|
55
|
+
const wrapper = /** @type {HTMLElement | null} */ (
|
|
56
|
+
el.querySelector('[data-more-button-wrapper]')
|
|
57
|
+
);
|
|
58
|
+
return wrapper?.style.display !== 'none';
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** @param {HTMLElement} el */
|
|
62
|
+
const isMoreButtonMenuShown = el => {
|
|
63
|
+
const menu = el.querySelector('[data-more-button-menu]');
|
|
64
|
+
return menu ? getComputedStyle(menu).width !== '0px' : false;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** @param {HTMLElement} el */
|
|
68
|
+
const getMoreButton = el =>
|
|
69
|
+
/** @type {HTMLElement | null} */ (el.querySelector('[data-more-button]'));
|
|
70
|
+
/** @param {HTMLElement} el */
|
|
71
|
+
const getMoreButtonMenu = el => el.querySelector('[data-more-button-menu]');
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {HTMLElement} element
|
|
75
|
+
*/
|
|
76
|
+
function getCoordinates(element) {
|
|
77
|
+
const { x, y, height } = element.getBoundingClientRect();
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
x: Math.floor(x + window.pageXOffset),
|
|
81
|
+
y: Math.floor(y + window.pageYOffset + height / 2),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** @param {HTMLElement} el */
|
|
85
|
+
const clickOnMoreButton = async el => {
|
|
86
|
+
const moreButton = getMoreButton(el);
|
|
87
|
+
if (!moreButton) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const { x, y } = getCoordinates(moreButton);
|
|
91
|
+
await sendMouse({ type: 'click', position: [x, y] });
|
|
92
|
+
clock?.tick(100);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** @param {HTMLElement} el */
|
|
96
|
+
const getDirectListItemsUnderMoreButtonMenu = el => [
|
|
97
|
+
...Array.from(
|
|
98
|
+
el.querySelectorAll('[data-more-button-menu] > [role="listitem"]'),
|
|
99
|
+
item => /** @type {HTMLElement} */ (item),
|
|
100
|
+
),
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
/** @param {HTMLElement} el */
|
|
104
|
+
const getDirectListItemsUnderMainMenu = el => [
|
|
105
|
+
...Array.from(
|
|
106
|
+
el.querySelectorAll('[slot="list"] > [role="listitem"]'),
|
|
107
|
+
item => /** @type {HTMLElement} */ (item),
|
|
108
|
+
),
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
/** @param {HTMLElement & { updateComplete: Promise<unknown> }} el */
|
|
112
|
+
const waitResizeEventDebounce = async el => {
|
|
113
|
+
await el.updateComplete;
|
|
114
|
+
// wait for resize event debouncer
|
|
115
|
+
clock?.tick(55);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
clock = sinon.useFakeTimers({
|
|
120
|
+
shouldAdvanceTime: true,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
afterEach(async () => {
|
|
124
|
+
clock?.restore();
|
|
125
|
+
await resetMouse();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should show no More button when all items fit', async () => {
|
|
129
|
+
const el = await fixture(html`
|
|
130
|
+
<${tag} name="foo" ._activeMode="${'tabbable-disclosure'}" .itemWrap="${true}"
|
|
131
|
+
data-has-full-width-flyout orientation="horizontal" style="min-width: 170px; max-width: 170px;">
|
|
132
|
+
<div role="listitem" id="item1" style="min-width: 50px; max-width: 50px;">
|
|
133
|
+
<a href="#">Item 1</a>
|
|
134
|
+
</div>
|
|
135
|
+
<div role="listitem" id="item2" style="min-width: 50px; max-width: 50px;">
|
|
136
|
+
<a href="#">Item 2</a>
|
|
137
|
+
</div>
|
|
138
|
+
<div role="listitem" id="item3" style="min-width: 50px; max-width: 50px;">
|
|
139
|
+
<a href="#">Item 3</a>
|
|
140
|
+
</div>
|
|
141
|
+
<div slot="more-button" style="min-width: 50px; max-width: 50px;">
|
|
142
|
+
<button>More</button>
|
|
143
|
+
</div>
|
|
144
|
+
</${tag}>
|
|
145
|
+
`);
|
|
146
|
+
await waitResizeEventDebounce(el);
|
|
147
|
+
// 3 items is 150px. They fit into 170px parent
|
|
148
|
+
expect(isMoreButtonShown(el)).to.equal(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should show More button when not all items fit', async () => {
|
|
152
|
+
const el = await fixture(html`
|
|
153
|
+
<${tag}
|
|
154
|
+
.itemWrap="${true}"
|
|
155
|
+
?data-has-full-width-flyout="${l1Config.hasFullWidthFlyout}"
|
|
156
|
+
.config="${l1Config.openableConfig}"
|
|
157
|
+
.bar="${l1Config.isBar}"
|
|
158
|
+
._activeMode="${'tabbable-disclosure'}"
|
|
159
|
+
style="min-width: 170px; max-width: 170px;"
|
|
160
|
+
>
|
|
161
|
+
<div role="listitem" id="item1" style="min-width: 50px; max-width: 50px;">
|
|
162
|
+
<a href="#">Item 1</a>
|
|
163
|
+
</div>
|
|
164
|
+
<div role="listitem" id="item2" style="min-width: 50px; max-width: 50px;">
|
|
165
|
+
<a href="#">Item 2</a>
|
|
166
|
+
</div>
|
|
167
|
+
<div role="listitem" id="item3" style="min-width: 50px; max-width: 50px;">
|
|
168
|
+
<a href="#">Item 3</a>
|
|
169
|
+
</div>
|
|
170
|
+
<div role="listitem" id="item4" style="min-width: 50px; max-width: 50px;">
|
|
171
|
+
<a href="#">Item 4</a>
|
|
172
|
+
</div>
|
|
173
|
+
<div slot="more-button" style="min-width: 50px; max-width: 50px;">
|
|
174
|
+
<button>More</button>
|
|
175
|
+
</div>
|
|
176
|
+
</${tag}>
|
|
177
|
+
<style>
|
|
178
|
+
[orientation='horizontal'] ::slotted([slot='list']) {
|
|
179
|
+
display: flex;
|
|
180
|
+
}
|
|
181
|
+
</style>
|
|
182
|
+
`);
|
|
183
|
+
|
|
184
|
+
await waitResizeEventDebounce(el);
|
|
185
|
+
// 4 items is 200px. They don't fit into 170px parent. So More button should be shown
|
|
186
|
+
expect(isMoreButtonShown(el)).to.equal(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should show 2 items when clicking on `More` button', async () => {
|
|
190
|
+
const el = await fixture(html`
|
|
191
|
+
<${tag}
|
|
192
|
+
.itemWrap="${true}"
|
|
193
|
+
?data-has-full-width-flyout="${l1Config.hasFullWidthFlyout}"
|
|
194
|
+
.config="${l1Config.openableConfig}"
|
|
195
|
+
.bar="${l1Config.isBar}"
|
|
196
|
+
._activeMode="${'tabbable-disclosure'}"
|
|
197
|
+
style="min-width: 170px; max-width: 170px;"
|
|
198
|
+
>
|
|
199
|
+
<div role="listitem" id="item1" style="min-width: 50px; max-width: 50px;">
|
|
200
|
+
<a href="#">Item 1</a>
|
|
201
|
+
</div>
|
|
202
|
+
<div role="listitem" id="item2" style="min-width: 50px; max-width: 50px;">
|
|
203
|
+
<a href="#">Item 2</a>
|
|
204
|
+
</div>
|
|
205
|
+
<div role="listitem" id="item3" style="min-width: 50px; max-width: 50px;">
|
|
206
|
+
<a href="#">Item 3</a>
|
|
207
|
+
</div>
|
|
208
|
+
<div role="listitem" id="item4" style="min-width: 50px; max-width: 50px;">
|
|
209
|
+
<a href="#">Item 4</a>
|
|
210
|
+
</div>
|
|
211
|
+
<div slot="more-button" style="min-width: 50px; max-width: 50px;">
|
|
212
|
+
<button>More</button>
|
|
213
|
+
</div>
|
|
214
|
+
</${tag}>
|
|
215
|
+
`);
|
|
216
|
+
|
|
217
|
+
await waitResizeEventDebounce(el);
|
|
218
|
+
await clickOnMoreButton(el);
|
|
219
|
+
|
|
220
|
+
expect(isMoreButtonMenuShown(el)).to.equal(true);
|
|
221
|
+
const labels = getDirectListItemsUnderMoreButtonMenu(el).map(
|
|
222
|
+
item => item.textContent?.trim() ?? '',
|
|
223
|
+
);
|
|
224
|
+
expect(labels).to.deep.equal(['Item 3', 'Item 4']);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should focus more button menu container when clicking on `More` button', async () => {
|
|
228
|
+
const el = await fixture(html`
|
|
229
|
+
<${tag}
|
|
230
|
+
.itemWrap="${true}"
|
|
231
|
+
?data-has-full-width-flyout="${l1Config.hasFullWidthFlyout}"
|
|
232
|
+
.config="${l1Config.openableConfig}"
|
|
233
|
+
.bar="${l1Config.isBar}"
|
|
234
|
+
._activeMode="${'tabbable-disclosure'}"
|
|
235
|
+
style="min-width: 170px; max-width: 170px;"
|
|
236
|
+
>
|
|
237
|
+
<div role="listitem" id="item1" style="min-width: 50px; max-width: 50px;">
|
|
238
|
+
<a href="#">Item 1</a>
|
|
239
|
+
</div>
|
|
240
|
+
<div role="listitem" id="item2" style="min-width: 50px; max-width: 50px;">
|
|
241
|
+
<a href="#">Item 2</a>
|
|
242
|
+
</div>
|
|
243
|
+
<div role="listitem" id="item3" style="min-width: 50px; max-width: 50px;">
|
|
244
|
+
<a href="#">Item 3</a>
|
|
245
|
+
</div>
|
|
246
|
+
<div role="listitem" id="item4" style="min-width: 50px; max-width: 50px;">
|
|
247
|
+
<a href="#">Item 4</a>
|
|
248
|
+
</div>
|
|
249
|
+
<div slot="more-button" style="min-width: 50px; max-width: 50px;">
|
|
250
|
+
<button>More</button>
|
|
251
|
+
</div>
|
|
252
|
+
</${tag}>
|
|
253
|
+
`);
|
|
254
|
+
|
|
255
|
+
await waitResizeEventDebounce(el);
|
|
256
|
+
await clickOnMoreButton(el);
|
|
257
|
+
|
|
258
|
+
expect(isMoreButtonMenuShown(el)).to.equal(true);
|
|
259
|
+
expect(getDeepActiveElement() === getMoreButtonMenu(el)).to.equal(true);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should open and then close More button menu when clicking 2 times on the More button', async () => {
|
|
263
|
+
const el = await fixture(html`
|
|
264
|
+
<${tag}
|
|
265
|
+
.itemWrap="${true}"
|
|
266
|
+
?data-has-full-width-flyout="${l1Config.hasFullWidthFlyout}"
|
|
267
|
+
.config="${l1Config.openableConfig}"
|
|
268
|
+
.bar="${l1Config.isBar}"
|
|
269
|
+
._activeMode="${'tabbable-disclosure'}"
|
|
270
|
+
style="min-width: 170px; max-width: 170px;"
|
|
271
|
+
>
|
|
272
|
+
<div role="listitem" id="item1" style="min-width: 50px; max-width: 50px;">
|
|
273
|
+
<a href="#">Item 1</a>
|
|
274
|
+
</div>
|
|
275
|
+
<div role="listitem" id="item2" style="min-width: 50px; max-width: 50px;">
|
|
276
|
+
<a href="#">Item 2</a>
|
|
277
|
+
</div>
|
|
278
|
+
<div role="listitem" id="item3" style="min-width: 50px; max-width: 50px;">
|
|
279
|
+
<a href="#">Item 3</a>
|
|
280
|
+
</div>
|
|
281
|
+
<div role="listitem" id="item4" style="min-width: 50px; max-width: 50px;">
|
|
282
|
+
<a href="#">Item 4</a>
|
|
283
|
+
</div>
|
|
284
|
+
<div slot="more-button" style="min-width: 50px; max-width: 50px;">
|
|
285
|
+
<button>More</button>
|
|
286
|
+
</div>
|
|
287
|
+
</${tag}>
|
|
288
|
+
`);
|
|
289
|
+
|
|
290
|
+
await waitResizeEventDebounce(el);
|
|
291
|
+
expect(isMoreButtonMenuShown(el)).to.equal(false);
|
|
292
|
+
await clickOnMoreButton(el);
|
|
293
|
+
expect(isMoreButtonMenuShown(el)).to.equal(true);
|
|
294
|
+
await clickOnMoreButton(el);
|
|
295
|
+
expect(isMoreButtonMenuShown(el)).to.equal(false);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should remove `More` button when making font smaller', async () => {
|
|
299
|
+
const el = await fixture(html`
|
|
300
|
+
<${tag} name="foo" ._activeMode="${'tabbable-disclosure'}" .itemWrap="${true}"
|
|
301
|
+
data-has-full-width-flyout orientation="horizontal" style="min-width: 170px; max-width: 170px;">
|
|
302
|
+
<div role="listitem" id="item1" style="min-width: 50px; max-width: 50px;">
|
|
303
|
+
<a href="#">Item 1</a>
|
|
304
|
+
</div>
|
|
305
|
+
<div role="listitem" id="item2" style="min-width: 50px; max-width: 50px;">
|
|
306
|
+
<a href="#">Item 2</a>
|
|
307
|
+
</div>
|
|
308
|
+
<div role="listitem" id="item3" style="min-width: 50px; max-width: 50px;">
|
|
309
|
+
<a href="#">Item 3</a>
|
|
310
|
+
</div>
|
|
311
|
+
<div slot="more-button" style="min-width: 50px; max-width: 50px;">
|
|
312
|
+
<button>More</button>
|
|
313
|
+
</div>
|
|
314
|
+
</${tag}>
|
|
315
|
+
`);
|
|
316
|
+
await waitResizeEventDebounce(el);
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Make font smaller for first level items to force
|
|
320
|
+
* the menu render all 4 items and no `More` button
|
|
321
|
+
*/
|
|
322
|
+
getDirectListItemsUnderMainMenu(el).forEach(item => {
|
|
323
|
+
// eslint-disable-next-line no-param-reassign
|
|
324
|
+
item.style.fontSize = '12px';
|
|
325
|
+
});
|
|
326
|
+
el?.dispatchEvent(new CustomEvent('resize', { composed: true, bubbles: true }));
|
|
327
|
+
await waitResizeEventDebounce(el);
|
|
328
|
+
expect(isMoreButtonShown(el)).to.equal(false);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should add `More` button when making font larger', async () => {
|
|
332
|
+
const el = await fixture(html`
|
|
333
|
+
<${tag}
|
|
334
|
+
.itemWrap="${true}"
|
|
335
|
+
?data-has-full-width-flyout="${l1Config.hasFullWidthFlyout}"
|
|
336
|
+
.config="${l1Config.openableConfig}"
|
|
337
|
+
.bar="${l1Config.isBar}"
|
|
338
|
+
._activeMode="${'tabbable-disclosure'}"
|
|
339
|
+
style="min-width: 170px; max-width: 170px;"
|
|
340
|
+
>
|
|
341
|
+
<div role="listitem" id="item1" style="min-width: 50px; max-width: 50px;">
|
|
342
|
+
<a href="#">Item 1</a>
|
|
343
|
+
</div>
|
|
344
|
+
<div role="listitem" id="item2" style="min-width: 50px; max-width: 50px;">
|
|
345
|
+
<a href="#">Item 2</a>
|
|
346
|
+
</div>
|
|
347
|
+
<div role="listitem" id="item3" style="min-width: 50px; max-width: 50px;">
|
|
348
|
+
<a href="#">Item 3</a>
|
|
349
|
+
</div>
|
|
350
|
+
<div slot="more-button" style="min-width: 50px; max-width: 50px;">
|
|
351
|
+
<button>More</button>
|
|
352
|
+
</div>
|
|
353
|
+
</${tag}>
|
|
354
|
+
`);
|
|
355
|
+
await waitResizeEventDebounce(el);
|
|
356
|
+
|
|
357
|
+
expect(getDirectListItemsUnderMainMenu(el).length).to.equal(3);
|
|
358
|
+
/**
|
|
359
|
+
* Make font larger for first level items to force
|
|
360
|
+
* the More button to appear
|
|
361
|
+
*/
|
|
362
|
+
getDirectListItemsUnderMainMenu(el).forEach(item => {
|
|
363
|
+
// eslint-disable-next-line no-param-reassign
|
|
364
|
+
item.style.fontSize = '40px';
|
|
365
|
+
});
|
|
366
|
+
el?.dispatchEvent(new CustomEvent('resize', { composed: true, bubbles: true }));
|
|
367
|
+
await waitResizeEventDebounce(el);
|
|
368
|
+
expect(isMoreButtonShown(el)).to.equal(true);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import { expect, html, fixture as _fixture, unsafeStatic, defineCE } from '@open-wc/testing';
|
|
3
|
+
import sinon from 'sinon';
|
|
4
|
+
import { MultiLevelListMixin } from '../src/MultiLevelListMixin.js';
|
|
5
|
+
import { InteractiveListMixin } from '../src/InteractiveListMixin.js';
|
|
6
|
+
import { mimicKeyPress } from '../test-helpers/mimicKeyPress.js';
|
|
7
|
+
|
|
8
|
+
class MultiLevelListClass extends MultiLevelListMixin(LitElement) {}
|
|
9
|
+
class InteractiveListClass extends InteractiveListMixin(LitElement) {}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import('../src/LionMenu.js').LionMenu} LionMenu
|
|
13
|
+
* @typedef {import('lit').TemplateResult} TemplateResult
|
|
14
|
+
*
|
|
15
|
+
* @typedef {Object} TestConfig
|
|
16
|
+
* @property {string} tagString
|
|
17
|
+
* @property {string} tagChildString
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fixture = /** @type {(arg: TemplateResult) => Promise<LionMenu>} */ (_fixture);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {TestConfig} [customConfig]
|
|
24
|
+
*/
|
|
25
|
+
export function runMultiLevelListMixinSuite(customConfig) {
|
|
26
|
+
const cfg = {
|
|
27
|
+
tagString: customConfig?.tagString || defineCE(MultiLevelListClass),
|
|
28
|
+
tagChildString: customConfig?.tagChildString || defineCE(InteractiveListClass),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const { tagString, tagChildString } = cfg;
|
|
32
|
+
const tag = unsafeStatic(tagString);
|
|
33
|
+
const tagChild = unsafeStatic(tagChildString);
|
|
34
|
+
|
|
35
|
+
describe('MultiLevelListMixin', () => {
|
|
36
|
+
it('has sublists', async () => {
|
|
37
|
+
const el = await fixture(html`
|
|
38
|
+
<${tag}>
|
|
39
|
+
<div role="listitem" id="item-0">
|
|
40
|
+
<button data-invoker>Item 0</button>
|
|
41
|
+
<${tagChild}>
|
|
42
|
+
<div role="listitem" id="item-0-0">
|
|
43
|
+
<a href="#foo">Sub item 0</a>
|
|
44
|
+
</div>
|
|
45
|
+
<div role="listitem" id="item-0-1">
|
|
46
|
+
<a href="#bar">Sub item 1</a>
|
|
47
|
+
</div>
|
|
48
|
+
</${tagChild}>
|
|
49
|
+
</div>
|
|
50
|
+
<div role="listitem" id="item-1">
|
|
51
|
+
<a href="#baz">Item 1</a>
|
|
52
|
+
</div>
|
|
53
|
+
</${tag}>
|
|
54
|
+
`);
|
|
55
|
+
const subList = /** @type {MultiLevelListClass} */ (el.querySelector(tagChildString));
|
|
56
|
+
expect(subList).to.exist;
|
|
57
|
+
expect(subList.level).to.equal(2);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('inherits checked state from children when checked via href match', async () => {
|
|
61
|
+
const el = await fixture(html`
|
|
62
|
+
<${tag}>
|
|
63
|
+
<div role="listitem" id="item-0">
|
|
64
|
+
<button data-invoker>Item 0</button>
|
|
65
|
+
<${tagChild}>
|
|
66
|
+
<div role="listitem" id="item-0-0">
|
|
67
|
+
<a href="${window.location.href}">Sub item 0</a>
|
|
68
|
+
</div>
|
|
69
|
+
<div role="listitem" id="item-0-1">
|
|
70
|
+
<a href="#bar">Sub item 1</a>
|
|
71
|
+
</div>
|
|
72
|
+
</${tagChild}>
|
|
73
|
+
</div>
|
|
74
|
+
<div role="listitem" id="item-1">
|
|
75
|
+
<a href="#baz">Item 1</a>
|
|
76
|
+
</div>
|
|
77
|
+
</${tag}>
|
|
78
|
+
`);
|
|
79
|
+
if (el.role === 'list') {
|
|
80
|
+
const item0 = el.listItems[0];
|
|
81
|
+
expect(item0.getAttribute('aria-current')).to.equal('true');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('Interactions', () => {
|
|
86
|
+
describe('Keyboard navigation', () => {
|
|
87
|
+
it('navigates between L1 items on [ArrowDown] [ArrowUp] keys', async () => {
|
|
88
|
+
const el = await fixture(html`
|
|
89
|
+
<${tag}>
|
|
90
|
+
<div role="listitem" id="item-0">
|
|
91
|
+
<button data-invoker>Item 0</button>
|
|
92
|
+
<${tagChild}>
|
|
93
|
+
<div role="listitem" id="item-0-0">
|
|
94
|
+
<a href="${window.location.href}">Sub item 0</a>
|
|
95
|
+
</div>
|
|
96
|
+
<div role="listitem" id="item-0-1">
|
|
97
|
+
<a href="#bar">Sub item 1</a>
|
|
98
|
+
</div>
|
|
99
|
+
</${tagChild}>
|
|
100
|
+
</div>
|
|
101
|
+
<div role="listitem" id="item-1">
|
|
102
|
+
<a href="#baz">Item 1</a>
|
|
103
|
+
</div>
|
|
104
|
+
<div role="listitem" id="item-2">
|
|
105
|
+
<a href="#foobar">Item 2</a>
|
|
106
|
+
</div>
|
|
107
|
+
</${tag}>
|
|
108
|
+
`);
|
|
109
|
+
|
|
110
|
+
const { _listNode } = el;
|
|
111
|
+
|
|
112
|
+
// Normalize
|
|
113
|
+
el.activeIndex = 0;
|
|
114
|
+
|
|
115
|
+
mimicKeyPress(_listNode, 'ArrowDown');
|
|
116
|
+
expect(el.activeIndex).to.be.equal(1);
|
|
117
|
+
mimicKeyPress(_listNode, 'ArrowDown');
|
|
118
|
+
expect(el.activeIndex).to.be.equal(2);
|
|
119
|
+
mimicKeyPress(_listNode, 'ArrowUp');
|
|
120
|
+
expect(el.activeIndex).to.be.equal(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('stops navigation by default at end of option list', async () => {
|
|
124
|
+
const el = await fixture(html`
|
|
125
|
+
<${tag}>
|
|
126
|
+
<div role="listitem" id="item-0">
|
|
127
|
+
<button data-invoker>Item 0</button>
|
|
128
|
+
<${tagChild}>
|
|
129
|
+
<div role="listitem" id="item-0-0">
|
|
130
|
+
<a href="${window.location.href}">Sub item 0</a>
|
|
131
|
+
</div>
|
|
132
|
+
<div role="listitem" id="item-0-1">
|
|
133
|
+
<a href="#bar">Sub item 1</a>
|
|
134
|
+
</div>
|
|
135
|
+
</${tagChild}>
|
|
136
|
+
</div>
|
|
137
|
+
<div role="listitem" id="item-1">
|
|
138
|
+
<a href="#baz">Item 1</a>
|
|
139
|
+
</div>
|
|
140
|
+
<div role="listitem" id="item-2">
|
|
141
|
+
<a href="#foobar">Item 2</a>
|
|
142
|
+
</div>
|
|
143
|
+
</${tag}>
|
|
144
|
+
`);
|
|
145
|
+
|
|
146
|
+
const { _listNode } = el;
|
|
147
|
+
|
|
148
|
+
// Normalize
|
|
149
|
+
el.activeIndex = 0;
|
|
150
|
+
|
|
151
|
+
mimicKeyPress(_listNode, 'ArrowUp');
|
|
152
|
+
expect(el.activeIndex).to.be.equal(0);
|
|
153
|
+
|
|
154
|
+
el.activeIndex = 2;
|
|
155
|
+
mimicKeyPress(_listNode, 'ArrowDown');
|
|
156
|
+
expect(el.activeIndex).to.be.equal(2);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('opens subList on [Enter] key press', async () => {
|
|
160
|
+
const el = await fixture(html`
|
|
161
|
+
<${tag}>
|
|
162
|
+
<div role="listitem" id="item-0">
|
|
163
|
+
<button data-invoker>Item 0</button>
|
|
164
|
+
<${tagChild}>
|
|
165
|
+
<div role="listitem" id="item-0-0">
|
|
166
|
+
<div>Sub item 0</div>
|
|
167
|
+
</div>
|
|
168
|
+
<div role="listitem" id="item-0-1">
|
|
169
|
+
<div>Sub item 1</div>
|
|
170
|
+
</div>
|
|
171
|
+
</${tagChild}>
|
|
172
|
+
</div>
|
|
173
|
+
<div role="listitem" id="item-1">
|
|
174
|
+
<div>Item 1</div>
|
|
175
|
+
</div>
|
|
176
|
+
</${tag}>
|
|
177
|
+
`);
|
|
178
|
+
|
|
179
|
+
const { _listNode } = el;
|
|
180
|
+
|
|
181
|
+
// Normalize
|
|
182
|
+
el.activeIndex = 0;
|
|
183
|
+
const clickSpy = sinon.spy(el.activeItem, 'click');
|
|
184
|
+
|
|
185
|
+
mimicKeyPress(_listNode, 'Enter');
|
|
186
|
+
expect(clickSpy).to.have.been.calledOnce;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('navigates between L2 items on [ArrowDown] [ArrowUp] keys', async () => {
|
|
190
|
+
const el = await fixture(html`
|
|
191
|
+
<${tag} orientation="horizontal">
|
|
192
|
+
<div role="listitem" id="item-0">
|
|
193
|
+
<button data-invoker>Item 0</button>
|
|
194
|
+
<${tagChild}>
|
|
195
|
+
<div role="listitem" id="item-0-0">
|
|
196
|
+
<a href="#foo">Sub item 0</a>
|
|
197
|
+
</div>
|
|
198
|
+
<div role="listitem" id="item-0-1">
|
|
199
|
+
<a href="#bar">Sub item 1</a>
|
|
200
|
+
</div>
|
|
201
|
+
<div role="listitem" id="item-0-2">
|
|
202
|
+
<a href="#baz">Sub item 2</a>
|
|
203
|
+
</div>
|
|
204
|
+
</${tagChild}>
|
|
205
|
+
</div>
|
|
206
|
+
<div role="listitem" id="item-1">
|
|
207
|
+
<a href="#foobar">Item 1</a>
|
|
208
|
+
</div>
|
|
209
|
+
</${tag}>
|
|
210
|
+
`);
|
|
211
|
+
|
|
212
|
+
const subList = /** @type {MultiLevelListClass} */ (el.querySelector(tagChildString));
|
|
213
|
+
const { _listNode } = subList;
|
|
214
|
+
|
|
215
|
+
// Normalize
|
|
216
|
+
subList.activeIndex = 0;
|
|
217
|
+
|
|
218
|
+
mimicKeyPress(_listNode, 'ArrowDown');
|
|
219
|
+
expect(subList.activeIndex).to.be.equal(1);
|
|
220
|
+
mimicKeyPress(_listNode, 'ArrowDown');
|
|
221
|
+
expect(subList.activeIndex).to.be.equal(2);
|
|
222
|
+
mimicKeyPress(_listNode, 'ArrowUp');
|
|
223
|
+
expect(subList.activeIndex).to.be.equal(1);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('close method gets called on [ArrowUp] key when in L2 and horizontal', async () => {
|
|
227
|
+
const el = await fixture(html`
|
|
228
|
+
<${tag}>
|
|
229
|
+
<div role="listitem" id="item-0">
|
|
230
|
+
<button data-invoker>Item 0</button>
|
|
231
|
+
<${tagChild}>
|
|
232
|
+
<div role="listitem" id="item-0-0">
|
|
233
|
+
<div>Sub item 0</div>
|
|
234
|
+
</div>
|
|
235
|
+
<div role="listitem" id="item-0-1">
|
|
236
|
+
<div>Sub item 1</div>
|
|
237
|
+
</div>
|
|
238
|
+
<div role="listitem" id="item-0-2">
|
|
239
|
+
<div>Sub item 2</div>
|
|
240
|
+
</div>
|
|
241
|
+
</${tagChild}>
|
|
242
|
+
</div>
|
|
243
|
+
<div role="listitem" id="item-1">
|
|
244
|
+
<div>Item 1</div>
|
|
245
|
+
</div>
|
|
246
|
+
</${tag}>
|
|
247
|
+
`);
|
|
248
|
+
const subList = /** @type {MultiLevelListClass} */ (el.querySelector(tagChildString));
|
|
249
|
+
// @ts-ignore
|
|
250
|
+
if (subList?.close) {
|
|
251
|
+
// @ts-ignore
|
|
252
|
+
const closeSpy = sinon.spy(subList, 'close');
|
|
253
|
+
const { _listNode } = subList;
|
|
254
|
+
|
|
255
|
+
// Normalize
|
|
256
|
+
subList.activeIndex = 0;
|
|
257
|
+
subList.orientation = 'horizontal';
|
|
258
|
+
|
|
259
|
+
mimicKeyPress(_listNode, 'ArrowUp');
|
|
260
|
+
expect(closeSpy).to.have.been.calledOnce;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('close method gets called on [ArrowLeft] key when in L2 and vertical', async () => {
|
|
265
|
+
const el = await fixture(html`
|
|
266
|
+
<${tag}>
|
|
267
|
+
<div role="listitem" id="item-0">
|
|
268
|
+
<button data-invoker>Item 0</button>
|
|
269
|
+
<${tagChild} orientation="vertical">
|
|
270
|
+
<div role="listitem" id="item-0-0">
|
|
271
|
+
<a href="#foo">Sub item 0</a>
|
|
272
|
+
</div>
|
|
273
|
+
<div role="listitem" id="item-0-1">
|
|
274
|
+
<a href="#bar">Sub item 1</a>
|
|
275
|
+
</div>
|
|
276
|
+
<div role="listitem" id="item-0-2">
|
|
277
|
+
<a href="#baz">Sub item 2</a>
|
|
278
|
+
</div>
|
|
279
|
+
</${tagChild}>
|
|
280
|
+
</div>
|
|
281
|
+
<div role="listitem" id="item-1">
|
|
282
|
+
<a href="#foobar">Item 1</a>
|
|
283
|
+
</div>
|
|
284
|
+
</${tag}>
|
|
285
|
+
`);
|
|
286
|
+
const subList = /** @type {MultiLevelListClass} */ (el.querySelector(tagChildString));
|
|
287
|
+
// @ts-ignore
|
|
288
|
+
if (subList?.close) {
|
|
289
|
+
// @ts-ignore
|
|
290
|
+
const closeSpy = sinon.spy(subList, 'close');
|
|
291
|
+
const { _listNode } = subList;
|
|
292
|
+
|
|
293
|
+
// Normalize
|
|
294
|
+
subList.activeIndex = 0;
|
|
295
|
+
|
|
296
|
+
mimicKeyPress(_listNode, 'ArrowLeft');
|
|
297
|
+
expect(closeSpy).to.have.been.calledOnce;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|