@oscarpalmer/oui 0.13.0 → 0.15.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 (38) hide show
  1. package/README.md +2 -0
  2. package/dist/css/popover.css +9 -0
  3. package/dist/css/tooltip.css +9 -3
  4. package/dist/css/value.css +1 -0
  5. package/dist/js/floatable.cjs +134 -0
  6. package/dist/js/floatable.js +134 -0
  7. package/dist/js/focus-trap/embedded.cjs +150 -0
  8. package/dist/js/focus-trap/embedded.js +150 -0
  9. package/dist/js/focus-trap/standalone.cjs +8 -0
  10. package/dist/js/focus-trap/standalone.js +7 -0
  11. package/dist/js/index.cjs +5 -1
  12. package/dist/js/index.js +6 -1
  13. package/dist/js/popover.cjs +160 -0
  14. package/dist/js/popover.js +160 -0
  15. package/dist/js/tooltip.cjs +83 -126
  16. package/dist/js/tooltip.js +83 -126
  17. package/package.json +7 -7
  18. package/src/css/popover.scss +9 -0
  19. package/src/css/tooltip.scss +7 -3
  20. package/src/js/floatable.ts +196 -0
  21. package/src/js/focus-trap/embedded.ts +215 -0
  22. package/src/js/focus-trap/standalone.ts +9 -0
  23. package/src/js/index.ts +3 -1
  24. package/src/js/popover.ts +221 -0
  25. package/src/js/tooltip.ts +97 -161
  26. package/types/floatable.d.cts +16 -0
  27. package/types/floatable.d.ts +12 -0
  28. package/types/focus-trap/embedded.d.cts +15 -0
  29. package/types/focus-trap/embedded.d.ts +11 -0
  30. package/types/index.d.cts +11 -0
  31. package/types/index.d.ts +3 -1
  32. package/types/popover.d.cts +14 -0
  33. package/types/popover.d.ts +15 -0
  34. package/dist/js/focus-trap.cjs +0 -104
  35. package/dist/js/focus-trap.js +0 -103
  36. package/src/js/focus-trap.ts +0 -133
  37. /package/types/{focus-trap.d.cts → focus-trap/standalone.d.cts} +0 -0
  38. /package/types/{focus-trap.d.ts → focus-trap/standalone.d.ts} +0 -0
@@ -1,144 +1,120 @@
1
1
  import { isNullableOrWhitespace } from "@oscarpalmer/atoms/is";
2
2
  import { on } from "@oscarpalmer/toretto/event";
3
3
  import { attributable } from "./attributable.js";
4
+ import { deactivate, Floatable, activate } from "./floatable.js";
4
5
  class Tooltip {
5
- constructor(parent) {
6
- this.parent = parent;
7
- this.element = getContent(parent);
8
- if (this.element != null) {
9
- parent.insertAdjacentElement("afterend", this.element);
10
- initialize(this);
11
- }
12
- }
13
6
  abortController = new AbortController();
14
- activate;
15
- active = false;
16
- deactivate;
17
- element;
18
- frame;
7
+ attributes;
8
+ floatable;
9
+ constructor(anchor, content) {
10
+ this.attributes = content.attributes;
11
+ this.floatable = new Floatable(anchor, content.element);
12
+ initialize(this);
13
+ }
19
14
  destroy() {
20
- deactivate(this);
15
+ deactivate(this.floatable, active);
16
+ reset(this.floatable.anchor, this.attributes);
21
17
  this.abortController.abort();
18
+ this.floatable.content.remove();
19
+ this.floatable.destroy();
22
20
  this.abortController = void 0;
23
- this.activate = void 0;
24
- this.deactivate = void 0;
25
- this.element = void 0;
26
- this.frame = void 0;
27
- this.parent = void 0;
21
+ this.attributes = void 0;
22
+ this.floatable = void 0;
28
23
  }
29
24
  }
30
- function activate(tooltip) {
31
- stop(tooltip);
32
- if (tooltip.active) {
25
+ function addTooltip(anchor) {
26
+ if (tooltips.has(anchor)) {
33
27
  return;
34
28
  }
35
- tooltip.activate = +setTimeout(() => {
36
- tooltip.active = true;
37
- active.add(tooltip);
38
- document.body.append(tooltip.element);
39
- tooltip.element.hidden = false;
40
- update(tooltip);
41
- }, 250);
42
- }
43
- function addTooltip(element) {
44
- if (!tooltips.has(element)) {
45
- tooltips.set(element, new Tooltip(element));
29
+ const content = getContent(anchor);
30
+ if (content != null) {
31
+ tooltips.set(anchor, new Tooltip(anchor, content));
46
32
  }
47
33
  }
48
- function deactivate(tooltip, time) {
49
- stop(tooltip);
50
- if (!tooltip.active) {
51
- return;
34
+ function getContent(anchor) {
35
+ const attributes = {
36
+ ariaDescribedby: anchor.getAttribute("aria-describedby"),
37
+ ariaLabelledBy: anchor.getAttribute("aria-labelledby")
38
+ };
39
+ const wrapper = getWrapper(anchor);
40
+ const labelledElements = getElements(attributes.ariaLabelledBy);
41
+ if (labelledElements.length > 0) {
42
+ anchor.setAttribute("aria-labelledby", wrapper.id);
43
+ for (const element of labelledElements) {
44
+ wrapper.append(element);
45
+ }
46
+ return {
47
+ attributes,
48
+ element: wrapper
49
+ };
52
50
  }
53
- tooltip.deactivate = +setTimeout(() => {
54
- tooltip.active = false;
55
- active.delete(tooltip);
56
- tooltip.element.hidden = true;
57
- tooltip.parent.insertAdjacentElement("afterend", tooltip.element);
58
- }, time ?? 250);
59
- }
60
- function getContent(parent) {
61
- const wrapper = getWrapper(parent);
62
- const ids = (parent.getAttribute("aria-describedby") ?? "").split(/\s+/);
63
- parent.setAttribute("aria-describedby", wrapper.id);
64
- const elements = ids.map(
65
- (id) => isNullableOrWhitespace(id) ? void 0 : document.querySelector(`#${id}`)
66
- ).filter((element) => element instanceof HTMLElement);
67
- if (elements.length > 0) {
68
- for (const element of elements) {
69
- element.hidden = false;
51
+ const describedElements = getElements(attributes.ariaDescribedby);
52
+ if (describedElements.length > 0) {
53
+ anchor.setAttribute("aria-describedby", wrapper.id);
54
+ for (const element of describedElements) {
70
55
  wrapper.append(element);
71
56
  }
72
- return wrapper;
57
+ return {
58
+ attributes,
59
+ element: wrapper
60
+ };
73
61
  }
74
- let content = parent.getAttribute("aria-description");
62
+ let content = anchor.getAttribute("aria-label");
75
63
  if (isNullableOrWhitespace(content)) {
76
- content = parent.getAttribute("aria-label");
77
- if (isNullableOrWhitespace(content)) {
78
- return;
79
- }
64
+ content = anchor.getAttribute("aria-description");
65
+ }
66
+ if (isNullableOrWhitespace(content)) {
67
+ return;
80
68
  }
81
69
  const paragraph = document.createElement("p");
82
70
  paragraph.textContent = content;
83
71
  wrapper.append(paragraph);
84
- return wrapper;
72
+ return {
73
+ attributes,
74
+ element: wrapper
75
+ };
85
76
  }
86
- function getTop(parent, content) {
87
- let top = parent.top - content.height - margin;
88
- if (top - margin >= 0) {
89
- return top;
90
- }
91
- top = parent.bottom + margin;
92
- if (top + content.height + margin <= window.innerHeight) {
93
- return top;
77
+ function getElements(attribute) {
78
+ const ids = attribute?.split(/\s+/) ?? [];
79
+ const elements = [];
80
+ for (const id of ids) {
81
+ if (isNullableOrWhitespace(id)) {
82
+ continue;
83
+ }
84
+ const element = document.querySelector(`#${id}`);
85
+ if (element instanceof HTMLElement) {
86
+ const cloned = element.cloneNode(true);
87
+ cloned.hidden = false;
88
+ elements.push(cloned);
89
+ }
94
90
  }
95
- return parent.top + (parent.height - content.height) / 2;
91
+ return elements;
96
92
  }
97
- function getWrapper(parent) {
93
+ function getWrapper(anchor) {
98
94
  const wrapper = document.createElement("div");
99
95
  wrapper.setAttribute(`${selector}-content`, "");
100
- wrapper.className = parent.getAttribute(`${selector}-class`) ?? "";
96
+ wrapper.className = anchor.getAttribute(`${selector}-class`) ?? "";
101
97
  wrapper.hidden = true;
102
98
  wrapper.id = `${selector}-${++index}`;
103
99
  wrapper.role = "tooltip";
104
100
  wrapper.tabIndex = -1;
105
101
  return wrapper;
106
102
  }
107
- function getX(parent, content) {
108
- if (content.width + 2 * margin >= window.innerWidth) {
109
- return {
110
- left: margin,
111
- right: margin
112
- };
113
- }
114
- const left = parent.left + (parent.width - content.width) / 2;
115
- const right = window.innerWidth - margin;
116
- if (left < margin) {
117
- return {
118
- left: margin
119
- };
120
- }
121
- if (left + content.width > right) {
122
- return {
123
- right: margin
124
- };
125
- }
126
- return {
127
- left
128
- };
129
- }
130
103
  function initialize(tooltip) {
131
- const elements = [tooltip.parent, tooltip.element];
104
+ const { abortController, floatable } = tooltip;
105
+ const { anchor, content } = floatable;
106
+ anchor.insertAdjacentElement("afterend", content);
107
+ const elements = [anchor, content];
132
108
  for (const element of elements) {
133
109
  for (const event of activateEvents) {
134
110
  on(
135
111
  element,
136
112
  event,
137
113
  () => {
138
- activate(tooltip);
114
+ activate(floatable, active, void 0, 250);
139
115
  },
140
116
  {
141
- signal: tooltip.abortController.signal
117
+ signal: abortController.signal
142
118
  }
143
119
  );
144
120
  }
@@ -147,10 +123,10 @@ function initialize(tooltip) {
147
123
  element,
148
124
  event,
149
125
  () => {
150
- deactivate(tooltip);
126
+ deactivate(floatable, active, void 0, 250);
151
127
  },
152
128
  {
153
- signal: tooltip.abortController.signal
129
+ signal: abortController.signal
154
130
  }
155
131
  );
156
132
  }
@@ -160,43 +136,24 @@ function removeTooltip(element) {
160
136
  tooltips.get(element)?.destroy();
161
137
  tooltips.delete(element);
162
138
  }
163
- function stop(tooltip) {
164
- if (tooltip.frame != null) {
165
- cancelAnimationFrame(tooltip.frame);
166
- tooltip.frame = void 0;
167
- }
168
- if (tooltip.activate != null) {
169
- clearTimeout(tooltip.activate);
170
- tooltip.activate = void 0;
171
- }
172
- if (tooltip.deactivate != null) {
173
- clearTimeout(tooltip.deactivate);
174
- tooltip.deactivate = void 0;
175
- }
176
- }
177
- function update(tooltip) {
178
- function run() {
179
- const parentRect = tooltip.parent.getBoundingClientRect();
180
- const contentRect = tooltip.element.getBoundingClientRect();
181
- const top = getTop(parentRect, contentRect);
182
- const { left, right } = getX(parentRect, contentRect);
183
- tooltip.element.style.inset = `${top}px ${right == null ? "auto" : `${right}px`} auto ${left == null ? "auto" : `${left}px`}`;
184
- tooltip.frame = requestAnimationFrame(run);
139
+ function reset(anchor, attributes) {
140
+ if (attributes.ariaDescribedby == null) {
141
+ anchor.removeAttribute("aria-describedby");
142
+ } else {
143
+ anchor.setAttribute("aria-describedby", attributes.ariaDescribedby);
185
144
  }
186
- tooltip.frame = requestAnimationFrame(run);
187
145
  }
188
146
  const active = /* @__PURE__ */ new Set();
189
147
  const activateEvents = ["focus", "mouseenter", "touchstart"];
190
148
  const deactivateEvents = ["blur", "mouseleave"];
191
- const margin = Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) / 4;
192
149
  const selector = "oui-tooltip";
193
150
  const tooltips = /* @__PURE__ */ new WeakMap();
194
151
  let index = 0;
195
152
  attributable(selector, addTooltip, removeTooltip);
196
153
  on(document, "keydown", (event) => {
197
154
  if (event.key === "Escape") {
198
- for (const tooltip of active) {
199
- deactivate(tooltip, 0);
155
+ for (const floatable of active) {
156
+ deactivate(floatable, active);
200
157
  }
201
158
  }
202
159
  });
package/package.json CHANGED
@@ -22,7 +22,7 @@
22
22
  "postcss-input-range": "^6",
23
23
  "postcss-scss": "^4",
24
24
  "sass": "^1.89",
25
- "stylelint": "^16.21",
25
+ "stylelint": "^16.22",
26
26
  "tslib": "^2.8",
27
27
  "typescript": "^5.8",
28
28
  "vite": "^7",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "./focus-trap": {
43
43
  "import": {
44
- "types": "./types/focus-trap.d.ts",
45
- "default": "./dist/js/focus-trap.js"
44
+ "types": "./types/focus-trap/standalone.d.ts",
45
+ "default": "./dist/js/focus-trap/standalone.js"
46
46
  },
47
47
  "require": {
48
- "types": "./types/focus-trap.d.cts",
49
- "default": "./dist/js/focus-trap.cjs"
48
+ "types": "./types/focus-trap/standalone.d.cts",
49
+ "default": "./dist/js/focus-trap/standalone.cjs"
50
50
  }
51
51
  },
52
52
  "./tooltip": {
@@ -85,7 +85,7 @@
85
85
  "build:postcss": "npx postcss ./src/css/*.scss -d ./postcss -u autoprefixer postcss-input-range --parser postcss-scss",
86
86
  "build:js": "npx vite build && npm run types",
87
87
  "clean": "rm -rf ./dist && rm -rf ./types && rm -f ./tsconfig.tsbuildinfo",
88
- "rollup": "npx rollup -c",
88
+ "rollup": "npx rollup -c --watch",
89
89
  "test": "npx vitest --coverage",
90
90
  "types": "npx tsc && npx dts-bundle-generator --config ./dts.config.cts --silent",
91
91
  "watch:css": "npx sass ./postcss:./dist/css --no-source-map --watch",
@@ -94,5 +94,5 @@
94
94
  },
95
95
  "type": "module",
96
96
  "types": "types/index.d.cts",
97
- "version": "0.13.0"
97
+ "version": "0.15.0"
98
98
  }
@@ -0,0 +1,9 @@
1
+ @use 'mixins.scss' as mixins;
2
+
3
+ :where([oui-popover-content]) {
4
+ @include mixins.shadowOverlay;
5
+
6
+ padding: 1rem;
7
+ background-color: var(--oui-absolute);
8
+ border-radius: 0.5rem;
9
+ }
@@ -1,5 +1,12 @@
1
1
  @use 'mixins.scss' as mixins;
2
2
 
3
+ :where([oui-tooltip]:is(abbr, b, cite, dfn, code, em, i, kbd, mark, q, samp, small, span, strong, sub, sup, time, var)) {
4
+ text-decoration: underline dotted;
5
+ text-decoration-skip-ink: auto;
6
+ text-underline-offset: 0.25em;
7
+ cursor: help;
8
+ }
9
+
3
10
  :where([oui-tooltip-content]) {
4
11
  @include mixins.shadowOverlay;
5
12
 
@@ -8,7 +15,4 @@
8
15
  background-color: var(--oui-absolute);
9
16
  border-radius: 0.25rem;
10
17
  font-size: 0.875rem;
11
- position: fixed;
12
- inset: 0 auto auto 0;
13
- z-index: 10;
14
18
  }
@@ -0,0 +1,196 @@
1
+ export class Floatable {
2
+ activate: number | undefined;
3
+ active = false;
4
+ deactivate: number | undefined;
5
+ frame: DOMHighResTimeStamp | undefined;
6
+
7
+ constructor(
8
+ public anchor: HTMLElement,
9
+ public content: HTMLElement,
10
+ ) {
11
+ setStyles(content);
12
+ }
13
+
14
+ destroy(): void {
15
+ this.activate = undefined;
16
+ this.deactivate = undefined;
17
+ this.frame = undefined;
18
+
19
+ this.anchor = undefined as never;
20
+ this.content = undefined as never;
21
+ }
22
+ }
23
+
24
+ export function activate(
25
+ floatable: Floatable,
26
+ active: Set<Floatable>,
27
+ order?: Floatable[],
28
+ time?: number,
29
+ ): void {
30
+ stop(floatable);
31
+
32
+ if (floatable.active) {
33
+ return;
34
+ }
35
+
36
+ if (time == null || time === 0) {
37
+ onActivate(floatable, active, order);
38
+ } else {
39
+ floatable.activate = +setTimeout(() => {
40
+ onActivate(floatable, active, order);
41
+ }, time);
42
+ }
43
+ }
44
+
45
+ export function deactivate(
46
+ floatable: Floatable,
47
+ active: Set<Floatable>,
48
+ order?: Floatable[],
49
+ time?: number,
50
+ ): void {
51
+ stop(floatable);
52
+
53
+ if (!floatable.active) {
54
+ return;
55
+ }
56
+
57
+ if (time == null || time === 0) {
58
+ onDeactivate(floatable, active, order);
59
+ } else {
60
+ floatable.deactivate = +setTimeout(() => {
61
+ onDeactivate(floatable, active, order);
62
+ }, time);
63
+ }
64
+ }
65
+
66
+ function getX(
67
+ anchor: DOMRect,
68
+ content: DOMRect,
69
+ ): {left?: number; right?: number} {
70
+ if (content.width + 2 * margin >= window.innerWidth) {
71
+ return {
72
+ left: margin,
73
+ right: margin,
74
+ };
75
+ }
76
+
77
+ const left = anchor.left + (anchor.width - content.width) / 2;
78
+ const right = window.innerWidth - margin;
79
+
80
+ if (left < margin) {
81
+ return {
82
+ left: margin,
83
+ };
84
+ }
85
+
86
+ if (left + content.width > right) {
87
+ return {
88
+ right: margin,
89
+ };
90
+ }
91
+
92
+ return {
93
+ left,
94
+ };
95
+ }
96
+
97
+ function getY(anchor: DOMRect, content: DOMRect): number {
98
+ let top = anchor.top - content.height - margin;
99
+
100
+ if (top - margin >= 0) {
101
+ return top;
102
+ }
103
+
104
+ top = anchor.bottom + margin;
105
+
106
+ if (top + content.height + margin <= window.innerHeight) {
107
+ return top;
108
+ }
109
+
110
+ return anchor.top + (anchor.height - content.height) / 2;
111
+ }
112
+
113
+ function onActivate(
114
+ floatable: Floatable,
115
+ active: Set<Floatable>,
116
+ order?: Floatable[],
117
+ ): void {
118
+ floatable.active = true;
119
+
120
+ active.add(floatable);
121
+
122
+ if (order != null) {
123
+ order.push(floatable);
124
+ }
125
+
126
+ document.body.append(floatable.content);
127
+
128
+ floatable.content.hidden = false;
129
+
130
+ update(floatable);
131
+ }
132
+
133
+ function onDeactivate(
134
+ floatable: Floatable,
135
+ active: Set<Floatable>,
136
+ order?: Floatable[],
137
+ ): void {
138
+ floatable.active = false;
139
+
140
+ active.delete(floatable);
141
+
142
+ const index = order?.indexOf(floatable) ?? -1;
143
+
144
+ if (index > -1) {
145
+ order?.splice(index, 1);
146
+ }
147
+
148
+ floatable.content.hidden = true;
149
+
150
+ floatable.anchor.insertAdjacentElement('afterend', floatable.content);
151
+ }
152
+
153
+ function update(floatable: Floatable): void {
154
+ function run(): void {
155
+ const anchor = floatable.anchor.getBoundingClientRect();
156
+ const content = floatable.content.getBoundingClientRect();
157
+
158
+ const {left, right} = getX(anchor, content);
159
+ const top = getY(anchor, content);
160
+
161
+ floatable.content.style.inset = `${top}px ${right == null ? 'auto' : `${right}px`} auto ${left == null ? 'auto' : `${left}px`}`;
162
+
163
+ floatable.frame = requestAnimationFrame(run);
164
+ }
165
+
166
+ floatable.frame = requestAnimationFrame(run);
167
+ }
168
+
169
+ function setStyles(content: HTMLElement): void {
170
+ content.style.position = 'fixed';
171
+ content.style.inset = '0 auto auto 0';
172
+ content.style.zIndex = '10';
173
+ }
174
+
175
+ function stop(floatable: Floatable): void {
176
+ if (floatable.frame != null) {
177
+ cancelAnimationFrame(floatable.frame);
178
+ }
179
+
180
+ if (floatable.activate != null) {
181
+ clearTimeout(floatable.activate);
182
+ }
183
+
184
+ if (floatable.deactivate != null) {
185
+ clearTimeout(floatable.deactivate);
186
+ }
187
+
188
+ floatable.activate = undefined;
189
+ floatable.deactivate = undefined;
190
+ floatable.frame = undefined;
191
+ }
192
+
193
+ //
194
+
195
+ const margin =
196
+ Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) / 4;