@oscarpalmer/oui 0.11.0 → 0.13.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.
@@ -0,0 +1,70 @@
1
+ export function attributable(
2
+ attribute: string,
3
+ add: (element: HTMLElement) => void,
4
+ remove: (element: HTMLElement) => void,
5
+ ): void {
6
+ const observer = new MutationObserver(entries => {
7
+ const {length} = entries;
8
+
9
+ for (let index = 0; index < length; index += 1) {
10
+ const entry = entries[index];
11
+
12
+ if (entry.type === 'attributes' && entry.target instanceof HTMLElement) {
13
+ if (entry.target.hasAttribute(attribute)) {
14
+ handleAdded(attribute, [entry.target], add);
15
+ } else {
16
+ handleRemoved([entry.target], remove);
17
+ }
18
+ } else if (entry.type === 'childList') {
19
+ handleAdded(attribute, entry.addedNodes, add);
20
+ handleRemoved(entry.removedNodes, remove);
21
+ }
22
+ }
23
+ });
24
+
25
+ observer.observe(document, {
26
+ attributeFilter: [attribute],
27
+ attributes: true,
28
+ childList: true,
29
+ subtree: true,
30
+ });
31
+
32
+ requestAnimationFrame(() => {
33
+ handleAdded(attribute, document.querySelectorAll(`[${attribute}]`), add);
34
+ });
35
+ }
36
+
37
+ function handleAdded(
38
+ attribute: string,
39
+ nodes: Node[] | NodeList,
40
+ add: (element: HTMLElement) => void,
41
+ ): void {
42
+ const elements = [...nodes].filter(
43
+ node => node instanceof HTMLElement,
44
+ ) as HTMLElement[];
45
+
46
+ const {length} = elements;
47
+
48
+ for (let index = 0; index < length; index += 1) {
49
+ const element = elements[index];
50
+
51
+ if (element.hasAttribute(attribute)) {
52
+ add(element);
53
+ }
54
+ }
55
+ }
56
+
57
+ function handleRemoved(
58
+ nodes: Node[] | NodeList,
59
+ remove: (element: HTMLElement) => void,
60
+ ): void {
61
+ const elements = [...nodes].filter(
62
+ node => node instanceof HTMLElement,
63
+ ) as HTMLElement[];
64
+
65
+ const {length} = elements;
66
+
67
+ for (let index = 0; index < length; index += 1) {
68
+ remove(elements[index]);
69
+ }
70
+ }
@@ -2,6 +2,7 @@ import {clamp} from '@oscarpalmer/atoms/number';
2
2
  import {on} from '@oscarpalmer/toretto/event';
3
3
  import {findAncestor} from '@oscarpalmer/toretto/find';
4
4
  import {getFocusable} from '@oscarpalmer/toretto/focusable';
5
+ import {attributable} from './attributable';
5
6
 
6
7
  class FocusTrap {
7
8
  active = true;
@@ -31,19 +32,9 @@ class FocusTrap {
31
32
  }
32
33
  }
33
34
 
34
- function addFocusTraps(nodes: Node[] | NodeList): void {
35
- const elements = [...nodes].filter(
36
- node => node instanceof HTMLElement,
37
- ) as HTMLElement[];
38
-
39
- const {length} = elements;
40
-
41
- for (let index = 0; index < length; index += 1) {
42
- const element = elements[index];
43
-
44
- if (element.hasAttribute(selector)) {
45
- focusTraps.set(element, new FocusTrap(element));
46
- }
35
+ function addFocusTrap(element: HTMLElement): void {
36
+ if (!focusTraps.has(element)) {
37
+ focusTraps.set(element, new FocusTrap(element));
47
38
  }
48
39
  }
49
40
 
@@ -117,65 +108,26 @@ function onTab(event: KeyboardEvent): void {
117
108
  }
118
109
  }
119
110
 
120
- function removeFocusTraps(nodes: Node[] | NodeList): void {
121
- const elements = [...nodes].filter(
122
- node => node instanceof HTMLElement,
123
- ) as HTMLElement[];
124
-
125
- const {length} = elements;
126
-
127
- for (let index = 0; index < length; index += 1) {
128
- const element = elements[index];
129
-
130
- focusTraps.get(element)?.destroy();
131
- focusTraps.delete(element);
132
- }
111
+ function removeFocusTraps(element: HTMLElement): void {
112
+ focusTraps.get(element)?.destroy();
113
+ focusTraps.delete(element);
133
114
  }
134
115
 
135
116
  const focusTraps = new WeakMap<HTMLElement, FocusTrap>();
136
117
  const inactive = new Set<FocusTrap>();
118
+ const selector = 'oui-focus-trap';
137
119
 
138
- const observer = new MutationObserver(entries => {
139
- const {length} = entries;
140
-
141
- for (let index = 0; index < length; index += 1) {
142
- const entry = entries[index];
120
+ attributable(selector, addFocusTrap, removeFocusTraps);
143
121
 
144
- if (entry.type === 'attributes' && entry.target instanceof HTMLElement) {
145
- if (entry.target.hasAttribute(selector)) {
146
- addFocusTraps([entry.target]);
147
- } else {
148
- removeFocusTraps([entry.target]);
149
- }
150
- } else if (entry.type === 'childList') {
151
- addFocusTraps(entry.addedNodes);
152
- removeFocusTraps(entry.removedNodes);
153
- }
154
- }
122
+ on(document, 'focusout', onFocusOut, {
123
+ capture: true,
155
124
  });
156
125
 
157
- const selector = 'oui-focus-trap';
158
-
159
- observer.observe(document, {
160
- attributeFilter: [selector],
161
- attributes: true,
162
- childList: true,
163
- subtree: true,
126
+ on(document, 'keydown', onTab, {
127
+ capture: true,
128
+ passive: false,
164
129
  });
165
130
 
166
- requestAnimationFrame(() => {
167
- addFocusTraps(document.querySelectorAll(`[${selector}]`));
168
-
169
- on(document, 'focusout', onFocusOut, {
170
- capture: true,
171
- });
172
-
173
- on(document, 'keydown', onTab, {
174
- capture: true,
175
- passive: false,
176
- });
177
-
178
- on(document, 'keydown', onEscape, {
179
- passive: false,
180
- });
131
+ on(document, 'keydown', onEscape, {
132
+ passive: false,
181
133
  });
@@ -0,0 +1,289 @@
1
+ import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
2
+ import {on} from '@oscarpalmer/toretto/event';
3
+ import {attributable} from './attributable';
4
+
5
+ class Tooltip {
6
+ abortController = new AbortController();
7
+
8
+ activate: number | undefined;
9
+
10
+ active = false;
11
+
12
+ deactivate: number | undefined;
13
+
14
+ element: HTMLElement;
15
+
16
+ frame: DOMHighResTimeStamp | undefined;
17
+
18
+ constructor(public parent: HTMLElement) {
19
+ this.element = getContent(parent) as HTMLElement;
20
+
21
+ if (this.element != null) {
22
+ parent.insertAdjacentElement('afterend', this.element);
23
+
24
+ initialize(this);
25
+ }
26
+ }
27
+
28
+ destroy() {
29
+ deactivate(this);
30
+
31
+ this.abortController.abort();
32
+
33
+ this.abortController = undefined as never;
34
+ this.activate = undefined as never;
35
+ this.deactivate = undefined as never;
36
+ this.element = undefined as never;
37
+ this.frame = undefined as never;
38
+ this.parent = undefined as never;
39
+ }
40
+ }
41
+
42
+ function activate(tooltip: Tooltip): void {
43
+ stop(tooltip);
44
+
45
+ if (tooltip.active) {
46
+ return;
47
+ }
48
+
49
+ tooltip.activate = +setTimeout(() => {
50
+ tooltip.active = true;
51
+
52
+ active.add(tooltip);
53
+
54
+ document.body.append(tooltip.element);
55
+
56
+ tooltip.element.hidden = false;
57
+
58
+ update(tooltip);
59
+ }, 250);
60
+ }
61
+
62
+ function addTooltip(element: HTMLElement): void {
63
+ if (!tooltips.has(element)) {
64
+ tooltips.set(element, new Tooltip(element));
65
+ }
66
+ }
67
+
68
+ function deactivate(tooltip: Tooltip, time?: number): void {
69
+ stop(tooltip);
70
+
71
+ if (!tooltip.active) {
72
+ return;
73
+ }
74
+
75
+ tooltip.deactivate = +setTimeout(() => {
76
+ tooltip.active = false;
77
+
78
+ active.delete(tooltip);
79
+
80
+ tooltip.element.hidden = true;
81
+
82
+ tooltip.parent.insertAdjacentElement('afterend', tooltip.element);
83
+ }, time ?? 250);
84
+ }
85
+
86
+ function getContent(parent: HTMLElement): HTMLElement | undefined {
87
+ const wrapper = getWrapper(parent);
88
+
89
+ const ids = (parent.getAttribute('aria-describedby') ?? '').split(/\s+/);
90
+
91
+ parent.setAttribute('aria-describedby', wrapper.id);
92
+
93
+ const elements = ids
94
+ .map(id =>
95
+ isNullableOrWhitespace(id) ? undefined : document.querySelector(`#${id}`),
96
+ )
97
+ .filter(element => element instanceof HTMLElement);
98
+
99
+ if (elements.length > 0) {
100
+ for (const element of elements) {
101
+ element.hidden = false;
102
+
103
+ wrapper.append(element);
104
+ }
105
+
106
+ return wrapper;
107
+ }
108
+
109
+ let content = parent.getAttribute('aria-description');
110
+
111
+ if (isNullableOrWhitespace(content)) {
112
+ content = parent.getAttribute('aria-label');
113
+
114
+ if (isNullableOrWhitespace(content)) {
115
+ return;
116
+ }
117
+ }
118
+
119
+ const paragraph = document.createElement('p');
120
+
121
+ paragraph.textContent = content;
122
+
123
+ wrapper.append(paragraph);
124
+
125
+ return wrapper;
126
+ }
127
+
128
+ function getTop(parent: DOMRect, content: DOMRect): number {
129
+ let top = parent.top - content.height - margin;
130
+
131
+ if (top - margin >= 0) {
132
+ return top;
133
+ }
134
+
135
+ top = parent.bottom + margin;
136
+
137
+ if (top + content.height + margin <= window.innerHeight) {
138
+ return top;
139
+ }
140
+
141
+ return parent.top + (parent.height - content.height) / 2;
142
+ }
143
+
144
+ function getWrapper(parent: HTMLElement): HTMLElement {
145
+ const wrapper = document.createElement('div');
146
+
147
+ wrapper.setAttribute(`${selector}-content`, '');
148
+
149
+ wrapper.className = parent.getAttribute(`${selector}-class`) ?? '';
150
+ wrapper.hidden = true;
151
+ wrapper.id = `${selector}-${++index}`;
152
+ wrapper.role = 'tooltip';
153
+ wrapper.tabIndex = -1;
154
+
155
+ return wrapper;
156
+ }
157
+
158
+ function getX(
159
+ parent: DOMRect,
160
+ content: DOMRect,
161
+ ): {left?: number; right?: number} {
162
+ if (content.width + 2 * margin >= window.innerWidth) {
163
+ return {
164
+ left: margin,
165
+ right: margin,
166
+ };
167
+ }
168
+
169
+ const left = parent.left + (parent.width - content.width) / 2;
170
+ const right = window.innerWidth - margin;
171
+
172
+ if (left < margin) {
173
+ return {
174
+ left: margin,
175
+ };
176
+ }
177
+
178
+ if (left + content.width > right) {
179
+ return {
180
+ right: margin,
181
+ };
182
+ }
183
+
184
+ return {
185
+ left,
186
+ };
187
+ }
188
+
189
+ function initialize(tooltip: Tooltip): void {
190
+ const elements = [tooltip.parent, tooltip.element];
191
+
192
+ for (const element of elements) {
193
+ for (const event of activateEvents) {
194
+ on(
195
+ element,
196
+ event,
197
+ () => {
198
+ activate(tooltip);
199
+ },
200
+ {
201
+ signal: tooltip.abortController.signal,
202
+ },
203
+ );
204
+ }
205
+
206
+ for (const event of deactivateEvents) {
207
+ on(
208
+ element,
209
+ event,
210
+ () => {
211
+ deactivate(tooltip);
212
+ },
213
+ {
214
+ signal: tooltip.abortController.signal,
215
+ },
216
+ );
217
+ }
218
+ }
219
+ }
220
+
221
+ function removeTooltip(element: HTMLElement): void {
222
+ tooltips.get(element)?.destroy();
223
+ tooltips.delete(element);
224
+ }
225
+
226
+ function stop(tooltip: Tooltip): void {
227
+ if (tooltip.frame != null) {
228
+ cancelAnimationFrame(tooltip.frame);
229
+
230
+ tooltip.frame = undefined;
231
+ }
232
+
233
+ if (tooltip.activate != null) {
234
+ clearTimeout(tooltip.activate);
235
+
236
+ tooltip.activate = undefined;
237
+ }
238
+
239
+ if (tooltip.deactivate != null) {
240
+ clearTimeout(tooltip.deactivate);
241
+
242
+ tooltip.deactivate = undefined;
243
+ }
244
+ }
245
+
246
+ function update(tooltip: Tooltip): void {
247
+ function run(): void {
248
+ const parentRect = tooltip.parent.getBoundingClientRect();
249
+ const contentRect = tooltip.element.getBoundingClientRect();
250
+
251
+ const top = getTop(parentRect, contentRect);
252
+ const {left, right} = getX(parentRect, contentRect);
253
+
254
+ tooltip.element.style.inset = `${top}px ${right == null ? 'auto' : `${right}px`} auto ${left == null ? 'auto' : `${left}px`}`;
255
+
256
+ tooltip.frame = requestAnimationFrame(run);
257
+ }
258
+
259
+ tooltip.frame = requestAnimationFrame(run);
260
+ }
261
+
262
+ //
263
+
264
+ const active = new Set<Tooltip>();
265
+
266
+ const activateEvents = ['focus', 'mouseenter', 'touchstart'];
267
+
268
+ const deactivateEvents = ['blur', 'mouseleave'];
269
+
270
+ const margin =
271
+ Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) / 4;
272
+
273
+ const selector = 'oui-tooltip';
274
+
275
+ const tooltips = new WeakMap<HTMLElement, Tooltip>();
276
+
277
+ let index = 0;
278
+
279
+ //
280
+
281
+ attributable(selector, addTooltip, removeTooltip);
282
+
283
+ on(document, 'keydown', event => {
284
+ if (event.key === 'Escape') {
285
+ for (const tooltip of active) {
286
+ deactivate(tooltip, 0);
287
+ }
288
+ }
289
+ });
@@ -0,0 +1,5 @@
1
+ // Generated by dts-bundle-generator v9.5.1
2
+
3
+ export declare function attributable(attribute: string, add: (element: HTMLElement) => void, remove: (element: HTMLElement) => void): void;
4
+
5
+ export {};
@@ -0,0 +1 @@
1
+ export declare function attributable(attribute: string, add: (element: HTMLElement) => void, remove: (element: HTMLElement) => void): void;
@@ -0,0 +1,3 @@
1
+ // Generated by dts-bundle-generator v9.5.1
2
+
3
+ export {};
@@ -0,0 +1 @@
1
+ export {};