@oscarpalmer/oui 0.36.0 → 0.38.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 (104) hide show
  1. package/dist/css/a11y.css +1 -0
  2. package/dist/css/badge.css +19 -15
  3. package/dist/css/button.css +40 -31
  4. package/dist/css/color-picker.css +377 -0
  5. package/dist/css/color.css +20 -21
  6. package/dist/css/combobox.css +140 -34
  7. package/dist/css/content.css +30 -10
  8. package/dist/css/floatable.css +12 -10
  9. package/dist/css/form.css +400 -129
  10. package/dist/css/listbox.css +43 -17
  11. package/dist/css/message.css +15 -10
  12. package/dist/css/mixins/badge.mixins.css +2 -0
  13. package/dist/css/mixins/button.mixins.css +2 -0
  14. package/dist/css/mixins/message.mixins.css +1 -0
  15. package/dist/css/mixins/misc.mixins.css +2 -0
  16. package/dist/css/mixins/shadow.mixins.css +1 -0
  17. package/dist/css/reset.css +31 -90
  18. package/dist/css/splitter.css +3 -3
  19. package/dist/css/tabs.css +103 -0
  20. package/dist/js/color-picker.d.mts +213 -0
  21. package/dist/js/color-picker.mjs +924 -0
  22. package/dist/js/combobox/dom.combobox.mjs +8 -4
  23. package/dist/js/combobox/event.combobox.mjs +1 -1
  24. package/dist/js/combobox/index.d.mts +1 -1
  25. package/dist/js/combobox/index.mjs +56 -2
  26. package/dist/js/combobox/misc.combobox.d.mts +1 -1
  27. package/dist/js/combobox/misc.combobox.mjs +1 -1
  28. package/dist/js/combobox/state.combobox.d.mts +1 -1
  29. package/dist/js/combobox/state.combobox.mjs +1 -1
  30. package/dist/js/dialog.mjs +2 -2
  31. package/dist/js/floatable/floatable.embedded.mjs +13 -10
  32. package/dist/js/focus-trap/focus-trap.embedded.mjs +9 -13
  33. package/dist/js/{index-EfkUSals.d.mts → index-DJdFlTLm.d.mts} +39 -1
  34. package/dist/js/index.d.mts +4 -2
  35. package/dist/js/index.mjs +5 -3
  36. package/dist/js/internal/draggable.mjs +20 -26
  37. package/dist/js/internal/listbox/listbox.data.mjs +1 -1
  38. package/dist/js/internal/listbox/listbox.dom.mjs +1 -1
  39. package/dist/js/internal/listbox/listbox.event.mjs +1 -1
  40. package/dist/js/internal/listbox/listbox.form.mjs +17 -16
  41. package/dist/js/internal/listbox/listbox.misc.mjs +1 -1
  42. package/dist/js/internal/listbox/listbox.state.mjs +4 -4
  43. package/dist/js/internal/navigable.d.mts +11 -5
  44. package/dist/js/internal/navigable.mjs +46 -19
  45. package/dist/js/listbox/event.listbox.mjs +1 -1
  46. package/dist/js/listbox/index.d.mts +38 -0
  47. package/dist/js/listbox/index.mjs +56 -2
  48. package/dist/js/listbox/misc.listbox.mjs +1 -1
  49. package/dist/js/listbox/state.listbox.mjs +1 -1
  50. package/dist/js/{listbox.data-BWd0PCy7.mjs → listbox.data-C7CRa5Ya.mjs} +5 -4
  51. package/dist/js/menu.mjs +1 -1
  52. package/dist/js/{misc.listbox-BI-XsTOf.mjs → misc.listbox-Cyz2CRDy.mjs} +2 -3
  53. package/dist/js/movable/movable.embedded.mjs +5 -8
  54. package/dist/js/popover.mjs +2 -2
  55. package/dist/js/resizable/resizable.embedded.mjs +21 -31
  56. package/dist/js/sortable/sortable.embedded.mjs +4 -4
  57. package/dist/js/splitter.mjs +20 -17
  58. package/dist/js/{state.combobox-4xq0FPCF.mjs → state.combobox-Qm3FWzYW.mjs} +3 -3
  59. package/dist/js/tabs.d.mts +25 -0
  60. package/dist/js/tabs.mjs +237 -0
  61. package/dist/js/tooltip.mjs +16 -15
  62. package/package.json +7 -5
  63. package/src/css/a11y.scss +2 -2
  64. package/src/css/badge.scss +8 -20
  65. package/src/css/button.scss +8 -47
  66. package/src/css/color-picker.scss +344 -0
  67. package/src/css/color.scss +20 -22
  68. package/src/css/combobox.scss +29 -57
  69. package/src/css/content.scss +34 -6
  70. package/src/css/floatable.scss +22 -19
  71. package/src/css/form.scss +94 -80
  72. package/src/css/listbox.scss +6 -34
  73. package/src/css/message.scss +16 -30
  74. package/src/css/mixins/badge.mixins.scss +53 -0
  75. package/src/css/mixins/button.mixins.scss +112 -0
  76. package/src/css/mixins/input.mixins.scss +114 -0
  77. package/src/css/mixins/message.mixins.scss +41 -0
  78. package/src/css/mixins/misc.mixins.scss +48 -0
  79. package/src/css/mixins/shadow.mixins.scss +57 -0
  80. package/src/css/reset.scss +39 -88
  81. package/src/css/splitter.scss +3 -3
  82. package/src/css/tabs.scss +74 -0
  83. package/src/js/color-picker.ts +1653 -0
  84. package/src/js/combobox/dom.combobox.ts +4 -0
  85. package/src/js/combobox/index.ts +63 -1
  86. package/src/js/dialog.ts +1 -1
  87. package/src/js/floatable/floatable.embedded.ts +18 -14
  88. package/src/js/focus-trap/focus-trap.embedded.ts +11 -19
  89. package/src/js/index.ts +2 -0
  90. package/src/js/internal/draggable.ts +29 -33
  91. package/src/js/internal/listbox/listbox.dom.ts +2 -1
  92. package/src/js/internal/listbox/listbox.form.ts +32 -22
  93. package/src/js/internal/listbox/listbox.state.ts +3 -3
  94. package/src/js/internal/navigable.ts +82 -23
  95. package/src/js/listbox/index.ts +62 -0
  96. package/src/js/listbox/state.listbox.ts +0 -2
  97. package/src/js/movable/movable.embedded.ts +11 -13
  98. package/src/js/popover.ts +1 -1
  99. package/src/js/resizable/resizable.embedded.ts +25 -39
  100. package/src/js/splitter.ts +21 -18
  101. package/src/js/tabs.ts +441 -0
  102. package/src/js/tooltip.ts +20 -17
  103. package/src/css/mixins.scss +0 -194
  104. /package/dist/css/{mixins.css → mixins/input.mixins.css} +0 -0
@@ -73,6 +73,7 @@ export function createComboboxButton(parameters: CreateListboxContentParameters)
73
73
  },
74
74
  property: {
75
75
  textContent: multiple ? '' : (select.selectedOptions[0]?.textContent ?? ''),
76
+ type: 'button',
76
77
  },
77
78
  });
78
79
 
@@ -189,6 +190,9 @@ export function createComboboxSelectionItem(
189
190
  data: {
190
191
  option: option.id,
191
192
  },
193
+ property: {
194
+ type: 'button',
195
+ },
192
196
  });
193
197
 
194
198
  const icon = createElement('span', {
@@ -69,6 +69,20 @@ export class OuiComboboxElement extends HTMLElement {
69
69
  setListboxFilter(this.#state, value);
70
70
  }
71
71
 
72
+ /**
73
+ * Form element associated with the combobox
74
+ */
75
+ get form(): HTMLFormElement | null {
76
+ return this.#internals.form;
77
+ }
78
+
79
+ /**
80
+ * Label elements associated with the combobox
81
+ */
82
+ get labels(): NodeList {
83
+ return this.#internals.labels;
84
+ }
85
+
72
86
  /**
73
87
  * Maximum number of selected options
74
88
  */
@@ -157,6 +171,20 @@ export class OuiComboboxElement extends HTMLElement {
157
171
  return this.#state.type;
158
172
  }
159
173
 
174
+ /**
175
+ * Validation message for the combobox
176
+ */
177
+ get validationMessage(): string {
178
+ return this.#internals.validationMessage;
179
+ }
180
+
181
+ /**
182
+ * Validity state for the combobox
183
+ */
184
+ get validity(): ValidityState {
185
+ return this.#internals.validity;
186
+ }
187
+
160
188
  /**
161
189
  * Current value
162
190
  */
@@ -185,6 +213,13 @@ export class OuiComboboxElement extends HTMLElement {
185
213
  setListboxValues(this.#state, values);
186
214
  }
187
215
 
216
+ /**
217
+ * Can the combobox be validated?
218
+ */
219
+ get willValidate(): boolean {
220
+ return this.#internals.willValidate;
221
+ }
222
+
188
223
  constructor() {
189
224
  super();
190
225
 
@@ -215,6 +250,15 @@ export class OuiComboboxElement extends HTMLElement {
215
250
  }
216
251
  }
217
252
 
253
+ /**
254
+ * Check if the combobox is valid
255
+ *
256
+ * @returns `true` if the combobox is valid, otherwise `false`
257
+ */
258
+ checkValidity(): boolean {
259
+ return this.#internals.checkValidity();
260
+ }
261
+
218
262
  /**
219
263
  * Clear all selected options
220
264
  */
@@ -244,7 +288,7 @@ export class OuiComboboxElement extends HTMLElement {
244
288
  setListboxValues(this.#state, this.#state.values.initial);
245
289
  }
246
290
 
247
- formStateRestoreCallback(state: string | File | FormData, reason: unknown): void {
291
+ formStateRestoreCallback(state: unknown, reason: unknown): void {
248
292
  if (reason !== 'restore') {
249
293
  return;
250
294
  }
@@ -262,6 +306,24 @@ export class OuiComboboxElement extends HTMLElement {
262
306
  }
263
307
  }
264
308
 
309
+ /**
310
+ * Check if the combobox is valid and report any validation errors
311
+ *
312
+ * @returns `true` if the combobox is valid, otherwise `false`
313
+ */
314
+ reportValidity(): boolean {
315
+ return this.#internals.reportValidity();
316
+ }
317
+
318
+ /**
319
+ * Set a custom validation message for the combobox
320
+ *
321
+ * @param message Validation message
322
+ */
323
+ setCustomValidity(message: string): void {
324
+ this.#internals.setValidity({customError: message !== ''}, message, this);
325
+ }
326
+
265
327
  /**
266
328
  * Set combobox localizations for a specific language
267
329
  *
package/src/js/dialog.ts CHANGED
@@ -199,7 +199,7 @@ function getState(element: HTMLDialogElement): OuiDialogState {
199
199
  }
200
200
  }),
201
201
  ],
202
- modal: getAria(element, 'modal') === 'true' || element.matches(':modal'),
202
+ modal: getAria(element, 'modal') === true || element.matches(':modal'),
203
203
  };
204
204
 
205
205
  state.dialog = new OuiDialog(state);
@@ -1,5 +1,5 @@
1
1
  import {isPlainObject} from '@oscarpalmer/atoms/is';
2
- import {getAria} from '@oscarpalmer/toretto/aria';
2
+ import {getAria, setAria} from '@oscarpalmer/toretto/aria';
3
3
  import {getAttribute, setAttribute} from '@oscarpalmer/toretto/attribute';
4
4
  import {on} from '@oscarpalmer/toretto/event';
5
5
  import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
@@ -45,18 +45,7 @@ export class OuiFloatable {
45
45
  constructor(state: OuiFloatableState, standalone: boolean) {
46
46
  this.#state = state;
47
47
 
48
- const {anchor, content} = state;
49
-
50
- if (standalone) {
51
- setAttribute(content, ATTRIBUTE_FLOATABLE, '');
52
-
53
- content.popover = 'manual';
54
- } else {
55
- content.popover ??= 'auto';
56
- }
57
-
58
- setStyle(anchor, 'anchorName', state.name);
59
- setStyle(content, 'positionAnchor', state.name);
48
+ this.#initialize(state, standalone);
60
49
  }
61
50
 
62
51
  /**
@@ -103,6 +92,21 @@ export class OuiFloatable {
103
92
  setPosition(this.#state, position);
104
93
  }
105
94
  }
95
+
96
+ #initialize(state: OuiFloatableState, standalone: boolean): void {
97
+ const {anchor, content} = state;
98
+
99
+ if (standalone) {
100
+ setAttribute(content, ATTRIBUTE_FLOATABLE, '');
101
+
102
+ content.popover = 'manual';
103
+ } else {
104
+ content.popover ??= 'auto';
105
+ }
106
+
107
+ setStyle(anchor, 'anchorName', state.name);
108
+ setStyle(content, 'positionAnchor', state.name);
109
+ }
106
110
  }
107
111
 
108
112
  export type OuiFloatableOptions = {
@@ -220,7 +224,7 @@ export function getOnBeforeToggleListener(element: HTMLElement): RemovableEventL
220
224
  event => {
221
225
  const {newState, source} = event;
222
226
 
223
- if (newState === 'open' && source != null && getAria(source, 'disabled') === 'true') {
227
+ if (newState === 'open' && source != null && getAria(source, 'disabled') === true) {
224
228
  event.preventDefault();
225
229
  }
226
230
  },
@@ -285,31 +285,23 @@ function onTab(event: KeyboardEvent): void {
285
285
 
286
286
  const instance = instances.get(element);
287
287
 
288
- if (instance == null) {
288
+ if (instance == null || dispatch(element, 'oui:focus-trap').defaultPrevented) {
289
289
  return;
290
290
  }
291
291
 
292
- const dispatched = dispatch(element, 'focus-trap');
293
-
294
- setTimeout(() => {
295
- if (dispatched.defaultPrevented) {
296
- return;
297
- }
298
-
299
- if (!instance.disabled) {
300
- event.preventDefault();
301
- }
292
+ if (!instance.disabled) {
293
+ event.preventDefault();
294
+ }
302
295
 
303
- const elements = getFocusable(element);
296
+ const elements = getFocusable(element);
304
297
 
305
- const index = elements.indexOf(event.target as HTMLElement);
298
+ const index = elements.indexOf(event.target as HTMLElement);
306
299
 
307
- if (index === -1 || elements.length === 0) {
308
- tabToTrap(instance, elements, element);
309
- } else {
310
- tabToElement(instance, elements, index, event.shiftKey ? -1 : 1);
311
- }
312
- });
300
+ if (index === -1 || elements.length === 0) {
301
+ tabToTrap(instance, elements, element);
302
+ } else {
303
+ tabToElement(instance, elements, index, event.shiftKey ? -1 : 1);
304
+ }
313
305
  }
314
306
 
315
307
  export function removeFocusTrap(element: HTMLElement): void {
package/src/js/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import './menu';
2
2
  import './toolbar';
3
3
 
4
+ export * from './color-picker';
4
5
  export * from './combobox/index';
5
6
  export * from './dialog';
6
7
  export * from './floatable/floatable.standalone';
@@ -11,4 +12,5 @@ export * from './popover';
11
12
  export * from './resizable/resizable.standalone';
12
13
  export * from './sortable/sortable.standalone';
13
14
  export * from './splitter';
15
+ export * from './tabs';
14
16
  export * from './tooltip';
@@ -258,7 +258,7 @@ function cancelDrag(item?: OuiDraggableItem): void {
258
258
 
259
259
  item.state.options.drag.onCancel?.(globals, item);
260
260
 
261
- dispatch(item.state.element, `${item.state.name}:cancel`, {
261
+ dispatch(item.state.element, `oui:${item.state.name}:cancel`, {
262
262
  detail: {
263
263
  element: item.state.options.getOrigin(),
264
264
  },
@@ -406,35 +406,33 @@ function onPointerdown(event: MouseEvent | TouchEvent): void {
406
406
  globals.offset = item.state.options.position.getOffset(globals, item, element, position);
407
407
  globals.original = item.state.options.position.getOriginal(globals, item, position);
408
408
 
409
- const dispatched = dispatch(item.state.element, `${item.state.name}:begin`, {
410
- detail: {
411
- element,
412
- position: {...globals.original},
413
- },
414
- });
415
-
416
- setTimeout(() => {
417
- if (dispatched.defaultPrevented) {
418
- item.state.options.drag.onCancel?.(globals, item);
409
+ if (
410
+ dispatch(item.state.element, `oui:${item.state.name}:begin`, {
411
+ detail: {
412
+ element,
413
+ position: {...globals.original},
414
+ },
415
+ }).defaultPrevented
416
+ ) {
417
+ item.state.options.drag.onCancel?.(globals, item);
419
418
 
420
- reset();
419
+ reset();
421
420
 
422
- return;
423
- }
421
+ return;
422
+ }
424
423
 
425
- const next = getNextPosition(item, position);
424
+ const next = getNextPosition(item, position);
426
425
 
427
- setStyles(dragged, {
428
- position: 'fixed',
429
- inset: `${next.y}px auto auto ${next.x}px`,
430
- transform: 'translate3d(0, 0, 0)',
431
- });
426
+ setStyles(dragged, {
427
+ position: 'fixed',
428
+ inset: `${next.y}px auto auto ${next.x}px`,
429
+ transform: 'translate3d(0, 0, 0)',
430
+ });
432
431
 
433
- item.state.options.drag.onBegin?.(event, globals, item, element, handle, next);
432
+ item.state.options.drag.onBegin?.(event, globals, item, element, handle, next);
434
433
 
435
- item.state.styling.set();
436
- styleToggler.set();
437
- });
434
+ item.state.styling.set();
435
+ styleToggler.set();
438
436
  }
439
437
 
440
438
  function onPointermove(event: MouseEvent | TouchEvent): void {
@@ -459,15 +457,13 @@ function onPointermove(event: MouseEvent | TouchEvent): void {
459
457
  original: position,
460
458
  });
461
459
 
462
- const dispatched = dispatch(current.state.element, `${current.state.name}:move`, {
463
- detail,
464
- });
465
-
466
- setTimeout(() => {
467
- if (dispatched.defaultPrevented) {
468
- cancelDrag(current);
469
- }
470
- });
460
+ if (
461
+ dispatch(current.state.element, `oui:${current.state.name}:move`, {
462
+ detail,
463
+ }).defaultPrevented
464
+ ) {
465
+ cancelDrag(current);
466
+ }
471
467
  }
472
468
 
473
469
  function onPointerup(event: MouseEvent | TouchEvent): void {
@@ -199,13 +199,14 @@ export function createListboxInput(
199
199
  autocorrect: 'off',
200
200
  autocomplete: 'off',
201
201
  form: '',
202
+ inputmode: 'text',
202
203
  spellcheck: 'false',
203
- type: 'text',
204
204
  },
205
205
  property: {
206
206
  id,
207
207
  value,
208
208
  role: isAutocomplete || listbox ? 'combobox' : 'searchbox',
209
+ type: 'text',
209
210
  },
210
211
  });
211
212
 
@@ -32,24 +32,38 @@ export function updateListboxForm<State extends OuiListboxState>(
32
32
  updateListboxValidity(state);
33
33
 
34
34
  if (emit ?? true) {
35
- dispatch(state.host, 'change');
35
+ dispatch(state.host, 'oui:change');
36
36
  }
37
37
  }
38
38
 
39
39
  export function updateListboxValidity(state: OuiListboxState): void {
40
- if (state.autocomplete.none && !state.listbox) {
41
- const valueMissing = state.required && state.filter.element.value.length === 0;
40
+ const {
41
+ autocomplete,
42
+ filter,
43
+ host,
44
+ internals,
45
+ listbox,
46
+ localizations,
47
+ max,
48
+ min,
49
+ multiple,
50
+ options,
51
+ required,
52
+ } = state;
53
+
54
+ if (autocomplete.none && !listbox) {
55
+ const valueMissing = required && filter.element.value.length === 0;
42
56
 
43
57
  const localization = getLocalization(
44
- state.localizations.global,
45
- state.host,
58
+ localizations.global,
59
+ host,
46
60
  'invalid',
47
- state.localizations.component,
61
+ localizations.component,
48
62
  );
49
63
 
50
- state.internals.setValidity({valueMissing}, localize(localization, {}, []));
64
+ internals.setValidity({valueMissing}, localize(localization, {}, []));
51
65
 
52
- state.internals.checkValidity();
66
+ internals.checkValidity();
53
67
 
54
68
  return;
55
69
  }
@@ -60,31 +74,27 @@ export function updateListboxValidity(state: OuiListboxState): void {
60
74
  let parameters: unknown[] = [];
61
75
 
62
76
  switch (true) {
63
- case state.required && state.options.selected.length === 0: {
77
+ case required && options.selected.length === 0: {
64
78
  error = 'badInput';
65
79
  key = 'input';
66
80
 
67
81
  break;
68
82
  }
69
83
 
70
- case state.multiple &&
71
- typeof state.max === 'number' &&
72
- state.options.selected.length > state.max: {
73
- data = {amount: state.max};
84
+ case multiple && typeof max === 'number' && options.selected.length > max: {
85
+ data = {amount: max};
74
86
  error = 'rangeOverflow';
75
87
  key = 'max';
76
- parameters = [state.max];
88
+ parameters = [max];
77
89
 
78
90
  break;
79
91
  }
80
92
 
81
- case state.multiple &&
82
- typeof state.min === 'number' &&
83
- state.options.selected.length < state.min: {
84
- data = {amount: state.min};
93
+ case multiple && typeof min === 'number' && options.selected.length < min: {
94
+ data = {amount: min};
85
95
  error = 'rangeUnderflow';
86
96
  key = 'min';
87
- parameters = [state.min];
97
+ parameters = [min];
88
98
 
89
99
  break;
90
100
  }
@@ -96,11 +106,11 @@ export function updateListboxValidity(state: OuiListboxState): void {
96
106
  const localization =
97
107
  key == null
98
108
  ? undefined
99
- : getLocalization(state.localizations.global, state.host, key, state.localizations.component);
109
+ : getLocalization(localizations.global, host, key, localizations.component);
100
110
 
101
111
  const message = localization == null ? undefined : localize(localization, data, parameters);
102
112
 
103
- state.internals.setValidity(
113
+ internals.setValidity(
104
114
  error == null
105
115
  ? {}
106
116
  : {
@@ -109,7 +119,7 @@ export function updateListboxValidity(state: OuiListboxState): void {
109
119
  message,
110
120
  );
111
121
 
112
- state.internals.checkValidity();
122
+ internals.checkValidity();
113
123
  }
114
124
 
115
125
  // #endregion
@@ -18,10 +18,10 @@ export function attachListbox<State extends OuiListboxState>(
18
18
  states: OuiListboxStates<State>,
19
19
  onAttach: (state: State) => void,
20
20
  ): void {
21
- const {filter, host, label} = state;
21
+ const {filter, host, id, label} = state;
22
22
 
23
23
  label.element.htmlFor = filter.element.id;
24
- label.element.id = `${state.id}_label`;
24
+ label.element.id = `${id}_label`;
25
25
 
26
26
  onAttach(state);
27
27
 
@@ -73,7 +73,7 @@ export function initializeListboxState<State extends OuiListboxState>(
73
73
  updateListboxValueLabel(state);
74
74
 
75
75
  if (state.options.selected.length > 0) {
76
- updateListboxPreview('none', state, state.options.selected[0]);
76
+ updateListboxPreview('none', state, state.options.selected[0], false);
77
77
  }
78
78
 
79
79
  return state;
@@ -1,6 +1,7 @@
1
1
  import {clamp} from '@oscarpalmer/atoms/number';
2
- import {getAria, setRole} from '@oscarpalmer/toretto/aria';
3
- import {dispatch, on} from '@oscarpalmer/toretto/event';
2
+ import {getAria, setAria, setRole} from '@oscarpalmer/toretto/aria';
3
+ import {getAttribute} from '@oscarpalmer/toretto/attribute';
4
+ import {on} from '@oscarpalmer/toretto/event';
4
5
  import {findAncestor} from '@oscarpalmer/toretto/find';
5
6
  import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
6
7
  import type {AriaRole, RemovableEventListener} from '@oscarpalmer/toretto/models';
@@ -25,6 +26,16 @@ export class OuiNavigable {
25
26
  this.options = options;
26
27
 
27
28
  this.listeners = [
29
+ on(
30
+ element,
31
+ 'click',
32
+ event => {
33
+ onClick(this, event);
34
+ },
35
+ {
36
+ passive: false,
37
+ },
38
+ ),
28
39
  on(element, 'focusin', event => {
29
40
  onFocusin(this, event);
30
41
  }),
@@ -40,7 +51,9 @@ export class OuiNavigable {
40
51
  ),
41
52
  ];
42
53
 
43
- this.orientation = getOrientation(element, options);
54
+ this.orientation = options?.orientation ?? getNavigableOrientation(element, options.vertical);
55
+
56
+ setAria(element, 'orientation', this.orientation);
44
57
 
45
58
  if (options.role != null) {
46
59
  setRole(element, options.role);
@@ -57,6 +70,14 @@ export class OuiNavigable {
57
70
  this.active.element = undefined;
58
71
  this.element = undefined as never;
59
72
  }
73
+
74
+ navigate(index: number, focus?: boolean): void {
75
+ const items = getItems(this.element, this.options.selector);
76
+
77
+ if (index >= 0 && index < items.length) {
78
+ onNavigate(this, index, items, focus ?? false);
79
+ }
80
+ }
60
81
  }
61
82
 
62
83
  type OuiNavigableActive = {
@@ -64,17 +85,25 @@ type OuiNavigableActive = {
64
85
  index: number;
65
86
  };
66
87
 
88
+ type OuiNavigableCallback<ReturnType> = (
89
+ event: KeyboardEvent,
90
+ navigable: OuiNavigable,
91
+ ) => ReturnType;
92
+
67
93
  export type OuiNavigableOptions = {
94
+ orientation?: OuiNavigableOrientation;
68
95
  role?: AriaRole;
69
96
  selector: string;
70
97
  vertical: boolean;
71
- onDeactivate?: (event: KeyboardEvent, navigable: OuiNavigable) => void;
72
- onPreventNavigation?: (event: KeyboardEvent, navigable: OuiNavigable) => boolean;
73
- onShouldActivate?: (event: KeyboardEvent, navigable: OuiNavigable) => boolean;
74
- onShouldDeactivate?: (event: KeyboardEvent, navigable: OuiNavigable) => boolean | undefined;
98
+ onActivate?: (navigable: OuiNavigable) => void;
99
+ onDeactivate?: OuiNavigableCallback<void>;
100
+ onNavigation?: (navigable: OuiNavigable) => void;
101
+ onPreventNavigation?: OuiNavigableCallback<boolean>;
102
+ onShouldActivate?: OuiNavigableCallback<boolean>;
103
+ onShouldDeactivate?: OuiNavigableCallback<boolean | undefined>;
75
104
  };
76
105
 
77
- type OuiNavigableOrientation = 'horizontal' | 'vertical';
106
+ export type OuiNavigableOrientation = 'horizontal' | 'vertical';
78
107
 
79
108
  // #endregion
80
109
 
@@ -88,15 +117,17 @@ export function getNavigable(element: HTMLElement, options: OuiNavigableOptions)
88
117
  return new OuiNavigable(element, options);
89
118
  }
90
119
 
91
- function getOrientation(
120
+ export function getNavigableOrientation(
92
121
  element: HTMLElement,
93
- options: OuiNavigableOptions,
122
+ vertical: boolean,
123
+ aria?: boolean,
94
124
  ): OuiNavigableOrientation {
95
- const attribute = getAria(element, 'orientation');
125
+ const attribute =
126
+ (aria ?? true) ? getAria(element, 'orientation') : getAttribute(element, 'orientation');
96
127
 
97
128
  return ORIENTATIONS.has(attribute as OuiNavigableOrientation)
98
129
  ? (attribute as OuiNavigableOrientation)
99
- : options.vertical
130
+ : vertical
100
131
  ? ORIENTATION_VERTICAL
101
132
  : ORIENTATION_HORIZONTAL;
102
133
  }
@@ -105,6 +136,39 @@ function isValidItem(element: Element): element is HTMLElement {
105
136
  return 'disabled' in element ? element.disabled !== true : true;
106
137
  }
107
138
 
139
+ function onClick(navigable: OuiNavigable, event: Event): void {
140
+ const item = findAncestor(event, navigable.options.selector) as HTMLElement;
141
+ const items = getItems(navigable.element, navigable.options.selector);
142
+
143
+ const index = items.indexOf(item);
144
+
145
+ if (index === -1) {
146
+ return;
147
+ }
148
+
149
+ if (index !== navigable.active.index) {
150
+ onNavigate(navigable, index, items, false);
151
+ }
152
+
153
+ navigable.options.onActivate?.(navigable);
154
+
155
+ item.focus();
156
+ }
157
+
158
+ function onNavigate(
159
+ navigable: OuiNavigable,
160
+ index: number,
161
+ items: HTMLElement[],
162
+ focus: boolean,
163
+ ): void {
164
+ navigable.active.element = items[index];
165
+ navigable.active.index = index;
166
+
167
+ updateNavigableFocus(navigable, focus, items);
168
+
169
+ navigable.options.onNavigation?.(navigable);
170
+ }
171
+
108
172
  function onFocusin(navigable: OuiNavigable, event: FocusEvent): void {
109
173
  const item = findAncestor(event, navigable.options.selector);
110
174
 
@@ -155,6 +219,8 @@ function onKeydown(navigable: OuiNavigable, event: KeyboardEvent): void {
155
219
  }
156
220
 
157
221
  if (mustActivate || shouldActivate) {
222
+ navigable.options.onActivate?.(navigable);
223
+
158
224
  return;
159
225
  }
160
226
 
@@ -165,6 +231,8 @@ function onKeydown(navigable: OuiNavigable, event: KeyboardEvent): void {
165
231
  return;
166
232
  }
167
233
 
234
+ event.preventDefault();
235
+
168
236
  let index: number | undefined;
169
237
 
170
238
  if (KEYS_ABSOLUTE.has(event.key)) {
@@ -188,14 +256,9 @@ function onKeydown(navigable: OuiNavigable, event: KeyboardEvent): void {
188
256
  }
189
257
  }
190
258
 
191
- if (index == null || index === navigable.active.index) {
192
- return;
259
+ if (index != null && index !== navigable.active.index) {
260
+ onNavigate(navigable, index, items, true);
193
261
  }
194
-
195
- navigable.active.element = items[index];
196
- navigable.active.index = index;
197
-
198
- updateNavigableFocus(navigable, true, items);
199
262
  }
200
263
 
201
264
  export function updateNavigableFocus(
@@ -219,10 +282,6 @@ export function updateNavigableFocus(
219
282
  if (focus) {
220
283
  navigable.active.element?.focus();
221
284
  }
222
-
223
- dispatch(navigable.active.element, 'navigable:change', {
224
- detail: {...navigable.active},
225
- });
226
285
  }
227
286
 
228
287
  // #endregion