@lmvz-ds/components 0.26.0 → 0.27.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 (116) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/cjs/{reactive-controller-host-DrtMkMd7.js → aria-validation-controller-BMV2tv9-.js} +0 -41
  3. package/cjs/list-keyboard-controller-CzZdPBeH.js +173 -0
  4. package/cjs/lmvz-button_2.cjs.entry.js +4 -3
  5. package/cjs/lmvz-checkbox.cjs.entry.js +8 -4
  6. package/cjs/lmvz-chip.cjs.entry.js +3 -2
  7. package/cjs/lmvz-components.cjs.js +1 -1
  8. package/cjs/lmvz-header_2.cjs.entry.js +7 -51
  9. package/cjs/lmvz-input.cjs.entry.js +3 -2
  10. package/cjs/lmvz-menuitem.cjs.entry.js +3 -2
  11. package/cjs/lmvz-modal.cjs.entry.js +3 -2
  12. package/cjs/lmvz-radio.cjs.entry.js +3 -2
  13. package/cjs/lmvz-select.cjs.entry.js +3 -2
  14. package/cjs/lmvz-tab.cjs.entry.js +31 -0
  15. package/cjs/lmvz-tabs.cjs.entry.js +256 -0
  16. package/cjs/lmvz-toggle.cjs.entry.js +4 -3
  17. package/cjs/loader.cjs.js +1 -1
  18. package/cjs/reactive-controller-host-B_lZtcA6.js +43 -0
  19. package/collection/collection-manifest.json +2 -0
  20. package/collection/components/lmvz-checkbox/lmvz-checkbox.css +10 -8
  21. package/collection/components/lmvz-checkbox/lmvz-checkbox.js +4 -1
  22. package/collection/components/lmvz-header/lmvz-header.js +3 -3
  23. package/collection/components/lmvz-tab/lmvz-tab.css +148 -0
  24. package/collection/components/lmvz-tab/lmvz-tab.js +125 -0
  25. package/collection/components/lmvz-tabs/lmvz-tabs.css +58 -0
  26. package/collection/components/lmvz-tabs/lmvz-tabs.js +399 -0
  27. package/collection/components/lmvz-toggle/lmvz-toggle.js +1 -1
  28. package/collection/integration/header-integration/header-integration.js +1 -1
  29. package/collection/utils/aria/list-keyboard-controller.js +151 -28
  30. package/components/index.d.ts +4 -0
  31. package/components/index.d.ts.bak +4 -0
  32. package/components/index.js +1 -1
  33. package/components/lmvz-button-group.js +1 -1
  34. package/components/lmvz-button.js +1 -1
  35. package/components/lmvz-checkbox.js +1 -1
  36. package/components/lmvz-chip.js +1 -1
  37. package/components/lmvz-header.js +1 -1
  38. package/components/lmvz-icon.js +1 -1
  39. package/components/lmvz-input.js +1 -1
  40. package/components/lmvz-menuitem.js +1 -1
  41. package/components/lmvz-modal.js +1 -1
  42. package/components/lmvz-radio.js +1 -1
  43. package/components/lmvz-select.js +1 -1
  44. package/components/lmvz-snackbar.js +1 -1
  45. package/components/lmvz-tab.d.ts +11 -0
  46. package/components/lmvz-tab.d.ts.bak +11 -0
  47. package/components/lmvz-tab.js +1 -0
  48. package/components/lmvz-tabs.d.ts +11 -0
  49. package/components/lmvz-tabs.d.ts.bak +11 -0
  50. package/components/lmvz-tabs.js +1 -0
  51. package/components/lmvz-toggle.js +1 -1
  52. package/components/p-0P2Wb3pE.js +1 -0
  53. package/components/p-2VdcUIrr.js +1 -0
  54. package/components/p-BJEQw1Sz.js +1 -0
  55. package/components/p-Cs7RCOHZ.js +1 -0
  56. package/components/p-CtaMrBNE.js +1 -0
  57. package/components/{p-JAKQdFhF.js → p-DOTK1OW3.js} +1 -1
  58. package/components/p-DjvbwRyl.js +1 -0
  59. package/esm/{reactive-controller-host-ZrGf1F2-.js → aria-validation-controller-D-KO0Asb.js} +1 -41
  60. package/esm/list-keyboard-controller-Coi8XfUH.js +171 -0
  61. package/esm/lmvz-button_2.entry.js +2 -1
  62. package/esm/lmvz-checkbox.entry.js +7 -3
  63. package/esm/lmvz-chip.entry.js +2 -1
  64. package/esm/lmvz-components.js +1 -1
  65. package/esm/lmvz-header_2.entry.js +6 -50
  66. package/esm/lmvz-input.entry.js +2 -1
  67. package/esm/lmvz-menuitem.entry.js +2 -1
  68. package/esm/lmvz-modal.entry.js +2 -1
  69. package/esm/lmvz-radio.entry.js +2 -1
  70. package/esm/lmvz-select.entry.js +2 -1
  71. package/esm/lmvz-tab.entry.js +29 -0
  72. package/esm/lmvz-tabs.entry.js +254 -0
  73. package/esm/lmvz-toggle.entry.js +3 -2
  74. package/esm/loader.js +1 -1
  75. package/esm/reactive-controller-host-FBuCCcFC.js +41 -0
  76. package/hydrate/index.js +461 -32
  77. package/hydrate/index.mjs +461 -32
  78. package/lmvz-components/lmvz-components.esm.js +1 -1
  79. package/lmvz-components/p-43b463bf.entry.js +1 -0
  80. package/lmvz-components/p-4bd71a3c.entry.js +1 -0
  81. package/lmvz-components/p-6484fbc6.entry.js +1 -0
  82. package/lmvz-components/p-6988c3ea.entry.js +1 -0
  83. package/lmvz-components/p-758dbb82.entry.js +1 -0
  84. package/lmvz-components/p-7b15cdce.entry.js +1 -0
  85. package/lmvz-components/p-8874ff19.entry.js +1 -0
  86. package/lmvz-components/p-CtaMrBNE.js +1 -0
  87. package/lmvz-components/p-FBuCCcFC.js +1 -0
  88. package/lmvz-components/p-a5c921dc.entry.js +1 -0
  89. package/lmvz-components/p-bb46a884.entry.js +1 -0
  90. package/lmvz-components/p-bd23eab3.entry.js +1 -0
  91. package/lmvz-components/p-c6228cee.entry.js +1 -0
  92. package/lmvz-components/p-da16ff58.entry.js +1 -0
  93. package/lmvz-components/p-hRb38wX6.js +1 -0
  94. package/manifest.json +394 -1
  95. package/package.json +9 -1
  96. package/types/components/lmvz-checkbox/lmvz-checkbox.d.ts +1 -0
  97. package/types/components/lmvz-header/lmvz-header.d.ts +2 -2
  98. package/types/components/lmvz-tab/lmvz-tab.d.ts +11 -0
  99. package/types/components/lmvz-tabs/lmvz-tabs.d.ts +43 -0
  100. package/types/components.d.ts +151 -0
  101. package/types/utils/aria/list-keyboard-controller.d.ts +28 -5
  102. package/components/p-BOzeYzKk.js +0 -1
  103. package/components/p-DYa3zcGE.js +0 -1
  104. package/components/p-WLZ7VWNX.js +0 -1
  105. package/components/p-lsUdmjdw.js +0 -1
  106. package/lmvz-components/p-01aeca60.entry.js +0 -1
  107. package/lmvz-components/p-14c3d837.entry.js +0 -1
  108. package/lmvz-components/p-24e63b0a.entry.js +0 -1
  109. package/lmvz-components/p-3da301a6.entry.js +0 -1
  110. package/lmvz-components/p-40228d48.entry.js +0 -1
  111. package/lmvz-components/p-4da9073a.entry.js +0 -1
  112. package/lmvz-components/p-758078db.entry.js +0 -1
  113. package/lmvz-components/p-8dae99f1.entry.js +0 -1
  114. package/lmvz-components/p-CwX1wKkM.js +0 -1
  115. package/lmvz-components/p-e1eaa7a2.entry.js +0 -1
  116. package/lmvz-components/p-f5cece32.entry.js +0 -1
@@ -0,0 +1,399 @@
1
+ import { Host, h } from "@stencil/core";
2
+ import { DirectionalFocusController } from "../../utils/aria/list-keyboard-controller";
3
+ import { ReactiveControllerHost } from "../../utils/reactive-controller-host";
4
+ let tabsIdCounter = 0;
5
+ export class LmvzTabs extends ReactiveControllerHost {
6
+ el;
7
+ instanceId = ++tabsIdCounter;
8
+ tabSlotEl;
9
+ panelSlotEl;
10
+ focusController = new DirectionalFocusController(this, {
11
+ orientation: 'horizontal',
12
+ manageTabIndex: true,
13
+ enableHomeAndEnd: true,
14
+ });
15
+ value;
16
+ orientation = 'horizontal';
17
+ lmvzChange;
18
+ resolvedValue;
19
+ constructor() {
20
+ super();
21
+ this.addController(this.focusController);
22
+ }
23
+ componentWillLoad() {
24
+ super.componentWillLoad();
25
+ this.syncOrientation();
26
+ }
27
+ componentDidLoad() {
28
+ this.tabSlotEl?.addEventListener('slotchange', this.handleTabSlotChange);
29
+ this.panelSlotEl?.addEventListener('slotchange', this.handlePanelSlotChange);
30
+ this.el.addEventListener('lmvzTabReady', this.handleTabReady);
31
+ this.handleTabSlotChange();
32
+ super.componentDidLoad();
33
+ }
34
+ disconnectedCallback() {
35
+ this.el.removeEventListener('lmvzTabReady', this.handleTabReady);
36
+ super.disconnectedCallback();
37
+ }
38
+ handleValueChange(newValue) {
39
+ this.applySelection(newValue);
40
+ }
41
+ handleOrientationChange() {
42
+ this.syncOrientation();
43
+ this.syncFocusTargets();
44
+ this.syncTabOrientation();
45
+ }
46
+ async activateTab(value) {
47
+ const tab = this.findTabByValue(value);
48
+ if (!tab || this.isTabDisabled(tab))
49
+ return;
50
+ this.selectTab(value);
51
+ }
52
+ async getTabElements() {
53
+ return this.slottedTabs;
54
+ }
55
+ async setTabPanelIds(pairs) {
56
+ const tabs = this.slottedTabs;
57
+ const panels = this.slottedPanels;
58
+ pairs.forEach(([tabId, panelId], index) => {
59
+ const tab = tabs[index];
60
+ const panel = panels[index];
61
+ const btnId = `${tabId}-btn`;
62
+ if (tab) {
63
+ tab.id = tabId;
64
+ const btn = tab.shadowRoot?.querySelector('button');
65
+ if (btn) {
66
+ btn.id = btnId;
67
+ btn.setAttribute('aria-controls', panelId);
68
+ }
69
+ }
70
+ if (panel) {
71
+ panel.id = panelId;
72
+ panel.setAttribute('aria-labelledby', btnId);
73
+ }
74
+ });
75
+ }
76
+ get slottedTabs() {
77
+ return (this.tabSlotEl?.assignedElements({ flatten: false }) ?? []).filter((el) => el.tagName.toLowerCase() === 'lmvz-tab');
78
+ }
79
+ get slottedPanels() {
80
+ return (this.panelSlotEl?.assignedElements({ flatten: false }) ?? []);
81
+ }
82
+ handleTabSlotChange = () => {
83
+ this.syncFocusTargets();
84
+ this.syncTabOrientation();
85
+ this.wireAriaIds();
86
+ this.applySelection(this.value);
87
+ this.updateIconTabsAttr();
88
+ };
89
+ handlePanelSlotChange = () => {
90
+ this.wireAriaIds();
91
+ this.applySelection(this.resolvedValue ?? this.value);
92
+ };
93
+ handleTabReady = () => {
94
+ this.syncFocusTargets();
95
+ this.wireAriaIds();
96
+ this.applySelection(this.resolvedValue ?? this.value);
97
+ this.updateIconTabsAttr();
98
+ };
99
+ updateIconTabsAttr() {
100
+ this.el.toggleAttribute('has-icon-tabs', this.slottedTabs.some((t) => t.hasAttribute('has-media')));
101
+ }
102
+ syncFocusTargets() {
103
+ const buttons = this.slottedTabs.map((tab) => tab.shadowRoot?.querySelector('button')).filter((b) => !!b);
104
+ this.focusController.updateElements(buttons);
105
+ }
106
+ syncOrientation() {
107
+ this.removeController(this.focusController);
108
+ this.focusController.hostDisconnected?.();
109
+ this.focusController = new DirectionalFocusController(this, {
110
+ orientation: this.orientation,
111
+ manageTabIndex: true,
112
+ enableHomeAndEnd: true,
113
+ });
114
+ this.addController(this.focusController);
115
+ if (this.el.isConnected) {
116
+ this.focusController.hostConnected?.();
117
+ }
118
+ }
119
+ syncTabOrientation() {
120
+ const isVertical = this.orientation === 'vertical';
121
+ this.slottedTabs.forEach((tab) => {
122
+ tab.toggleAttribute('vertical', isVertical);
123
+ });
124
+ }
125
+ wireAriaIds() {
126
+ const tabs = this.slottedTabs;
127
+ const panels = this.slottedPanels;
128
+ const prefix = `lmvz-tabs-${this.instanceId}`;
129
+ tabs.forEach((tab, index) => {
130
+ const tabId = tab.id || `${prefix}-tab-${index}`;
131
+ tab.id = tabId;
132
+ const panel = panels[index];
133
+ if (!panel)
134
+ return;
135
+ const panelId = panel.id || `${prefix}-panel-${index}`;
136
+ panel.id = panelId;
137
+ const btn = tab.shadowRoot?.querySelector('button');
138
+ if (btn) {
139
+ btn.setAttribute('aria-controls', panelId);
140
+ btn.setAttribute('id', `${tabId}-btn`);
141
+ }
142
+ panel.setAttribute('role', panel.getAttribute('role') ?? 'tabpanel');
143
+ panel.setAttribute('aria-labelledby', `${tabId}-btn`);
144
+ });
145
+ }
146
+ applySelection(requested) {
147
+ const tabs = this.slottedTabs;
148
+ if (!tabs.length)
149
+ return;
150
+ let target = this.findTabByValue(requested);
151
+ if (!target || this.isTabDisabled(target)) {
152
+ target = this.findNearestEnabled(requested) ?? undefined;
153
+ }
154
+ if (!target)
155
+ return;
156
+ const effectiveValue = target.getAttribute('value') ?? '';
157
+ this.resolvedValue = effectiveValue;
158
+ if (this.value !== effectiveValue) {
159
+ this.value = effectiveValue;
160
+ }
161
+ tabs.forEach((tab) => {
162
+ const isSelected = tab.getAttribute('value') === effectiveValue;
163
+ tab.toggleAttribute('selected', isSelected);
164
+ const btn = tab.shadowRoot?.querySelector('button');
165
+ if (btn)
166
+ btn.setAttribute('aria-selected', String(isSelected));
167
+ });
168
+ this.slottedPanels.forEach((panel, index) => {
169
+ const tab = tabs[index];
170
+ const isActive = tab?.getAttribute('value') === effectiveValue;
171
+ panel.toggleAttribute('hidden', !isActive);
172
+ });
173
+ const selectedBtn = target.shadowRoot?.querySelector('button');
174
+ if (selectedBtn) {
175
+ this.focusController.setCurrentElement(selectedBtn);
176
+ }
177
+ }
178
+ isTabDisabled(tab) {
179
+ return tab.hasAttribute('disabled') || !!tab.disabled;
180
+ }
181
+ findTabByValue(value) {
182
+ return this.slottedTabs.find((tab) => tab.getAttribute('value') === value);
183
+ }
184
+ findNearestEnabled(fromValue) {
185
+ const tabs = this.slottedTabs;
186
+ const fromIndex = tabs.findIndex((t) => t.getAttribute('value') === fromValue);
187
+ for (let offset = 1; offset < tabs.length; offset++) {
188
+ const next = tabs[(fromIndex + offset) % tabs.length];
189
+ if (next && !this.isTabDisabled(next))
190
+ return next;
191
+ const prev = tabs[(fromIndex - offset + tabs.length) % tabs.length];
192
+ if (prev && !this.isTabDisabled(prev))
193
+ return prev;
194
+ }
195
+ return undefined;
196
+ }
197
+ selectTab(value) {
198
+ if (value === this.resolvedValue)
199
+ return;
200
+ this.applySelection(value);
201
+ this.lmvzChange.emit({ value });
202
+ }
203
+ handleTabClick = (event) => {
204
+ const target = event.target;
205
+ const tab = target.closest('lmvz-tab');
206
+ if (!tab)
207
+ return;
208
+ const value = tab.getAttribute('value');
209
+ if (!value || this.isTabDisabled(tab))
210
+ return;
211
+ this.selectTab(value);
212
+ };
213
+ handleKeydown = (event) => {
214
+ if (event.key !== 'Enter' && event.key !== ' ')
215
+ return;
216
+ const active = this.el.ownerDocument.activeElement;
217
+ if (!active)
218
+ return;
219
+ let tab = null;
220
+ for (const t of this.slottedTabs) {
221
+ if (t === active) {
222
+ tab = t;
223
+ break;
224
+ }
225
+ }
226
+ if (!tab)
227
+ return;
228
+ const value = tab.getAttribute('value');
229
+ if (!value || this.isTabDisabled(tab))
230
+ return;
231
+ event.preventDefault();
232
+ this.selectTab(value);
233
+ };
234
+ render() {
235
+ return (h(Host, { key: '3a503531069e0d83be9bde0e9bc2e1747498baf2', onClick: this.handleTabClick, onKeydown: this.handleKeydown }, h("div", { key: '25a37d52ba6c8c1b46c1e624da13b918e9bd33d1', role: "tablist", "aria-orientation": this.orientation, class: "tablist" }, h("slot", { key: '00b4ba4cf88081d8cd4ee21976398646e4f19679', ref: (el) => (this.tabSlotEl = el) })), h("div", { key: '872aba20a4d26facafc71821f7fc88cf96b05a33', part: "panels-container", class: "panels-container" }, h("slot", { key: '7681c24945e7d50e95f749bd5c056704db120b11', name: "panels", ref: (el) => (this.panelSlotEl = el) }))));
236
+ }
237
+ static get is() { return "lmvz-tabs"; }
238
+ static get encapsulation() { return "shadow"; }
239
+ static get originalStyleUrls() {
240
+ return {
241
+ "$": ["lmvz-tabs.css"]
242
+ };
243
+ }
244
+ static get styleUrls() {
245
+ return {
246
+ "$": ["lmvz-tabs.css"]
247
+ };
248
+ }
249
+ static get properties() {
250
+ return {
251
+ "value": {
252
+ "type": "string",
253
+ "mutable": true,
254
+ "complexType": {
255
+ "original": "string",
256
+ "resolved": "string",
257
+ "references": {}
258
+ },
259
+ "required": true,
260
+ "optional": false,
261
+ "docs": {
262
+ "tags": [],
263
+ "text": "The `value` of the currently selected tab."
264
+ },
265
+ "getter": false,
266
+ "setter": false,
267
+ "reflect": true,
268
+ "attribute": "value"
269
+ },
270
+ "orientation": {
271
+ "type": "string",
272
+ "mutable": true,
273
+ "complexType": {
274
+ "original": "'horizontal' | 'vertical'",
275
+ "resolved": "\"horizontal\" | \"vertical\"",
276
+ "references": {}
277
+ },
278
+ "required": false,
279
+ "optional": false,
280
+ "docs": {
281
+ "tags": [{
282
+ "name": "default",
283
+ "text": "'horizontal'"
284
+ }],
285
+ "text": "Layout and keyboard-navigation orientation."
286
+ },
287
+ "getter": false,
288
+ "setter": false,
289
+ "reflect": true,
290
+ "attribute": "orientation",
291
+ "defaultValue": "'horizontal'"
292
+ }
293
+ };
294
+ }
295
+ static get states() {
296
+ return {
297
+ "resolvedValue": {}
298
+ };
299
+ }
300
+ static get events() {
301
+ return [{
302
+ "method": "lmvzChange",
303
+ "name": "lmvzChange",
304
+ "bubbles": true,
305
+ "cancelable": true,
306
+ "composed": true,
307
+ "docs": {
308
+ "tags": [],
309
+ "text": "Emitted when a tab is activated.\nDetail: `{ value: string }`."
310
+ },
311
+ "complexType": {
312
+ "original": "{ value: string }",
313
+ "resolved": "{ value: string; }",
314
+ "references": {}
315
+ }
316
+ }];
317
+ }
318
+ static get methods() {
319
+ return {
320
+ "activateTab": {
321
+ "complexType": {
322
+ "signature": "(value: string) => Promise<void>",
323
+ "parameters": [{
324
+ "name": "value",
325
+ "type": "string",
326
+ "docs": ""
327
+ }],
328
+ "references": {
329
+ "Promise": {
330
+ "location": "global",
331
+ "id": "global::Promise"
332
+ }
333
+ },
334
+ "return": "Promise<void>"
335
+ },
336
+ "docs": {
337
+ "text": "Programmatically activate a tab by value.\nDisabled tabs are ignored; emits `lmvzChange` on change.",
338
+ "tags": []
339
+ }
340
+ },
341
+ "getTabElements": {
342
+ "complexType": {
343
+ "signature": "() => Promise<HTMLElement[]>",
344
+ "parameters": [],
345
+ "references": {
346
+ "Promise": {
347
+ "location": "global",
348
+ "id": "global::Promise"
349
+ },
350
+ "HTMLElement": {
351
+ "location": "global",
352
+ "id": "global::HTMLElement"
353
+ }
354
+ },
355
+ "return": "Promise<HTMLElement[]>"
356
+ },
357
+ "docs": {
358
+ "text": "Returns all slotted `lmvz-tab` elements.\nExposed for external orchestration.",
359
+ "tags": []
360
+ }
361
+ },
362
+ "setTabPanelIds": {
363
+ "complexType": {
364
+ "signature": "(pairs: Array<[string, string]>) => Promise<void>",
365
+ "parameters": [{
366
+ "name": "pairs",
367
+ "type": "[string, string][]",
368
+ "docs": ""
369
+ }],
370
+ "references": {
371
+ "Promise": {
372
+ "location": "global",
373
+ "id": "global::Promise"
374
+ },
375
+ "Array": {
376
+ "location": "global",
377
+ "id": "global::Array"
378
+ }
379
+ },
380
+ "return": "Promise<void>"
381
+ },
382
+ "docs": {
383
+ "text": "Wires `aria-controls` on each tab and `aria-labelledby` on each panel\nusing the provided ordered array of `[tabId, panelId]` pairs.\nCallers only need this for explicit control; `lmvz-tabs` wires positional\npairs automatically on `slotchange`.",
384
+ "tags": []
385
+ }
386
+ }
387
+ };
388
+ }
389
+ static get elementRef() { return "el"; }
390
+ static get watchers() {
391
+ return [{
392
+ "propName": "value",
393
+ "methodName": "handleValueChange"
394
+ }, {
395
+ "propName": "orientation",
396
+ "methodName": "handleOrientationChange"
397
+ }];
398
+ }
399
+ }
@@ -65,7 +65,7 @@ export class LmvzToggle extends ReactiveControllerHost {
65
65
  this.lmvzChange.emit(newChecked);
66
66
  };
67
67
  render() {
68
- return (h(Host, { key: '202842250b2c22f64b608e4d5a517a045e50a54c' }, h("span", { key: '105869ad771c9146a6a31e767e51807bb7e0b565', class: "track" }, h("input", { key: '8113919f2c474b61a2745a000fd4995f81b58c1e', type: "checkbox", role: "switch", id: this.toggleId, checked: this.checked, disabled: this.disabled, required: this.required, name: this.name, value: this.value, form: this.form, ref: (el) => (this.nativeInputElement = el), onChange: this.handleChange }), h("span", { key: '9d7201d14abdb987b20f52f21a314434051c9f84', class: "thumb", "aria-hidden": "true" })), h("label", { key: '7291494dd6f440099e9cbd04b3cff03a338a0e93', htmlFor: this.toggleId }, this.label)));
68
+ return (h(Host, { key: '452ea30df1191b45c606d7512982a0f743b7bf7a' }, h("span", { key: 'c538806b64de8bce70d0bc55c6a9eb279f4eff4c', class: "track" }, h("input", { key: 'afe04f4305744b3110092f9bfcbe8b4ee6f21c37', type: "checkbox", role: "switch", id: this.toggleId, checked: this.checked, disabled: this.disabled, required: this.required, name: this.name, value: this.value, form: this.form, ref: (el) => (this.nativeInputElement = el), onChange: this.handleChange }), h("span", { key: 'e8089a6e8ee084b3f316c837cf3225017c33a15e', class: "thumb", "aria-hidden": "true" })), h("label", { key: '085055f461f4240df2eb2b4ea441a5e782240276', htmlFor: this.toggleId }, this.label)));
69
69
  }
70
70
  static get is() { return "lmvz-toggle"; }
71
71
  static get encapsulation() { return "scoped"; }
@@ -20,7 +20,7 @@ export class HeaderIntegration {
20
20
  this.activeNav = navId;
21
21
  }
22
22
  render() {
23
- return (h(Host, { key: 'c7ca0e05f67f09a13ac814b6003855c08fb1b1f0' }, h("lmvz-header", { key: '6f495333a582e0bdba065ea223e631ab22ad6648', lmvzActiveNav: this.activeNav }, h("lmvz-menuitem", { key: 'e4da0b537a42460290f8b0c833e93b311c93bd7e', slot: "nav-primary", id: "lehrmittel", onLmvzActivation: this.activate.bind(this) }, h("a", { key: '232bcc2c0b5ea1131681e3bbad1e59c57e86f345', href: "#" }, "Lehrmittel")), h("lmvz-menuitem", { key: '5ffad2212873c2bed0b2cf9478bf9fb60c64c2d9', slot: "nav-primary", id: "verwaltung", onLmvzActivation: this.activate.bind(this) }, h("a", { key: '12f600046c6f3f13c0724752b833bd9591e68ced', href: "#" }, "Verwaltung")), h("lmvz-menuitem", { key: '55c6ddec956960b57d4ddbdbde5b73c45bd3125b', slot: "connect-nav-lehrmittel" }, h("lmvz-icon", { key: 'cac4736814aae4dc3f8725f09c24ee5a4ba60be0', ...typedIconFromSet('lmvz', 'edit') }), "Deutsch 7"), h("lmvz-menuitem", { key: 'f62ef7953883cb4f989e43e1268f9fe9d3324a15', slot: "connect-nav-lehrmittel" }, h("lmvz-icon", { key: '01ffb4c65e99644dff9fbeb0dc7af43bb0429d2b', ...typedIconFromSet('lmvz', 'edit') }), "Mathe 2"), h("lmvz-menuitem", { key: '1bf7ea0bb514eed4940ba5d0a739c6b56293c2c2', slot: "connect-nav-verwaltung" }, h("lmvz-icon", { key: '5ac636bc4a0939efdf3461b75908335c1e6910ed', ...typedIconFromSet('lmvz', 'settings') }), "iwas mit Verwaltung"), h("lmvz-menuitem", { key: '4ca67b477a656236b353e5333aa183db140beb7d', slot: "connect-nav-verwaltung", "aria-label": "Einstellungen" }, h("lmvz-icon", { key: '94641076d563ab7d970abeada484d7800906fe30', ...typedIconFromSet('lmvz', 'settings') })), h("lmvz-button", { key: '5f5116dae74e1563eaf06fab2fdb1de7061a22fd', slot: "actions", "aria-label": "Benutzerkonto" }, h("lmvz-icon", { key: '781e4b379ee2d4f5c6e940e167fd55310efbb65a', ...typedIconFromSet('lmvz', 'user'), size: "lg" })))));
23
+ return (h(Host, { key: '472abfaab29d554ecfa263b1f6b85d2c1990ceb1' }, h("lmvz-header", { key: '633622c96723608bfac8a9bc7ae9e1a972b105bf', lmvzActiveNav: this.activeNav }, h("lmvz-menuitem", { key: '15f74edb9e88bb246775d540c05cbdc81a3a618f', slot: "nav-primary", id: "lehrmittel", onLmvzActivation: this.activate.bind(this) }, h("a", { key: '6b82ecbd8e2ba53a68d5e05ca3f8ce8800b8dd5a', href: "#" }, "Lehrmittel")), h("lmvz-menuitem", { key: '97922b8a6c0dfba9c5d5b410b1d55aa288e67e88', slot: "nav-primary", id: "verwaltung", onLmvzActivation: this.activate.bind(this) }, h("a", { key: 'bacddd5a3fafecb3573a30dabff10a54f1996288', href: "#" }, "Verwaltung")), h("lmvz-menuitem", { key: 'bbfba0969436a5c95338ff01163658df0b64d680', slot: "connect-nav-lehrmittel" }, h("lmvz-icon", { key: '474927ef7491b1512f7849899859fddb6f368696', ...typedIconFromSet('lmvz', 'edit') }), "Deutsch 7"), h("lmvz-menuitem", { key: '32ef36d6787e3368a4c4e36fdaf00f765192cc6d', slot: "connect-nav-lehrmittel" }, h("lmvz-icon", { key: '58811b4b344a0168478ecda8fa7eee109ae51ea8', ...typedIconFromSet('lmvz', 'edit') }), "Mathe 2"), h("lmvz-menuitem", { key: '079715602610bd44465756e0b5a6daa0f67ea758', slot: "connect-nav-verwaltung" }, h("lmvz-icon", { key: '0c8b1d230579ab20e9dc9ef3b20c5f08a7a16d91', ...typedIconFromSet('lmvz', 'settings') }), "iwas mit Verwaltung"), h("lmvz-menuitem", { key: '31408441bbfd25dd66b209526709280613e1fd48', slot: "connect-nav-verwaltung", "aria-label": "Einstellungen" }, h("lmvz-icon", { key: 'e56d4b45f05d155561730d5298f7e6596eb8e64c', ...typedIconFromSet('lmvz', 'settings') })), h("lmvz-button", { key: '322f1858d41d8416ac8e4a073da348497d6f7557', slot: "actions", "aria-label": "Benutzerkonto" }, h("lmvz-icon", { key: 'd6b22f6b3e7357f9bf89b20463d146855feab625', ...typedIconFromSet('lmvz', 'user'), size: "lg" })))));
24
24
  }
25
25
  static get is() { return "header-integration"; }
26
26
  static get encapsulation() { return "shadow"; }
@@ -1,46 +1,169 @@
1
- import { canReceiveFocus, getDeepActiveElement } from "../component";
2
- export class ListKeyboardNavigationController {
1
+ import { getDeepActiveElement } from "../component";
2
+ const defaultConfig = {
3
+ orientation: 'both',
4
+ enableHomeAndEnd: false,
5
+ manageTabIndex: false,
6
+ };
7
+ export class DirectionalFocusController {
3
8
  host;
4
9
  elements = [];
5
- constructor(host) {
10
+ currentElement;
11
+ config;
12
+ constructor(host, config = {}) {
6
13
  this.host = host;
14
+ this.config = {
15
+ ...defaultConfig,
16
+ ...config,
17
+ };
7
18
  }
8
- hostDidRender() {
19
+ hostConnected() {
9
20
  this.host.el.addEventListener('keydown', this.handleKeydown);
21
+ this.syncManagedFocusTarget();
22
+ }
23
+ hostDisconnected() {
24
+ this.host.el.removeEventListener('keydown', this.handleKeydown);
10
25
  }
11
26
  updateElements(elements) {
12
- this.elements = elements;
27
+ const previousElement = this.currentElement ?? this.getManagedActiveElement() ?? undefined;
28
+ this.elements = elements.filter((element) => 'focus' in element && element.nodeType === 1);
29
+ this.syncManagedFocusTarget(previousElement);
30
+ }
31
+ setCurrentElement(element) {
32
+ const previousElement = this.currentElement;
33
+ this.currentElement = element !== null && element !== undefined && 'focus' in element && element.nodeType === 1 ? element : undefined;
34
+ this.syncManagedFocusTarget(previousElement);
13
35
  }
14
36
  handleKeydown = (event) => {
15
- const { key } = event;
16
- if (!['ArrowDown', 'ArrowRight', 'ArrowUp', 'ArrowLeft'].includes(key))
37
+ const intent = this.getFocusIntent(event.key);
38
+ if (!intent)
17
39
  return;
18
- const activeElement = getDeepActiveElement(document);
19
- const nextElement = this.getNextFocusableElement(activeElement, ['ArrowDown', 'ArrowRight'].includes(key) ? 'down' : 'up');
20
- if (nextElement) {
21
- event.preventDefault();
22
- nextElement.focus?.();
23
- }
40
+ const activeElement = this.getManagedActiveElement();
41
+ const nextElement = this.getTargetElement(activeElement, intent);
42
+ if (!nextElement)
43
+ return;
44
+ event.preventDefault();
45
+ this.applyFocusTarget(nextElement, true);
24
46
  };
25
- getNextFocusableElement(current, direction) {
26
- if (!current)
47
+ getFocusIntent(key) {
48
+ if (this.config.enableHomeAndEnd && key === 'Home')
49
+ return 'first';
50
+ if (this.config.enableHomeAndEnd && key === 'End')
51
+ return 'last';
52
+ if (this.config.orientation === 'horizontal') {
53
+ if (key === 'ArrowRight')
54
+ return 'next';
55
+ if (key === 'ArrowLeft')
56
+ return 'previous';
27
57
  return undefined;
28
- const { elements } = this;
29
- const currentIndex = elements.indexOf(current);
30
- if (currentIndex === -1)
58
+ }
59
+ if (this.config.orientation === 'vertical') {
60
+ if (key === 'ArrowDown')
61
+ return 'next';
62
+ if (key === 'ArrowUp')
63
+ return 'previous';
31
64
  return undefined;
32
- let nextIndex = undefined;
33
- if (direction === 'down') {
34
- nextIndex = (currentIndex + 1) % elements.length;
35
65
  }
36
- else if (direction === 'up') {
37
- nextIndex = (currentIndex - 1 + elements.length) % elements.length;
66
+ if (key === 'ArrowDown' || key === 'ArrowRight')
67
+ return 'next';
68
+ if (key === 'ArrowUp' || key === 'ArrowLeft')
69
+ return 'previous';
70
+ return undefined;
71
+ }
72
+ getManagedActiveElement() {
73
+ const activeElement = getDeepActiveElement(this.host.el.ownerDocument ?? document);
74
+ if (activeElement instanceof HTMLElement && this.elements.includes(activeElement) && this.canMoveFocusTo(activeElement)) {
75
+ return activeElement;
76
+ }
77
+ if (this.currentElement && this.elements.includes(this.currentElement) && this.canMoveFocusTo(this.currentElement)) {
78
+ return this.currentElement;
79
+ }
80
+ return undefined;
81
+ }
82
+ getTargetElement(current, intent) {
83
+ if (!this.elements.length)
84
+ return undefined;
85
+ if (intent === 'first')
86
+ return this.findFocusableFromIndex(-1, 'next');
87
+ if (intent === 'last')
88
+ return this.findFocusableFromIndex(this.elements.length, 'previous');
89
+ const currentIndex = current ? this.elements.indexOf(current) : -1;
90
+ if (currentIndex === -1) {
91
+ return intent === 'next' ? this.findFocusableFromIndex(-1, 'next') : this.findFocusableFromIndex(this.elements.length, 'previous');
38
92
  }
39
- if (nextIndex == undefined)
93
+ return this.findFocusableFromIndex(currentIndex, intent);
94
+ }
95
+ findFocusableFromIndex(startIndex, intent) {
96
+ if (!this.elements.length)
40
97
  return undefined;
41
- const candidate = elements[nextIndex];
42
- if (!canReceiveFocus(candidate))
43
- return this.getNextFocusableElement(candidate, direction);
44
- return candidate;
98
+ const step = intent === 'next' ? 1 : -1;
99
+ for (let offset = 1; offset <= this.elements.length; offset++) {
100
+ const index = (startIndex + step * offset + this.elements.length) % this.elements.length;
101
+ const candidate = this.elements[index];
102
+ if (this.canMoveFocusTo(candidate))
103
+ return candidate;
104
+ }
105
+ return undefined;
106
+ }
107
+ syncManagedFocusTarget(previousElement) {
108
+ const nextElement = this.selectManagedElement(previousElement);
109
+ const didChange = nextElement !== this.currentElement;
110
+ this.currentElement = nextElement;
111
+ if (this.config.manageTabIndex) {
112
+ this.updateTabIndexes(nextElement);
113
+ }
114
+ if (didChange && nextElement) {
115
+ this.config.onFocusTargetChange?.(nextElement);
116
+ }
117
+ }
118
+ selectManagedElement(previousElement) {
119
+ const activeElement = this.getManagedActiveElement();
120
+ if (activeElement)
121
+ return activeElement;
122
+ if (this.currentElement && this.elements.includes(this.currentElement) && this.canMoveFocusTo(this.currentElement)) {
123
+ return this.currentElement;
124
+ }
125
+ if (previousElement && this.elements.includes(previousElement)) {
126
+ return this.findFocusableFromIndex(this.elements.indexOf(previousElement), 'next') ?? this.findFocusableFromIndex(-1, 'next');
127
+ }
128
+ const currentRovingTarget = this.elements.find((element) => element.tabIndex === 0 && this.canMoveFocusTo(element));
129
+ if (currentRovingTarget)
130
+ return currentRovingTarget;
131
+ return this.findFocusableFromIndex(-1, 'next');
132
+ }
133
+ updateTabIndexes(activeElement) {
134
+ this.elements.forEach((element) => {
135
+ element.tabIndex = activeElement && element === activeElement ? 0 : -1;
136
+ });
137
+ }
138
+ applyFocusTarget(element, shouldFocus) {
139
+ const didChange = element !== this.currentElement;
140
+ this.currentElement = element;
141
+ if (this.config.manageTabIndex) {
142
+ this.updateTabIndexes(element);
143
+ }
144
+ if (shouldFocus) {
145
+ element.focus();
146
+ }
147
+ if (didChange) {
148
+ this.config.onFocusTargetChange?.(element);
149
+ }
150
+ }
151
+ canMoveFocusTo(element) {
152
+ if (!element)
153
+ return false;
154
+ if (element.hasAttribute('disabled'))
155
+ return false;
156
+ if ('disabled' in element && Boolean(element.disabled))
157
+ return false;
158
+ if (element.getAttribute('aria-disabled') === 'true')
159
+ return false;
160
+ if (this.config.isFocusableElement)
161
+ return this.config.isFocusableElement(element);
162
+ const isTabOrderable = this.config.manageTabIndex || element.tabIndex >= 0;
163
+ return (isTabOrderable &&
164
+ element.getAttribute('aria-hidden') !== 'true' &&
165
+ (element.checkVisibility?.() ?? true) &&
166
+ element.offsetParent !== null);
45
167
  }
46
168
  }
169
+ export { DirectionalFocusController as ListKeyboardNavigationController };
@@ -27,6 +27,10 @@ export { LmvzSelect as LmvzSelect } from '../../types/components/lmvz-select/lmv
27
27
  export { defineCustomElement as defineCustomElementLmvzSelect } from './lmvz-select';
28
28
  export { LmvzSnackbar as LmvzSnackbar } from '../../types/components/lmvz-snackbar/lmvz-snackbar';
29
29
  export { defineCustomElement as defineCustomElementLmvzSnackbar } from './lmvz-snackbar';
30
+ export { LmvzTab as LmvzTab } from '../../types/components/lmvz-tab/lmvz-tab';
31
+ export { defineCustomElement as defineCustomElementLmvzTab } from './lmvz-tab';
32
+ export { LmvzTabs as LmvzTabs } from '../../types/components/lmvz-tabs/lmvz-tabs';
33
+ export { defineCustomElement as defineCustomElementLmvzTabs } from './lmvz-tabs';
30
34
  export { LmvzToggle as LmvzToggle } from '../../types/components/lmvz-toggle/lmvz-toggle';
31
35
  export { defineCustomElement as defineCustomElementLmvzToggle } from './lmvz-toggle';
32
36
 
@@ -27,6 +27,10 @@ export { LmvzSelect as LmvzSelect } from '../../types/components/lmvz-select/lmv
27
27
  export { defineCustomElement as defineCustomElementLmvzSelect } from './lmvz-select';
28
28
  export { LmvzSnackbar as LmvzSnackbar } from '../../types/components/lmvz-snackbar/lmvz-snackbar';
29
29
  export { defineCustomElement as defineCustomElementLmvzSnackbar } from './lmvz-snackbar';
30
+ export { LmvzTab as LmvzTab } from '../../types/components/lmvz-tab/lmvz-tab';
31
+ export { defineCustomElement as defineCustomElementLmvzTab } from './lmvz-tab';
32
+ export { LmvzTabs as LmvzTabs } from '../../types/components/lmvz-tabs/lmvz-tabs';
33
+ export { defineCustomElement as defineCustomElementLmvzTabs } from './lmvz-tabs';
30
34
  export { LmvzToggle as LmvzToggle } from '../../types/components/lmvz-toggle/lmvz-toggle';
31
35
  export { defineCustomElement as defineCustomElementLmvzToggle } from './lmvz-toggle';
32
36