@lion/ui 0.22.0 → 0.23.0

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