@oscarpalmer/oui 0.35.0 → 0.37.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 (128) hide show
  1. package/dist/css/badge.css +18 -15
  2. package/dist/css/button.css +35 -31
  3. package/dist/css/color.css +20 -20
  4. package/dist/css/combobox.css +134 -35
  5. package/dist/css/content.css +29 -10
  6. package/dist/css/floatable.css +10 -10
  7. package/dist/css/form.css +393 -129
  8. package/dist/css/listbox.css +176 -0
  9. package/dist/css/message.css +13 -10
  10. package/dist/css/reset.css +29 -88
  11. package/dist/css/splitter.css +2 -2
  12. package/dist/css/tabs.css +103 -0
  13. package/dist/js/combobox/constants.combobox.d.mts +2 -2
  14. package/dist/js/combobox/constants.combobox.mjs +40 -33
  15. package/dist/js/combobox/dom.combobox.d.mts +8 -91
  16. package/dist/js/combobox/dom.combobox.mjs +204 -2
  17. package/dist/js/combobox/event.combobox.d.mts +14 -8
  18. package/dist/js/combobox/event.combobox.mjs +2 -2
  19. package/dist/js/combobox/index.d.mts +1 -1
  20. package/dist/js/combobox/index.mjs +20 -18
  21. package/dist/js/combobox/misc.combobox.d.mts +2 -2
  22. package/dist/js/combobox/misc.combobox.mjs +2 -2
  23. package/dist/js/combobox/models.combobox.d.mts +44 -2
  24. package/dist/js/combobox/state.combobox.d.mts +9 -10
  25. package/dist/js/combobox/state.combobox.mjs +2 -2
  26. package/dist/js/constants.combobox-DmRAMC64.d.mts +26 -0
  27. package/dist/js/constants.listbox-DRsXUxd3.d.mts +14 -0
  28. package/dist/js/focus-trap/focus-trap.embedded.mjs +6 -10
  29. package/dist/js/index-EfkUSals.d.mts +139 -0
  30. package/dist/js/index.d.mts +5 -2
  31. package/dist/js/index.mjs +5 -2
  32. package/dist/js/internal/attributable.mjs +4 -3
  33. package/dist/js/internal/draggable.mjs +16 -22
  34. package/dist/js/internal/listbox/listbox.constants.d.mts +11 -0
  35. package/dist/js/internal/listbox/listbox.constants.mjs +23 -0
  36. package/dist/js/internal/listbox/listbox.data.d.mts +20 -0
  37. package/dist/js/internal/listbox/listbox.data.mjs +2 -0
  38. package/dist/js/internal/listbox/listbox.dom.d.mts +66 -0
  39. package/dist/js/internal/listbox/listbox.dom.mjs +2 -0
  40. package/dist/js/internal/listbox/listbox.event.d.mts +5 -0
  41. package/dist/js/internal/listbox/listbox.event.mjs +63 -0
  42. package/dist/js/internal/listbox/listbox.form.d.mts +6 -0
  43. package/dist/js/internal/listbox/listbox.form.mjs +52 -0
  44. package/dist/js/internal/listbox/listbox.misc.d.mts +12 -0
  45. package/dist/js/internal/listbox/listbox.misc.mjs +2 -0
  46. package/dist/js/internal/listbox/listbox.model.d.mts +2 -0
  47. package/dist/js/internal/listbox/listbox.model.mjs +1 -0
  48. package/dist/js/internal/listbox/listbox.state.d.mts +7 -0
  49. package/dist/js/internal/listbox/listbox.state.mjs +40 -0
  50. package/dist/js/internal/misc.d.mts +6 -0
  51. package/dist/js/internal/misc.mjs +13 -0
  52. package/dist/js/internal/navigable.d.mts +11 -5
  53. package/dist/js/internal/navigable.mjs +44 -18
  54. package/dist/js/l10n-Bn-7RcwR.d.mts +20 -0
  55. package/dist/js/l10n.d.mts +2 -20
  56. package/dist/js/listbox/constants.listbox.d.mts +2 -0
  57. package/dist/js/listbox/constants.listbox.mjs +40 -0
  58. package/dist/js/listbox/event.listbox.d.mts +8 -0
  59. package/dist/js/listbox/event.listbox.mjs +2 -0
  60. package/dist/js/listbox/index.d.mts +116 -0
  61. package/dist/js/listbox/index.mjs +189 -0
  62. package/dist/js/listbox/misc.listbox.d.mts +2 -0
  63. package/dist/js/listbox/misc.listbox.mjs +2 -0
  64. package/dist/js/listbox/state.listbox.d.mts +11 -0
  65. package/dist/js/listbox/state.listbox.mjs +2 -0
  66. package/dist/js/listbox.data-BWd0PCy7.mjs +424 -0
  67. package/dist/js/listbox.model-CRReGOmu.d.mts +125 -0
  68. package/dist/js/misc.listbox-BI-XsTOf.mjs +197 -0
  69. package/dist/js/misc.listbox-gnoJhrTr.d.mts +20 -0
  70. package/dist/js/movable/movable.embedded.mjs +3 -6
  71. package/dist/js/resizable/resizable.embedded.mjs +16 -26
  72. package/dist/js/sortable/sortable.embedded.mjs +4 -2
  73. package/dist/js/splitter.mjs +15 -12
  74. package/dist/js/state.combobox-4xq0FPCF.mjs +365 -0
  75. package/dist/js/tabs.d.mts +25 -0
  76. package/dist/js/tabs.mjs +240 -0
  77. package/dist/js/tooltip.mjs +13 -12
  78. package/package.json +7 -3
  79. package/src/css/badge.scss +6 -18
  80. package/src/css/button.scss +6 -45
  81. package/src/css/color.scss +20 -20
  82. package/src/css/combobox.scss +21 -52
  83. package/src/css/content.scss +30 -3
  84. package/src/css/floatable.scss +17 -17
  85. package/src/css/form.scss +84 -72
  86. package/src/css/listbox.scss +120 -0
  87. package/src/css/message.scss +13 -28
  88. package/src/css/mixins.scss +253 -28
  89. package/src/css/reset.scss +37 -86
  90. package/src/css/splitter.scss +2 -2
  91. package/src/css/tabs.scss +85 -0
  92. package/src/js/combobox/constants.combobox.ts +46 -25
  93. package/src/js/combobox/dom.combobox.ts +36 -454
  94. package/src/js/combobox/event.combobox.ts +129 -218
  95. package/src/js/combobox/index.ts +37 -30
  96. package/src/js/combobox/misc.combobox.ts +30 -114
  97. package/src/js/combobox/models.combobox.ts +7 -47
  98. package/src/js/combobox/state.combobox.ts +126 -127
  99. package/src/js/focus-trap/focus-trap.embedded.ts +11 -19
  100. package/src/js/index.ts +2 -0
  101. package/src/js/internal/draggable.ts +29 -33
  102. package/src/js/internal/listbox/listbox.constants.ts +25 -0
  103. package/src/js/{combobox/data.combobox.ts → internal/listbox/listbox.data.ts} +94 -43
  104. package/src/js/internal/listbox/listbox.dom.ts +445 -0
  105. package/src/js/internal/listbox/listbox.event.ts +129 -0
  106. package/src/js/{combobox/form.combobox.ts → internal/listbox/listbox.form.ts} +13 -14
  107. package/src/js/internal/listbox/listbox.misc.ts +135 -0
  108. package/src/js/internal/listbox/listbox.model.ts +169 -0
  109. package/src/js/internal/listbox/listbox.state.ts +82 -0
  110. package/src/js/internal/misc.ts +27 -0
  111. package/src/js/internal/navigable.ts +80 -23
  112. package/src/js/l10n.ts +8 -8
  113. package/src/js/listbox/constants.listbox.ts +61 -0
  114. package/src/js/listbox/event.listbox.ts +106 -0
  115. package/src/js/listbox/index.ts +302 -0
  116. package/src/js/listbox/misc.listbox.ts +70 -0
  117. package/src/js/listbox/state.listbox.ts +196 -0
  118. package/src/js/movable/movable.embedded.ts +11 -13
  119. package/src/js/resizable/resizable.embedded.ts +25 -39
  120. package/src/js/splitter.ts +21 -18
  121. package/src/js/tabs.ts +447 -0
  122. package/src/js/tooltip.ts +20 -17
  123. package/dist/js/combobox/data.combobox.d.mts +0 -18
  124. package/dist/js/combobox/data.combobox.mjs +0 -2
  125. package/dist/js/combobox/form.combobox.d.mts +0 -6
  126. package/dist/js/combobox/form.combobox.mjs +0 -2
  127. package/dist/js/constants.combobox-h_kyDegw.d.mts +0 -248
  128. package/dist/js/data.combobox-B8HMkDOh.mjs +0 -1030
@@ -0,0 +1,365 @@
1
+ import { createEmbeddedFloatable } from "./floatable/floatable.embedded.mjs";
2
+ import { getLocalizations, updateLocalizations } from "./l10n.mjs";
3
+ import { LISTBOX_KEYS, LISTBOX_TYPES } from "./internal/listbox/listbox.constants.mjs";
4
+ import { b as updateListboxValueLabel, i as filterListboxOptions, n as filterListbox, r as filterListboxOption, s as toggleListboxOption, t as applyListboxSelection, v as updateListboxMessage, x as generateListboxIds, y as updateListboxPreview } from "./listbox.data-BWd0PCy7.mjs";
5
+ import { initializeListboxState } from "./internal/listbox/listbox.state.mjs";
6
+ import { COMBOBOX_ATTRIBUTES, COMBOBOX_ATTRIBUTE_SELECTION_ITEM, COMBOBOX_AUTOCOMPLETES, COMBOBOX_MESSAGES, COMBOBOX_SELECTORS, COMBOBOX_TAGNAME } from "./combobox/constants.combobox.mjs";
7
+ import { getAttributeValue, getBooleanAriaValue } from "./internal/misc.mjs";
8
+ import { createComboboxAnchor, createComboboxContent, updateComboboxAutocomplete, updateComboboxSelection, updateComboboxSelectionItems, updateComboboxTabindices } from "./combobox/dom.combobox.mjs";
9
+ import { processListboxKeydown } from "./internal/listbox/listbox.event.mjs";
10
+ import { setAria } from "@oscarpalmer/toretto/aria";
11
+ import { on } from "@oscarpalmer/toretto/event";
12
+ import { setAttributes } from "@oscarpalmer/toretto/attribute";
13
+ import { clamp } from "@oscarpalmer/atoms/number";
14
+ import { getData } from "@oscarpalmer/toretto/data";
15
+ import { findAncestor, getData as getData$1 } from "@oscarpalmer/toretto";
16
+ import { noop } from "@oscarpalmer/atoms/function";
17
+ //#region src/js/combobox/misc.combobox.ts
18
+ function getComboboxAutoComplete(element, select) {
19
+ let value;
20
+ if (select) value = "list";
21
+ else value = getAttributeValue(element, "autocomplete", COMBOBOX_AUTOCOMPLETES, "none");
22
+ return {
23
+ value,
24
+ both: value === "both",
25
+ inline: value === "inline",
26
+ list: value === "list",
27
+ none: value === "none"
28
+ };
29
+ }
30
+ function getComboboxId() {
31
+ index += 1;
32
+ return `oui_combobox_${index}`;
33
+ }
34
+ function hasTextSelection(element) {
35
+ return element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
36
+ }
37
+ function setComboboxLocalizations(first, second) {
38
+ updateComboboxLocalizations(comboboxLocalizations, comboboxLocalizations.values, first, second);
39
+ }
40
+ function updateComboboxLocalizations(instance, destination, first, second) {
41
+ updateLocalizations(instance, typeof first === "string" ? { [first]: second } : first, destination, () => {
42
+ for (const state of comboboxStates.set) {
43
+ updateListboxMessage(state);
44
+ updateListboxValueLabel(state);
45
+ updateComboboxSelectionItems(state);
46
+ }
47
+ });
48
+ }
49
+ let index = 0;
50
+ //#endregion
51
+ //#region src/js/combobox/event.combobox.ts
52
+ function onComboboxActivation(state) {
53
+ state.floatable.show();
54
+ }
55
+ function onComboboxApply(state, option, _, added, selected) {
56
+ updateComboboxSelection(state, option, added, selected);
57
+ }
58
+ function onComboboxClickContent(event, state) {
59
+ if (state == null) return;
60
+ if (state.disabled) return;
61
+ const option = findAncestor(event, COMBOBOX_SELECTORS.option);
62
+ if (!(option instanceof HTMLElement)) state.filter.element.focus();
63
+ else if (state.readonly) onComboboxPreview("set", state, option);
64
+ else toggleListboxOption(state, option);
65
+ }
66
+ function onComboboxClickDocument(event) {
67
+ const combobox = findAncestor(event, COMBOBOX_TAGNAME);
68
+ const state = comboboxStates.map.get(combobox);
69
+ if (combobox == null || state == null) return;
70
+ if (findAncestor(event, COMBOBOX_SELECTORS.content) != null) {
71
+ onComboboxClickContent(event, state);
72
+ return;
73
+ }
74
+ const label = findAncestor(event, "label");
75
+ if (label != null) {
76
+ if (label === state.label.element && combobox.type === "select") {
77
+ state.anchor.element.focus();
78
+ if (!state.disabled) state.floatable.show();
79
+ }
80
+ return;
81
+ }
82
+ const selection = findAncestor(event, `[${COMBOBOX_ATTRIBUTE_SELECTION_ITEM}]`);
83
+ if (selection != null) {
84
+ const option = state.options.selected.find((option) => option.id === getData$1(selection, "option", false));
85
+ if (state.disabled) state.anchor.element.focus();
86
+ else if (state.readonly) updateComboboxTabindices(state, option);
87
+ else toggleListboxOption(state, option);
88
+ return;
89
+ }
90
+ if (findAncestor(event, COMBOBOX_SELECTORS.anchor) != null && !state.disabled) {
91
+ state.floatable.show();
92
+ return;
93
+ }
94
+ }
95
+ function onComboboxEnter(state) {
96
+ if (state.disabled || state.readonly) return;
97
+ if (hasTextSelection(state.anchor.element)) onComboboxSelection(state, true);
98
+ else toggleListboxOption(state, state.active);
99
+ }
100
+ function onComboboxEscape(state) {
101
+ state.floatable.hide();
102
+ }
103
+ function onComboboxFilter(preview, state, update, active, emit) {
104
+ if (state.autocomplete.none) applyListboxSelection({
105
+ state,
106
+ added: false,
107
+ emit: (emit ?? true) && update
108
+ });
109
+ else updateListboxPreview(preview, state, active);
110
+ }
111
+ function onComboboxFocusout(event) {
112
+ setTimeout(() => {
113
+ const combobox = findAncestor(event, COMBOBOX_TAGNAME);
114
+ const state = comboboxStates.map.get(combobox);
115
+ if (combobox == null || state == null || state.disabled || state.readonly || state.type === "select" || state.host === document.activeElement || state.host.contains(document.activeElement)) return;
116
+ const option = filterListboxOption(state, state.filter.element.value, true);
117
+ applyListboxSelection({
118
+ option,
119
+ state,
120
+ added: false,
121
+ selected: option == null ? [] : [option]
122
+ });
123
+ });
124
+ }
125
+ function onComboboxInput(event) {
126
+ const combobox = findAncestor(event, COMBOBOX_TAGNAME);
127
+ const state = comboboxStates.map.get(combobox);
128
+ if (combobox != null && state != null && event.target === state.filter.element) filterListbox(event.inputType, state);
129
+ }
130
+ function onComboboxKeydown(event) {
131
+ const combobox = findAncestor(event, COMBOBOX_TAGNAME);
132
+ const state = comboboxStates.map.get(combobox);
133
+ if (combobox == null || state == null) return;
134
+ const item = findAncestor(event, COMBOBOX_SELECTORS.selectionItem);
135
+ if (item instanceof HTMLElement) onComboboxKeydownSelection(event, state, item);
136
+ else processListboxKeydown(event, state);
137
+ }
138
+ function onComboboxKeydownSelection(event, state, item) {
139
+ if (LISTBOX_KEYS.activation.has(event.key)) {
140
+ event.preventDefault();
141
+ onComboboxRemoveSelection(state, item);
142
+ return;
143
+ }
144
+ if (state.disabled || state.selection == null) return;
145
+ const { length } = state.selection.items;
146
+ const index = state.selection.items.findIndex((selection) => selection.button === item);
147
+ let next;
148
+ switch (event.key) {
149
+ case "ArrowDown":
150
+ state.anchor.element.focus();
151
+ state.anchor.element.click();
152
+ return;
153
+ case "ArrowRight":
154
+ case "ArrowLeft":
155
+ next = clamp(index + (event.key === "ArrowRight" ? 1 : -1), 0, length - 1, true);
156
+ break;
157
+ case "End":
158
+ next = length - 1;
159
+ break;
160
+ case "Home": next = 0;
161
+ }
162
+ if (next == null) return;
163
+ event.preventDefault();
164
+ for (let index = 0; index < length; index += 1) {
165
+ const { button } = state.selection.items[index];
166
+ const selected = index === next;
167
+ button.tabIndex = selected ? 0 : -1;
168
+ if (selected) button.focus();
169
+ }
170
+ }
171
+ function onComboboxPreview(preview, state, active) {
172
+ updateComboboxAutocomplete(preview, state, active);
173
+ }
174
+ function onComboboxRemoveSelection(state, item) {
175
+ if (state.disabled || state.readonly) return;
176
+ const option = state.options.selected.find((option) => option.id === getData$1(item, "option", false));
177
+ if (option != null) toggleListboxOption(state, option);
178
+ }
179
+ function onComboboxNavigation(preview, state, active) {
180
+ updateListboxPreview(preview, state, active);
181
+ }
182
+ function onComboboxSelection(state, hasSelection) {
183
+ if (!(hasSelection ?? hasTextSelection(state.anchor.element))) return;
184
+ state.filter.value = state.anchor.element === state.filter.element ? state.filter.element.value : "";
185
+ toggleListboxOption(state, filterListboxOptions(state) ?? state.active);
186
+ }
187
+ function onComboboxTab(state) {
188
+ setTimeout(() => {
189
+ if (state.host === document.activeElement) return;
190
+ if (state.selection?.container.contains(document.activeElement) || !state.host.contains(document.activeElement)) state.floatable.hide();
191
+ });
192
+ }
193
+ function onComboboxToggle(state) {
194
+ if (!state.multiple) state.floatable.hide();
195
+ }
196
+ function preventComboboxActivation(state) {
197
+ return state.floatable.open;
198
+ }
199
+ function skipComboboxNavigation(state) {
200
+ if (state.floatable.open) return false;
201
+ state.floatable.show();
202
+ return true;
203
+ }
204
+ on(document, "click", onComboboxClickDocument);
205
+ on(document, "focusout", onComboboxFocusout);
206
+ on(document, "input", onComboboxInput, { passive: false });
207
+ on(document, "keydown", onComboboxKeydown, { passive: false });
208
+ //#endregion
209
+ //#region src/js/combobox/state.combobox.ts
210
+ function createComboboxState(element, internals) {
211
+ const select = element.querySelector("select");
212
+ if (select == null || select.id === "") throw new Error(COMBOBOX_MESSAGES.select);
213
+ const label = element.querySelector(`label[for="${select.id}"]`);
214
+ if (label == null) throw new Error(COMBOBOX_MESSAGES.label);
215
+ element.id = select.id;
216
+ const disabled = getBooleanAriaValue(element, "disabled") || select.disabled;
217
+ const multiple = element.multiple || select.multiple;
218
+ const readonly = getBooleanAriaValue(element, "readonly");
219
+ const required = getBooleanAriaValue(element, "required") || select.required;
220
+ const id = getComboboxId();
221
+ const type = multiple ? "select" : getAttributeValue(element, "type", LISTBOX_TYPES, "select");
222
+ const autocomplete = getComboboxAutoComplete(element, type === "select");
223
+ setAttributes(element, {
224
+ autocomplete: autocomplete.value,
225
+ name: select.name === "" ? select.id : select.name
226
+ });
227
+ const labelId = label.id;
228
+ label.htmlFor = id;
229
+ label.id = `${id}_label`;
230
+ generateListboxIds(id, select);
231
+ const parameters = {
232
+ autocomplete,
233
+ element,
234
+ id,
235
+ label,
236
+ multiple,
237
+ select,
238
+ type,
239
+ attributes: COMBOBOX_ATTRIBUTES,
240
+ listbox: false
241
+ };
242
+ const anchor = createComboboxAnchor(parameters);
243
+ const content = createComboboxContent(parameters);
244
+ const { selected } = content.list.options;
245
+ const values = selected.length === 0 ? [""] : selected.map((option) => getData(option, "value", false) ?? "");
246
+ const state = {
247
+ autocomplete,
248
+ disabled,
249
+ id,
250
+ internals,
251
+ multiple,
252
+ readonly,
253
+ required,
254
+ select,
255
+ type,
256
+ active: selected[0],
257
+ anchor: {
258
+ container: anchor.container,
259
+ element: anchor.element
260
+ },
261
+ attributes: COMBOBOX_ATTRIBUTES,
262
+ callbacks: comboboxCallbacks,
263
+ content: content.element,
264
+ elements: [
265
+ element,
266
+ anchor.element,
267
+ content.filter?.element
268
+ ],
269
+ filter: {
270
+ element: content.filter?.element ?? anchor.element,
271
+ value: ""
272
+ },
273
+ floatable: void 0,
274
+ groups: content.list.groups,
275
+ host: element,
276
+ label: {
277
+ element: label,
278
+ id: labelId
279
+ },
280
+ list: content.list.element,
281
+ listbox: false,
282
+ listeners: [],
283
+ localizations: {
284
+ component: {},
285
+ global: comboboxLocalizations
286
+ },
287
+ message: content.message,
288
+ name: element.name,
289
+ options: {
290
+ selected,
291
+ all: content.list.options.all
292
+ },
293
+ selection: anchor.selection,
294
+ values: {
295
+ current: values.slice(),
296
+ initial: values.slice(),
297
+ label: content.value
298
+ }
299
+ };
300
+ return initializeListboxState(state, comboboxStates, onAttachCombobox);
301
+ }
302
+ function onAttachCombobox(state) {
303
+ const { anchor, content, filter, select } = state;
304
+ select.hidden = true;
305
+ select.insertAdjacentElement("afterend", anchor.container ?? anchor.element);
306
+ (anchor.container ?? anchor.element).insertAdjacentElement("afterend", content);
307
+ select.remove();
308
+ state.floatable = createEmbeddedFloatable(anchor.container ?? anchor.element, content, {
309
+ position: "below-start",
310
+ reusable: false
311
+ });
312
+ state.floatable.update();
313
+ state.listeners.push(on(content, "toggle", () => {
314
+ setAria(anchor.element, "expanded", state.floatable.open);
315
+ setAria(filter.element, "activedescendant", state.floatable.open ? state.active?.id : void 0);
316
+ if (state.floatable.open) filter.element.focus();
317
+ }));
318
+ }
319
+ function onDetachCombobox(state) {
320
+ const { content, floatable, label, select } = state;
321
+ floatable.destroy();
322
+ const anchor = state.anchor.container ?? state.anchor.element;
323
+ anchor.insertAdjacentElement("beforebegin", select);
324
+ anchor.remove();
325
+ content.remove();
326
+ label.element.htmlFor = select.id;
327
+ label.element.id = state.label.id;
328
+ select.hidden = false;
329
+ }
330
+ const comboboxCallbacks = {
331
+ data: {
332
+ onApply: onComboboxApply,
333
+ onFilter: onComboboxFilter,
334
+ onForm: noop,
335
+ onPreview: onComboboxPreview,
336
+ onToggle: onComboboxToggle
337
+ },
338
+ keydown: {
339
+ onActivation: onComboboxActivation,
340
+ onEnter: onComboboxEnter,
341
+ onEscape: onComboboxEscape,
342
+ onNavigation: onComboboxNavigation,
343
+ onSelection: onComboboxSelection,
344
+ onTab: onComboboxTab,
345
+ onToggle: onComboboxToggle,
346
+ preventActivation: preventComboboxActivation,
347
+ skipNavigation: skipComboboxNavigation
348
+ }
349
+ };
350
+ const comboboxLocalizations = getLocalizations({
351
+ empty: "No results found",
352
+ input: "Please enter a value",
353
+ invalid: "Please select an option from the list",
354
+ max: (max) => `Please select fewer options (maximum is ${max})`,
355
+ min: (min) => `Please select more options (minimum is ${min})`,
356
+ remove: (label) => `Remove ${label}`,
357
+ results: (amount) => `${amount} results available`,
358
+ selected: (amount) => `${amount} selected`
359
+ });
360
+ const comboboxStates = {
361
+ map: /* @__PURE__ */ new WeakMap(),
362
+ set: /* @__PURE__ */ new Set()
363
+ };
364
+ //#endregion
365
+ export { skipComboboxNavigation as C, setComboboxLocalizations as D, hasTextSelection as E, updateComboboxLocalizations as O, preventComboboxActivation as S, getComboboxId as T, onComboboxPreview as _, onDetachCombobox as a, onComboboxTab as b, onComboboxClickContent as c, onComboboxFilter as d, onComboboxFocusout as f, onComboboxNavigation as g, onComboboxKeydownSelection as h, onAttachCombobox as i, onComboboxEnter as l, onComboboxKeydown as m, comboboxStates as n, onComboboxActivation as o, onComboboxInput as p, createComboboxState as r, onComboboxApply as s, comboboxLocalizations as t, onComboboxEscape as u, onComboboxRemoveSelection as v, getComboboxAutoComplete as w, onComboboxToggle as x, onComboboxSelection as y };
@@ -0,0 +1,25 @@
1
+ //#region src/js/tabs.d.ts
2
+ declare global {
3
+ interface HTMLElementTagNameMap {
4
+ [TAGNAME]: OuiTabsElement;
5
+ }
6
+ }
7
+ declare class OuiTabsEvent extends Event {
8
+ oldIndex: number;
9
+ newIndex: number;
10
+ constructor(name: 'oui:beforechange' | 'oui:change', oldIndex: number, newIndex: number, cancelable: boolean);
11
+ }
12
+ declare class OuiTabsElement extends HTMLElement {
13
+ #private;
14
+ static observedAttributes: string[];
15
+ get active(): number;
16
+ set active(value: number);
17
+ get panel(): HTMLElement;
18
+ get tab(): HTMLButtonElement;
19
+ attributeChangedCallback(name: string, _: string | null, value: string | null): void;
20
+ connectedCallback(): void;
21
+ disconnectedCallback(): void;
22
+ }
23
+ declare const TAGNAME = "oui-tabs";
24
+ //#endregion
25
+ export { OuiTabsElement, OuiTabsEvent };
@@ -0,0 +1,240 @@
1
+ import { OuiNavigable, getNavigableOrientation } from "./internal/navigable.mjs";
2
+ import { getAria, setAria } from "@oscarpalmer/toretto/aria";
3
+ import { getAttribute, setAttribute } from "@oscarpalmer/toretto/attribute";
4
+ import { createElement } from "@oscarpalmer/toretto/create";
5
+ //#region src/js/tabs.ts
6
+ var OuiTabsEvent = class extends Event {
7
+ oldIndex;
8
+ newIndex;
9
+ constructor(name, oldIndex, newIndex, cancelable) {
10
+ super(name, {
11
+ cancelable,
12
+ bubbles: true,
13
+ composed: true
14
+ });
15
+ this.oldIndex = oldIndex;
16
+ this.newIndex = newIndex;
17
+ }
18
+ };
19
+ var OuiTabsElement = class extends HTMLElement {
20
+ static observedAttributes = ["active"];
21
+ #state;
22
+ get active() {
23
+ return this.#state?.active ?? 0;
24
+ }
25
+ set active(value) {
26
+ setActive(this.#state, value);
27
+ }
28
+ get panel() {
29
+ return this.#state?.panels.elements[this.active];
30
+ }
31
+ get tab() {
32
+ return this.#state?.tabList.tabs[this.active];
33
+ }
34
+ attributeChangedCallback(name, _, value) {
35
+ switch (name) {
36
+ case "active": setActive(this.#state, value);
37
+ }
38
+ }
39
+ connectedCallback() {
40
+ if (this.#state == null) this.#state = initializeTabs(this);
41
+ else attachTabs(this.#state);
42
+ }
43
+ disconnectedCallback() {
44
+ if (this.#state != null) detachTabs(this.#state);
45
+ }
46
+ };
47
+ function attachTabs(state) {
48
+ state.navigable = new OuiNavigable(state.tabList.element, {
49
+ orientation: state.orientation,
50
+ selector: SELECTOR_TAB,
51
+ vertical: false,
52
+ onActivate: (navigable) => {
53
+ const index = state.tabList.tabs.indexOf(navigable.active.element);
54
+ if (index > -1 && index !== state.active) onActivate(state, index);
55
+ }
56
+ });
57
+ const { length } = state.panels.elements;
58
+ for (let index = 0; index < length; index += 1) {
59
+ const panel = state.panels.elements[index];
60
+ panel.hidden = index !== state.active;
61
+ panel.role = "tabpanel";
62
+ if (index === 0) {
63
+ panel.insertAdjacentElement("beforebegin", state.tabList.element);
64
+ panel.insertAdjacentElement("beforebegin", state.content);
65
+ }
66
+ state.content.append(panel);
67
+ }
68
+ }
69
+ function createContent(prefix, panels, active) {
70
+ const content = createElement("div", { attribute: { [ATTRIBUTE_CONTENT]: "" } });
71
+ const { length } = panels;
72
+ const hidden = [];
73
+ const roles = [];
74
+ for (let index = 0; index < length; index += 1) {
75
+ const panel = panels[index];
76
+ hidden.push(panel.hidden);
77
+ roles.push(panel.role);
78
+ panel.hidden = index !== active;
79
+ if (panel.id === "") panel.id = `${prefix}_panel_${index}`;
80
+ }
81
+ return {
82
+ element: content,
83
+ panels: {
84
+ hidden,
85
+ roles,
86
+ elements: panels
87
+ }
88
+ };
89
+ }
90
+ function createTab(prefix, index, panel, label, selected) {
91
+ return createElement("button", {
92
+ aria: {
93
+ selected,
94
+ controls: panel.id
95
+ },
96
+ attribute: { [ATTRIBUTE_TAB]: "" },
97
+ property: {
98
+ id: `${prefix}_tab_${index}`,
99
+ role: "tab",
100
+ tabIndex: selected ? 0 : -1,
101
+ textContent: label,
102
+ type: "button"
103
+ }
104
+ });
105
+ }
106
+ function createTabList(prefix, panels, labels, active) {
107
+ const element = createElement("div", {
108
+ attribute: { [ATTRIBUTE_TABLIST]: "" },
109
+ property: { role: "tablist" }
110
+ });
111
+ const { length } = panels;
112
+ const tabs = [];
113
+ for (let index = 0; index < length; index += 1) {
114
+ const tab = createTab(prefix, index, panels[index], labels[index], index === active);
115
+ element.append(tab);
116
+ tabs.push(tab);
117
+ }
118
+ return {
119
+ element,
120
+ tabs
121
+ };
122
+ }
123
+ function detachTabs(state) {
124
+ state.navigable?.destroy();
125
+ const { length } = state.panels.elements;
126
+ for (let index = 0; index < length; index += 1) {
127
+ const panel = state.panels.elements[index];
128
+ panel.hidden = state.panels.hidden[index];
129
+ panel.role = state.panels.roles[index];
130
+ state.content.before(panel);
131
+ }
132
+ state.tabList.element.remove();
133
+ state.content.remove();
134
+ }
135
+ function getActive(element, max) {
136
+ const attribute = getAttribute(element, "active");
137
+ let index = 0;
138
+ if (attribute != null) {
139
+ const value = Number.parseInt(attribute, 10);
140
+ if (!Number.isNaN(value) && value >= 0) index = value > max ? max : value;
141
+ }
142
+ return index;
143
+ }
144
+ function getId() {
145
+ index += 1;
146
+ return `oui_tabs_${index}`;
147
+ }
148
+ function getLabels(panels) {
149
+ const labels = [];
150
+ const { length } = panels;
151
+ for (let index = 0; index < length; index += 1) {
152
+ const panel = panels[index];
153
+ const labelledBy = getAria(panel, "labelledby");
154
+ if (typeof labelledBy === "string" && labelledBy.trim().length > 0) {
155
+ const label = [];
156
+ const selectors = labelledBy.split(/\s+/);
157
+ for (const selector of selectors) {
158
+ const element = document.getElementById(selector);
159
+ if (element == null) return;
160
+ label.push(element.textContent);
161
+ }
162
+ labels.push(label.join(" "));
163
+ continue;
164
+ }
165
+ const label = getAria(panel, "label");
166
+ if (typeof label === "string" && label.trim().length > 0) {
167
+ labels.push(label);
168
+ continue;
169
+ }
170
+ return;
171
+ }
172
+ return labels;
173
+ }
174
+ function initializeTabs(element) {
175
+ const panels = [...element.querySelectorAll(SELECTOR_PANEL)];
176
+ if (panels.length === 0) throw new Error(MESSAGE_EMPTY);
177
+ const labels = getLabels(panels);
178
+ if (labels == null) throw new Error(MESSAGE_INVALID);
179
+ let id;
180
+ if (element.id === "") id = getId();
181
+ else id = element.id;
182
+ const active = getActive(element, panels.length - 1);
183
+ const orientation = getNavigableOrientation(element, false, false);
184
+ const content = createContent(id, panels, active);
185
+ const tabList = createTabList(id, panels, labels, active);
186
+ const state = {
187
+ active,
188
+ id,
189
+ orientation,
190
+ tabList,
191
+ content: content.element,
192
+ host: element,
193
+ panels: content.panels
194
+ };
195
+ attachTabs(state);
196
+ return state;
197
+ }
198
+ function onActivate(state, next) {
199
+ const current = state.active;
200
+ const event = new OuiTabsEvent("oui:beforechange", current, next, true);
201
+ state.host.dispatchEvent(event);
202
+ if (event.defaultPrevented) return;
203
+ state.active = next;
204
+ const { length } = state.panels.elements;
205
+ for (let index = 0; index < length; index += 1) {
206
+ const panel = state.panels.elements[index];
207
+ panel.hidden = index !== next;
208
+ setAria(state.tabList.tabs[index], "selected", index === next);
209
+ }
210
+ setAttribute(state.host, "active", next);
211
+ state.host.dispatchEvent(new OuiTabsEvent("oui:change", current, next, false));
212
+ }
213
+ function setActive(state, value) {
214
+ if (state == null) return;
215
+ const max = state.panels.elements.length - 1;
216
+ let index;
217
+ if (typeof value === "number" && !Number.isNaN(value) && value >= 0) index = value > max ? max : value;
218
+ else if (typeof value === "string") {
219
+ const parsed = Number.parseInt(value, 10);
220
+ if (!Number.isNaN(parsed) && parsed >= 0) index = parsed > max ? max : parsed;
221
+ }
222
+ if (index == null) setAttribute(state.host, "active", state.active);
223
+ else if (index !== state.active) {
224
+ state.navigable?.navigate(index, false);
225
+ onActivate(state, index);
226
+ }
227
+ }
228
+ const TAGNAME = "oui-tabs";
229
+ const ATTRIBUTE_CONTENT = `${TAGNAME}-content`;
230
+ const ATTRIBUTE_PANEL = `${TAGNAME}-panel`;
231
+ const ATTRIBUTE_TAB = `${TAGNAME}-tab`;
232
+ const ATTRIBUTE_TABLIST = `${TAGNAME}-tablist`;
233
+ const MESSAGE_EMPTY = `<${TAGNAME}> must contain at least one [${ATTRIBUTE_PANEL}] child element`;
234
+ const MESSAGE_INVALID = `A [${ATTRIBUTE_PANEL}] child element of <${TAGNAME}> must have a valid 'aria-label' or 'aria-labelledby' attribute`;
235
+ const SELECTOR_PANEL = `:scope > [${ATTRIBUTE_PANEL}]`;
236
+ const SELECTOR_TAB = `[${ATTRIBUTE_TAB}]`;
237
+ let index = 0;
238
+ customElements.define(TAGNAME, OuiTabsElement);
239
+ //#endregion
240
+ export { OuiTabsElement, OuiTabsEvent };
@@ -103,24 +103,25 @@ function findElements(attribute) {
103
103
  return elements;
104
104
  }
105
105
  function getContent() {
106
- const content = createElement("div", {
107
- popover: "hint",
108
- role: "tooltip",
109
- tabIndex: -1
106
+ return createElement("div", {
107
+ attribute: {
108
+ [ATTRIBUTE_CONTENT]: "",
109
+ popover: "hint"
110
+ },
111
+ property: {
112
+ role: "tooltip",
113
+ tabIndex: -1
114
+ }
110
115
  });
111
- setAttribute(content, ATTRIBUTE_CONTENT, "");
112
- return content;
113
116
  }
114
117
  function getElements(anchor) {
115
118
  let elements = findElements(getAria(anchor, "labelledby"));
116
119
  if (elements == null || elements.length === 0) elements = findElements(getAria(anchor, "describedby"));
117
120
  if (elements != null && elements.length > 0) return elements;
118
- let text = getAria(anchor, "label");
119
- if (isNullableOrWhitespace(text)) text = getAria(anchor, "description");
120
- if (isNullableOrWhitespace(text)) return;
121
- const paragraph = document.createElement("p");
122
- paragraph.textContent = text;
123
- return [paragraph];
121
+ let textContent = getAria(anchor, "label");
122
+ if (isNullableOrWhitespace(textContent)) textContent = getAria(anchor, "description");
123
+ if (isNullableOrWhitespace(textContent)) return;
124
+ return [createElement("p", { property: { textContent } })];
124
125
  }
125
126
  function getState(anchor, elements) {
126
127
  const state = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oscarpalmer/oui",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
4
4
  "description": "Styles, components, and utilities for building websites and applications.",
5
5
  "keywords": [
6
6
  "css",
@@ -49,6 +49,10 @@
49
49
  "types": "./dist/js/focus-trap/focus-trap.standalone.d.mts",
50
50
  "default": "./dist/js/focus-trap/focus-trap.standalone.mjs"
51
51
  },
52
+ "./listbox": {
53
+ "types": "./dist/js/listbox/index.d.mts",
54
+ "default": "./dist/js/listbox/index.mjs"
55
+ },
52
56
  "./movable": {
53
57
  "types": "./dist/js/movable/movable.standalone.d.mts",
54
58
  "default": "./dist/js/movable/movable.standalone.mjs"
@@ -95,10 +99,10 @@
95
99
  },
96
100
  "dependencies": {
97
101
  "@oscarpalmer/atoms": "^0.188.1",
98
- "@oscarpalmer/toretto": "^0.50.1"
102
+ "@oscarpalmer/toretto": "^0.51"
99
103
  },
100
104
  "devDependencies": {
101
- "@oxlint/plugins": "^1.77",
105
+ "@oxlint/plugins": "^1.78",
102
106
  "@vitest/coverage-istanbul": "^4.1",
103
107
  "autoprefixer": "^10.5",
104
108
  "concurrently": "^10",