@oscarpalmer/oui 0.36.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 (51) 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 +133 -34
  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 +41 -17
  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/focus-trap/focus-trap.embedded.mjs +6 -10
  14. package/dist/js/index.d.mts +2 -1
  15. package/dist/js/index.mjs +2 -1
  16. package/dist/js/internal/draggable.mjs +16 -22
  17. package/dist/js/internal/listbox/listbox.form.mjs +1 -1
  18. package/dist/js/internal/listbox/listbox.state.mjs +1 -1
  19. package/dist/js/internal/navigable.d.mts +11 -5
  20. package/dist/js/internal/navigable.mjs +44 -18
  21. package/dist/js/movable/movable.embedded.mjs +3 -6
  22. package/dist/js/resizable/resizable.embedded.mjs +16 -26
  23. package/dist/js/splitter.mjs +15 -12
  24. package/dist/js/tabs.d.mts +25 -0
  25. package/dist/js/tabs.mjs +240 -0
  26. package/dist/js/tooltip.mjs +13 -12
  27. package/package.json +1 -1
  28. package/src/css/badge.scss +6 -18
  29. package/src/css/button.scss +6 -45
  30. package/src/css/color.scss +20 -20
  31. package/src/css/combobox.scss +20 -51
  32. package/src/css/content.scss +30 -3
  33. package/src/css/floatable.scss +17 -17
  34. package/src/css/form.scss +84 -72
  35. package/src/css/listbox.scss +2 -31
  36. package/src/css/message.scss +13 -28
  37. package/src/css/mixins.scss +253 -28
  38. package/src/css/reset.scss +37 -86
  39. package/src/css/splitter.scss +2 -2
  40. package/src/css/tabs.scss +85 -0
  41. package/src/js/focus-trap/focus-trap.embedded.ts +11 -19
  42. package/src/js/index.ts +1 -0
  43. package/src/js/internal/draggable.ts +29 -33
  44. package/src/js/internal/listbox/listbox.form.ts +1 -1
  45. package/src/js/internal/listbox/listbox.state.ts +1 -1
  46. package/src/js/internal/navigable.ts +80 -23
  47. package/src/js/movable/movable.embedded.ts +11 -13
  48. package/src/js/resizable/resizable.embedded.ts +25 -39
  49. package/src/js/splitter.ts +21 -18
  50. package/src/js/tabs.ts +447 -0
  51. package/src/js/tooltip.ts +20 -17
@@ -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
 
@@ -188,14 +254,9 @@ function onKeydown(navigable: OuiNavigable, event: KeyboardEvent): void {
188
254
  }
189
255
  }
190
256
 
191
- if (index == null || index === navigable.active.index) {
192
- return;
257
+ if (index != null && index !== navigable.active.index) {
258
+ onNavigate(navigable, index, items, true);
193
259
  }
194
-
195
- navigable.active.element = items[index];
196
- navigable.active.index = index;
197
-
198
- updateNavigableFocus(navigable, true, items);
199
260
  }
200
261
 
201
262
  export function updateNavigableFocus(
@@ -219,10 +280,6 @@ export function updateNavigableFocus(
219
280
  if (focus) {
220
281
  navigable.active.element?.focus();
221
282
  }
222
-
223
- dispatch(navigable.active.element, 'navigable:change', {
224
- detail: {...navigable.active},
225
- });
226
283
  }
227
284
 
228
285
  // #endregion
@@ -199,20 +199,18 @@ function onEnd(
199
199
  setOffset(state, container);
200
200
  }
201
201
 
202
- const dispatched = dispatch(state.element, 'movable:end', {
203
- detail: {
204
- from: {...globals.original},
205
- to: getEventPosition(event),
206
- },
207
- });
208
-
209
- setTimeout(() => {
210
- if (dispatched.defaultPrevented) {
211
- onCancel(globals, item);
212
- }
202
+ if (
203
+ dispatch(state.element, 'oui:movable:end', {
204
+ detail: {
205
+ from: {...globals.original},
206
+ to: getEventPosition(event),
207
+ },
208
+ }).defaultPrevented
209
+ ) {
210
+ onCancel(globals, item);
211
+ }
213
212
 
214
- reset();
215
- });
213
+ reset();
216
214
  }
217
215
 
218
216
  function onMove(
@@ -101,7 +101,7 @@ function cancelResize(item?: OuiResizableItem): void {
101
101
 
102
102
  resetResizable();
103
103
 
104
- dispatch(item.state.element, 'resizable:cancel');
104
+ dispatch(item.state.element, 'oui:resizable:cancel');
105
105
  }
106
106
 
107
107
  /**
@@ -289,19 +289,15 @@ function onPointerdown(event: MouseEvent | TouchEvent): void {
289
289
  event.preventDefault();
290
290
  }
291
291
 
292
- const dispatched = dispatch(element!, 'resizable:begin');
293
-
294
- setTimeout(() => {
295
- if (dispatched.defaultPrevented) {
296
- return;
297
- }
292
+ if (dispatch(element!, 'oui:resizable:begin').defaultPrevented) {
293
+ return;
294
+ }
298
295
 
299
- current = stored;
300
- start = position;
296
+ current = stored;
297
+ start = position;
301
298
 
302
- current.state.styles.set();
303
- styleToggler.set();
304
- });
299
+ current.state.styles.set();
300
+ styleToggler.set();
305
301
  }
306
302
 
307
303
  function onPointermove(event: MouseEvent | TouchEvent): void {
@@ -320,41 +316,31 @@ function onPointermove(event: MouseEvent | TouchEvent): void {
320
316
 
321
317
  getAndSetDimensions(current.state.element, current.state.options, {height, width});
322
318
 
323
- const dispatched = dispatch(current.state.element, 'resizable:resize', {
324
- detail: {
325
- height,
326
- width,
327
- },
328
- });
329
-
330
- setTimeout(() => {
331
- if (dispatched.defaultPrevented) {
332
- cancelResize(current);
333
- }
334
- });
319
+ if (
320
+ dispatch(current.state.element, 'oui:resizable:resize', {
321
+ detail: {
322
+ height,
323
+ width,
324
+ },
325
+ }).defaultPrevented
326
+ ) {
327
+ cancelResize(current);
328
+ }
335
329
  }
336
330
 
337
331
  function onPointerup(): void {
338
- if (current == null) {
332
+ if (current == null || dispatch(current.state.element, 'oui:resizable:end').defaultPrevented) {
333
+ cancelResize(current);
334
+
339
335
  return;
340
336
  }
341
337
 
342
- const dispatched = dispatch(current.state.element, 'resizable:end');
343
-
344
- setTimeout(() => {
345
- if (current == null || dispatched.defaultPrevented) {
346
- cancelResize(current);
338
+ const {height, width} = current.state.element.getBoundingClientRect();
347
339
 
348
- return;
349
- }
350
-
351
- const {height, width} = current.state.element.getBoundingClientRect();
340
+ current.state.height = height;
341
+ current.state.width = width;
352
342
 
353
- current.state.height = height;
354
- current.state.width = width;
355
-
356
- resetResizable();
357
- });
343
+ resetResizable();
358
344
  }
359
345
 
360
346
  function removeResizable(element: HTMLElement): void {
@@ -3,6 +3,7 @@ import {round} from '@oscarpalmer/atoms/math';
3
3
  import {clamp} from '@oscarpalmer/atoms/number';
4
4
  import {setAria} from '@oscarpalmer/toretto/aria';
5
5
  import {getAttribute, setAttribute} from '@oscarpalmer/toretto/attribute';
6
+ import {createElement} from '@oscarpalmer/toretto/create';
6
7
  import {getEventPosition, on} from '@oscarpalmer/toretto/event';
7
8
  import {findAncestor} from '@oscarpalmer/toretto/find';
8
9
  import {setStyle, toggleStyles} from '@oscarpalmer/toretto/style';
@@ -238,28 +239,30 @@ type OuiSplitterValuesNow = {
238
239
  // #region Functions
239
240
 
240
241
  function createHandle(): HTMLSpanElement {
241
- const handle = document.createElement('span');
242
-
243
- setAria(handle, 'hidden', 'true');
244
- setAttribute(handle, ATTRIBUTE_HANDLE, '');
245
-
246
- return handle;
242
+ return createElement('span', {
243
+ aria: {
244
+ hidden: true,
245
+ },
246
+ attribute: {
247
+ [ATTRIBUTE_HANDLE]: '',
248
+ },
249
+ });
247
250
  }
248
251
 
249
252
  function createSeparator(splitter: OuiSplitter): HTMLDivElement {
250
- const separator = document.createElement('div');
251
-
252
- separator.role = 'separator';
253
- separator.tabIndex = 0;
254
-
255
- setAria(separator, {
256
- controls: splitter.element.id,
257
- label: 'Resize panels',
253
+ return createElement('div', {
254
+ aria: {
255
+ controls: splitter.element.id,
256
+ label: 'Resize panels',
257
+ },
258
+ attribute: {
259
+ [ATTRIBUTE_SEPARATOR]: '',
260
+ },
261
+ property: {
262
+ role: 'separator',
263
+ tabIndex: 0,
264
+ },
258
265
  });
259
-
260
- setAttribute(separator, ATTRIBUTE_SEPARATOR, '');
261
-
262
- return separator;
263
266
  }
264
267
 
265
268
  function getContained(min: number, value: number, max: number): number {