@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
@@ -1,8 +1,10 @@
1
+ import { setAria } from "@oscarpalmer/toretto/aria";
1
2
  import { getPosition, on } from "@oscarpalmer/toretto/event";
2
3
  import { isNullableOrWhitespace } from "@oscarpalmer/atoms/is";
4
+ import { getAttribute, setAttribute } from "@oscarpalmer/toretto/attribute";
5
+ import { setStyle, toggleStyles } from "@oscarpalmer/toretto/style";
3
6
  import { clamp } from "@oscarpalmer/atoms/number";
4
7
  import { findAncestor } from "@oscarpalmer/toretto/find";
5
- import { toggleStyles } from "@oscarpalmer/toretto/style";
6
8
  import supportsTouch from "@oscarpalmer/toretto/touch";
7
9
  import { round } from "@oscarpalmer/atoms/math";
8
10
  //#region src/js/splitter.ts
@@ -42,8 +44,7 @@ var OuiSplitter = class {
42
44
  this.types = getTypes(this);
43
45
  if (this.types.auto) RESIZE_OBSERVER.observe(this.element);
44
46
  else RESIZE_OBSERVER.unobserve(this.element);
45
- if (this.types.horizontal) this.element.setAttribute(ATTRIBUTE_HORIZONTAL, "");
46
- else this.element.removeAttribute(ATTRIBUTE_HORIZONTAL);
47
+ setAttribute(this.element, ATTRIBUTE_HORIZONTAL, this.types.horizontal ? "" : void 0);
47
48
  setAriaOrientation(this);
48
49
  }
49
50
  updateValues() {
@@ -72,7 +73,7 @@ var OuiSplitterElement = class extends HTMLElement {
72
73
  * Maximum size for the first panel
73
74
  */
74
75
  set max(value) {
75
- this.setAttribute(ATTRIBUTE_MAX, String(value));
76
+ if (typeof value === "number") setAttribute(this, ATTRIBUTE_MAX, value.toString());
76
77
  }
77
78
  /**
78
79
  * Minimum size for the first panel
@@ -84,7 +85,7 @@ var OuiSplitterElement = class extends HTMLElement {
84
85
  * Minimum size for the first panel
85
86
  */
86
87
  set min(value) {
87
- this.setAttribute(ATTRIBUTE_MIN, String(value));
88
+ if (typeof value === "number") setAttribute(this, ATTRIBUTE_MIN, value.toString());
88
89
  }
89
90
  /**
90
91
  * Current size for the first panel
@@ -96,23 +97,23 @@ var OuiSplitterElement = class extends HTMLElement {
96
97
  * Current size for the first panel
97
98
  */
98
99
  set size(value) {
99
- this.setAttribute(ATTRIBUTE_SIZE, String(value));
100
+ if (typeof value === "number") setAttribute(this, ATTRIBUTE_SIZE, value.toString());
100
101
  }
101
102
  /**
102
103
  * Orientation of the splitter
103
104
  */
104
105
  get type() {
105
- return this.getAttribute(ATTRIBUTE_TYPE) ?? "vertical";
106
+ return getAttribute(this, ATTRIBUTE_TYPE) ?? "vertical";
106
107
  }
107
108
  /**
108
109
  * Orientation of the splitter
109
110
  */
110
111
  set type(value) {
111
- this.setAttribute(ATTRIBUTE_TYPE, value);
112
+ setAttribute(this, ATTRIBUTE_TYPE, value);
112
113
  }
113
114
  constructor() {
114
115
  super();
115
- const panels = [...this.children].filter((child) => child instanceof HTMLElement);
116
+ const panels = [...this.children].filter((child) => child instanceof Element);
116
117
  if (panels.length !== 2) throw new Error(MESSAGE);
117
118
  this.#splitter = new OuiSplitter(this, panels);
118
119
  }
@@ -138,17 +139,19 @@ var OuiSplitterElement = class extends HTMLElement {
138
139
  };
139
140
  function createHandle() {
140
141
  const handle = document.createElement("span");
141
- handle.setAttribute("aria-hidden", "true");
142
- handle.setAttribute(ATTRIBUTE_HANDLE, "");
142
+ setAria(handle, "hidden", "true");
143
+ setAttribute(handle, ATTRIBUTE_HANDLE, "");
143
144
  return handle;
144
145
  }
145
146
  function createSeparator(splitter) {
146
147
  const separator = document.createElement("div");
147
148
  separator.role = "separator";
148
149
  separator.tabIndex = 0;
149
- separator.setAttribute("aria-controls", splitter.element.id);
150
- separator.setAttribute("aria-label", "Resize panels");
151
- separator.setAttribute(ATTRIBUTE_SEPARATOR, "");
150
+ setAria(separator, {
151
+ controls: splitter.element.id,
152
+ label: "Resize panels"
153
+ });
154
+ setAttribute(separator, ATTRIBUTE_SEPARATOR, "");
152
155
  return separator;
153
156
  }
154
157
  function getContained(min, value, max) {
@@ -160,7 +163,7 @@ function getSize(splitter, value) {
160
163
  return getContained(splitter.values.min, fraction, splitter.values.max);
161
164
  }
162
165
  function getTypes(splitter, width, height) {
163
- const type = splitter.element.getAttribute("type")?.trim() ?? "";
166
+ const type = getAttribute(splitter.element, "type");
164
167
  if (type === "horizontal") return {
165
168
  auto: false,
166
169
  horizontal: true
@@ -175,7 +178,7 @@ function getTypes(splitter, width, height) {
175
178
  };
176
179
  }
177
180
  function getValue(element, name, defaultValue) {
178
- const value = element.getAttribute(name);
181
+ const value = getAttribute(element, name);
179
182
  if (isNullableOrWhitespace(value)) return defaultValue;
180
183
  const parsed = Number.parseFloat(value);
181
184
  if (Number.isNaN(parsed)) return defaultValue;
@@ -235,8 +238,7 @@ function onObservation(entries) {
235
238
  const splitter = MAPPED_ELEMENTS.get(entry.target);
236
239
  if (splitter == null) return;
237
240
  splitter.types = getTypes(splitter, entry.contentRect.width, entry.contentRect.height);
238
- if (splitter.types.horizontal) splitter.element.setAttribute(ATTRIBUTE_HORIZONTAL, "");
239
- else splitter.element.removeAttribute(ATTRIBUTE_HORIZONTAL);
241
+ setAttribute(splitter.element, ATTRIBUTE_HORIZONTAL, splitter.types.horizontal ? "" : void 0);
240
242
  setAriaOrientation(splitter);
241
243
  }
242
244
  frame = void 0;
@@ -247,8 +249,8 @@ function onPointerdown(event) {
247
249
  splitter = MAPPED_ELEMENTS.get(findAncestor(event, SELECTOR_HANDLE));
248
250
  if (splitter == null) return;
249
251
  if (supportsTouch.value) event.preventDefault();
250
- splitter.handle.setAttribute(ATTRIBUTE_ACTIVE, "");
251
- splitter.separator.setAttribute(ATTRIBUTE_ACTIVE, "");
252
+ setAttribute(splitter.handle, ATTRIBUTE_ACTIVE, "");
253
+ setAttribute(splitter.separator, ATTRIBUTE_ACTIVE, "");
252
254
  splitter.rectangle = splitter.element.getBoundingClientRect();
253
255
  STYLING_TOGGLER.set();
254
256
  }
@@ -264,17 +266,19 @@ function onPointerup() {
264
266
  if (splitter != null) onEnd(false);
265
267
  }
266
268
  function setAriaOrientation(splitter) {
267
- splitter.separator.setAttribute("aria-orientation", splitter.types.horizontal ? "horizontal" : "vertical");
269
+ setAria(splitter.separator, "orientation", splitter.types.horizontal ? "horizontal" : "vertical");
268
270
  }
269
271
  function setAriaValue(splitter) {
270
- splitter.separator.setAttribute("aria-valuemax", String(splitter.values.max));
271
- splitter.separator.setAttribute("aria-valuemin", String(splitter.values.min));
272
- splitter.separator.setAttribute("aria-valuenow", String(splitter.values.now.current));
272
+ setAria(splitter.separator, {
273
+ valuemax: splitter.values.max,
274
+ valuemin: splitter.values.min,
275
+ valuenow: splitter.values.now.current
276
+ });
273
277
  }
274
278
  function setSize(splitter, size, previous) {
275
279
  splitter.values.now.current = size;
276
280
  if (previous) splitter.values.now.previous = size;
277
- splitter.element.style.setProperty("--oui-splitter-size", `${size}`);
281
+ setStyle(splitter.element, "--oui-splitter-size", `${size}`);
278
282
  setAriaValue(splitter);
279
283
  }
280
284
  const SELECTOR = "oui-splitter";
@@ -315,7 +319,7 @@ on(document, "keydown", onKeydown);
315
319
  on(document, "mousedown", onPointerdown);
316
320
  on(document, "pointermove", onPointermove);
317
321
  on(document, "pointerup", onPointerup);
318
- on(document, "touchcancel", onPointerup, { passive: false });
322
+ on(document, "touchcancel", onPointerup);
319
323
  on(document, "touchstart", onPointerdown, { passive: false });
320
324
  //#endregion
321
325
  export { OuiSplitterElement };
@@ -1,105 +1,29 @@
1
1
  import { attributable } from "./internal/attributable.mjs";
2
- import { on } from "@oscarpalmer/toretto/event";
3
- import { isHTMLOrSVGElement } from "@oscarpalmer/toretto/is";
4
- import { clamp } from "@oscarpalmer/atoms/number";
5
- import { findAncestor } from "@oscarpalmer/toretto/find";
2
+ import { getNavigable, updateNavigableFocus } from "./internal/navigable.mjs";
6
3
  //#region src/js/toolbar.ts
7
4
  var OuiToolbar = class {
8
- #state;
9
- constructor(state) {
10
- this.#state = state;
11
- updateFocus(this.#state, false);
5
+ #navigable;
6
+ constructor(navigable) {
7
+ this.#navigable = navigable;
8
+ updateNavigableFocus(this.#navigable, false);
12
9
  }
13
10
  destroy() {
14
- for (const listener of this.#state.listeners) listener();
15
- this.#state.active = void 0;
16
- this.#state.element = void 0;
17
- this.#state.listeners = void 0;
11
+ this.#navigable.destroy();
12
+ this.#navigable = void 0;
18
13
  }
19
14
  };
20
15
  function addToolbar(element) {
21
- if (!instances.has(element)) instances.set(element, new OuiToolbar(getState(element)));
22
- }
23
- function getItems(element) {
24
- return [...element.querySelectorAll(SELECTOR)].filter(isValidItem);
25
- }
26
- function getOrientation(element) {
27
- const attribute = element.getAttribute(ARIA_ORIENTATION);
28
- return ORIENTATIONS.has(attribute) ? attribute : ORIENTATION_HORIZONTAL;
29
- }
30
- function getState(element) {
31
- const state = {
32
- element,
33
- active: { index: 0 },
34
- listeners: [on(element, EVENT_FOCUSIN, (event) => {
35
- onFocusin(state, event);
36
- }), on(element, EVENT_KEYDOWN, (event) => {
37
- onKeydown(state, event);
38
- }, { passive: false })],
39
- orientation: getOrientation(element)
40
- };
41
- return state;
42
- }
43
- function isValidItem(element) {
44
- return PROPERTY_DISABLED in element ? element.disabled !== true : true;
45
- }
46
- function onKeydown(state, event) {
47
- if (!KEYS_ALL.has(event.key)) return;
48
- const items = getItems(state.element);
49
- if (items.length === 0) return;
50
- let index;
51
- if (KEYS_ABSOLUTE.has(event.key)) index = event.key === KEY_HOME ? 0 : items.length - 1;
52
- else if (state.orientation === ORIENTATION_HORIZONTAL ? KEYS_HORIZONTAL.has(event.key) : KEYS_VERTICAL.has(event.key)) {
53
- if (state.active.element instanceof HTMLSelectElement) event.preventDefault();
54
- index = clamp(state.active.index + (KEYS_NEXT.has(event.key) ? 1 : -1), 0, items.length - 1, true);
55
- }
56
- if (index == null || index === state.active.index) return;
57
- state.active.element = items[index];
58
- state.active.index = index;
59
- updateFocus(state, true, items);
60
- }
61
- function onFocusin(state, event) {
62
- const item = findAncestor(event, SELECTOR);
63
- if (!isHTMLOrSVGElement(item) || !isValidItem(item)) return;
64
- const items = getItems(state.element);
65
- const index = items.indexOf(item);
66
- if (index === -1) return;
67
- state.active.element = item;
68
- state.active.index = index;
69
- updateFocus(state, false, items);
16
+ if (!instances.has(element)) instances.set(element, new OuiToolbar(getNavigable(element, {
17
+ role: "toolbar",
18
+ selector: SELECTOR,
19
+ vertical: false
20
+ })));
70
21
  }
71
22
  function removeToolbar(element) {
72
23
  instances.get(element)?.destroy();
73
24
  instances.delete(element);
74
25
  }
75
- function updateFocus(state, focus, items) {
76
- const updated = items ?? getItems(state.element);
77
- const { length } = updated;
78
- if (length === 0) return;
79
- for (let index = 0; index < length; index += 1) updated[index].tabIndex = index === state.active.index ? 0 : -1;
80
- state.active.element ??= updated[state.active.index];
81
- if (focus) state.active.element?.focus();
82
- }
83
- const ARIA_ORIENTATION = "aria-orientation";
84
26
  const ATTRIBUTE = "oui-toolbar";
85
- const EVENT_KEYDOWN = "keydown";
86
- const EVENT_FOCUSIN = "focusin";
87
- const KEY_ARROW_DOWN = "ArrowDown";
88
- const KEY_ARROW_RIGHT = "ArrowRight";
89
- const KEY_END = "End";
90
- const KEY_HOME = "Home";
91
- const KEYS_ABSOLUTE = /* @__PURE__ */ new Set([KEY_END, KEY_HOME]);
92
- const KEYS_HORIZONTAL = /* @__PURE__ */ new Set([KEY_ARROW_RIGHT, "ArrowLeft"]);
93
- const KEYS_VERTICAL = /* @__PURE__ */ new Set([KEY_ARROW_DOWN, "ArrowUp"]);
94
- const KEYS_ALL = /* @__PURE__ */ new Set([
95
- ...KEYS_ABSOLUTE,
96
- ...KEYS_HORIZONTAL,
97
- ...KEYS_VERTICAL
98
- ]);
99
- const KEYS_NEXT = /* @__PURE__ */ new Set([KEY_ARROW_DOWN, KEY_ARROW_RIGHT]);
100
- const ORIENTATION_HORIZONTAL = "horizontal";
101
- const ORIENTATIONS = /* @__PURE__ */ new Set([ORIENTATION_HORIZONTAL, "vertical"]);
102
- const PROPERTY_DISABLED = "disabled";
103
27
  const SELECTOR = `[${ATTRIBUTE}-item]`;
104
28
  const instances = /* @__PURE__ */ new WeakMap();
105
29
  attributable(ATTRIBUTE, addToolbar, removeToolbar);
@@ -1,4 +1,4 @@
1
- import { i as OuiFloatable } from "./floatable.embedded-YxGwzgbE.mjs";
1
+ import { r as OuiFloatable } from "./floatable.embedded-DUR8bQBK.mjs";
2
2
 
3
3
  //#region src/js/tooltip.d.ts
4
4
  declare class OuiTooltip {
@@ -1,8 +1,9 @@
1
1
  import { createEmbeddedFloatable } from "./floatable/floatable.embedded.mjs";
2
2
  import { attributable } from "./internal/attributable.mjs";
3
+ import { getAria } from "@oscarpalmer/toretto/aria";
3
4
  import { on } from "@oscarpalmer/toretto/event";
4
5
  import { isNullableOrWhitespace } from "@oscarpalmer/atoms/is";
5
- import { isHTMLOrSVGElement } from "@oscarpalmer/toretto/is";
6
+ import { setAttribute } from "@oscarpalmer/toretto/attribute";
6
7
  import { findAncestor } from "@oscarpalmer/toretto/find";
7
8
  import { createElement } from "@oscarpalmer/toretto/create";
8
9
  //#region src/js/tooltip.ts
@@ -87,12 +88,13 @@ function destroyTooltip(state) {
87
88
  state.tooltip = void 0;
88
89
  }
89
90
  function findElements(attribute) {
91
+ if (attribute == null) return;
90
92
  const ids = attribute?.split(EXPRESSION_WHITESPACE) ?? [];
91
93
  const elements = [];
92
94
  for (const id of ids) {
93
95
  if (isNullableOrWhitespace(id)) continue;
94
96
  const element = document.querySelector(`#${id}`);
95
- if (isHTMLOrSVGElement(element)) {
97
+ if (element != null) {
96
98
  const cloned = element.cloneNode(true);
97
99
  cloned.hidden = false;
98
100
  elements.push(cloned);
@@ -101,21 +103,20 @@ function findElements(attribute) {
101
103
  return elements;
102
104
  }
103
105
  function getContent() {
104
- const content = createElement(CONTENT_TAGNAME, {
106
+ const content = createElement("div", {
105
107
  popover: "hint",
106
108
  role: "tooltip",
107
109
  tabIndex: -1
108
110
  });
109
- content.setAttribute(ATTRIBUTE_CONTENT, "");
111
+ setAttribute(content, ATTRIBUTE_CONTENT, "");
110
112
  return content;
111
113
  }
112
114
  function getElements(anchor) {
113
- let elements = findElements(anchor.getAttribute(ARIA_LABELLEDBY));
114
- if (elements.length > 0) return elements;
115
- elements = findElements(anchor.getAttribute(ARIA_DESCRIBEDBY));
116
- if (elements.length > 0) return elements;
117
- let text = anchor.getAttribute(ARIA_LABEL);
118
- if (isNullableOrWhitespace(text)) text = anchor.getAttribute(ARIA_DESCRIPTION);
115
+ let elements = findElements(getAria(anchor, "labelledby"));
116
+ if (elements == null || elements.length === 0) elements = findElements(getAria(anchor, "describedby"));
117
+ if (elements != null && elements.length > 0) return elements;
118
+ let text = getAria(anchor, "label");
119
+ if (isNullableOrWhitespace(text)) text = getAria(anchor, "description");
119
120
  if (isNullableOrWhitespace(text)) return;
120
121
  const paragraph = document.createElement("p");
121
122
  paragraph.textContent = text;
@@ -157,8 +158,7 @@ function onPointermove(event) {
157
158
  activate(event, false);
158
159
  }
159
160
  function onToggle(event) {
160
- if (event.newState === "open") content.setAttribute(ATTRIBUTE_CONTENT_OPEN, "");
161
- else content.removeAttribute(ATTRIBUTE_CONTENT_OPEN);
161
+ setAttribute(content, ATTRIBUTE_CONTENT_OPEN, event.newState === "open" ? "" : void 0);
162
162
  }
163
163
  function removeTooltip(element) {
164
164
  const state = states.get(element);
@@ -175,11 +175,6 @@ function removeTooltip(element) {
175
175
  function setTooltipDelay(value) {
176
176
  if (typeof value === "number" && !Number.isNaN(value) && value >= 0) delay = value;
177
177
  }
178
- const ARIA_DESCRIBEDBY = "aria-describedby";
179
- const ARIA_DESCRIPTION = "aria-description";
180
- const ARIA_LABEL = "aria-label";
181
- const ARIA_LABELLEDBY = "aria-labelledby";
182
- const CONTENT_TAGNAME = "div";
183
178
  const EXPRESSION_WHITESPACE = /\s+/;
184
179
  const ATTRIBUTE = "oui-tooltip";
185
180
  const ATTRIBUTE_CONTENT = `${ATTRIBUTE}-content`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oscarpalmer/oui",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
4
4
  "description": "Styles, components, and utilities for building websites and applications.",
5
5
  "keywords": [
6
6
  "css",
@@ -49,10 +49,18 @@
49
49
  "types": "./dist/js/movable/movable.standalone.d.mts",
50
50
  "default": "./dist/js/movable/movable.standalone.mjs"
51
51
  },
52
+ "./menu": {
53
+ "types": "./dist/js/menu.d.mts",
54
+ "default": "./dist/js/menu.mjs"
55
+ },
52
56
  "./popover": {
53
57
  "types": "./dist/js/popover.d.mts",
54
58
  "default": "./dist/js/popover.mjs"
55
59
  },
60
+ "./resizable": {
61
+ "types": "./dist/js/resizable/resizable.standalone.d.mts",
62
+ "default": "./dist/js/resizable/resizable.standalone.mjs"
63
+ },
56
64
  "./sortable": {
57
65
  "types": "./dist/js/sortable/sortable.standalone.d.mts",
58
66
  "default": "./dist/js/sortable/sortable.standalone.mjs"
@@ -83,10 +91,10 @@
83
91
  },
84
92
  "dependencies": {
85
93
  "@oscarpalmer/atoms": "^0.188.1",
86
- "@oscarpalmer/toretto": "^0.46"
94
+ "@oscarpalmer/toretto": "^0.47.3"
87
95
  },
88
96
  "devDependencies": {
89
- "@oxlint/plugins": "^1.72",
97
+ "@oxlint/plugins": "^1.73",
90
98
  "@vitest/coverage-istanbul": "^4.1",
91
99
  "autoprefixer": "^10.5",
92
100
  "concurrently": "^10",
@@ -36,6 +36,15 @@
36
36
  transition-delay: 0.125s;
37
37
  }
38
38
 
39
+ :where([oui-dialog]) {
40
+ position: relative;
41
+ }
42
+
43
+ :where([oui-dialog-resizer]) {
44
+ position: absolute;
45
+ inset: auto 0 0 auto;
46
+ }
47
+
39
48
  :where([oui-popover], [oui-tooltip-content]) {
40
49
  @include mixins.floatable;
41
50
  }
package/src/css/form.scss CHANGED
@@ -173,9 +173,9 @@
173
173
 
174
174
  :where(
175
175
  &.oui-validate:not([disabled], [readonly], [aria-disabled='true' i]):is(
176
- :user-invalid:not([aria-invalid='false' i]),
177
- [aria-invalid='true' i]
178
- )
176
+ :user-invalid:not([aria-invalid='false' i]),
177
+ [aria-invalid='true' i]
178
+ )
179
179
  ) {
180
180
  @include invalid;
181
181
 
@@ -201,9 +201,9 @@
201
201
 
202
202
  :where(
203
203
  &.oui-validate:not([disabled], [readonly], [aria-disabled='true' i]):is(
204
- :user-valid:not([aria-invalid='true' i]),
205
- [aria-invalid='false' i]
206
- )
204
+ :user-valid:not([aria-invalid='true' i]),
205
+ [aria-invalid='false' i]
206
+ )
207
207
  ) {
208
208
  @include valid;
209
209
 
package/src/js/dialog.ts CHANGED
@@ -1,9 +1,14 @@
1
+ import {getAria, setAria} from '@oscarpalmer/toretto/aria';
1
2
  import {on} from '@oscarpalmer/toretto/event';
2
3
  import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
3
4
  import {getOnBeforeToggleListener} from './floatable/floatable.embedded';
4
5
  import {attributable} from './internal/attributable';
5
6
  import {ATTRIBUTE_MOVABLE_HANDLE} from './movable/movable.embedded';
6
7
  import {createMovable, type OuiMovable} from './movable/movable.standalone';
8
+ import {createResizable, RESIZABLE_ATTRIBUTE_HANDLE} from './resizable/resizable.embedded';
9
+ import type {OuiResizable} from './resizable/resizable.standalone';
10
+ import {getAttribute, setAttribute} from '@oscarpalmer/toretto/attribute';
11
+ import {setStyles} from '@oscarpalmer/toretto/style';
7
12
 
8
13
  // #region Types
9
14
 
@@ -66,7 +71,7 @@ class OuiDialog {
66
71
 
67
72
  const isModal = modal !== false;
68
73
 
69
- element.setAttribute(ARIA_MODAL, String(isModal));
74
+ setAria(element, 'modal', isModal);
70
75
 
71
76
  if (isModal) {
72
77
  element.showModal();
@@ -82,6 +87,7 @@ type OuiDialogState = {
82
87
  listeners: RemovableEventListener[];
83
88
  modal: boolean;
84
89
  movable?: OuiMovable;
90
+ resizable?: OuiResizable;
85
91
  };
86
92
 
87
93
  // #endregion
@@ -101,16 +107,22 @@ function addDialog(element: HTMLElement): void {
101
107
  function destroyDialog(state: OuiDialogState): void {
102
108
  state.element.close();
103
109
 
104
- state.element.setAttribute(ARIA_MODAL, String(state.modal));
110
+ setAria(state.element, 'modal', state.modal);
111
+
105
112
  state.element.removeAttribute(ATTRIBUTE_OPEN);
106
113
 
107
114
  for (const listener of state.listeners) {
108
115
  listener();
109
116
  }
110
117
 
118
+ state.movable?.destroy();
119
+ state.resizable?.destroy();
120
+
111
121
  state.dialog = undefined as never;
112
122
  state.element = undefined as never;
113
123
  state.listeners = undefined as never;
124
+ state.movable = undefined;
125
+ state.resizable = undefined;
114
126
  }
115
127
 
116
128
  /**
@@ -131,47 +143,69 @@ function getMovable(element: HTMLDialogElement): OuiMovable | undefined {
131
143
  }
132
144
 
133
145
  for (const handle of handles) {
134
- handle.setAttribute(ATTRIBUTE_MOVABLE_HANDLE, '');
146
+ setAttribute(handle, ATTRIBUTE_MOVABLE_HANDLE, '');
135
147
  }
136
148
 
137
149
  return createMovable(element, {
138
- container: element.getAttribute(ATTRIBUTE_CONTAINER) ?? undefined,
150
+ container: getAttribute(element, ATTRIBUTE_CONTAINER),
139
151
  });
140
152
  }
141
153
 
154
+ function getResizable(element: HTMLDialogElement): OuiResizable | undefined {
155
+ const handle = element.querySelector(SELECTOR_RESIZER);
156
+
157
+ if (handle == null) {
158
+ return;
159
+ }
160
+
161
+ setAttribute(handle, RESIZABLE_ATTRIBUTE_HANDLE, '');
162
+
163
+ return createResizable(element);
164
+ }
165
+
142
166
  function getState(element: HTMLDialogElement): OuiDialogState {
143
167
  const state: OuiDialogState = {
144
168
  element,
145
169
  dialog: undefined as never,
146
170
  listeners: [
147
171
  getOnBeforeToggleListener(element),
148
- on(element, EVENT_TOGGLE, event => {
172
+ on(element, 'toggle', event => {
149
173
  const {source} = event;
150
174
 
151
175
  if (element.open) {
152
176
  const modal =
153
- (source instanceof HTMLButtonElement && source.command === COMMAND_SHOW_MODAL) ||
154
- element.matches(SELECTOR_MODAL);
177
+ (source instanceof HTMLButtonElement && source.command === 'show-modal') ||
178
+ element.matches(':modal');
155
179
 
156
- element.setAttribute(ARIA_MODAL, String(modal));
157
- element.setAttribute(ATTRIBUTE_OPEN, '');
180
+ setAria(element, 'modal', modal);
181
+ setAttribute(element, ATTRIBUTE_OPEN, '');
158
182
 
159
183
  element.focus();
184
+
185
+ setTimeout(() => {
186
+ state.resizable?.set();
187
+ }, 500);
160
188
  } else {
161
- element.removeAttribute(ATTRIBUTE_OPEN);
162
- element.setAttribute(ARIA_MODAL, String(state.modal));
189
+ setAttribute(element, ATTRIBUTE_OPEN);
190
+ setAria(element, 'modal', state.modal);
163
191
 
164
- element.style.inset = '';
165
- element.style.position = '';
166
- element.style.transform = '';
192
+ setStyles(element, {
193
+ inset: '',
194
+ position: '',
195
+ transform: '',
196
+ });
197
+
198
+ state.resizable?.reset();
167
199
  }
168
200
  }),
169
201
  ],
170
- modal: element.getAttribute(ARIA_MODAL) === TRUE,
202
+ modal: getAria(element, 'modal') === 'true' || element.matches(':modal'),
171
203
  };
172
204
 
173
205
  state.dialog = new OuiDialog(state);
206
+
174
207
  state.movable = getMovable(element);
208
+ state.resizable = getResizable(element);
175
209
 
176
210
  return state;
177
211
  }
@@ -192,25 +226,17 @@ function removeDialog(element: HTMLElement): void {
192
226
 
193
227
  // #region Variables
194
228
 
195
- const ARIA_MODAL = 'aria-modal';
196
-
197
229
  const ATTRIBUTE = 'oui-dialog';
198
230
 
199
231
  const ATTRIBUTE_CONTAINER = `${ATTRIBUTE}-container`;
200
232
 
201
233
  const ATTRIBUTE_OPEN = `${ATTRIBUTE}-open`;
202
234
 
203
- const COMMAND_SHOW_MODAL = 'show-modal';
204
-
205
- const EVENT_TOGGLE = 'toggle';
206
-
207
235
  const MESSAGE = 'The element must be an instance of HTMLDialogElement';
208
236
 
209
237
  const SELECTOR_MOVER = `[${ATTRIBUTE}-mover]`;
210
238
 
211
- const SELECTOR_MODAL = ':modal';
212
-
213
- const TRUE = 'true';
239
+ const SELECTOR_RESIZER = `[${ATTRIBUTE}-resizer]`;
214
240
 
215
241
  const states = new WeakMap<HTMLElement, OuiDialogState>();
216
242