@oscarpalmer/oui 0.24.0 → 0.25.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.
@@ -1,64 +1,103 @@
1
- import { createFloatable, removeFloatable } from "./floatable.mjs";
1
+ import { createFloatable, getOnBeforeToggleListener } from "./floatable/embedded.mjs";
2
2
  import { attributable } from "./internal/attributable.mjs";
3
- import { createFocusTrap, removeFocusTrap } from "./focus-trap/embedded.mjs";
4
- import { isHTMLOrSVGElement } from "@oscarpalmer/toretto/is";
3
+ import { createFocusTrap } from "./focus-trap/embedded.mjs";
5
4
  import { on } from "@oscarpalmer/toretto/event";
5
+ import { isHTMLOrSVGElement } from "@oscarpalmer/toretto/is";
6
6
  //#region src/js/popover.ts
7
- var Popover = class {
8
- element;
9
- anchor;
10
- floatable;
11
- focusTrap;
12
- listener;
13
- constructor(element) {
14
- this.element = element;
15
- const anchor = element.ownerDocument.querySelector(`[popovertarget="${element.id}"]`);
16
- if (!isHTMLOrSVGElement(anchor)) throw new Error(`No anchor found for popover with id '${element.id}'.`);
17
- if (element.contains(anchor)) throw new Error("Popover content cannot contain its anchor.");
18
- this.anchor = anchor;
19
- this.floatable = createFloatable(this.anchor, element, options, false);
20
- this.focusTrap = createFocusTrap(element, { noescape: true });
21
- this.listener = on(element, EVENT_TOGGLE, (event) => {
22
- if (event.newState === STATE_OPEN) {
23
- ignoreFocus = false;
24
- this.floatable?.update();
25
- element.focus();
26
- } else if (!ignoreFocus) this.anchor?.focus();
27
- });
28
- this.element.setAttribute(ATTRIBUTE_CONTENT, "");
29
- this.floatable?.update();
7
+ var OuiPopover = class {
8
+ #state;
9
+ /**
10
+ * Is the popover open?
11
+ */
12
+ get open() {
13
+ return this.#state.content.checkVisibility();
14
+ }
15
+ constructor(state) {
16
+ this.#state = state;
17
+ this.#state.floatable.update();
18
+ }
19
+ /**
20
+ * Closes the popover
21
+ */
22
+ hide() {
23
+ this.#state.floatable.hide();
30
24
  }
31
- destroy() {
32
- this.focusTrap?.destroy();
33
- removeFloatable(this.element);
34
- removeFocusTrap(this.element);
35
- this.element.removeAttribute(ATTRIBUTE_CONTENT);
36
- this.anchor = null;
37
- this.element = null;
25
+ /**
26
+ * Opens the popover
27
+ */
28
+ show() {
29
+ this.#state.floatable.show();
38
30
  }
39
31
  };
32
+ function destroyPopover(state) {
33
+ state.floatable.destroy();
34
+ state.focusTrap?.destroy();
35
+ for (const listener of state.listeners) listener();
36
+ state.anchor = void 0;
37
+ state.content = void 0;
38
+ state.floatable = void 0;
39
+ state.focusTrap = void 0;
40
+ state.listeners = void 0;
41
+ state.popover = void 0;
42
+ }
43
+ /**
44
+ * Get the _OuiPopover_ for an element
45
+ *
46
+ * @param element Element to get _OuiPopover_ for
47
+ * @returns _OuiPopover_ instance
48
+ */
49
+ function getPopover(element) {
50
+ return states.get(element)?.popover;
51
+ }
52
+ function getState(anchor, content) {
53
+ const state = {
54
+ anchor,
55
+ content,
56
+ floatable: createFloatable(anchor, content, options, false),
57
+ focusTrap: content.getAttribute(ARIA_MODAL) === TRUE ? createFocusTrap(content, { noescape: true }) : void 0,
58
+ listeners: [getOnBeforeToggleListener(content), on(content, EVENT_TOGGLE, (event) => {
59
+ if (event.newState === STATE_OPEN) {
60
+ ignoreFocus = false;
61
+ state.floatable?.update();
62
+ content.setAttribute(ATTRIBUTE_OPEN, "");
63
+ content.focus();
64
+ } else {
65
+ content.removeAttribute(ATTRIBUTE_OPEN);
66
+ if (!ignoreFocus) anchor?.focus();
67
+ }
68
+ })],
69
+ popover: void 0
70
+ };
71
+ state.popover = new OuiPopover(state);
72
+ return state;
73
+ }
40
74
  function onAdd(element) {
41
- if (!instances.has(element) && element.getAttribute(ATTRIBUTE) !== POPOVER_HINT && !element.hasAttribute("oui-floatable")) instances.set(element, new Popover(element));
75
+ if (states.has(element) || element.hasAttribute("oui-floatable")) return;
76
+ const anchor = element.ownerDocument.querySelector(`[popovertarget="${element.id}"]`);
77
+ if (isHTMLOrSVGElement(anchor)) states.set(element, getState(anchor, element));
42
78
  }
43
79
  function onPointerdown() {
44
80
  ignoreFocus = true;
45
81
  }
46
82
  function onRemove(element) {
47
- instances.get(element)?.destroy();
48
- instances.delete(element);
83
+ const state = states.get(element);
84
+ if (state == null) return;
85
+ destroyPopover(state);
86
+ states.delete(element);
49
87
  }
50
- const ATTRIBUTE = "popover";
51
- const ATTRIBUTE_CONTENT = "oui-popover-content";
88
+ const ATTRIBUTE = "oui-popover";
89
+ const ARIA_MODAL = "aria-modal";
90
+ const ATTRIBUTE_OPEN = `${ATTRIBUTE}-open`;
52
91
  const EVENT_TOGGLE = "toggle";
53
- const POPOVER_HINT = "hint";
54
92
  const STATE_OPEN = "open";
55
- const instances = /* @__PURE__ */ new WeakMap();
93
+ const TRUE = "true";
56
94
  const options = {
57
95
  attribute: `${ATTRIBUTE}position`,
58
96
  position: "below-start"
59
97
  };
98
+ const states = /* @__PURE__ */ new WeakMap();
60
99
  let ignoreFocus = false;
61
100
  on(document, "pointerdown", onPointerdown, { capture: true });
62
101
  attributable(ATTRIBUTE, onAdd, onRemove);
63
102
  //#endregion
64
- export {};
103
+ export { getPopover };
@@ -1,7 +1,7 @@
1
1
  import { attributable } from "./internal/attributable.mjs";
2
2
  import { Draggable, addDraggable, removeDraggable } from "./internal/draggable.mjs";
3
- import { isHTMLOrSVGElement } from "@oscarpalmer/toretto/is";
4
3
  import { getPosition } from "@oscarpalmer/toretto/event";
4
+ import { isHTMLOrSVGElement } from "@oscarpalmer/toretto/is";
5
5
  import { findAncestor, getElementFromPosition } from "@oscarpalmer/toretto/find";
6
6
  import { createElement } from "@oscarpalmer/toretto/create";
7
7
  //#region src/js/sortable.ts
@@ -1,6 +1,6 @@
1
+ import { getPosition, on } from "@oscarpalmer/toretto/event";
1
2
  import { isNullableOrWhitespace } from "@oscarpalmer/atoms/is";
2
3
  import { clamp } from "@oscarpalmer/atoms/number";
3
- import { getPosition, on } from "@oscarpalmer/toretto/event";
4
4
  import { findAncestor } from "@oscarpalmer/toretto/find";
5
5
  import { toggleStyles } from "@oscarpalmer/toretto/style";
6
6
  import supportsTouch from "@oscarpalmer/toretto/touch";
@@ -1,4 +1,30 @@
1
+ import { n as OuiFloatable } from "./embedded-84gxpReS.mjs";
2
+
1
3
  //#region src/js/tooltip.d.ts
4
+ declare class OuiTooltip {
5
+ #private;
6
+ /**
7
+ * Is the tooltip open?
8
+ */
9
+ get open(): boolean;
10
+ constructor(state: OuiTooltipState);
11
+ /**
12
+ * Closes the tooltip
13
+ */
14
+ hide(): void;
15
+ /**
16
+ * Opens the tooltip
17
+ */
18
+ show(): void;
19
+ }
20
+ type OuiTooltipState = {
21
+ anchor: HTMLElement;
22
+ content: HTMLElement;
23
+ floatable: OuiFloatable;
24
+ timer?: number;
25
+ tooltip: OuiTooltip;
26
+ };
27
+ declare function getTooltip(element: HTMLElement): OuiTooltip | undefined;
2
28
  /**
3
29
  * Set the delay before showing the tooltip, in milliseconds
4
30
  *
@@ -7,4 +33,4 @@
7
33
  */
8
34
  declare function setTooltipDelay(value: number): void;
9
35
  //#endregion
10
- export { setTooltipDelay };
36
+ export { type OuiTooltip, getTooltip, setTooltipDelay };
@@ -1,28 +1,34 @@
1
- import { Floatable } from "./floatable.mjs";
1
+ import { createFloatable } from "./floatable/embedded.mjs";
2
2
  import { attributable } from "./internal/attributable.mjs";
3
+ import { on } from "@oscarpalmer/toretto/event";
3
4
  import { isNullableOrWhitespace } from "@oscarpalmer/atoms/is";
4
5
  import { isHTMLOrSVGElement } from "@oscarpalmer/toretto/is";
5
- import { on } from "@oscarpalmer/toretto/event";
6
6
  import { findAncestor } from "@oscarpalmer/toretto/find";
7
7
  import { createElement } from "@oscarpalmer/toretto/create";
8
8
  //#region src/js/tooltip.ts
9
- var Tooltip = class {
10
- anchor;
11
- content;
12
- floatable;
13
- timer;
14
- constructor(anchor, content) {
15
- this.anchor = anchor;
16
- this.content = content;
17
- instances.set(content, this);
18
- anchor.insertAdjacentElement("afterend", content);
19
- this.floatable = new Floatable(anchor, content, options, false);
20
- this.floatable.update();
9
+ var OuiTooltip = class {
10
+ #state;
11
+ /**
12
+ * Is the tooltip open?
13
+ */
14
+ get open() {
15
+ return this.#state.content.checkVisibility();
16
+ }
17
+ constructor(state) {
18
+ this.#state = state;
19
+ state.floatable.update();
20
+ }
21
+ /**
22
+ * Closes the tooltip
23
+ */
24
+ hide() {
25
+ this.#state.floatable.hide();
21
26
  }
22
- destroy() {
23
- instances.delete(this.content);
24
- this.anchor = null;
25
- this.content = null;
27
+ /**
28
+ * Opens the tooltip
29
+ */
30
+ show() {
31
+ this.#state.floatable.show();
26
32
  }
27
33
  };
28
34
  function activate(event, focus) {
@@ -30,34 +36,47 @@ function activate(event, focus) {
30
36
  const content = findAncestor(event, SELECTOR_CONTENT);
31
37
  if (anchor == null && content == null) {
32
38
  if (!focused) {
33
- closeTooltip(active);
34
- active = void 0;
39
+ closeTooltip(activeState);
40
+ activeState = void 0;
41
+ activeTooltip = void 0;
35
42
  }
36
43
  return;
37
44
  }
38
- const instance = instances.get(anchor ?? content);
39
- if (instance == null || instance === active) return;
40
- const shouldDelay = active == null;
41
- closeTooltip(active);
42
- active = instance;
45
+ const state = states.get(anchor ?? content);
46
+ if (state == null || state.tooltip === activeTooltip) return;
47
+ const shouldDelay = activeTooltip == null;
48
+ closeTooltip(activeState);
49
+ activeState = state;
50
+ activeTooltip = state.tooltip;
43
51
  focused = focus;
44
- instance.timer = setTimeout(() => {
45
- instance.content.showPopover({ source: instance.anchor });
52
+ state.timer = setTimeout(() => {
53
+ state.content.showPopover({ source: state.anchor });
46
54
  setTimeout(() => {
47
- instance.floatable.update();
48
- instance.content.setAttribute(ATTRIBUTE_CONTENT_ACTIVE, "");
55
+ state.floatable.update();
56
+ state.content.setAttribute(ATTRIBUTE_CONTENT_OPEN, "");
49
57
  });
50
58
  }, shouldDelay ? delay : 0);
51
59
  }
52
- function closeTooltip(instance) {
53
- if (instance == null) return;
54
- if (instance.timer != null) clearTimeout(instance.timer);
55
- instance.content?.hidePopover();
56
- instance.content?.removeAttribute(ATTRIBUTE_CONTENT_ACTIVE);
60
+ function closeTooltip(state) {
61
+ if (state == null) return;
62
+ if (state.timer != null) clearTimeout(state.timer);
63
+ state.content?.hidePopover();
64
+ state.content?.removeAttribute(ATTRIBUTE_CONTENT_OPEN);
57
65
  }
58
66
  function createTooltip(anchor) {
59
67
  const content = getContent(anchor);
60
- if (content != null) instances.set(anchor, new Tooltip(anchor, content));
68
+ if (content == null) return;
69
+ anchor.insertAdjacentElement("afterend", content);
70
+ states.set(anchor, getState(anchor, content));
71
+ }
72
+ function destroyTooltip(state) {
73
+ clearTimeout(state.timer);
74
+ state.content.remove();
75
+ state.floatable.destroy();
76
+ state.anchor = void 0;
77
+ state.content = void 0;
78
+ state.floatable = void 0;
79
+ state.tooltip = void 0;
61
80
  }
62
81
  function findElements(attribute) {
63
82
  const ids = attribute?.split(EXPRESSION_WHITESPACE) ?? [];
@@ -97,27 +116,43 @@ function getElements(anchor) {
97
116
  paragraph.textContent = content;
98
117
  return [paragraph];
99
118
  }
119
+ function getState(anchor, content) {
120
+ const state = {
121
+ anchor,
122
+ content,
123
+ floatable: createFloatable(anchor, content, options, false),
124
+ tooltip: null
125
+ };
126
+ state.tooltip = new OuiTooltip(state);
127
+ return state;
128
+ }
129
+ function getTooltip(element) {
130
+ return states.get(element)?.tooltip;
131
+ }
100
132
  function onAdd(element) {
101
- if (!instances.has(element)) createTooltip(element);
133
+ if (!states.has(element)) createTooltip(element);
102
134
  }
103
135
  function onFocusin(event) {
104
136
  activate(event, true);
105
137
  }
106
138
  function onFocusout() {
107
- if (active == null) return;
139
+ if (activeTooltip == null) return;
108
140
  if (focusTimer != null) clearTimeout(focusTimer);
109
141
  focusTimer = setTimeout(() => {
110
- if (active == null || document.activeElement == null || findAncestor(document.activeElement, SELECTOR) != null) return;
111
- closeTooltip(active);
112
- active = void 0;
142
+ if (activeTooltip == null || document.activeElement == null || findAncestor(document.activeElement, SELECTOR) != null) return;
143
+ closeTooltip(activeState);
144
+ activeState = void 0;
145
+ activeTooltip = void 0;
113
146
  });
114
147
  }
115
148
  function onPointermove(event) {
116
149
  activate(event, false);
117
150
  }
118
151
  function onRemove(element) {
119
- instances.get(element)?.destroy();
120
- instances.delete(element);
152
+ const state = states.get(element);
153
+ if (state == null) return;
154
+ destroyTooltip(state);
155
+ states.delete(element);
121
156
  }
122
157
  /**
123
158
  * Set the delay before showing the tooltip, in milliseconds
@@ -136,21 +171,22 @@ const CONTENT_TAGNAME = "div";
136
171
  const EXPRESSION_WHITESPACE = /\s+/;
137
172
  const ATTRIBUTE = "oui-tooltip";
138
173
  const ATTRIBUTE_CONTENT = `${ATTRIBUTE}-content`;
139
- const ATTRIBUTE_CONTENT_ACTIVE = `${ATTRIBUTE_CONTENT}-active`;
174
+ const ATTRIBUTE_CONTENT_OPEN = `${ATTRIBUTE_CONTENT}-open`;
140
175
  const SELECTOR = `[${ATTRIBUTE}]`;
141
176
  const SELECTOR_CONTENT = `[${ATTRIBUTE_CONTENT}]`;
142
- const instances = /* @__PURE__ */ new WeakMap();
177
+ const states = /* @__PURE__ */ new WeakMap();
143
178
  const options = {
144
179
  attribute: `${ATTRIBUTE}-position`,
145
180
  position: "above"
146
181
  };
147
182
  let delay = 500;
148
183
  let focused = false;
149
- let active;
184
+ let activeState;
185
+ let activeTooltip;
150
186
  let focusTimer;
151
187
  on(document, "focusin", onFocusin);
152
188
  on(document, "focusout", onFocusout);
153
189
  on(document, "pointermove", onPointermove);
154
190
  attributable(ATTRIBUTE, onAdd, onRemove);
155
191
  //#endregion
156
- export { setTooltipDelay };
192
+ export { getTooltip, setTooltipDelay };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oscarpalmer/oui",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Styles, components, and utilities for building websites and applications.",
5
5
  "keywords": [
6
6
  "css",
@@ -74,7 +74,7 @@
74
74
  "@oscarpalmer/toretto": "^0.46"
75
75
  },
76
76
  "devDependencies": {
77
- "@oxlint/plugins": "^1.69",
77
+ "@oxlint/plugins": "^1.70",
78
78
  "@vitest/coverage-istanbul": "^4.1",
79
79
  "autoprefixer": "^10.5",
80
80
  "concurrently": "^10",
@@ -1,5 +1,51 @@
1
1
  @use 'mixins.scss' as mixins;
2
2
 
3
+ :where([oui-dialog], [oui-popover], [oui-tooltip-content]),
4
+ :where([oui-dialog])::backdrop {
5
+ opacity: 0;
6
+ transition: 0.125s ease-in-out;
7
+ transition-property: opacity, scale;
8
+ }
9
+
10
+ :where([oui-dialog], [oui-popover], [oui-tooltip-content]) {
11
+ @include mixins.shadow-overlay;
12
+
13
+ background-color: var(--oui-absolute);
14
+ scale: 0.9;
15
+ }
16
+
17
+ :where([oui-dialog]:focus, [oui-popover]:focus, [oui-tooltip-content]:focus) {
18
+ outline: none;
19
+ }
20
+
21
+ :where(
22
+ [oui-dialog][oui-dialog-open],
23
+ [oui-popover][oui-popover-open],
24
+ [oui-tooltip-content][oui-tooltip-content-open]
25
+ ),
26
+ :where([oui-dialog][oui-dialog-open])::backdrop {
27
+ opacity: 1;
28
+ }
29
+
30
+ :where(
31
+ [oui-dialog][oui-dialog-open],
32
+ [oui-popover][oui-popover-open],
33
+ [oui-tooltip-content][oui-tooltip-content-open]
34
+ ) {
35
+ scale: 1;
36
+ transition-delay: 0.125s;
37
+ }
38
+
39
+ :where([oui-popover], [oui-tooltip-content]) {
40
+ @include mixins.floatable;
41
+ }
42
+
43
+ :where([oui-popover]) {
44
+ padding: 1rem;
45
+ background-color: var(--oui-absolute);
46
+ border-radius: 0.25rem;
47
+ }
48
+
3
49
  :where(
4
50
  [oui-tooltip]:is(
5
51
  abbr,
@@ -29,8 +75,6 @@
29
75
  }
30
76
 
31
77
  :where([oui-tooltip-content]) {
32
- @include mixins.floatable;
33
-
34
78
  max-width: 48ch;
35
79
  padding: 0.75em 1em;
36
80
  overflow: visible;
@@ -39,8 +83,6 @@
39
83
  border-radius: 0.25rem;
40
84
  font-size: 0.875rem;
41
85
  pointer-events: none;
42
- transition: opacity 0.125s ease-in-out 0s;
43
- transition-behavior: allow-discrete;
44
86
 
45
87
  &::after {
46
88
  content: '';
@@ -97,7 +139,7 @@
97
139
  }
98
140
  }
99
141
 
100
- :where([oui-tooltip-content][oui-tooltip-content-active]) {
142
+ :where([oui-tooltip-content][oui-tooltip-content-open]) {
101
143
  opacity: 1;
102
144
  pointer-events: all;
103
145
  }
@@ -52,8 +52,6 @@
52
52
  }
53
53
 
54
54
  @mixin floatable() {
55
- @include shadow-overlay;
56
-
57
55
  --margin-bottom: 0.5em;
58
56
  --margin-left: 0.5em;
59
57
  --margin-right: 0.5em;
@@ -121,6 +121,23 @@
121
121
  display: list-item;
122
122
  }
123
123
 
124
+ :where(dialog) {
125
+ display: none !important;
126
+ overscroll-behavior: contain;
127
+ border: none;
128
+ color: currentcolor;
129
+ }
130
+
131
+ :where([oui-dialog][aria-modal='true' i]) {
132
+ position: fixed;
133
+ inset: 50% auto auto 50%;
134
+ transform: translate3d(-50%, -50%, 0);
135
+ }
136
+
137
+ :where(dialog[open]) {
138
+ display: inherit !important;
139
+ }
140
+
124
141
  :where(dl, ol, ul) :where(dl, ol, ul) {
125
142
  /** Remove the margin on nested lists in Chrome, Edge, and Safari [SE] */
126
143
  margin: 0;