@oscarpalmer/oui 0.22.1 → 0.23.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.
@@ -2,75 +2,61 @@ import { attributable } from "./internal/attributable.mjs";
2
2
  import { Floatable } from "./internal/floatable.mjs";
3
3
  import { on } from "@oscarpalmer/toretto/event";
4
4
  import { findAncestor } from "@oscarpalmer/toretto/find";
5
+ import { createElement } from "@oscarpalmer/toretto/create";
5
6
  import { isNullableOrWhitespace } from "@oscarpalmer/atoms/is";
6
7
  //#region src/js/tooltip.ts
7
8
  var Tooltip = class {
8
- elements;
9
+ anchor;
10
+ content;
9
11
  floatable;
10
- constructor(anchor, elements) {
11
- const content = getContent();
12
- this.elements = elements;
13
- this.floatable = new Floatable({
14
- anchor,
15
- content,
16
- defaultPosition: "above",
17
- interactive: false,
18
- positionAttribute: ATTRIBUTE_POSITION,
19
- reusable: true,
20
- onAfter: (active) => {
21
- if (active) {
22
- content.innerHTML = "";
23
- content.append(...this.elements);
24
- }
25
- if (active || TOOLTIPS_ACTIVE.size > 0) return;
26
- content.removeAttribute(ATTRIBUTE_CONTENT_ACTIVE);
27
- contentTimer = setTimeout(() => {
28
- content.innerHTML = "";
29
- content.remove();
30
- }, delayClose);
31
- }
32
- });
12
+ timer;
13
+ constructor(anchor, content) {
14
+ this.anchor = anchor;
15
+ this.content = content;
16
+ instances.set(content, this);
17
+ anchor.insertAdjacentElement("afterend", content);
18
+ this.floatable = new Floatable(anchor, content, options);
19
+ this.floatable.update();
33
20
  }
34
21
  destroy() {
35
- const { floatable } = this;
36
- TOOLTIPS_ACTIVE.delete(this);
37
- TOOLTIPS_PENDING.delete(this);
38
- TOOLTIPS_ALL.delete(floatable.options.anchor);
39
- TOOLTIPS_ALL.delete(floatable.options.content);
40
- floatable.destroy();
41
- this.elements = [];
42
- this.floatable = void 0;
22
+ instances.delete(this.content);
23
+ this.anchor = null;
24
+ this.content = null;
43
25
  }
44
26
  };
45
- function addTooltip(anchor) {
46
- if (TOOLTIPS_ALL.has(anchor)) return;
47
- const elements = getElements(anchor);
48
- if (elements != null) TOOLTIPS_ALL.set(anchor, new Tooltip(anchor, elements));
49
- }
50
- function closeTooltips(current) {
51
- const pending = [...TOOLTIPS_PENDING];
52
- for (const item of pending) {
53
- if (item === current) continue;
54
- TOOLTIPS_PENDING.delete(item);
55
- }
56
- const active = [...TOOLTIPS_ACTIVE];
57
- for (const item of active) {
58
- if (item === current) continue;
59
- TOOLTIPS_ACTIVE.delete(item);
60
- item.floatable.toggle(false);
27
+ function activate(event, focus) {
28
+ const anchor = findAncestor(event, SELECTOR);
29
+ const content = findAncestor(event, SELECTOR_CONTENT);
30
+ if (anchor == null && content == null) {
31
+ if (!focused) {
32
+ closeTooltip(active);
33
+ active = void 0;
34
+ }
35
+ return;
61
36
  }
37
+ const instance = instances.get(anchor ?? content);
38
+ if (instance == null || instance === active) return;
39
+ const shouldDelay = active == null;
40
+ closeTooltip(active);
41
+ active = instance;
42
+ focused = focus;
43
+ instance.timer = setTimeout(() => {
44
+ instance.content.showPopover({ source: instance.anchor });
45
+ setTimeout(() => {
46
+ instance.floatable.update();
47
+ instance.content.setAttribute(ATTRIBUTE_CONTENT_ACTIVE, "");
48
+ });
49
+ }, shouldDelay ? delay : 0);
62
50
  }
63
- function getElements(anchor) {
64
- let elements = findElements(anchor.getAttribute(ARIA_LABELLEDBY));
65
- if (elements.length > 0) return elements.map((element) => element.cloneNode(true));
66
- elements = findElements(anchor.getAttribute(ARIA_DESCRIBEDBY));
67
- if (elements.length > 0) return elements.map((element) => element.cloneNode(true));
68
- let content = anchor.getAttribute(ARIA_LABEL);
69
- if (isNullableOrWhitespace(content)) content = anchor.getAttribute(ARIA_DESCRIPTION);
70
- if (isNullableOrWhitespace(content)) return;
71
- const paragraph = document.createElement("p");
72
- paragraph.textContent = content;
73
- return [paragraph];
51
+ function closeTooltip(instance) {
52
+ if (instance == null) return;
53
+ if (instance.timer != null) clearTimeout(instance.timer);
54
+ instance.content.hidePopover();
55
+ instance.content.removeAttribute(ATTRIBUTE_CONTENT_ACTIVE);
56
+ }
57
+ function createTooltip(anchor) {
58
+ const content = getContent(anchor);
59
+ if (content != null) instances.set(anchor, new Tooltip(anchor, content));
74
60
  }
75
61
  function findElements(attribute) {
76
62
  const ids = attribute?.split(EXPRESSION_WHITESPACE) ?? [];
@@ -86,89 +72,78 @@ function findElements(attribute) {
86
72
  }
87
73
  return elements;
88
74
  }
89
- function getContent() {
90
- if (contentElement != null) return contentElement;
91
- contentElement = document.createElement("div");
92
- contentElement.setAttribute(ATTRIBUTE_CONTENT, "");
93
- contentElement.role = "tooltip";
94
- contentElement.tabIndex = -1;
95
- return contentElement;
96
- }
97
- function onActivate(event) {
98
- onToggle(event, true);
75
+ function getContent(anchor) {
76
+ const elements = getElements(anchor);
77
+ if (elements == null) return;
78
+ const content = createElement(CONTENT_TAGNAME, {
79
+ popover: "hint",
80
+ role: "tooltip",
81
+ tabIndex: -1
82
+ });
83
+ content.setAttribute(ATTRIBUTE_CONTENT, "");
84
+ content.append(...elements);
85
+ return content;
99
86
  }
100
- function onChange() {
101
- if (TOOLTIPS_ACTIVE.size === 0) return;
87
+ function getElements(anchor) {
88
+ let elements = findElements(anchor.getAttribute(ARIA_LABELLEDBY));
89
+ if (elements.length > 0) return elements.map((element) => element.cloneNode(true));
90
+ elements = findElements(anchor.getAttribute(ARIA_DESCRIBEDBY));
91
+ if (elements.length > 0) return elements.map((element) => element.cloneNode(true));
92
+ let content = anchor.getAttribute(ARIA_LABEL);
93
+ if (isNullableOrWhitespace(content)) content = anchor.getAttribute(ARIA_DESCRIPTION);
94
+ if (isNullableOrWhitespace(content)) return;
95
+ const paragraph = document.createElement("p");
96
+ paragraph.textContent = content;
97
+ return [paragraph];
102
98
  }
103
- function onClick(event) {
104
- if (findAncestor(event, SELECTOR_CONTENT) != null) event.stopPropagation();
99
+ function onAdd(element) {
100
+ if (!instances.has(element)) createTooltip(element);
105
101
  }
106
- function onDeactivate(event) {
107
- onToggle(event, false);
102
+ function onFocusin(event) {
103
+ activate(event, true);
108
104
  }
109
- function onToggle(event, activate) {
110
- if (toggle != null) cancelAnimationFrame(toggle);
111
- if (findAncestor(event, SELECTOR_CONTENT) != null) {
112
- if (contentTimer != null) clearTimeout(contentTimer);
113
- return;
114
- }
115
- toggle = requestAnimationFrame(() => {
116
- toggle = void 0;
117
- const element = findAncestor(event, SELECTOR);
118
- const tooltip = TOOLTIPS_ALL.get(element);
119
- if (TOOLTIPS_ACTIVE.size > 0 || TOOLTIPS_PENDING.size > 0) closeTooltips(tooltip);
120
- if (tooltip == null || tooltip.floatable.active === activate) return;
121
- if (activate) {
122
- clearTimeout(contentTimer);
123
- TOOLTIPS_PENDING.add(tooltip);
124
- contentTimer = setTimeout(() => {
125
- TOOLTIPS_ACTIVE.add(tooltip);
126
- TOOLTIPS_PENDING.delete(tooltip);
127
- tooltip.floatable.options.content.style.positionAnchor = tooltip.floatable.options.anchor.style.anchorName;
128
- tooltip.floatable.toggle(true);
129
- setTimeout(() => {
130
- tooltip.floatable.options.content.setAttribute(ATTRIBUTE_CONTENT_ACTIVE, "");
131
- });
132
- }, delayOpen);
133
- return;
134
- }
135
- TOOLTIPS_ACTIVE.delete(tooltip);
136
- tooltip.floatable.toggle(false);
105
+ function onFocusout() {
106
+ if (active == null) return;
107
+ if (focusTimer != null) clearTimeout(focusTimer);
108
+ focusTimer = setTimeout(() => {
109
+ if (active == null || document.activeElement == null || findAncestor(document.activeElement, SELECTOR) != null) return;
110
+ closeTooltip(active);
111
+ active = void 0;
137
112
  });
138
113
  }
139
- function removeTooltip(element) {
140
- if (!element.hasAttribute(ATTRIBUTE_CONTENT)) TOOLTIPS_ALL.get(element)?.destroy();
114
+ function onPointermove(event) {
115
+ activate(event, false);
116
+ }
117
+ function onRemove(element) {
118
+ instances.get(element)?.destroy();
119
+ instances.delete(element);
141
120
  }
142
- function setTooltipDelay(open, close) {
143
- delayClose = typeof close === "number" && close >= 0 ? close : delayClose;
144
- delayOpen = typeof open === "number" && open >= 0 ? open : delayOpen;
121
+ function setTooltipDelay(value) {
122
+ if (typeof value === "number" && !Number.isNaN(value) && value >= 0) delay = value;
145
123
  }
146
124
  const ARIA_DESCRIBEDBY = "aria-describedby";
147
125
  const ARIA_DESCRIPTION = "aria-description";
148
126
  const ARIA_LABEL = "aria-label";
149
127
  const ARIA_LABELLEDBY = "aria-labelledby";
128
+ const CONTENT_TAGNAME = "div";
129
+ const EXPRESSION_WHITESPACE = /\s+/;
150
130
  const ATTRIBUTE = "oui-tooltip";
151
131
  const ATTRIBUTE_CONTENT = `${ATTRIBUTE}-content`;
152
132
  const ATTRIBUTE_CONTENT_ACTIVE = `${ATTRIBUTE_CONTENT}-active`;
153
- const ATTRIBUTE_POSITION = `${ATTRIBUTE}-position`;
154
133
  const SELECTOR = `[${ATTRIBUTE}]`;
155
134
  const SELECTOR_CONTENT = `[${ATTRIBUTE_CONTENT}]`;
156
- const EXPRESSION_WHITESPACE = /\s+/;
157
- const TOOLTIPS_ACTIVE = /* @__PURE__ */ new Set();
158
- const TOOLTIPS_ALL = /* @__PURE__ */ new WeakMap();
159
- const TOOLTIPS_PENDING = /* @__PURE__ */ new Set();
160
- let contentElement;
161
- let contentTimer;
162
- let delayClose = 250;
163
- let delayOpen = 500;
164
- let toggle;
165
- attributable(ATTRIBUTE, addTooltip, removeTooltip);
166
- on(document, "click", onClick, { capture: true });
167
- on(document, "focusin", onActivate);
168
- on(document, "focusout", onDeactivate);
169
- on(document, "mousemove", onActivate);
170
- on(window, "resize", onChange);
171
- on(document, "scroll", onChange);
172
- on(document, "touchstart", onActivate);
135
+ const instances = /* @__PURE__ */ new WeakMap();
136
+ const options = {
137
+ attribute: "oui-tooltip-position",
138
+ position: "above"
139
+ };
140
+ let delay = 500;
141
+ let focused = false;
142
+ let active;
143
+ let focusTimer;
144
+ on(document, "focusin", onFocusin);
145
+ on(document, "focusout", onFocusout);
146
+ on(document, "pointermove", onPointermove);
147
+ attributable(ATTRIBUTE, onAdd, onRemove);
173
148
  //#endregion
174
149
  export { setTooltipDelay };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oscarpalmer/oui",
3
- "version": "0.22.1",
3
+ "version": "0.23.0",
4
4
  "description": "Styles, components, and utilities for building websites and applications.",
5
5
  "keywords": [
6
6
  "css",
@@ -70,11 +70,11 @@
70
70
  "watch": "vp run clean && concurrently \"php -S localhost:3000\" \"vp run postcss:watch\" \"vp run css:watch\" \"vp run tsdown\""
71
71
  },
72
72
  "dependencies": {
73
- "@oscarpalmer/atoms": "^0.187.2",
73
+ "@oscarpalmer/atoms": "^0.188.1",
74
74
  "@oscarpalmer/toretto": "^0.46"
75
75
  },
76
76
  "devDependencies": {
77
- "@oxlint/plugins": "^1.68",
77
+ "@oxlint/plugins": "^1.69",
78
78
  "@vitest/coverage-istanbul": "^4.1",
79
79
  "autoprefixer": "^10.5",
80
80
  "concurrently": "^10",
@@ -82,8 +82,8 @@
82
82
  "postcss-cli": "^11",
83
83
  "postcss-input-range": "^6",
84
84
  "postcss-scss": "^4",
85
- "sass": "^1.100",
86
- "stylelint": "^17.12",
85
+ "sass": "^1.101",
86
+ "stylelint": "^17.13",
87
87
  "stylelint-config-standard-scss": "^17",
88
88
  "tsdown": "^0.22",
89
89
  "typescript": "^6",
@@ -92,4 +92,4 @@
92
92
  "vitest": "npm:@voidzero-dev/vite-plus-test@latest"
93
93
  },
94
94
  "packageManager": "npm@11.12.1"
95
- }
95
+ }
@@ -54,26 +54,28 @@
54
54
  @mixin floatable() {
55
55
  @include shadow-overlay;
56
56
 
57
- --margin-left: 0;
58
- --margin-top: 0;
57
+ --margin-bottom: 0.5em;
58
+ --margin-left: 0.5em;
59
+ --margin-right: 0.5em;
60
+ --margin-top: 0.5em;
59
61
 
60
- margin: var(--margin-top, 0) 0 0 var(--margin-left, 0);
61
- pointer-events: all !important;
62
+ inset-block: var(--margin-top) var(--margin-bottom);
63
+ inset-inline: var(--margin-left) var(--margin-right);
62
64
 
63
- &[oui-position^='above'] {
64
- margin: 0 0 0.5em;
65
+ &[oui-position$='bottom'] {
66
+ --margin-bottom: 0;
65
67
  }
66
68
 
67
- &[oui-position^='below'] {
68
- margin: 0.5em 0 0;
69
+ &[oui-position$='end'] {
70
+ --margin-right: 0;
69
71
  }
70
72
 
71
- &[oui-position^='end'] {
72
- margin: 0 0 0 0.5em;
73
+ &[oui-position$='start'] {
74
+ --margin-left: 0;
73
75
  }
74
76
 
75
- &[oui-position^='start'] {
76
- margin: 0 0.5em 0 0;
77
+ &[oui-position$='top'] {
78
+ --margin-top: 0;
77
79
  }
78
80
  }
79
81
 
@@ -4,15 +4,7 @@
4
4
  @include mixins.floatable;
5
5
 
6
6
  padding: 1rem;
7
- overscroll-behavior: contain;
8
7
  background-color: var(--oui-absolute);
8
+ border: none;
9
9
  border-radius: 0.25rem;
10
-
11
- &:focus {
12
- outline: none;
13
- }
14
-
15
- * {
16
- overscroll-behavior: contain;
17
- }
18
10
  }
@@ -353,6 +353,14 @@
353
353
  * Attributes
354
354
  */
355
355
 
356
+ :where([aria-busy='true' i]) {
357
+ cursor: progress;
358
+ }
359
+
360
+ :where([aria-controls]) {
361
+ cursor: pointer;
362
+ }
363
+
356
364
  :where([disabled], [aria-disabled='true' i]) {
357
365
  cursor: default;
358
366
  }
@@ -361,12 +369,16 @@
361
369
  display: none !important;
362
370
  }
363
371
 
364
- :where([aria-busy='true' i]) {
365
- cursor: progress;
372
+ :where([popover]) {
373
+ display: none !important;
374
+ overflow: visible;
375
+ overscroll-behavior: contain;
376
+ border: none;
377
+ color: currentcolor;
366
378
  }
367
379
 
368
- :where([aria-controls]) {
369
- cursor: pointer;
380
+ :where([popover]:popover-open) {
381
+ display: inherit !important;
370
382
  }
371
383
 
372
384
  /**
@@ -33,16 +33,18 @@
33
33
 
34
34
  max-width: 48ch;
35
35
  padding: 0.75em 1em;
36
+ overflow: visible;
36
37
  opacity: 0;
37
38
  background-color: var(--oui-absolute);
38
39
  border-radius: 0.25rem;
39
40
  font-size: 0.875rem;
40
41
  pointer-events: none;
41
- transition: opacity 0.125s ease-in-out;
42
+ transition: opacity 0.125s ease-in-out 0s;
43
+ transition-behavior: allow-discrete;
42
44
 
43
45
  &::after {
44
46
  content: '';
45
- opacity: 0;
47
+ display: block;
46
48
  position: absolute;
47
49
  }
48
50
 
@@ -52,14 +54,14 @@
52
54
  &[oui-position^='top'] {
53
55
  &::after {
54
56
  width: 100%;
55
- height: 0.5em;
57
+ height: 1em;
56
58
  }
57
59
  }
58
60
 
59
61
  &[oui-position^='above'],
60
62
  &[oui-position^='top'] {
61
63
  &::after {
62
- inset: 100% 0 0;
64
+ inset: calc(100% - 0.5em) 0 0;
63
65
  }
64
66
  }
65
67
 
@@ -75,7 +77,7 @@
75
77
  &[oui-position^='right'],
76
78
  &[oui-position^='start'] {
77
79
  &::after {
78
- width: 0.5em;
80
+ width: 1em;
79
81
  height: 100%;
80
82
  }
81
83
  }
@@ -90,12 +92,12 @@
90
92
  &[oui-position^='left'],
91
93
  &[oui-position^='start'] {
92
94
  &::after {
93
- inset: 0 0 0 100%;
95
+ inset: 0 0 0 calc(100% - 0.5em);
94
96
  }
95
97
  }
96
98
  }
97
99
 
98
100
  :where([oui-tooltip-content][oui-tooltip-content-active]) {
99
101
  opacity: 1;
100
- pointer-events: auto;
102
+ pointer-events: all;
101
103
  }
@@ -179,6 +179,11 @@ function onTab(event: KeyboardEvent): void {
179
179
  }
180
180
  }
181
181
 
182
+ export function removeFocusTrap(element: HTMLElement): void {
183
+ FOCUSTRAPS_ALL.get(element)?.destroy();
184
+ FOCUSTRAPS_ALL.delete(element);
185
+ }
186
+
182
187
  function setConnected(focusTrap: FocusTrap, value: boolean): void {
183
188
  if (!focusTrap.embedded || focusTrap.connected === value) {
184
189
  return;
@@ -262,7 +267,7 @@ export const FOCUS_TRAP_CONTAIN = `${FOCUS_TRAP_SELECTOR}-contain`;
262
267
 
263
268
  export const FOCUS_TRAP_NOESCAPE = `${FOCUS_TRAP_SELECTOR}-noescape`;
264
269
 
265
- export const FOCUSTRAPS_ALL: WeakMap<HTMLElement, FocusTrap> = new WeakMap();
270
+ const FOCUSTRAPS_ALL: WeakMap<HTMLElement, FocusTrap> = new WeakMap();
266
271
 
267
272
  const FOCUSTRAPS_DISABLED: Set<FocusTrap> = new Set();
268
273
 
@@ -1,9 +1,4 @@
1
1
  import {attributable} from '../internal/attributable';
2
- import {createFocusTrap, FOCUS_TRAP_SELECTOR, FOCUSTRAPS_ALL} from './embedded';
2
+ import {createFocusTrap, FOCUS_TRAP_SELECTOR, removeFocusTrap} from './embedded';
3
3
 
4
- function removeFocusTraps(element: HTMLElement): void {
5
- FOCUSTRAPS_ALL.get(element)?.destroy();
6
- FOCUSTRAPS_ALL.delete(element);
7
- }
8
-
9
- attributable(FOCUS_TRAP_SELECTOR, createFocusTrap, removeFocusTraps);
4
+ attributable(FOCUS_TRAP_SELECTOR, createFocusTrap, removeFocusTrap);
package/src/js/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import './focus-trap/standalone';
2
2
  import './movable';
3
- export * from './popover';
3
+ import './popover';
4
4
  import './sortable';
5
+
5
6
  export * from './splitter';
6
7
  export * from './tooltip';