@lion/ui 0.18.0-menu-system.4 → 0.18.0-menu-system.6

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 (88) hide show
  1. package/components/collapsible/test-suites/DisclosureMixin.suite.js +30 -0
  2. package/components/menu/src/InteractiveListMixin.js +60 -24
  3. package/components/menu/src/LionMenu.js +1 -1
  4. package/components/menu/src/LionToolbar.js +1 -1
  5. package/components/menu/src/MoreButtonMenuMixin.js +18 -27
  6. package/components/menu/src/utils/listItemInteractions.js +27 -7
  7. package/components/menu/test/lion-menu.test.js +4 -1
  8. package/components/menu/test-suites/InteractiveListMixin.suite.js +502 -2
  9. package/components/menu/test-suites/LionMenuHybrid.suite.js +583 -0
  10. package/components/overlays/src/OverlayController.js +1 -1
  11. package/custom-elements.json +263 -19
  12. package/dist-types/components/collapsible/test-suites/DisclosureMixin.suite.d.ts +2 -0
  13. package/dist-types/components/collapsible/test-suites/DisclosureMixin.suite.d.ts.map +1 -0
  14. package/dist-types/components/menu/src/InteractiveListMixin.d.ts +11 -5
  15. package/dist-types/components/menu/src/InteractiveListMixin.d.ts.map +1 -1
  16. package/dist-types/components/menu/src/LionMenu.d.ts +4 -2
  17. package/dist-types/components/menu/src/LionMenu.d.ts.map +1 -1
  18. package/dist-types/components/menu/src/LionMenuOverlay.d.ts +8 -4
  19. package/dist-types/components/menu/src/LionMenuOverlay.d.ts.map +1 -1
  20. package/dist-types/components/menu/src/LionTree.d.ts +4 -2
  21. package/dist-types/components/menu/src/LionTree.d.ts.map +1 -1
  22. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts +0 -1
  23. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts.map +1 -1
  24. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts +4 -2
  25. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts.map +1 -1
  26. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts +8 -2
  27. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts.map +1 -1
  28. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts +6 -5
  29. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts.map +1 -1
  30. package/dist-types/components/menu/test-suites/LionMenuHybrid.suite.d.ts +5 -0
  31. package/dist-types/components/menu/test-suites/LionMenuHybrid.suite.d.ts.map +1 -0
  32. package/dist-types/exports/menu-test-suites.d.ts +2 -0
  33. package/dist-types/exports/menu-test-suites.d.ts.map +1 -0
  34. package/dist-types/tsconfig.tsbuildinfo +1 -1
  35. package/docs/components/checkbox-group/overview.md +2 -2
  36. package/docs/components/collapsible/overview.md +1 -1
  37. package/docs/components/combobox/overview.md +1 -1
  38. package/docs/components/combobox/use-cases.md +1 -1
  39. package/docs/components/dialog/overview.md +1 -1
  40. package/docs/components/fieldset/overview.md +5 -5
  41. package/docs/components/form/overview.md +3 -3
  42. package/docs/components/form/use-cases.md +2 -2
  43. package/docs/components/icon/use-cases.md +1 -1
  44. package/docs/components/input/overview.md +1 -1
  45. package/docs/components/input-amount/overview.md +3 -3
  46. package/docs/components/input-date/overview.md +3 -3
  47. package/docs/components/input-datepicker/overview.md +5 -5
  48. package/docs/components/input-email/overview.md +2 -2
  49. package/docs/components/input-file/overview.md +1 -1
  50. package/docs/components/input-iban/overview.md +2 -2
  51. package/docs/components/input-range/overview.md +2 -2
  52. package/docs/components/input-stepper/overview.md +1 -1
  53. package/docs/components/input-tel/overview.md +1 -1
  54. package/docs/components/input-tel/use-cases.md +6 -6
  55. package/docs/components/input-tel-dropdown/overview.md +1 -1
  56. package/docs/components/listbox/use-cases.md +1 -1
  57. package/docs/components/menu/api-table.md +197 -192
  58. package/docs/components/menu/overview.md +39 -39
  59. package/docs/components/radio-group/overview.md +3 -3
  60. package/docs/components/select/use-cases.md +1 -1
  61. package/docs/components/select-rich/use-cases.md +2 -2
  62. package/docs/components/tabs/examples.md +1 -1
  63. package/docs/fundamentals/index.md +4 -4
  64. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  65. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  66. package/docs/fundamentals/rationales/accessibility.md +1 -1
  67. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  68. package/docs/fundamentals/systems/core/overview.md +1 -1
  69. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  70. package/docs/fundamentals/systems/form/model-value.md +1 -1
  71. package/docs/fundamentals/systems/form/overview.md +28 -28
  72. package/docs/fundamentals/systems/form/validate.md +2 -2
  73. package/docs/fundamentals/systems/localize/overview.md +4 -4
  74. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  75. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  76. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  77. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  78. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  79. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  80. package/docs/guides/how-to/create-a-form.md +4 -4
  81. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  82. package/docs/guides/how-to/index.md +1 -1
  83. package/docs/guides/index.md +8 -8
  84. package/docs/guides/principles/index.md +5 -5
  85. package/docs/guides/principles/styling.md +1 -1
  86. package/exports/menu-test-suites.js +2 -0
  87. package/menu-test-suites.js +2 -0
  88. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ // import { LitElement } from 'lit';
2
+ // import { expect, html, fixture as _fixture, unsafeStatic, defineCE } from '@open-wc/testing';
3
+ // import { DisclosureMixin } from '../src/DisclosureMixin.js';
4
+
5
+ // import '@lion/ui/define/lion-menu.js';
6
+ // import '@lion/ui/define/lion-item.js';
7
+
8
+ // class DisclosureClass extends DisclosureMixin(LitElement) {}
9
+
10
+ // /**
11
+ // * @typedef {import('lit').TemplateResult} TemplateResult
12
+ // */
13
+
14
+ // const fixture = /** @type {(arg: TemplateResult) => Promise<LionMenu>} */ (_fixture);
15
+
16
+ // /**
17
+ // * @param { {tagString?:string, itemTagString?:string} } [customConfig]
18
+ // */
19
+ // export function runDisclosureMixinSuite(customConfig = {}) {
20
+ // const cfg = {
21
+ // tagString: 'lion-menu',
22
+ // ...customConfig,
23
+ // };
24
+
25
+ // const tagString = cfg.tagString || defineCE(DisclosureClass);
26
+ // const tag = unsafeStatic(tagString);
27
+
28
+ // describe('DisclosureMixin', () => {
29
+ // });
30
+ // }
@@ -22,7 +22,7 @@ import {
22
22
 
23
23
  // TODO: consider renaming to FocusGroupMixin
24
24
 
25
- // TODO: make all available in controller/directve (same logic with an elegent prop-to-host-mapping)
25
+ // TODO: make all available in controller/directive (same logic with an elegant prop-to-host-mapping)
26
26
 
27
27
  /**
28
28
  * @param {Element} potentialFocusable
@@ -55,13 +55,25 @@ function isFocusableElement(potentialFocusable) {
55
55
  export class LionItem extends LitElement {
56
56
  /** @type {any} */
57
57
  static get properties() {
58
- return { type: String };
58
+ return {
59
+ type: { type: String },
60
+ };
59
61
  }
60
62
 
61
63
  get _invokerNode() {
62
- return Array.from(this.children).find(child => child.slot === 'invoker');
64
+ return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
65
+ child => child.slot === 'invoker',
66
+ );
63
67
  }
64
68
 
69
+ static styles = [
70
+ css`
71
+ :host {
72
+ display: block;
73
+ }
74
+ `,
75
+ ];
76
+
65
77
  constructor() {
66
78
  super();
67
79
  /** @type {InteractiveListItemRole |''} */
@@ -81,14 +93,6 @@ export class LionItem extends LitElement {
81
93
  }
82
94
  }
83
95
 
84
- static styles = [
85
- css`
86
- :host {
87
- display: block;
88
- }
89
- `,
90
- ];
91
-
92
96
  render() {
93
97
  return html`
94
98
  <slot name="invoker"></slot>
@@ -289,7 +293,7 @@ const InteractiveListMixinImplementation = superclass =>
289
293
  }
290
294
 
291
295
  // @ts-ignore - focusableEl is safely used as HTMLElement here
292
- if (!isInView(this._scrollTargetNode, focusableEl)) {
296
+ if (this.orientation === 'vertical' && !isInView(this._scrollTargetNode, focusableEl)) {
293
297
  focusableEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
294
298
  }
295
299
 
@@ -332,11 +336,10 @@ const InteractiveListMixinImplementation = superclass =>
332
336
  setCheckedIndex(index) {
333
337
  const item = this.listItems[index];
334
338
  if (!item) return;
335
-
336
339
  if (!this.multipleChoice) {
337
340
  // Uncheck all
338
341
  this.listItems.forEach(listItem => {
339
- setChecked(listItem, false);
342
+ setChecked(listItem, true);
340
343
  });
341
344
  setChecked(this.listItems[index]);
342
345
  } else {
@@ -377,7 +380,7 @@ const InteractiveListMixinImplementation = superclass =>
377
380
  * - [role="menubar"](https://www.w3.org/TR/wai-aria-practices-1.1/examples/menubar/menubar-1/menubar-1.html)
378
381
  * - [role="tree"](https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-1/treeview-1b.html)
379
382
  * For disclosure menus, we need keyboard navigation, but treat every list item
380
- * (more precisely its anchor child) as a tab stop. Also we apply aria-current
383
+ * (more precisely its anchor child) as a tab stop.
381
384
  * @type {'activedescendant'|'roving-tabindex'|'tabbable-disclosure'|'none'}
382
385
  */
383
386
  this._activeMode = 'activedescendant';
@@ -426,6 +429,16 @@ const InteractiveListMixinImplementation = superclass =>
426
429
  this._onListFocusIn = this._onListFocusIn.bind(this);
427
430
  }
428
431
 
432
+ connectedCallback() {
433
+ super.connectedCallback();
434
+ window.addEventListener('popstate', this._onPopState);
435
+ }
436
+
437
+ disconnectedCallback() {
438
+ super.disconnectedCallback();
439
+ window.removeEventListener('popstate', this._onPopState);
440
+ }
441
+
429
442
  /**
430
443
  * @param {import('lit').PropertyValues } changedProperties
431
444
  */
@@ -461,7 +474,6 @@ const InteractiveListMixinImplementation = superclass =>
461
474
  * we search for a[href]
462
475
  */
463
476
  if (node.getAttribute('role') === 'group') {
464
- // console.debug('checking children of', node, node.children);
465
477
  // @ts-ignore - HTMLCollection is iterable
466
478
  for (const child of node.children) {
467
479
  // @ts-ignore
@@ -568,6 +580,15 @@ const InteractiveListMixinImplementation = superclass =>
568
580
  if (!this.noPreselect && this.checkedIndex === -1) {
569
581
  this.checkedIndex = 0;
570
582
  }
583
+ newItems.forEach(item => {
584
+ if (!item.hasAttribute('checked')) {
585
+ setChecked(item, true);
586
+ } else {
587
+ setChecked(item);
588
+ }
589
+ });
590
+
591
+ this.__syncCurrentPageWithLocationHref(window.location);
571
592
  }
572
593
 
573
594
  /**
@@ -706,16 +727,8 @@ const InteractiveListMixinImplementation = superclass =>
706
727
  // const item = /** @type {HTMLElement} */ (target.closest('[role]'));
707
728
  // @ts-ignore - target is HTMLElement
708
729
  const foundIndex = this.listItems.indexOf(target);
709
- // console.debug('focusin', target, foundIndex, this.listItems);
710
- const previousActiveIndex = this.activeIndex;
711
730
  if (foundIndex > -1 && foundIndex !== this.activeIndex) {
712
731
  this.activeIndex = foundIndex;
713
- if (this._activeMode === 'tabbable-disclosure') {
714
- const previousActiveItem = this.listItems[previousActiveIndex];
715
- previousActiveItem?.removeAttribute('aria-current');
716
- const activeItem = this.listItems[this.activeIndex];
717
- activeItem.setAttribute('aria-current', 'page');
718
- }
719
732
  }
720
733
  }
721
734
 
@@ -800,6 +813,29 @@ const InteractiveListMixinImplementation = superclass =>
800
813
  _getPreviousEnabledOption(currentIndex, offset = -1) {
801
814
  return this.__getNextOption(currentIndex, offset);
802
815
  }
816
+
817
+ /**
818
+ * @param {Location} location
819
+ */
820
+ __syncCurrentPageWithLocationHref(location) {
821
+ this.listItems.forEach(item => {
822
+ if (/** @type {HTMLAnchorElement} */ (item).href) {
823
+ const pathname = location.pathname.endsWith('/')
824
+ ? location.pathname.substring(0, location.pathname.length - 1)
825
+ : location.pathname;
826
+ if (pathname === /** @type {HTMLAnchorElement} */ (item).href) {
827
+ setChecked(item);
828
+ } else {
829
+ setChecked(item, true);
830
+ }
831
+ }
832
+ });
833
+ }
834
+
835
+ /** @protected */
836
+ _onPopState = () => {
837
+ this.__syncCurrentPageWithLocationHref(document.location);
838
+ };
803
839
  };
804
840
  // @ts-ignore
805
841
  export const InteractiveListMixin = dedupeMixin(InteractiveListMixinImplementation);
@@ -189,7 +189,7 @@ export class LionMenu extends MultiLevelListMixin(LitElement) {
189
189
  if (!multiple) {
190
190
  // Uncheck all
191
191
  listItemsWithinGroup.forEach(listItem => {
192
- setChecked(listItem, false);
192
+ setChecked(listItem, true);
193
193
  });
194
194
  setChecked(this.listItems[index]);
195
195
  } else {
@@ -60,7 +60,7 @@ export class LionToolbar extends LionMenu {
60
60
  if (!multiple) {
61
61
  // Uncheck all
62
62
  listItemsWithinGroup.forEach(listItem => {
63
- setChecked(listItem, false);
63
+ setChecked(listItem, true);
64
64
  });
65
65
  setChecked(this.listItems[index]);
66
66
  } else {
@@ -18,8 +18,8 @@ const styles = css`
18
18
  /** N.B. this is a simplified version of sr-only. when we move to overlayctrl, we should build this into overlayctrl */
19
19
  [data-more-button-menu] {
20
20
  overflow: hidden;
21
- height: 1px;
22
- width: 1px;
21
+ height: 0;
22
+ width: 0;
23
23
  }
24
24
 
25
25
  [role='list']:has(> [data-more-button-wrapper]) > [role='listitem'] > :is(button, a) {
@@ -75,11 +75,13 @@ export const MoreButtonMenuMixin = superclass =>
75
75
  moreButton?.setAttribute('aria-expanded', 'false');
76
76
  const moreButtonMenu = document.createElement('div');
77
77
  moreButtonMenu.setAttribute('data-more-button-menu', '');
78
+ moreButtonMenu.setAttribute('tabindex', '-1');
78
79
  moreButtonWrapper.appendChild(moreButtonMenu);
79
80
  this._listNode.appendChild(moreButtonWrapper);
80
81
 
81
82
  const isElementDirectFocusableItemUnderMoreButtonMenu = (/** @type {Element} */ element) =>
82
- this.listItems.includes(element) && moreButtonMenu.contains(element);
83
+ (this.listItems.includes(element) && moreButtonMenu.contains(element)) ||
84
+ element === moreButtonMenu;
83
85
 
84
86
  moreButtonMenu.addEventListener(
85
87
  'focusin',
@@ -87,6 +89,9 @@ export const MoreButtonMenuMixin = superclass =>
87
89
  const target = /** @type {HTMLElement} */ (event?.target);
88
90
  if (!isElementDirectFocusableItemUnderMoreButtonMenu(target)) return;
89
91
 
92
+ // If applicable hide any other L2 menus
93
+ this.querySelector('[level="2"]:has([data-open])')?._overlayCtrl?.hide();
94
+
90
95
  // A user did `Shift + Tab` from L2 menu to More menu
91
96
  if (target?.getAttribute('aria-expanded') === 'true') {
92
97
  // close L2 menu
@@ -110,31 +115,11 @@ export const MoreButtonMenuMixin = superclass =>
110
115
  }, 100);
111
116
  });
112
117
 
113
- this.hasMoreButtonMenuAnyFocusedFirstLevelItems = false;
114
-
115
- moreButton?.addEventListener('mousedown', () => {
116
- this.hasMoreButtonMenuAnyFocusedFirstLevelItems = false;
117
- const focusableFirstLevelItems = moreButtonMenu.querySelectorAll(
118
- ':scope > [role="listitem"] > :is(a, button)',
119
- );
120
-
121
- // @ts-ignore Type 'NodeListOf<Element>' must have a '[Symbol.iterator]()' method that returns an iterator.
122
- for (const focusableFirstLevelItem of focusableFirstLevelItems) {
123
- if (this._getDeepActiveElement() === focusableFirstLevelItem) {
124
- this.hasMoreButtonMenuAnyFocusedFirstLevelItems = true;
125
- }
126
- }
127
- });
128
-
129
118
  moreButton?.addEventListener('click', () => {
130
- if (this.hasMoreButtonMenuAnyFocusedFirstLevelItems) return;
131
- /** @type {HTMLElement | null} */
132
- const firstInteractiveItem = moreButtonMenu.querySelector(
133
- ':scope > [role="listitem"] > :is(a, button)',
134
- );
135
-
136
- firstInteractiveItem?.focus();
137
- moreButtonMenu.setAttribute('data-open', '');
119
+ if (moreButtonMenu.getAttribute('data-open') === null) {
120
+ moreButtonMenu?.focus({ preventScroll: true });
121
+ moreButtonMenu.setAttribute('data-open', '');
122
+ }
138
123
  });
139
124
  }
140
125
 
@@ -186,6 +171,9 @@ export const MoreButtonMenuMixin = superclass =>
186
171
  const previousNodes = [];
187
172
  let { previousSibling } = listItem;
188
173
  while (previousSibling) {
174
+ if (previousSibling.nodeType === Node.ELEMENT_NODE) {
175
+ break;
176
+ }
189
177
  previousNodes.unshift(previousSibling);
190
178
  if (isMarkerComment(previousSibling)) {
191
179
  break;
@@ -196,6 +184,9 @@ export const MoreButtonMenuMixin = superclass =>
196
184
  const nextNodes = [];
197
185
  let { nextSibling } = listItem;
198
186
  while (nextSibling) {
187
+ if (nextSibling.nodeType === Node.ELEMENT_NODE) {
188
+ break;
189
+ }
199
190
  nextNodes.push(nextSibling);
200
191
 
201
192
  if (isMarkerComment(nextSibling)) {
@@ -3,6 +3,10 @@
3
3
  * 'smart' (LionOption) and 'regular' items
4
4
  */
5
5
 
6
+ /**
7
+ * @typedef {import('@lion/ui/checkbox-group.js').LionCheckboxIndeterminate} LionCheckboxIndeterminate
8
+ */
9
+
6
10
  /**
7
11
  * @param {HTMLElement} item
8
12
  */
@@ -34,18 +38,33 @@ export function toggleDisabled(item) {
34
38
  * @param {HTMLElement} item
35
39
  */
36
40
  export function isChecked(item) {
37
- return item.hasAttribute('checked') || item.getAttribute('aria-selected') === 'true';
41
+ return item.hasAttribute('checked');
38
42
  }
39
43
 
40
44
  /**
41
45
  * @param {HTMLElement} item
42
- * @param {boolean} [shouldSet=false]
46
+ * @param {Object} [options]
47
+ * @param {boolean} [options.unset=false]
48
+ * @param {'page'|'mixed'} [options.text] - optional text to set for aria-checked/aria-selected/aria-current (e.g. "mixed")
43
49
  */
44
- export function setChecked(item, shouldSet = true) {
45
- const ariaAttr = item.getAttribute('role') === 'option' ? 'selected' : 'checked';
46
- if (shouldSet) {
50
+ export function setChecked(item, unset = false) {
51
+ const role = item.getAttribute('role');
52
+ let ariaAttr = 'current';
53
+ if (role?.endsWith('checkbox') || role?.endsWith('radio')) {
54
+ ariaAttr = 'checked';
55
+ } else if (role === 'option' || role === 'treeitem') {
56
+ ariaAttr = 'selected';
57
+ }
58
+ let text;
59
+ if (/** @type {HTMLAnchorElement} */ (item).href) {
60
+ text = 'page';
61
+ }
62
+ if (/** @type {LionCheckboxIndeterminate} */ (item).mixedState) {
63
+ text = 'mixed';
64
+ }
65
+ if (!unset) {
47
66
  item.setAttribute('checked', '');
48
- item.setAttribute(`aria-${ariaAttr}`, 'true');
67
+ item.setAttribute(`aria-${ariaAttr}`, text || 'true');
49
68
  } else {
50
69
  item.removeAttribute('checked');
51
70
  item.setAttribute(`aria-${ariaAttr}`, 'false');
@@ -56,7 +75,8 @@ export function setChecked(item, shouldSet = true) {
56
75
  * @param {HTMLElement} item
57
76
  */
58
77
  export function toggleChecked(item) {
59
- setChecked(item, !isChecked(item));
78
+ const shouldUnset = isChecked(item);
79
+ setChecked(item, shouldUnset);
60
80
  }
61
81
 
62
82
  /**
@@ -1,3 +1,6 @@
1
1
  import { runInteractiveListMixinSuite } from '../test-suites/InteractiveListMixin.suite.js';
2
+ import '@lion/ui/define/lion-menu.js';
3
+ import { runLionMenuHybridSuite } from '../test-suites/LionMenuHybrid.suite.js';
2
4
 
3
- runInteractiveListMixinSuite();
5
+ runInteractiveListMixinSuite({ tagString: 'lion-menu' });
6
+ runLionMenuHybridSuite();