@oscarpalmer/oui 0.27.0 → 0.29.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 (54) hide show
  1. package/dist/css/floatable.css +9 -0
  2. package/dist/js/dialog.d.mts +3 -1
  3. package/dist/js/dialog.mjs +35 -19
  4. package/dist/js/{draggable-zszaW1qS.d.mts → draggable-fIYXd8eO.d.mts} +3 -3
  5. package/dist/js/floatable/floatable.embedded.d.mts +2 -2
  6. package/dist/js/floatable/floatable.embedded.mjs +17 -16
  7. package/dist/js/floatable/floatable.standalone.d.mts +1 -1
  8. package/dist/js/{floatable.embedded-YxGwzgbE.d.mts → floatable.embedded-DUR8bQBK.d.mts} +1 -2
  9. package/dist/js/focus-trap/focus-trap.embedded.mjs +6 -5
  10. package/dist/js/index.d.mts +4 -3
  11. package/dist/js/index.mjs +6 -2
  12. package/dist/js/internal/attributable.mjs +3 -4
  13. package/dist/js/internal/draggable.d.mts +1 -1
  14. package/dist/js/internal/draggable.mjs +12 -9
  15. package/dist/js/internal/navigable.d.mts +32 -0
  16. package/dist/js/internal/navigable.mjs +107 -0
  17. package/dist/js/menu.d.mts +1 -0
  18. package/dist/js/menu.mjs +168 -0
  19. package/dist/js/movable/movable.embedded.d.mts +1 -1
  20. package/dist/js/movable/movable.embedded.mjs +8 -13
  21. package/dist/js/movable/movable.standalone.d.mts +1 -1
  22. package/dist/js/popover.d.mts +1 -1
  23. package/dist/js/popover.mjs +27 -12
  24. package/dist/js/resizable/resizable.embedded.d.mts +2 -0
  25. package/dist/js/resizable/resizable.embedded.mjs +191 -0
  26. package/dist/js/resizable/resizable.standalone.d.mts +2 -0
  27. package/dist/js/resizable/resizable.standalone.mjs +2 -0
  28. package/dist/js/resizable.embedded-CXrUVPzR.d.mts +35 -0
  29. package/dist/js/sortable/sortable.embedded.d.mts +1 -1
  30. package/dist/js/sortable/sortable.embedded.mjs +15 -13
  31. package/dist/js/sortable/sortable.standalone.d.mts +1 -1
  32. package/dist/js/splitter.mjs +30 -26
  33. package/dist/js/toolbar.mjs +12 -88
  34. package/dist/js/tooltip.d.mts +1 -1
  35. package/dist/js/tooltip.mjs +12 -17
  36. package/package.json +11 -3
  37. package/src/css/floatable.scss +9 -0
  38. package/src/css/form.scss +6 -6
  39. package/src/js/dialog.ts +50 -24
  40. package/src/js/floatable/floatable.embedded.ts +19 -18
  41. package/src/js/focus-trap/focus-trap.embedded.ts +6 -5
  42. package/src/js/index.ts +3 -1
  43. package/src/js/internal/attributable.ts +3 -5
  44. package/src/js/internal/draggable.ts +13 -10
  45. package/src/js/internal/navigable.ts +247 -0
  46. package/src/js/menu.ts +345 -0
  47. package/src/js/movable/movable.embedded.ts +10 -13
  48. package/src/js/popover.ts +41 -15
  49. package/src/js/resizable/resizable.embedded.ts +390 -0
  50. package/src/js/resizable/resizable.standalone.ts +5 -0
  51. package/src/js/sortable/sortable.embedded.ts +17 -22
  52. package/src/js/splitter.ts +41 -36
  53. package/src/js/toolbar.ts +27 -178
  54. package/src/js/tooltip.ts +18 -30
package/src/js/menu.ts ADDED
@@ -0,0 +1,345 @@
1
+ import {setAria, setRole} from '@oscarpalmer/toretto/aria';
2
+ import {setAttribute} from '@oscarpalmer/toretto/attribute';
3
+ import {on} from '@oscarpalmer/toretto/event';
4
+ import {findAncestor} from '@oscarpalmer/toretto/find';
5
+ import {createEmbeddedFloatable, getOnBeforeToggleListener} from './floatable/floatable.embedded';
6
+ import type {OuiFloatable} from './floatable/floatable.standalone';
7
+ import {attributable} from './internal/attributable';
8
+ import {
9
+ NAVIGABLE_KEYS_HORIZONTAL,
10
+ NAVIGABLE_KEYS_VERTICAL,
11
+ type OuiNavigable,
12
+ type OuiNavigableOptions,
13
+ getNavigable,
14
+ updateNavigableFocus,
15
+ } from './internal/navigable';
16
+
17
+ // #region Types
18
+
19
+ type OuiBasicMenu = {
20
+ destroy(): void;
21
+ };
22
+
23
+ type OuiBasicMenuOptions = Pick<OuiNavigableOptions, 'role' | 'selector' | 'vertical'>;
24
+
25
+ class OuiMenu {
26
+ content: HTMLElement;
27
+
28
+ element: HTMLElement;
29
+
30
+ floatable: OuiFloatable;
31
+
32
+ navigable: OuiNavigable;
33
+
34
+ toggle: HTMLElement;
35
+
36
+ get open(): boolean {
37
+ return this.floatable.open;
38
+ }
39
+
40
+ constructor(element: HTMLElement, navigable: OuiNavigable) {
41
+ this.element = element;
42
+ this.navigable = navigable;
43
+
44
+ this.content = navigable.element;
45
+
46
+ this.toggle = element.querySelector(SELECTOR_MENU_TOGGLE)!;
47
+
48
+ this.floatable = createEmbeddedFloatable(this.toggle, this.content, {
49
+ attribute: ATTRIBUTE_MENU_POSITION,
50
+ position:
51
+ findAncestor(element.parentElement!, SELECTOR_MENU_CONTENT) == null
52
+ ? 'below-start'
53
+ : 'end-top',
54
+ reusable: false,
55
+ });
56
+
57
+ this.navigable.listeners.push(
58
+ getOnBeforeToggleListener(this.content),
59
+ on(this.content, 'click', event => {
60
+ event.stopPropagation();
61
+
62
+ const toggle = findAncestor(event, SELECTOR_MENU_TOGGLE);
63
+
64
+ if (toggle != null) {
65
+ return;
66
+ }
67
+
68
+ const elements = document.querySelectorAll(SELECTOR_MENU_CONTENT);
69
+
70
+ for (const element of elements) {
71
+ menus.get(element as HTMLElement)?.floatable.hide();
72
+ }
73
+
74
+ const menu = menus.get(elements[0] as HTMLElement);
75
+
76
+ if (menu != null && event.pointerId === -1) {
77
+ updateNavigableFocus(menu.navigable, true);
78
+ }
79
+ }),
80
+ on(this.content, 'toggle', event => {
81
+ const expanded = event.newState === 'open';
82
+
83
+ if (expanded) {
84
+ this.floatable?.update();
85
+
86
+ updateNavigableFocus(this.navigable, true);
87
+ } else {
88
+ }
89
+
90
+ setAria(this.toggle, 'expanded', expanded);
91
+ }),
92
+ );
93
+
94
+ this.floatable.update();
95
+
96
+ initializeMenu(this);
97
+
98
+ updateNavigableFocus(this.navigable, false);
99
+ }
100
+
101
+ destroy(): void {
102
+ this.floatable.destroy();
103
+ this.navigable.destroy();
104
+
105
+ this.floatable = undefined as never;
106
+ this.navigable = undefined as never;
107
+ }
108
+ }
109
+
110
+ class OuiMenubar {
111
+ element: HTMLElement;
112
+
113
+ navigable: OuiNavigable;
114
+
115
+ constructor(element: HTMLElement, navigable: OuiNavigable) {
116
+ this.element = element;
117
+ this.navigable = navigable;
118
+
119
+ updateNavigableFocus(this.navigable, false);
120
+ }
121
+
122
+ destroy(): void {
123
+ this.navigable.destroy();
124
+
125
+ this.navigable = undefined as never;
126
+ }
127
+ }
128
+
129
+ // #endregion
130
+
131
+ // #region Functions
132
+
133
+ function addInstance<Instance>(
134
+ instances: WeakMap<HTMLElement, Instance>,
135
+ ctor: new (element: HTMLElement, navigable: OuiNavigable) => Instance,
136
+ element: HTMLElement,
137
+ content: HTMLElement,
138
+ options: OuiBasicMenuOptions,
139
+ ): void {
140
+ if (!instances.has(content)) {
141
+ instances.set(
142
+ content,
143
+ new ctor(
144
+ element,
145
+ getNavigable(content, {
146
+ ...options,
147
+ onDeactivate,
148
+ onPreventNavigation,
149
+ onShouldActivate,
150
+ onShouldDeactivate,
151
+ }),
152
+ ),
153
+ );
154
+ }
155
+ }
156
+
157
+ function addMenu(element: HTMLElement): void {
158
+ const content = element.querySelector(SELECTOR_MENU_CONTENT);
159
+ const toggle = element.querySelector(SELECTOR_MENU_TOGGLE);
160
+
161
+ if (content != null && toggle != null) {
162
+ addInstance(menus, OuiMenu, element, content as never, {
163
+ selector: SELECTOR_MENU_ITEM,
164
+ vertical: true,
165
+ });
166
+ }
167
+ }
168
+
169
+ function addMenubar(element: HTMLElement): void {
170
+ addInstance(menubars, OuiMenubar, element, element, {
171
+ role: 'menubar',
172
+ selector: SELECTOR_MENUBAR_ITEM,
173
+ vertical: false,
174
+ });
175
+ }
176
+
177
+ function getId(element: HTMLElement, value?: string): string {
178
+ let {id} = element;
179
+
180
+ if (id == null || id.trim().length === 0) {
181
+ id = value == null ? `oui_menu_${++index}` : `${value}_toggle`;
182
+
183
+ element.id = id;
184
+ }
185
+
186
+ return id;
187
+ }
188
+
189
+ function initializeMenu(menu: OuiMenu): void {
190
+ const id = getId(menu.content);
191
+
192
+ setAttribute(menu.toggle, 'popovertarget', id);
193
+
194
+ setAria(menu.toggle, {
195
+ controls: id,
196
+ expanded: 'false',
197
+ haspopup: 'menu',
198
+ });
199
+
200
+ setAttribute(menu.content, 'tabindex', '-1');
201
+
202
+ setRole(menu.content, 'menu');
203
+ }
204
+
205
+ function onDeactivate(_: KeyboardEvent, navigable: OuiNavigable): void {
206
+ const menu = menus.get(navigable.element);
207
+
208
+ if (menu == null) {
209
+ return;
210
+ }
211
+
212
+ menu.floatable.hide();
213
+
214
+ const element = findAncestor(menu.element.parentElement!, SELECTORS) as HTMLElement | null;
215
+
216
+ if (element == null) {
217
+ return;
218
+ }
219
+
220
+ const instance = menubars.get(element) ?? menus.get(element);
221
+
222
+ if (instance != null) {
223
+ updateNavigableFocus(instance.navigable, true);
224
+ }
225
+ }
226
+
227
+ function onPreventNavigation(_: KeyboardEvent, navigable: OuiNavigable): boolean {
228
+ const instance = menubars.get(navigable.element) ?? menus.get(navigable.element);
229
+
230
+ return findAncestor(instance?.element.parentElement!, SELECTORS) != null;
231
+ }
232
+
233
+ function onShouldActivate(event: KeyboardEvent, navigable: OuiNavigable): boolean {
234
+ let activate = false;
235
+
236
+ if (menubars.has(navigable.element)) {
237
+ activate =
238
+ navigable.orientation === 'horizontal'
239
+ ? NAVIGABLE_KEYS_VERTICAL.has(event.key)
240
+ : NAVIGABLE_KEYS_HORIZONTAL.has(event.key);
241
+ } else if (menus.has(navigable.element)) {
242
+ activate =
243
+ navigable.orientation === 'horizontal'
244
+ ? NAVIGABLE_KEYS_VERTICAL.has(event.key)
245
+ : event.key === 'ArrowRight';
246
+ }
247
+
248
+ if (activate) {
249
+ const target =
250
+ event.target instanceof HTMLElement && event.target.matches(SELECTOR_MENU_TOGGLE)
251
+ ? event.target
252
+ : undefined;
253
+
254
+ if (target == null) {
255
+ activate = false;
256
+ } else {
257
+ target.click();
258
+ }
259
+ }
260
+
261
+ return activate;
262
+ }
263
+
264
+ function onShouldDeactivate(event: KeyboardEvent, navigable: OuiNavigable): boolean | undefined {
265
+ if (menubars.has(navigable.element)) {
266
+ return;
267
+ }
268
+
269
+ const menu = menus.get(navigable.element);
270
+
271
+ if (menu == null) {
272
+ return;
273
+ }
274
+
275
+ const horizontal = navigable.orientation === 'horizontal';
276
+
277
+ const altKey = horizontal ? 'ArrowDown' : 'ArrowRight';
278
+ const baseKey = horizontal ? 'ArrowUp' : 'ArrowLeft';
279
+
280
+ if (event.key === baseKey) {
281
+ return findAncestor(menu.element.parentElement!, SELECTOR_MENU_CONTENT) != null;
282
+ }
283
+
284
+ if (
285
+ event.key === altKey &&
286
+ event.target instanceof HTMLElement &&
287
+ !event.target.hasAttribute('popovertarget')
288
+ ) {
289
+ return false;
290
+ }
291
+ }
292
+
293
+ function removeInstance<Instance extends OuiBasicMenu>(
294
+ instances: WeakMap<HTMLElement, Instance>,
295
+ element: HTMLElement,
296
+ ): void {
297
+ instances.get(element)?.destroy();
298
+
299
+ instances.delete(element);
300
+ }
301
+
302
+ function removeMenu(element: HTMLElement): void {
303
+ removeInstance(menus, element);
304
+ }
305
+
306
+ function removeMenubar(element: HTMLElement): void {
307
+ removeInstance(menubars, element);
308
+ }
309
+
310
+ // #endregion
311
+
312
+ // #region Variables
313
+
314
+ const ATTRIBUTE_MENU = 'oui-menu';
315
+
316
+ const ATTRIBUTE_MENU_POSITION = 'oui-menu-position';
317
+
318
+ const ATTRIBUTE_MENUBAR = 'oui-menubar';
319
+
320
+ const SELECTOR_MENUBAR = `[${ATTRIBUTE_MENUBAR}]`;
321
+
322
+ const SELECTOR_MENUBAR_ITEM = `:scope > [${ATTRIBUTE_MENUBAR}-item], :scope > * > [${ATTRIBUTE_MENUBAR}-item]`;
323
+
324
+ const SELECTOR_MENU_CONTENT = `[${ATTRIBUTE_MENU}-content]`;
325
+
326
+ const SELECTOR_MENU_ITEM = `:scope > [${ATTRIBUTE_MENU}-item], :scope > * > [${ATTRIBUTE_MENU}-item]`;
327
+
328
+ const SELECTOR_MENU_TOGGLE = `[${ATTRIBUTE_MENU}-toggle]`;
329
+
330
+ const SELECTORS = `${SELECTOR_MENUBAR}, ${SELECTOR_MENU_CONTENT}`;
331
+
332
+ const menubars = new WeakMap<HTMLElement, OuiMenubar>();
333
+
334
+ const menus = new WeakMap<HTMLElement, OuiMenu>();
335
+
336
+ let index = 0;
337
+
338
+ // #endregion
339
+
340
+ // #region Initialization
341
+
342
+ attributable(ATTRIBUTE_MENU, addMenu, removeMenu);
343
+ attributable(ATTRIBUTE_MENUBAR, addMenubar, removeMenubar);
344
+
345
+ // #endregion
@@ -1,6 +1,8 @@
1
1
  import type {EventPosition, PlainObject} from '@oscarpalmer/atoms/models';
2
+ import {setAttribute} from '@oscarpalmer/toretto/attribute';
2
3
  import {getPosition, on} from '@oscarpalmer/toretto/event';
3
4
  import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
5
+ import {getStyles, setStyle, setStyles} from '@oscarpalmer/toretto/style';
4
6
  import {attributable} from '../internal/attributable';
5
7
  import {
6
8
  addDraggable,
@@ -23,10 +25,7 @@ export class OuiMovable extends OuiDraggable<OuiMovableState> {
23
25
  #style: OuiMovableStyle;
24
26
 
25
27
  constructor(element: HTMLElement, options?: CreateOuiDraggableOptions) {
26
- const style = {
27
- inset: element.style.inset,
28
- position: element.style.position,
29
- };
28
+ const style = getStyles(element, ['inset', 'position']);
30
29
 
31
30
  super(
32
31
  'movable',
@@ -80,8 +79,7 @@ export class OuiMovable extends OuiDraggable<OuiMovableState> {
80
79
  override destroy(): void {
81
80
  MOVED.delete(this);
82
81
 
83
- this.#element.style.inset = this.#style.inset;
84
- this.#element.style.position = this.#style.position;
82
+ setStyles(this.#element, this.#style);
85
83
 
86
84
  this.#element = undefined as never;
87
85
  this.#style = undefined as never;
@@ -98,8 +96,8 @@ type OuiMovableState = {
98
96
  };
99
97
 
100
98
  type OuiMovableStyle = {
101
- inset: string;
102
- position: string;
99
+ inset: string | undefined;
100
+ position: string | undefined;
103
101
  };
104
102
 
105
103
  // #endregion
@@ -168,10 +166,9 @@ function onCancel(globals: OuiDraggableGlobals, item: OuiDraggableItem): void {
168
166
  const {x, y} = globals.original ?? {x: 0, y: 0};
169
167
 
170
168
  if (moved) {
171
- element.style.inset = `${y}px auto auto ${x}px`;
169
+ setStyle(element, 'inset', `${y}px auto auto ${x}px`);
172
170
  } else {
173
- element.style.position = style.position;
174
- element.style.inset = style.inset;
171
+ setStyles(element, style);
175
172
  }
176
173
 
177
174
  element.removeAttribute(ATTRIBUTE_MOVING);
@@ -227,7 +224,7 @@ function onMove(
227
224
  __: OuiDraggableItem,
228
225
  position: OuiDragMovePosition,
229
226
  ): PlainObject | undefined {
230
- globals.element?.node.setAttribute(ATTRIBUTE_MOVING, '');
227
+ setAttribute(globals.element?.node as never, ATTRIBUTE_MOVING, '');
231
228
 
232
229
  return {
233
230
  from: {...globals.original},
@@ -270,7 +267,7 @@ function reposition(movable: OuiMovable): void {
270
267
  const x = container.left + (state.offset?.x ?? 0);
271
268
  const y = container.top + (state.offset?.y ?? 0);
272
269
 
273
- state.element.style.inset = `${y}px auto auto ${x}px`;
270
+ setStyle(state.element, 'inset', `${y}px auto auto ${x}px`);
274
271
  }
275
272
 
276
273
  function setOffset(state: OuiDraggableState & OuiMovableState, container: DOMRect): void {
package/src/js/popover.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import {getAria, setAria} from '@oscarpalmer/toretto/aria';
2
+ import {setAttribute} from '@oscarpalmer/toretto/attribute';
1
3
  import {on} from '@oscarpalmer/toretto/event';
2
4
  import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
3
5
  import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
@@ -34,6 +36,8 @@ class OuiPopover {
34
36
  constructor(state: OuiPopoverState) {
35
37
  this.#state = state;
36
38
 
39
+ initialize(state);
40
+
37
41
  this.#state.floatable.update();
38
42
  }
39
43
 
@@ -93,6 +97,18 @@ function destroyPopover(state: OuiPopoverState): void {
93
97
  state.popover = undefined as never;
94
98
  }
95
99
 
100
+ function getId(element: HTMLElement, value?: string): string {
101
+ let {id} = element;
102
+
103
+ if (id == null || id.trim().length === 0) {
104
+ id = value == null ? `oui_popover_${++index}` : `${value}_toggle`;
105
+
106
+ element.id = id;
107
+ }
108
+
109
+ return id;
110
+ }
111
+
96
112
  /**
97
113
  * Get the _OuiPopover_ for an element
98
114
  *
@@ -109,29 +125,30 @@ function getState(anchor: HTMLElement, content: HTMLElement): OuiPopoverState {
109
125
  content,
110
126
  floatable: createEmbeddedFloatable(anchor, content, options),
111
127
  focusTrap:
112
- content.getAttribute(ARIA_MODAL) === TRUE
128
+ getAria(content, 'modal') === 'true'
113
129
  ? createFocusTrap(content, {
114
130
  noescape: true,
115
131
  })
116
132
  : undefined,
117
133
  listeners: [
118
134
  getOnBeforeToggleListener(content),
119
- on(content, EVENT_TOGGLE, event => {
120
- if (event.newState === STATE_OPEN) {
135
+ on(content, 'toggle', event => {
136
+ const expanded = event.newState === 'open';
137
+
138
+ if (expanded) {
121
139
  ignoreFocus = false;
122
140
 
123
141
  state.floatable?.update();
124
142
 
125
- content.setAttribute(ATTRIBUTE_OPEN, '');
126
-
127
143
  content.focus();
128
144
  } else {
129
- content.removeAttribute(ATTRIBUTE_OPEN);
130
-
131
145
  if (!ignoreFocus) {
132
146
  anchor?.focus();
133
147
  }
134
148
  }
149
+
150
+ setAria(anchor, 'expanded', expanded);
151
+ setAttribute(content, ATTRIBUTE_OPEN, expanded ? '' : undefined);
135
152
  }),
136
153
  ],
137
154
  popover: undefined as never,
@@ -142,6 +159,21 @@ function getState(anchor: HTMLElement, content: HTMLElement): OuiPopoverState {
142
159
  return state;
143
160
  }
144
161
 
162
+ function initialize(state: OuiPopoverState): void {
163
+ const id = getId(state.content);
164
+
165
+ setAria(state.anchor, {
166
+ controls: id,
167
+ expanded: 'false',
168
+ });
169
+
170
+ const label = getAria(state.content, 'label') ?? getAria(state.content, 'labelledby');
171
+
172
+ if (label == null) {
173
+ setAria(state.content, 'labelledby', getId(state.anchor, id));
174
+ }
175
+ }
176
+
145
177
  function onPointerdown(): void {
146
178
  ignoreFocus = true;
147
179
  }
@@ -164,16 +196,8 @@ function removePopover(element: HTMLElement): void {
164
196
 
165
197
  const ATTRIBUTE = 'oui-popover';
166
198
 
167
- const ARIA_MODAL = 'aria-modal';
168
-
169
199
  const ATTRIBUTE_OPEN = `${ATTRIBUTE}-open`;
170
200
 
171
- const EVENT_TOGGLE = 'toggle';
172
-
173
- const STATE_OPEN = 'open';
174
-
175
- const TRUE = 'true';
176
-
177
201
  const options: OuiFloatableOptions = {
178
202
  attribute: `${ATTRIBUTE}position`,
179
203
  position: 'below-start',
@@ -184,6 +208,8 @@ const states = new WeakMap<HTMLElement, OuiPopoverState>();
184
208
 
185
209
  let ignoreFocus = false;
186
210
 
211
+ let index = 0;
212
+
187
213
  // #endregion
188
214
 
189
215
  // #region Initialization