@lion/ui 0.18.0-menu-system.6 → 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.
Files changed (115) hide show
  1. package/components/menu/src/InteractiveListMixin.js +9 -10
  2. package/components/menu/src/LionMenu.js +1 -1
  3. package/components/menu/src/LionTree.js +1 -1
  4. package/components/menu/src/MultiLevelListMixin.js +46 -9
  5. package/components/menu/test/InteractiveListMixin.test.js +3 -0
  6. package/components/menu/test/MoreButtonMenuMixin.test.js +4 -0
  7. package/components/menu/test/MultiLevelListMixin.test.js +3 -0
  8. package/components/menu/test/lion-menu-hybrid.test.js +3 -0
  9. package/components/menu/test/lion-menu.test.js +7 -2
  10. package/components/menu/test/lion-toolbar.test.js +8 -0
  11. package/components/menu/test/lion-tree.test.js +7 -0
  12. package/components/menu/test-helpers/mimicKeyPress.js +9 -0
  13. package/components/menu/test-suites/InteractiveListMixin.suite.js +154 -342
  14. package/components/menu/test-suites/LionMenu.suite.js +113 -0
  15. package/components/menu/test-suites/LionMenuHybrid.suite.js +1 -1
  16. package/components/menu/test-suites/MoreButtonMenuMixin.suite.js +372 -0
  17. package/components/menu/test-suites/MultiLevelListMixin.suite.js +303 -0
  18. package/components/menu/types/InteractiveListMixinTypes.d.ts +2 -0
  19. package/components/overlays/types/OverlayMixinTypes.ts +2 -0
  20. package/custom-elements.json +245 -153
  21. package/dist-types/components/combobox/src/LionCombobox.d.ts +1 -1
  22. package/dist-types/components/dialog/src/LionDialog.d.ts +1 -1
  23. package/dist-types/components/input-datepicker/src/LionInputDatepicker.d.ts +1 -1
  24. package/dist-types/components/menu/src/InteractiveListMixin.d.ts +3 -3
  25. package/dist-types/components/menu/src/InteractiveListMixin.d.ts.map +1 -1
  26. package/dist-types/components/menu/src/LionMenu.d.ts +75 -5
  27. package/dist-types/components/menu/src/LionMenu.d.ts.map +1 -1
  28. package/dist-types/components/menu/src/LionMenuOverlay.d.ts +4 -2
  29. package/dist-types/components/menu/src/LionMenuOverlay.d.ts.map +1 -1
  30. package/dist-types/components/menu/src/LionTree.d.ts +2 -2
  31. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts +10 -2
  32. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts.map +1 -1
  33. package/dist-types/components/menu/test/InteractiveListMixin.test.d.ts +2 -0
  34. package/dist-types/components/menu/test/InteractiveListMixin.test.d.ts.map +1 -0
  35. package/dist-types/components/menu/test/MoreButtonMenuMixin.test.d.ts +2 -0
  36. package/dist-types/components/menu/test/MoreButtonMenuMixin.test.d.ts.map +1 -0
  37. package/dist-types/components/menu/test/MultiLevelListMixin.test.d.ts +2 -0
  38. package/dist-types/components/menu/test/MultiLevelListMixin.test.d.ts.map +1 -0
  39. package/dist-types/components/menu/test/lion-menu-hybrid.test.d.ts +2 -0
  40. package/dist-types/components/menu/test/lion-menu-hybrid.test.d.ts.map +1 -0
  41. package/dist-types/components/menu/test/lion-toolbar.test.d.ts +2 -0
  42. package/dist-types/components/menu/test/lion-toolbar.test.d.ts.map +1 -0
  43. package/dist-types/components/menu/test/lion-tree.test.d.ts +2 -0
  44. package/dist-types/components/menu/test/lion-tree.test.d.ts.map +1 -0
  45. package/dist-types/components/menu/test-helpers/mimicKeyPress.d.ts +7 -0
  46. package/dist-types/components/menu/test-helpers/mimicKeyPress.d.ts.map +1 -0
  47. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts.map +1 -1
  48. package/dist-types/components/menu/test-suites/LionMenu.suite.d.ts +9 -0
  49. package/dist-types/components/menu/test-suites/LionMenu.suite.d.ts.map +1 -0
  50. package/dist-types/components/menu/test-suites/MoreButtonMenuMixin.suite.d.ts +14 -0
  51. package/dist-types/components/menu/test-suites/MoreButtonMenuMixin.suite.d.ts.map +1 -0
  52. package/dist-types/components/menu/test-suites/MultiLevelListMixin.suite.d.ts +11 -0
  53. package/dist-types/components/menu/test-suites/MultiLevelListMixin.suite.d.ts.map +1 -0
  54. package/dist-types/components/overlays/types/OverlayMixinTypes.d.ts +1 -0
  55. package/dist-types/components/overlays/types/OverlayMixinTypes.d.ts.map +1 -1
  56. package/dist-types/components/select-rich/src/LionSelectRich.d.ts +1 -1
  57. package/dist-types/components/tooltip/src/LionTooltip.d.ts +1 -1
  58. package/dist-types/exports/menu-test-suites.d.ts +2 -0
  59. package/dist-types/tsconfig.tsbuildinfo +1 -1
  60. package/docs/components/checkbox-group/overview.md +2 -2
  61. package/docs/components/collapsible/overview.md +1 -1
  62. package/docs/components/combobox/overview.md +1 -1
  63. package/docs/components/combobox/use-cases.md +1 -1
  64. package/docs/components/dialog/overview.md +1 -1
  65. package/docs/components/fieldset/overview.md +5 -5
  66. package/docs/components/form/overview.md +3 -3
  67. package/docs/components/form/use-cases.md +2 -2
  68. package/docs/components/icon/use-cases.md +1 -1
  69. package/docs/components/input/overview.md +1 -1
  70. package/docs/components/input-amount/overview.md +3 -3
  71. package/docs/components/input-date/overview.md +3 -3
  72. package/docs/components/input-datepicker/overview.md +5 -5
  73. package/docs/components/input-email/overview.md +2 -2
  74. package/docs/components/input-file/overview.md +1 -1
  75. package/docs/components/input-iban/overview.md +2 -2
  76. package/docs/components/input-range/overview.md +2 -2
  77. package/docs/components/input-stepper/overview.md +1 -1
  78. package/docs/components/input-tel/overview.md +1 -1
  79. package/docs/components/input-tel/use-cases.md +6 -6
  80. package/docs/components/input-tel-dropdown/overview.md +1 -1
  81. package/docs/components/listbox/use-cases.md +1 -1
  82. package/docs/components/menu/api-table.md +44 -39
  83. package/docs/components/radio-group/overview.md +3 -3
  84. package/docs/components/select/use-cases.md +1 -1
  85. package/docs/components/select-rich/use-cases.md +2 -2
  86. package/docs/components/tabs/examples.md +1 -1
  87. package/docs/fundamentals/index.md +4 -4
  88. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  89. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  90. package/docs/fundamentals/rationales/accessibility.md +1 -1
  91. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  92. package/docs/fundamentals/systems/core/overview.md +1 -1
  93. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  94. package/docs/fundamentals/systems/form/model-value.md +1 -1
  95. package/docs/fundamentals/systems/form/overview.md +28 -28
  96. package/docs/fundamentals/systems/form/validate.md +2 -2
  97. package/docs/fundamentals/systems/localize/overview.md +4 -4
  98. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  99. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  100. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  101. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  102. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  103. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  104. package/docs/guides/how-to/create-a-form.md +4 -4
  105. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  106. package/docs/guides/how-to/index.md +1 -1
  107. package/docs/guides/index.md +8 -8
  108. package/docs/guides/principles/index.md +5 -5
  109. package/docs/guides/principles/styling.md +1 -1
  110. package/exports/menu-test-suites.js +2 -0
  111. package/menu-test-suites.js +2 -0
  112. package/package.json +1 -1
  113. package/components/menu/src/utils/uuid.js +0 -3
  114. package/dist-types/components/menu/src/utils/uuid.d.ts +0 -2
  115. package/dist-types/components/menu/src/utils/uuid.d.ts.map +0 -1
@@ -1,9 +1,7 @@
1
1
  import { LitElement } from 'lit';
2
2
  import { expect, html, fixture as _fixture, unsafeStatic, defineCE } from '@open-wc/testing';
3
- import { sendMouse, resetMouse } from '@web/test-runner-commands';
4
- import { useFakeTimers } from 'sinon';
5
- import { getDeepActiveElement } from '@lion/ui/overlays.js';
6
3
  import { InteractiveListMixin } from '../src/InteractiveListMixin.js';
4
+ import { mimicKeyPress } from '../test-helpers/mimicKeyPress.js';
7
5
 
8
6
  class InteractiveListClass extends InteractiveListMixin(LitElement) {}
9
7
 
@@ -32,19 +30,6 @@ const supportedRoles = [
32
30
 
33
31
  const fixture = /** @type {(arg: TemplateResult) => Promise<LionMenu>} */ (_fixture);
34
32
 
35
- const l1Config = {
36
- isBar: true,
37
- hasFullWidthFlyout: true,
38
- openableConfig: {
39
- // We want disclosure behavior
40
- placementMode: 'custom',
41
- // N.B. we don't have an invoker...
42
- isActivated: false,
43
- inheritsReferenceWidth: 'none',
44
- focusContentOnOpen: true,
45
- },
46
- };
47
-
48
33
  /**
49
34
  * @param {TestConfig} [customConfig]
50
35
  */
@@ -143,6 +128,28 @@ export function runInteractiveListMixinSuite(customConfig) {
143
128
  });
144
129
  });
145
130
 
131
+ describe('preselect', () => {
132
+ it('sets the checkedIndex to 0 by default when noPreselect is false', async () => {
133
+ const el = await fixture(html`
134
+ <${tag}>
135
+ <div role="menuitem" id="item1">Item 1</div>
136
+ </${tag}>
137
+ `);
138
+ if (!el.noPreselect) {
139
+ expect(el.checkedIndex).to.equal(0);
140
+ }
141
+ });
142
+
143
+ it('sets the checkedIndex to -1 when noPreselect is true', async () => {
144
+ const el = await fixture(html`
145
+ <${tag} no-preselect>
146
+ <div role="menuitem" id="item1">Item 1</div>
147
+ </${tag}>
148
+ `);
149
+ expect(el.checkedIndex).to.equal(-1);
150
+ });
151
+ });
152
+
146
153
  describe('Programmatic interaction', () => {
147
154
  // If we have the item line directly in the fixture the linting rule lit-a11y/role-has-required-aria-attrs
148
155
  // will fail. Inside our code we set the required aria attributes, but the linter doesn't know that.
@@ -226,7 +233,7 @@ export function runInteractiveListMixinSuite(customConfig) {
226
233
 
227
234
  it('set checked state sets aria-checked attribute with role="menuitemcheckbox"', async () => {
228
235
  const el = await fixture(html`
229
- <${tag}>
236
+ <${tag} multiple-choice>
230
237
  ${item(1, 'menuitemcheckbox')}
231
238
  ${item(2, 'menuitemcheckbox')}
232
239
  </${tag}>
@@ -255,7 +262,7 @@ export function runInteractiveListMixinSuite(customConfig) {
255
262
 
256
263
  it('can set checked state to "mixed" for role="menuitemcheckbox"', async () => {
257
264
  const el = await fixture(html`
258
- <${tag}>
265
+ <${tag} multiple-choice>
259
266
  ${item(1, 'menuitemcheckbox')}
260
267
  ${item(2, 'menuitemcheckbox')}
261
268
  </${tag}>
@@ -271,7 +278,7 @@ export function runInteractiveListMixinSuite(customConfig) {
271
278
 
272
279
  it('does not allow to set checkedIndex or activeIndex to be out of bound', async () => {
273
280
  const el = await fixture(html`
274
- <${tag} has-no-default-selected autocomplete="list">
281
+ <${tag} no-preselect>
275
282
  <div role="menuitem" id="item1">Item 1</div>
276
283
  </${tag}>
277
284
  `);
@@ -299,344 +306,149 @@ export function runInteractiveListMixinSuite(customConfig) {
299
306
  });
300
307
  });
301
308
 
309
+ it('sets aria-current="page" when the location href matches the item href', async () => {
310
+ const el = await fixture(html`
311
+ <${tag}>
312
+ <div role="menuitem" id="item0">
313
+ <a href="${window.location.href}">Item 0</a>
314
+ </div>
315
+ <div role="menuitem" id="item1">
316
+ <a href="#bar">Item 1</a>
317
+ </div>
318
+ </${tag}>
319
+ `);
320
+ const item0 = el.listItems[0];
321
+ expect(item0.getAttribute('aria-current')).to.equal('page');
322
+ });
323
+
302
324
  // Copy tests from ListboxMixin. Later, make this mixin a fundament of the ListboxMixin
303
325
 
304
326
  // orientation
305
327
  // multipleChoice
306
328
  // selectionFollowsFocus
307
329
  // rotateKeyboardNavigation
308
- // noPreselect
309
330
  // _scrollTargetNode
310
331
 
311
- describe('Widget extensions', () => {
312
- it('can be extended to [role=listbox]', async () => {});
313
- it('can be extended to [role=menu]', async () => {});
314
- it('can be extended to [role=menubar]', async () => {});
315
- it('can be extended to [role=toolbar]', async () => {});
316
- it('can be extended to [role=tree]', async () => {});
317
- });
318
-
319
- describe('Overflow handling', () => {
320
- /** @type {import('sinon').SinonFakeTimers | null} */
321
- let clock = null;
322
-
323
- /** @param {HTMLElement} el */
324
- const isMoreButtonShown = el => {
325
- const wrapper = /** @type {HTMLElement | null} */ (
326
- el.querySelector('[data-more-button-wrapper]')
327
- );
328
- return wrapper?.style.display !== 'none';
329
- };
330
-
331
- /** @param {HTMLElement} el */
332
- const isMoreButtonMenuShown = el => {
333
- const menu = el.querySelector('[data-more-button-menu]');
334
- return menu ? getComputedStyle(menu).width !== '0px' : false;
335
- };
336
-
337
- /** @param {HTMLElement} el */
338
- const getMoreButton = el =>
339
- /** @type {HTMLElement | null} */ (el.querySelector('[data-more-button]'));
340
- /** @param {HTMLElement} el */
341
- const getMoreButtonMenu = el => el.querySelector('[data-more-button-menu]');
342
-
343
- /**
344
- * @param {HTMLElement} element
345
- */
346
- function getCoordinates(element) {
347
- const { x, y, height } = element.getBoundingClientRect();
348
-
349
- return {
350
- x: Math.floor(x + window.pageXOffset),
351
- y: Math.floor(y + window.pageYOffset + height / 2),
352
- };
353
- }
354
- /** @param {HTMLElement} el */
355
- const clickOnMoreButton = async el => {
356
- const moreButton = getMoreButton(el);
357
- if (!moreButton) {
358
- return;
359
- }
360
- const { x, y } = getCoordinates(moreButton);
361
- await sendMouse({ type: 'click', position: [x, y] });
362
- clock?.tick(100);
363
- };
364
-
365
- /** @param {HTMLElement} el */
366
- const getDirectListItemsUnderMoreButtonMenu = el => [
367
- ...Array.from(
368
- el.querySelectorAll('[data-more-button-menu] > [role="listitem"]'),
369
- item => /** @type {HTMLElement} */ (item),
370
- ),
371
- ];
372
-
373
- /** @param {HTMLElement} el */
374
- const getDirectListItemsUnderMainMenu = el => [
375
- ...Array.from(
376
- el.querySelectorAll('[slot="list"] > [role="listitem"]'),
377
- item => /** @type {HTMLElement} */ (item),
378
- ),
379
- ];
380
-
381
- /** @param {HTMLElement & { updateComplete: Promise<unknown> }} el */
382
- const waitResizeEventDebounce = async el => {
383
- await el.updateComplete;
384
- // wait for resize event debouncer
385
- clock?.tick(55);
386
- };
387
-
388
- beforeEach(() => {
389
- clock = useFakeTimers({
390
- shouldAdvanceTime: true,
332
+ describe('Interactions', () => {
333
+ describe('Keyboard navigation', () => {
334
+ it('navigates between items on [ArrowDown] [ArrowUp] keys', async () => {
335
+ const el = await fixture(html`
336
+ <${tag}>
337
+ <div role="listitem" id="item-0">
338
+ <button data-invoker>Item 0</button>
339
+ </div>
340
+ <div role="listitem" id="item-1">
341
+ <a href="#baz">Item 1</a>
342
+ </div>
343
+ <div role="listitem" id="item-2">
344
+ <a href="#foobar">Item 2</a>
345
+ </div>
346
+ </${tag}>
347
+ `);
348
+
349
+ const { _listNode } = el;
350
+
351
+ // Normalize
352
+ el.activeIndex = 0;
353
+
354
+ mimicKeyPress(_listNode, 'ArrowDown');
355
+ expect(el.activeIndex).to.be.equal(1);
356
+ mimicKeyPress(_listNode, 'ArrowDown');
357
+ expect(el.activeIndex).to.be.equal(2);
358
+ mimicKeyPress(_listNode, 'ArrowUp');
359
+ expect(el.activeIndex).to.be.equal(1);
391
360
  });
392
- });
393
- afterEach(async () => {
394
- clock?.restore();
395
- await resetMouse();
396
- });
397
-
398
- it('should show no More button when all items fit', async () => {
399
- const el = await fixture(html`
400
- <${tag} name="foo" ._activeMode="${'tabbable-disclosure'}" .itemWrap="${true}"
401
- data-has-full-width-flyout orientation="horizontal" style="min-width: 170px; max-width: 170px;">
402
- <div role="listitem" id="item1" style="min-width: 50px; max-width: 50px;">
403
- <a href="#">Item 1</a>
404
- </div>
405
- <div role="listitem" id="item2" style="min-width: 50px; max-width: 50px;">
406
- <a href="#">Item 2</a>
407
- </div>
408
- <div role="listitem" id="item3" style="min-width: 50px; max-width: 50px;">
409
- <a href="#">Item 3</a>
410
- </div>
411
- <div slot="more-button" style="min-width: 50px; max-width: 50px;">
412
- <button>More</button>
413
- </div>
414
- </${tag}>
415
- `);
416
- await waitResizeEventDebounce(el);
417
- // 3 items is 150px. They fit into 170px parent
418
- expect(isMoreButtonShown(el)).to.equal(false);
419
- });
420
361
 
421
- it('should show More button when not all items fit', async () => {
422
- const el = await fixture(html`
423
- <${tag}
424
- .itemWrap="${true}"
425
- ?data-has-full-width-flyout="${l1Config.hasFullWidthFlyout}"
426
- .config="${l1Config.openableConfig}"
427
- .bar="${l1Config.isBar}"
428
- ._activeMode="${'tabbable-disclosure'}"
429
- style="min-width: 170px; max-width: 170px;"
430
- >
431
- <div role="listitem" id="item1" style="min-width: 50px; max-width: 50px;">
432
- <a href="#">Item 1</a>
433
- </div>
434
- <div role="listitem" id="item2" style="min-width: 50px; max-width: 50px;">
435
- <a href="#">Item 2</a>
436
- </div>
437
- <div role="listitem" id="item3" style="min-width: 50px; max-width: 50px;">
438
- <a href="#">Item 3</a>
439
- </div>
440
- <div role="listitem" id="item4" style="min-width: 50px; max-width: 50px;">
441
- <a href="#">Item 4</a>
442
- </div>
443
- <div slot="more-button" style="min-width: 50px; max-width: 50px;">
444
- <button>More</button>
445
- </div>
446
- </${tag}>
447
- <style>
448
- [orientation='horizontal'] ::slotted([slot='list']) {
449
- display: flex;
450
- }
451
- </style>
452
- `);
453
-
454
- await waitResizeEventDebounce(el);
455
- // 4 items is 200px. They don't fit into 170px parent. So More button should be shown
456
- expect(isMoreButtonShown(el)).to.equal(true);
457
- });
458
-
459
- it('should show 2 items when clicking on `More` button', async () => {
460
- const el = await fixture(html`
461
- <${tag}
462
- .itemWrap="${true}"
463
- ?data-has-full-width-flyout="${l1Config.hasFullWidthFlyout}"
464
- .config="${l1Config.openableConfig}"
465
- .bar="${l1Config.isBar}"
466
- ._activeMode="${'tabbable-disclosure'}"
467
- style="min-width: 170px; max-width: 170px;"
468
- >
469
- <div role="listitem" id="item1" style="min-width: 50px; max-width: 50px;">
470
- <a href="#">Item 1</a>
471
- </div>
472
- <div role="listitem" id="item2" style="min-width: 50px; max-width: 50px;">
473
- <a href="#">Item 2</a>
474
- </div>
475
- <div role="listitem" id="item3" style="min-width: 50px; max-width: 50px;">
476
- <a href="#">Item 3</a>
477
- </div>
478
- <div role="listitem" id="item4" style="min-width: 50px; max-width: 50px;">
479
- <a href="#">Item 4</a>
480
- </div>
481
- <div slot="more-button" style="min-width: 50px; max-width: 50px;">
482
- <button>More</button>
483
- </div>
484
- </${tag}>
485
- `);
486
-
487
- await waitResizeEventDebounce(el);
488
- await clickOnMoreButton(el);
489
-
490
- expect(isMoreButtonMenuShown(el)).to.equal(true);
491
- const labels = getDirectListItemsUnderMoreButtonMenu(el).map(
492
- item => item.textContent?.trim() ?? '',
493
- );
494
- expect(labels).to.deep.equal(['Item 3', 'Item 4']);
495
- });
496
-
497
- it('should focus more button menu container when clicking on `More` button', async () => {
498
- const el = await fixture(html`
499
- <${tag}
500
- .itemWrap="${true}"
501
- ?data-has-full-width-flyout="${l1Config.hasFullWidthFlyout}"
502
- .config="${l1Config.openableConfig}"
503
- .bar="${l1Config.isBar}"
504
- ._activeMode="${'tabbable-disclosure'}"
505
- style="min-width: 170px; max-width: 170px;"
506
- >
507
- <div role="listitem" id="item1" style="min-width: 50px; max-width: 50px;">
508
- <a href="#">Item 1</a>
509
- </div>
510
- <div role="listitem" id="item2" style="min-width: 50px; max-width: 50px;">
511
- <a href="#">Item 2</a>
512
- </div>
513
- <div role="listitem" id="item3" style="min-width: 50px; max-width: 50px;">
514
- <a href="#">Item 3</a>
515
- </div>
516
- <div role="listitem" id="item4" style="min-width: 50px; max-width: 50px;">
517
- <a href="#">Item 4</a>
518
- </div>
519
- <div slot="more-button" style="min-width: 50px; max-width: 50px;">
520
- <button>More</button>
521
- </div>
522
- </${tag}>
523
- `);
524
-
525
- await waitResizeEventDebounce(el);
526
- await clickOnMoreButton(el);
527
-
528
- expect(isMoreButtonMenuShown(el)).to.equal(true);
529
- expect(getDeepActiveElement() === getMoreButtonMenu(el)).to.equal(true);
530
- });
531
-
532
- it('should open and then close More button menu when clicking 2 times on the More button', async () => {
533
- const el = await fixture(html`
534
- <${tag}
535
- .itemWrap="${true}"
536
- ?data-has-full-width-flyout="${l1Config.hasFullWidthFlyout}"
537
- .config="${l1Config.openableConfig}"
538
- .bar="${l1Config.isBar}"
539
- ._activeMode="${'tabbable-disclosure'}"
540
- style="min-width: 170px; max-width: 170px;"
541
- >
542
- <div role="listitem" id="item1" style="min-width: 50px; max-width: 50px;">
543
- <a href="#">Item 1</a>
544
- </div>
545
- <div role="listitem" id="item2" style="min-width: 50px; max-width: 50px;">
546
- <a href="#">Item 2</a>
547
- </div>
548
- <div role="listitem" id="item3" style="min-width: 50px; max-width: 50px;">
549
- <a href="#">Item 3</a>
550
- </div>
551
- <div role="listitem" id="item4" style="min-width: 50px; max-width: 50px;">
552
- <a href="#">Item 4</a>
553
- </div>
554
- <div slot="more-button" style="min-width: 50px; max-width: 50px;">
555
- <button>More</button>
556
- </div>
557
- </${tag}>
558
- `);
559
-
560
- await waitResizeEventDebounce(el);
561
- expect(isMoreButtonMenuShown(el)).to.equal(false);
562
- await clickOnMoreButton(el);
563
- expect(isMoreButtonMenuShown(el)).to.equal(true);
564
- await clickOnMoreButton(el);
565
- expect(isMoreButtonMenuShown(el)).to.equal(false);
566
- });
362
+ it('navigates between items on [ArrowRight] [ArrowLeft] keys when orientation is "horizontal" and _activeMode="tabbable-disclosure"', async () => {
363
+ const el = await fixture(html`
364
+ <${tag} orientation="horizontal">
365
+ <div role="listitem" id="item-0">
366
+ <button data-invoker>Item 0</button>
367
+ </div>
368
+ <div role="listitem" id="item-1">
369
+ <a href="#baz">Item 1</a>
370
+ </div>
371
+ <div role="listitem" id="item-2">
372
+ <a href="#foobar">Item 2</a>
373
+ </div>
374
+ </${tag}>
375
+ `);
376
+ el._activeMode = 'tabbable-disclosure';
377
+ const { _listNode } = el;
378
+
379
+ // Normalize
380
+ el.activeIndex = 0;
381
+
382
+ mimicKeyPress(_listNode, 'ArrowRight');
383
+ expect(el.activeIndex).to.be.equal(1);
384
+ mimicKeyPress(_listNode, 'ArrowRight');
385
+ expect(el.activeIndex).to.be.equal(2);
386
+ mimicKeyPress(_listNode, 'ArrowLeft');
387
+ expect(el.activeIndex).to.be.equal(1);
388
+ });
567
389
 
568
- it('should remove `More` button when making font smaller', async () => {
569
- const el = await fixture(html`
570
- <${tag} name="foo" ._activeMode="${'tabbable-disclosure'}" .itemWrap="${true}"
571
- data-has-full-width-flyout orientation="horizontal" style="min-width: 170px; max-width: 170px;">
572
- <div role="listitem" id="item1" style="min-width: 50px; max-width: 50px;">
573
- <a href="#">Item 1</a>
574
- </div>
575
- <div role="listitem" id="item2" style="min-width: 50px; max-width: 50px;">
576
- <a href="#">Item 2</a>
577
- </div>
578
- <div role="listitem" id="item3" style="min-width: 50px; max-width: 50px;">
579
- <a href="#">Item 3</a>
580
- </div>
581
- <div slot="more-button" style="min-width: 50px; max-width: 50px;">
582
- <button>More</button>
583
- </div>
584
- </${tag}>
585
- `);
586
- await waitResizeEventDebounce(el);
587
-
588
- /**
589
- * Make font smaller for first level items to force
590
- * the menu render all 4 items and no `More` button
591
- */
592
- getDirectListItemsUnderMainMenu(el).forEach(item => {
593
- // eslint-disable-next-line no-param-reassign
594
- item.style.fontSize = '12px';
390
+ it('stops navigation by default at end of option list', async () => {
391
+ const el = await fixture(html`
392
+ <${tag}>
393
+ <div role="listitem" id="item-0">
394
+ <button data-invoker>Item 0</button>
395
+ </div>
396
+ <div role="listitem" id="item-1">
397
+ <a href="#baz">Item 1</a>
398
+ </div>
399
+ <div role="listitem" id="item-2">
400
+ <a href="#foobar">Item 2</a>
401
+ </div>
402
+ </${tag}>
403
+ `);
404
+
405
+ const { _listNode } = el;
406
+
407
+ // Normalize
408
+ el.activeIndex = 0;
409
+
410
+ mimicKeyPress(_listNode, 'ArrowUp');
411
+ expect(el.activeIndex).to.be.equal(0);
412
+
413
+ el.activeIndex = 2;
414
+ mimicKeyPress(_listNode, 'ArrowDown');
415
+ expect(el.activeIndex).to.be.equal(2);
595
416
  });
596
- el?.dispatchEvent(new CustomEvent('resize', { composed: true, bubbles: true }));
597
- await waitResizeEventDebounce(el);
598
- expect(isMoreButtonShown(el)).to.equal(false);
599
- });
600
417
 
601
- it('should add `More` button when making font larger', async () => {
602
- const el = await fixture(html`
603
- <${tag}
604
- .itemWrap="${true}"
605
- ?data-has-full-width-flyout="${l1Config.hasFullWidthFlyout}"
606
- .config="${l1Config.openableConfig}"
607
- .bar="${l1Config.isBar}"
608
- ._activeMode="${'tabbable-disclosure'}"
609
- style="min-width: 170px; max-width: 170px;"
610
- >
611
- <div role="listitem" id="item1" style="min-width: 50px; max-width: 50px;">
612
- <a href="#">Item 1</a>
613
- </div>
614
- <div role="listitem" id="item2" style="min-width: 50px; max-width: 50px;">
615
- <a href="#">Item 2</a>
616
- </div>
617
- <div role="listitem" id="item3" style="min-width: 50px; max-width: 50px;">
618
- <a href="#">Item 3</a>
619
- </div>
620
- <div slot="more-button" style="min-width: 50px; max-width: 50px;">
621
- <button>More</button>
622
- </div>
623
- </${tag}>
624
- `);
625
- await waitResizeEventDebounce(el);
626
-
627
- expect(getDirectListItemsUnderMainMenu(el).length).to.equal(3);
628
- /**
629
- * Make font larger for first level items to force
630
- * the More button to appear
631
- */
632
- getDirectListItemsUnderMainMenu(el).forEach(item => {
633
- // eslint-disable-next-line no-param-reassign
634
- item.style.fontSize = '40px';
418
+ it('navigates to first and last option with [Home] and [End] keys', async () => {
419
+ const el = await fixture(html`
420
+ <${tag} orientation="horizontal">
421
+ <div role="listitem" id="item-0">
422
+ <button data-invoker>Item 0</button>
423
+ </div>
424
+ <div role="listitem" id="item-1">
425
+ <a href="#baz">Item 1</a>
426
+ </div>
427
+ <div role="listitem" id="item-2">
428
+ <a href="#foobar">Item 2</a>
429
+ </div>
430
+ </${tag}>
431
+ `);
432
+
433
+ const { _listNode } = el;
434
+
435
+ // Normalize
436
+ el.activeIndex = 2;
437
+
438
+ mimicKeyPress(_listNode, 'Home');
439
+ expect(el.activeIndex).to.equal(0);
440
+ mimicKeyPress(_listNode, 'End');
441
+ expect(el.activeIndex).to.equal(2);
635
442
  });
636
- el?.dispatchEvent(new CustomEvent('resize', { composed: true, bubbles: true }));
637
- await waitResizeEventDebounce(el);
638
- expect(isMoreButtonShown(el)).to.equal(true);
639
443
  });
640
444
  });
445
+
446
+ describe('Widget extensions', () => {
447
+ it('can be extended to [role=listbox]', async () => {});
448
+ it('can be extended to [role=menu]', async () => {});
449
+ it('can be extended to [role=menubar]', async () => {});
450
+ it('can be extended to [role=toolbar]', async () => {});
451
+ it('can be extended to [role=tree]', async () => {});
452
+ });
641
453
  });
642
454
  }