@oscarpalmer/oui 0.26.1 → 0.28.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 (73) hide show
  1. package/dist/css/floatable.css +9 -2
  2. package/dist/css/message.css +1 -0
  3. package/dist/css/reset.css +1 -0
  4. package/dist/js/dialog.d.mts +4 -0
  5. package/dist/js/dialog.mjs +33 -1
  6. package/dist/js/draggable-zszaW1qS.d.mts +155 -0
  7. package/dist/js/floatable/floatable.embedded.d.mts +2 -0
  8. package/dist/js/floatable/{embedded.mjs → floatable.embedded.mjs} +16 -11
  9. package/dist/js/floatable/floatable.standalone.d.mts +2 -0
  10. package/dist/js/floatable/floatable.standalone.mjs +2 -0
  11. package/dist/js/{embedded-CSOh5njI.d.mts → floatable.embedded-YxGwzgbE.d.mts} +5 -2
  12. package/dist/js/focus-trap/focus-trap.embedded.d.mts +2 -0
  13. package/dist/js/focus-trap/{embedded.mjs → focus-trap.embedded.mjs} +17 -15
  14. package/dist/js/focus-trap/focus-trap.standalone.d.mts +2 -0
  15. package/dist/js/focus-trap/focus-trap.standalone.mjs +2 -0
  16. package/dist/js/{embedded-CHK82Uh5.d.mts → focus-trap.embedded-DR8NpZfr.d.mts} +18 -18
  17. package/dist/js/index.d.mts +5 -3
  18. package/dist/js/index.mjs +11 -7
  19. package/dist/js/internal/draggable.d.mts +2 -2
  20. package/dist/js/internal/draggable.mjs +147 -95
  21. package/dist/js/movable/movable.embedded.d.mts +2 -0
  22. package/dist/js/movable/movable.embedded.mjs +184 -0
  23. package/dist/js/movable/movable.standalone.d.mts +2 -0
  24. package/dist/js/movable/movable.standalone.mjs +2 -0
  25. package/dist/js/popover.d.mts +2 -2
  26. package/dist/js/popover.mjs +4 -3
  27. package/dist/js/resizable/resizable.embedded.d.mts +2 -0
  28. package/dist/js/resizable/resizable.embedded.mjs +186 -0
  29. package/dist/js/resizable/resizable.standalone.d.mts +2 -0
  30. package/dist/js/resizable/resizable.standalone.mjs +2 -0
  31. package/dist/js/resizable.embedded-CXrUVPzR.d.mts +35 -0
  32. package/dist/js/sortable/sortable.embedded.d.mts +2 -0
  33. package/dist/js/{sortable.mjs → sortable/sortable.embedded.mjs} +88 -63
  34. package/dist/js/sortable/sortable.standalone.d.mts +2 -0
  35. package/dist/js/sortable/sortable.standalone.mjs +2 -0
  36. package/dist/js/splitter.mjs +6 -6
  37. package/dist/js/toolbar.mjs +6 -6
  38. package/dist/js/tooltip.d.mts +2 -2
  39. package/dist/js/tooltip.mjs +36 -33
  40. package/package.json +15 -11
  41. package/src/css/floatable.scss +9 -2
  42. package/src/css/form.scss +6 -6
  43. package/src/css/message.scss +1 -0
  44. package/src/css/reset.scss +1 -0
  45. package/src/js/dialog.ts +61 -1
  46. package/src/js/floatable/{embedded.ts → floatable.embedded.ts} +18 -10
  47. package/src/js/floatable/floatable.standalone.ts +6 -0
  48. package/src/js/focus-trap/{embedded.ts → focus-trap.embedded.ts} +20 -17
  49. package/src/js/focus-trap/focus-trap.standalone.ts +1 -0
  50. package/src/js/index.ts +5 -4
  51. package/src/js/internal/draggable.ts +310 -169
  52. package/src/js/movable/movable.embedded.ts +328 -0
  53. package/src/js/movable/movable.standalone.ts +2 -0
  54. package/src/js/popover.ts +3 -2
  55. package/src/js/resizable/resizable.embedded.ts +383 -0
  56. package/src/js/resizable/resizable.standalone.ts +5 -0
  57. package/src/js/{sortable.ts → sortable/sortable.embedded.ts} +170 -103
  58. package/src/js/sortable/sortable.standalone.ts +2 -0
  59. package/src/js/splitter.ts +1 -1
  60. package/src/js/tooltip.ts +49 -43
  61. package/dist/js/floatable/embedded.d.mts +0 -2
  62. package/dist/js/floatable/standalone.d.mts +0 -2
  63. package/dist/js/floatable/standalone.mjs +0 -2
  64. package/dist/js/focus-trap/embedded.d.mts +0 -2
  65. package/dist/js/focus-trap/standalone.d.mts +0 -2
  66. package/dist/js/focus-trap/standalone.mjs +0 -6
  67. package/dist/js/movable-dNqBctx3.d.mts +0 -101
  68. package/dist/js/movable.d.mts +0 -2
  69. package/dist/js/movable.mjs +0 -152
  70. package/dist/js/sortable.d.mts +0 -2
  71. package/src/js/floatable/standalone.ts +0 -1
  72. package/src/js/focus-trap/standalone.ts +0 -6
  73. package/src/js/movable.ts +0 -260
@@ -1,4 +1,4 @@
1
- import { createEmbeddedFloatable } from "./floatable/embedded.mjs";
1
+ import { createEmbeddedFloatable } from "./floatable/floatable.embedded.mjs";
2
2
  import { attributable } from "./internal/attributable.mjs";
3
3
  import { on } from "@oscarpalmer/toretto/event";
4
4
  import { isNullableOrWhitespace } from "@oscarpalmer/atoms/is";
@@ -12,7 +12,7 @@ var OuiTooltip = class {
12
12
  * Is the tooltip open?
13
13
  */
14
14
  get open() {
15
- return this.#state.content.checkVisibility();
15
+ return this === activeTooltip && content.checkVisibility();
16
16
  }
17
17
  /**
18
18
  * Open or close the tooltip
@@ -39,52 +39,50 @@ var OuiTooltip = class {
39
39
  }
40
40
  };
41
41
  function activate(event, focus) {
42
- const anchor = findAncestor(event, SELECTOR);
43
- const content = findAncestor(event, SELECTOR_CONTENT);
44
- if (anchor == null && content == null) {
42
+ const targetAnchor = findAncestor(event, SELECTOR);
43
+ const targetContent = findAncestor(event, SELECTOR_CONTENT);
44
+ if (targetAnchor == null && targetContent == null) {
45
45
  if (!focused) {
46
- closeTooltip(activeState);
46
+ closeTooltip(true, activeState);
47
47
  activeState = void 0;
48
48
  activeTooltip = void 0;
49
49
  }
50
50
  return;
51
51
  }
52
- const state = states.get(anchor ?? content);
52
+ const state = states.get(targetAnchor ?? targetContent);
53
53
  if (state == null || state.tooltip === activeTooltip) return;
54
54
  const shouldDelay = activeTooltip == null;
55
- closeTooltip(activeState);
55
+ closeTooltip(false, activeState);
56
56
  activeState = state;
57
57
  activeTooltip = state.tooltip;
58
58
  focused = focus;
59
59
  state.timer = setTimeout(() => {
60
- state.content.showPopover({ source: state.anchor });
60
+ content.innerHTML = "";
61
+ content.append(...state.elements);
62
+ state.floatable.show();
61
63
  setTimeout(() => {
62
64
  state.floatable.update();
63
- state.content.setAttribute(ATTRIBUTE_CONTENT_OPEN, "");
64
65
  });
65
66
  }, shouldDelay ? delay : 0);
66
67
  }
67
68
  function addTooltip(element) {
68
69
  if (!states.has(element)) createTooltip(element);
69
70
  }
70
- function closeTooltip(state) {
71
+ function closeTooltip(clear, state) {
71
72
  if (state == null) return;
72
73
  if (state.timer != null) clearTimeout(state.timer);
73
- state.content?.hidePopover();
74
- state.content?.removeAttribute(ATTRIBUTE_CONTENT_OPEN);
74
+ if (clear) content?.hidePopover();
75
75
  }
76
76
  function createTooltip(anchor) {
77
- const content = getContent(anchor);
78
- if (content == null) return;
79
- anchor.insertAdjacentElement("afterend", content);
80
- states.set(anchor, getState(anchor, content));
77
+ const elements = getElements(anchor);
78
+ if (elements == null) return;
79
+ states.set(anchor, getState(anchor, elements));
81
80
  }
82
81
  function destroyTooltip(state) {
83
82
  clearTimeout(state.timer);
84
- state.content.remove();
85
83
  state.floatable.destroy();
86
84
  state.anchor = void 0;
87
- state.content = void 0;
85
+ state.elements = void 0;
88
86
  state.floatable = void 0;
89
87
  state.tooltip = void 0;
90
88
  }
@@ -102,34 +100,31 @@ function findElements(attribute) {
102
100
  }
103
101
  return elements;
104
102
  }
105
- function getContent(anchor) {
106
- const elements = getElements(anchor);
107
- if (elements == null) return;
103
+ function getContent() {
108
104
  const content = createElement(CONTENT_TAGNAME, {
109
105
  popover: "hint",
110
106
  role: "tooltip",
111
107
  tabIndex: -1
112
108
  });
113
109
  content.setAttribute(ATTRIBUTE_CONTENT, "");
114
- content.append(...elements);
115
110
  return content;
116
111
  }
117
112
  function getElements(anchor) {
118
113
  let elements = findElements(anchor.getAttribute(ARIA_LABELLEDBY));
119
- if (elements.length > 0) return elements.map((element) => element.cloneNode(true));
114
+ if (elements.length > 0) return elements;
120
115
  elements = findElements(anchor.getAttribute(ARIA_DESCRIBEDBY));
121
- if (elements.length > 0) return elements.map((element) => element.cloneNode(true));
122
- let content = anchor.getAttribute(ARIA_LABEL);
123
- if (isNullableOrWhitespace(content)) content = anchor.getAttribute(ARIA_DESCRIPTION);
124
- if (isNullableOrWhitespace(content)) return;
116
+ if (elements.length > 0) return elements;
117
+ let text = anchor.getAttribute(ARIA_LABEL);
118
+ if (isNullableOrWhitespace(text)) text = anchor.getAttribute(ARIA_DESCRIPTION);
119
+ if (isNullableOrWhitespace(text)) return;
125
120
  const paragraph = document.createElement("p");
126
- paragraph.textContent = content;
121
+ paragraph.textContent = text;
127
122
  return [paragraph];
128
123
  }
129
- function getState(anchor, content) {
124
+ function getState(anchor, elements) {
130
125
  const state = {
131
126
  anchor,
132
- content,
127
+ elements,
133
128
  floatable: createEmbeddedFloatable(anchor, content, options),
134
129
  tooltip: null
135
130
  };
@@ -153,7 +148,7 @@ function onFocusout() {
153
148
  if (focusTimer != null) clearTimeout(focusTimer);
154
149
  focusTimer = setTimeout(() => {
155
150
  if (activeTooltip == null || document.activeElement == null || findAncestor(document.activeElement, SELECTOR) != null) return;
156
- closeTooltip(activeState);
151
+ closeTooltip(true, activeState);
157
152
  activeState = void 0;
158
153
  activeTooltip = void 0;
159
154
  });
@@ -161,6 +156,10 @@ function onFocusout() {
161
156
  function onPointermove(event) {
162
157
  activate(event, false);
163
158
  }
159
+ function onToggle(event) {
160
+ if (event.newState === "open") content.setAttribute(ATTRIBUTE_CONTENT_OPEN, "");
161
+ else content.removeAttribute(ATTRIBUTE_CONTENT_OPEN);
162
+ }
164
163
  function removeTooltip(element) {
165
164
  const state = states.get(element);
166
165
  if (state == null) return;
@@ -187,16 +186,20 @@ const ATTRIBUTE_CONTENT = `${ATTRIBUTE}-content`;
187
186
  const ATTRIBUTE_CONTENT_OPEN = `${ATTRIBUTE_CONTENT}-open`;
188
187
  const SELECTOR = `[${ATTRIBUTE}]`;
189
188
  const SELECTOR_CONTENT = `[${ATTRIBUTE_CONTENT}]`;
189
+ const content = getContent();
190
190
  const states = /* @__PURE__ */ new WeakMap();
191
191
  const options = {
192
192
  attribute: `${ATTRIBUTE}-position`,
193
- position: "above"
193
+ position: "above",
194
+ reusable: true
194
195
  };
195
196
  let delay = 500;
196
197
  let focused = false;
197
198
  let activeState;
198
199
  let activeTooltip;
199
200
  let focusTimer;
201
+ document.body.append(content);
202
+ on(content, "toggle", onToggle);
200
203
  on(document, "focusin", onFocusin);
201
204
  on(document, "focusout", onFocusout);
202
205
  on(document, "pointermove", onPointermove);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oscarpalmer/oui",
3
- "version": "0.26.1",
3
+ "version": "0.28.0",
4
4
  "description": "Styles, components, and utilities for building websites and applications.",
5
5
  "keywords": [
6
6
  "css",
@@ -38,24 +38,28 @@
38
38
  "default": "./dist/js/dialog.mjs"
39
39
  },
40
40
  "./floatable": {
41
- "types": "./dist/js/floatable/standalone.d.mts",
42
- "default": "./dist/js/floatable/standalone.mjs"
41
+ "types": "./dist/js/floatable/floatable.standalone.d.mts",
42
+ "default": "./dist/js/floatable/floatable.standalone.mjs"
43
43
  },
44
44
  "./focus-trap": {
45
- "types": "./dist/js/focus-trap/standalone.d.mts",
46
- "default": "./dist/js/focus-trap/standalone.mjs"
45
+ "types": "./dist/js/focus-trap/focus-trap.standalone.d.mts",
46
+ "default": "./dist/js/focus-trap/focus-trap.standalone.mjs"
47
47
  },
48
48
  "./movable": {
49
- "types": "./dist/js/movable.d.mts",
50
- "default": "./dist/js/movable.mjs"
49
+ "types": "./dist/js/movable/movable.standalone.d.mts",
50
+ "default": "./dist/js/movable/movable.standalone.mjs"
51
51
  },
52
52
  "./popover": {
53
53
  "types": "./dist/js/popover.d.mts",
54
54
  "default": "./dist/js/popover.mjs"
55
55
  },
56
+ "./resizable": {
57
+ "types": "./dist/js/resizable/resizable.standalone.d.mts",
58
+ "default": "./dist/js/resizable/resizable.standalone.mjs"
59
+ },
56
60
  "./sortable": {
57
- "types": "./dist/js/sortable.d.mts",
58
- "default": "./dist/js/sortable.mjs"
61
+ "types": "./dist/js/sortable/sortable.standalone.d.mts",
62
+ "default": "./dist/js/sortable/sortable.standalone.mjs"
59
63
  },
60
64
  "./splitter": {
61
65
  "types": "./dist/js/splitter.d.mts",
@@ -86,7 +90,7 @@
86
90
  "@oscarpalmer/toretto": "^0.46"
87
91
  },
88
92
  "devDependencies": {
89
- "@oxlint/plugins": "^1.70",
93
+ "@oxlint/plugins": "^1.72",
90
94
  "@vitest/coverage-istanbul": "^4.1",
91
95
  "autoprefixer": "^10.5",
92
96
  "concurrently": "^10",
@@ -95,7 +99,7 @@
95
99
  "postcss-input-range": "^6",
96
100
  "postcss-scss": "^4",
97
101
  "sass": "^1.101",
98
- "stylelint": "^17.13",
102
+ "stylelint": "^17.14",
99
103
  "stylelint-config-standard-scss": "^17",
100
104
  "tsdown": "^0.22",
101
105
  "typescript": "^6",
@@ -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
  }
@@ -77,8 +86,6 @@
77
86
  :where([oui-tooltip-content]) {
78
87
  max-width: 48ch;
79
88
  padding: 0.75em 1em;
80
- overflow: visible;
81
- opacity: 0;
82
89
  background-color: var(--oui-absolute);
83
90
  border-radius: 0.25rem;
84
91
  font-size: 0.875rem;
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
 
@@ -24,6 +24,7 @@
24
24
  }
25
25
 
26
26
  :where(& h2, & h3, & h4, & h5, & h6) {
27
+ color: var(--oui-foreground, var(--oui-gray-10));
27
28
  font-size: 1em;
28
29
  font-weight: bold;
29
30
  }
@@ -123,6 +123,7 @@
123
123
 
124
124
  :where(dialog) {
125
125
  display: none !important;
126
+ overflow: visible;
126
127
  overscroll-behavior: contain;
127
128
  border: none;
128
129
  color: currentcolor;
package/src/js/dialog.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  import {on} from '@oscarpalmer/toretto/event';
2
2
  import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
3
- import {getOnBeforeToggleListener} from './floatable/embedded';
3
+ import {getOnBeforeToggleListener} from './floatable/floatable.embedded';
4
4
  import {attributable} from './internal/attributable';
5
+ import {ATTRIBUTE_MOVABLE_HANDLE} from './movable/movable.embedded';
6
+ import {createMovable, type OuiMovable} from './movable/movable.standalone';
7
+ import {createResizable, RESIZABLE_ATTRIBUTE_HANDLE} from './resizable/resizable.embedded';
8
+ import type {OuiResizable} from './resizable/resizable.standalone';
5
9
 
6
10
  // #region Types
7
11
 
@@ -79,6 +83,8 @@ type OuiDialogState = {
79
83
  element: HTMLDialogElement;
80
84
  listeners: RemovableEventListener[];
81
85
  modal: boolean;
86
+ movable?: OuiMovable;
87
+ resizable?: OuiResizable;
82
88
  };
83
89
 
84
90
  // #endregion
@@ -105,9 +111,14 @@ function destroyDialog(state: OuiDialogState): void {
105
111
  listener();
106
112
  }
107
113
 
114
+ state.movable?.destroy();
115
+ state.resizable?.destroy();
116
+
108
117
  state.dialog = undefined as never;
109
118
  state.element = undefined as never;
110
119
  state.listeners = undefined as never;
120
+ state.movable = undefined;
121
+ state.resizable = undefined;
111
122
  }
112
123
 
113
124
  /**
@@ -120,6 +131,34 @@ function getDialog(element: HTMLDialogElement): OuiDialog | undefined {
120
131
  return states.get(element)?.dialog;
121
132
  }
122
133
 
134
+ function getMovable(element: HTMLDialogElement): OuiMovable | undefined {
135
+ const handles = element.querySelectorAll(SELECTOR_MOVER);
136
+
137
+ if (handles.length === 0) {
138
+ return;
139
+ }
140
+
141
+ for (const handle of handles) {
142
+ handle.setAttribute(ATTRIBUTE_MOVABLE_HANDLE, '');
143
+ }
144
+
145
+ return createMovable(element, {
146
+ container: element.getAttribute(ATTRIBUTE_CONTAINER) ?? undefined,
147
+ });
148
+ }
149
+
150
+ function getResizable(element: HTMLDialogElement): OuiResizable | undefined {
151
+ const handle = element.querySelector(SELECTOR_RESIZER);
152
+
153
+ if (handle == null) {
154
+ return;
155
+ }
156
+
157
+ handle.setAttribute(RESIZABLE_ATTRIBUTE_HANDLE, '');
158
+
159
+ return createResizable(element);
160
+ }
161
+
123
162
  function getState(element: HTMLDialogElement): OuiDialogState {
124
163
  const state: OuiDialogState = {
125
164
  element,
@@ -136,9 +175,21 @@ function getState(element: HTMLDialogElement): OuiDialogState {
136
175
 
137
176
  element.setAttribute(ARIA_MODAL, String(modal));
138
177
  element.setAttribute(ATTRIBUTE_OPEN, '');
178
+
179
+ element.focus();
180
+
181
+ setTimeout(() => {
182
+ state.resizable?.set();
183
+ }, 500);
139
184
  } else {
140
185
  element.removeAttribute(ATTRIBUTE_OPEN);
141
186
  element.setAttribute(ARIA_MODAL, String(state.modal));
187
+
188
+ element.style.inset = '';
189
+ element.style.position = '';
190
+ element.style.transform = '';
191
+
192
+ state.resizable?.reset();
142
193
  }
143
194
  }),
144
195
  ],
@@ -147,6 +198,9 @@ function getState(element: HTMLDialogElement): OuiDialogState {
147
198
 
148
199
  state.dialog = new OuiDialog(state);
149
200
 
201
+ state.movable = getMovable(element);
202
+ state.resizable = getResizable(element);
203
+
150
204
  return state;
151
205
  }
152
206
 
@@ -170,6 +224,8 @@ const ARIA_MODAL = 'aria-modal';
170
224
 
171
225
  const ATTRIBUTE = 'oui-dialog';
172
226
 
227
+ const ATTRIBUTE_CONTAINER = `${ATTRIBUTE}-container`;
228
+
173
229
  const ATTRIBUTE_OPEN = `${ATTRIBUTE}-open`;
174
230
 
175
231
  const COMMAND_SHOW_MODAL = 'show-modal';
@@ -178,8 +234,12 @@ const EVENT_TOGGLE = 'toggle';
178
234
 
179
235
  const MESSAGE = 'The element must be an instance of HTMLDialogElement';
180
236
 
237
+ const SELECTOR_MOVER = `[${ATTRIBUTE}-mover]`;
238
+
181
239
  const SELECTOR_MODAL = ':modal';
182
240
 
241
+ const SELECTOR_RESIZER = `[${ATTRIBUTE}-resizer]`;
242
+
183
243
  const TRUE = 'true';
184
244
 
185
245
  const states = new WeakMap<HTMLElement, OuiDialogState>();
@@ -5,7 +5,7 @@ import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
5
5
 
6
6
  // #region Types
7
7
 
8
- type CreateOuiFloatableOptions = {
8
+ export type CreateOuiFloatableOptions = {
9
9
  /**
10
10
  * Open the floatable when created? _(defaults to `true`)_
11
11
  */
@@ -50,10 +50,8 @@ export class OuiFloatable {
50
50
  content.popover = 'manual';
51
51
  }
52
52
 
53
- const anchorName = getAnchorName(anchor);
54
-
55
- anchor.style.anchorName = anchorName;
56
- content.style.positionAnchor = anchorName;
53
+ anchor.style.anchorName = state.name;
54
+ content.style.positionAnchor = state.name;
57
55
  }
58
56
 
59
57
  /**
@@ -105,6 +103,7 @@ export class OuiFloatable {
105
103
  export type OuiFloatableOptions = {
106
104
  attribute?: string;
107
105
  position: OuiFloatablePosition;
106
+ reusable: boolean;
108
107
  };
109
108
 
110
109
  export type OuiFloatablePosition =
@@ -124,6 +123,7 @@ export type OuiFloatablePosition =
124
123
  type OuiFloatableState = {
125
124
  anchor: HTMLElement;
126
125
  content: HTMLElement;
126
+ name: string;
127
127
  options: OuiFloatableOptions;
128
128
  };
129
129
 
@@ -171,6 +171,7 @@ export function createFloatable(
171
171
  content,
172
172
  {
173
173
  position,
174
+ reusable: false,
174
175
  },
175
176
  true,
176
177
  );
@@ -194,12 +195,14 @@ function createInstance(
194
195
  throw new TypeError(MESSAGE);
195
196
  }
196
197
 
197
- let floatable = instances.get(content);
198
+ let floatable = options.reusable ? undefined : instances.get(content);
198
199
 
199
200
  if (floatable == null) {
200
201
  floatable = new OuiFloatable(getState(anchor, content, options), standalone);
201
202
 
202
- instances.set(content, floatable);
203
+ if (!options.reusable) {
204
+ instances.set(content, floatable);
205
+ }
203
206
  }
204
207
 
205
208
  return floatable;
@@ -212,7 +215,7 @@ export function getOnBeforeToggleListener(element: HTMLElement): RemovableEventL
212
215
  event => {
213
216
  const {newState, source} = event;
214
217
 
215
- if (newState === STATE_OPEN && source?.getAttribute(ARIA_DISABLED) === TRUE) {
218
+ if (newState === FLOATABLE_STATE_OPEN && source?.getAttribute(ARIA_DISABLED) === TRUE) {
216
219
  event.preventDefault();
217
220
  }
218
221
  },
@@ -255,9 +258,12 @@ function getState(
255
258
  content: HTMLElement,
256
259
  options: OuiFloatableOptions,
257
260
  ): OuiFloatableState {
261
+ const name = getAnchorName(anchor);
262
+
258
263
  const state: OuiFloatableState = {
259
264
  anchor,
260
265
  content,
266
+ name,
261
267
  options,
262
268
  };
263
269
 
@@ -266,6 +272,7 @@ function getState(
266
272
 
267
273
  export function removeFloatable(content: HTMLElement): void {
268
274
  instances.get(content)?.destroy();
275
+
269
276
  instances.delete(content);
270
277
  }
271
278
 
@@ -274,6 +281,7 @@ function setPosition(state: OuiFloatableState, override?: OuiFloatablePosition):
274
281
 
275
282
  const area = AREAS[position];
276
283
 
284
+ state.content.style.positionAnchor = state.name;
277
285
  state.content.style.positionArea = area;
278
286
 
279
287
  state.content.setAttribute(ATTRIBUTE_POSITION, POSITIONS[area]);
@@ -306,14 +314,14 @@ const ATTRIBUTE_POSITION = 'oui-position';
306
314
 
307
315
  const EVENT_BEFORE = 'beforetoggle';
308
316
 
317
+ export const FLOATABLE_STATE_OPEN = 'open';
318
+
309
319
  const MESSAGE = 'Anchor and content must be an HTMLElement or SVGElement';
310
320
 
311
321
  const POSITIONS = Object.fromEntries(
312
322
  Object.entries(AREAS).map(([position, area]) => [area, position]),
313
323
  ) as Record<string, OuiFloatablePosition>;
314
324
 
315
- const STATE_OPEN = 'open';
316
-
317
325
  const TRUE = 'true';
318
326
 
319
327
  const instances = new WeakMap<HTMLElement, OuiFloatable>();
@@ -0,0 +1,6 @@
1
+ export {
2
+ createFloatable,
3
+ type CreateOuiFloatableOptions,
4
+ type OuiFloatable,
5
+ type OuiFloatablePosition,
6
+ } from './floatable.embedded';
@@ -4,6 +4,7 @@ import {on} from '@oscarpalmer/toretto/event';
4
4
  import {findAncestor} from '@oscarpalmer/toretto/find';
5
5
  import {getFocusable} from '@oscarpalmer/toretto/focusable';
6
6
  import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
7
+ import {attributable} from '../internal/attributable';
7
8
 
8
9
  // #region Types
9
10
 
@@ -14,14 +15,14 @@ export class OuiFocusTrap {
14
15
  readonly #state: OuiFocusTrapState;
15
16
 
16
17
  /**
17
- * Is focused maintained within the _FocusTrap_ when clicking outside of it?
18
+ * Is focused maintained within the _focus trap_ when clicking outside of it?
18
19
  */
19
20
  get contain(): boolean {
20
21
  return this.#state.element.hasAttribute(ATTRIBUTE_FOCUSTRAP_CONTAIN);
21
22
  }
22
23
 
23
24
  /**
24
- * Should focus be maintained within the _FocusTrap_ when clicking outside of it?
25
+ * Should focus be maintained within the _focus trap_ when clicking outside of it?
25
26
  */
26
27
  set contain(value: boolean) {
27
28
  if (value === true) {
@@ -32,14 +33,14 @@ export class OuiFocusTrap {
32
33
  }
33
34
 
34
35
  /**
35
- * Is the _FocusTrap_ disabled?
36
+ * Is the _focus trap_ disabled?
36
37
  */
37
38
  get disabled(): boolean {
38
39
  return this.#state.disabled;
39
40
  }
40
41
 
41
42
  /**
42
- * Should the _FocusTrap_ be disabled?
43
+ * Should the _focus trap_ be disabled?
43
44
  */
44
45
  set disabled(value: boolean) {
45
46
  if (value === true) {
@@ -50,14 +51,14 @@ export class OuiFocusTrap {
50
51
  }
51
52
 
52
53
  /**
53
- * Is focus allowed to escape when pressing _Escape_ within the _FocusTrap_?
54
+ * Is focus allowed to escape when pressing _Escape_ within the _focus trap_?
54
55
  */
55
56
  get noescape(): boolean {
56
57
  return this.#state.element.hasAttribute(ATTRIBUTE_FOCUSTRAP_NOESCAPE);
57
58
  }
58
59
 
59
60
  /**
60
- * Should the _FocusTrap_ allow focus to escape when pressing _Escape_?
61
+ * Should the _focus trap_ allow focus to escape when pressing _Escape_?
61
62
  */
62
63
  set noescape(value: boolean) {
63
64
  if (value === true) {
@@ -82,7 +83,7 @@ export class OuiFocusTrap {
82
83
  }
83
84
 
84
85
  /**
85
- * Destroys the _FocusTrap_
86
+ * Destroys the _focus trap_
86
87
  */
87
88
  destroy(): void {
88
89
  if (this.#destroyed) {
@@ -105,7 +106,7 @@ export class OuiFocusTrap {
105
106
  }
106
107
 
107
108
  /**
108
- * Disables the _FocusTrap_
109
+ * Disables the _focus trap_
109
110
  */
110
111
  disable(): void {
111
112
  if (!this.#destroyed && !(this.disabled || this.#state.options.noescape)) {
@@ -114,7 +115,7 @@ export class OuiFocusTrap {
114
115
  }
115
116
 
116
117
  /**
117
- * Enables the _FocusTrap_
118
+ * Enables the _focus trap_
118
119
  */
119
120
  enable(): void {
120
121
  if (!this.#destroyed && this.disabled) {
@@ -123,9 +124,9 @@ export class OuiFocusTrap {
123
124
  }
124
125
 
125
126
  /**
126
- * Focuses the _FocusTrap_
127
+ * Focuses the _focus trap_
127
128
  *
128
- * @param last When `true`, focuses the last focusable element within the _FocusTrap_ instead of the first
129
+ * @param last When `true`, focuses the last focusable element within the _focus trap_ instead of the first
129
130
  */
130
131
  focus(last: boolean): void {
131
132
  if (!this.#destroyed && !this.disabled) {
@@ -134,13 +135,13 @@ export class OuiFocusTrap {
134
135
  }
135
136
  }
136
137
 
137
- type OuiFocusTrapOptions = {
138
+ export type OuiFocusTrapOptions = {
138
139
  /**
139
- * Should focus be maintained within the _FocusTrap_ when clicking outside of it?
140
+ * Should focus be maintained within the _focus trap_ when clicking outside of it?
140
141
  */
141
142
  contain?: boolean;
142
143
  /**
143
- * Should the _FocusTrap_ allow focus to escape when pressing _Escape_?
144
+ * Should the _focus trap_ allow focus to escape when pressing _Escape_?
144
145
  */
145
146
  noescape?: boolean;
146
147
  };
@@ -156,11 +157,11 @@ type OuiFocusTrapState = {
156
157
  // #region Functions
157
158
 
158
159
  /**
159
- * Creates _(or retrieves)_ a _FocusTrap_ for an element
160
+ * Creates _(or retrieves)_ a _focus trap_ for an element
160
161
  *
161
162
  * @param element Element to trap focus within
162
- * @param options _FocusTrap_ options
163
- * @returns _FocusTrap_ instance
163
+ * @param options _Focus trap_ options
164
+ * @returns _Focus trap_ instance
164
165
  */
165
166
  export function createFocusTrap(element: HTMLElement, options?: OuiFocusTrapOptions): OuiFocusTrap {
166
167
  if (!isHTMLOrSVGElement(element)) {
@@ -399,6 +400,8 @@ let lastTarget: HTMLElement | undefined;
399
400
 
400
401
  // #region Initialization
401
402
 
403
+ attributable(ATTRIBUTE_FOCUSTRAP, createFocusTrap, removeFocusTrap);
404
+
402
405
  on(document, 'focusin', onFocusIn, EVENT_OPTIONS);
403
406
  on(document, 'keydown', onKeydown, EVENT_OPTIONS);
404
407
  on(document, 'mousedown', onPointerdown, EVENT_OPTIONS);
@@ -0,0 +1 @@
1
+ export {createFocusTrap, type OuiFocusTrap, type OuiFocusTrapOptions} from './focus-trap.embedded';