@oscarpalmer/oui 0.13.0 → 0.14.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.
package/src/js/tooltip.ts CHANGED
@@ -1,119 +1,102 @@
1
1
  import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
2
2
  import {on} from '@oscarpalmer/toretto/event';
3
3
  import {attributable} from './attributable';
4
+ import {activate, deactivate, Floatable} from './floatable';
4
5
 
5
- class Tooltip {
6
- abortController = new AbortController();
7
-
8
- activate: number | undefined;
9
-
10
- active = false;
11
-
12
- deactivate: number | undefined;
6
+ type Attributes = {
7
+ ariaDescribedby: string | null;
8
+ ariaLabelledBy: string | null;
9
+ };
13
10
 
11
+ type Content = {
12
+ attributes: Attributes;
14
13
  element: HTMLElement;
14
+ };
15
15
 
16
- frame: DOMHighResTimeStamp | undefined;
17
-
18
- constructor(public parent: HTMLElement) {
19
- this.element = getContent(parent) as HTMLElement;
16
+ class Tooltip {
17
+ abortController = new AbortController();
18
+ attributes: Attributes;
19
+ floatable: Floatable;
20
20
 
21
- if (this.element != null) {
22
- parent.insertAdjacentElement('afterend', this.element);
21
+ constructor(anchor: HTMLElement, content: Content) {
22
+ this.attributes = content.attributes;
23
+ this.floatable = new Floatable(anchor, content.element);
23
24
 
24
- initialize(this);
25
- }
25
+ initialize(this);
26
26
  }
27
27
 
28
- destroy() {
29
- deactivate(this);
28
+ destroy(): void {
29
+ deactivate(this.floatable, active);
30
+ reset(this.floatable.anchor, this.attributes);
30
31
 
31
32
  this.abortController.abort();
33
+ this.floatable.content.remove();
34
+ this.floatable.destroy();
32
35
 
33
36
  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;
37
+ this.attributes = undefined as never;
38
+ this.floatable = undefined as never;
39
39
  }
40
40
  }
41
41
 
42
- function activate(tooltip: Tooltip): void {
43
- stop(tooltip);
44
-
45
- if (tooltip.active) {
42
+ function addTooltip(anchor: HTMLElement): void {
43
+ if (tooltips.has(anchor)) {
46
44
  return;
47
45
  }
48
46
 
49
- tooltip.activate = +setTimeout(() => {
50
- tooltip.active = true;
51
-
52
- active.add(tooltip);
53
-
54
- document.body.append(tooltip.element);
47
+ const content = getContent(anchor);
55
48
 
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));
49
+ if (content != null) {
50
+ tooltips.set(anchor, new Tooltip(anchor, content));
65
51
  }
66
52
  }
67
53
 
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;
54
+ function getContent(anchor: HTMLElement): Content | undefined {
55
+ const attributes = {
56
+ ariaDescribedby: anchor.getAttribute('aria-describedby'),
57
+ ariaLabelledBy: anchor.getAttribute('aria-labelledby'),
58
+ };
77
59
 
78
- active.delete(tooltip);
60
+ const wrapper = getWrapper(anchor);
79
61
 
80
- tooltip.element.hidden = true;
62
+ const labelledElements = getElements(attributes.ariaLabelledBy);
81
63
 
82
- tooltip.parent.insertAdjacentElement('afterend', tooltip.element);
83
- }, time ?? 250);
84
- }
64
+ if (labelledElements.length > 0) {
65
+ anchor.setAttribute('aria-labelledby', wrapper.id);
85
66
 
86
- function getContent(parent: HTMLElement): HTMLElement | undefined {
87
- const wrapper = getWrapper(parent);
88
-
89
- const ids = (parent.getAttribute('aria-describedby') ?? '').split(/\s+/);
67
+ for (const element of labelledElements) {
68
+ wrapper.append(element);
69
+ }
90
70
 
91
- parent.setAttribute('aria-describedby', wrapper.id);
71
+ return {
72
+ attributes,
73
+ element: wrapper,
74
+ };
75
+ }
92
76
 
93
- const elements = ids
94
- .map(id =>
95
- isNullableOrWhitespace(id) ? undefined : document.querySelector(`#${id}`),
96
- )
97
- .filter(element => element instanceof HTMLElement);
77
+ const describedElements = getElements(attributes.ariaDescribedby);
98
78
 
99
- if (elements.length > 0) {
100
- for (const element of elements) {
101
- element.hidden = false;
79
+ if (describedElements.length > 0) {
80
+ anchor.setAttribute('aria-describedby', wrapper.id);
102
81
 
82
+ for (const element of describedElements) {
103
83
  wrapper.append(element);
104
84
  }
105
85
 
106
- return wrapper;
86
+ return {
87
+ attributes,
88
+ element: wrapper,
89
+ };
107
90
  }
108
91
 
109
- let content = parent.getAttribute('aria-description');
92
+ let content = anchor.getAttribute('aria-label');
110
93
 
111
94
  if (isNullableOrWhitespace(content)) {
112
- content = parent.getAttribute('aria-label');
95
+ content = anchor.getAttribute('aria-description');
96
+ }
113
97
 
114
- if (isNullableOrWhitespace(content)) {
115
- return;
116
- }
98
+ if (isNullableOrWhitespace(content)) {
99
+ return;
117
100
  }
118
101
 
119
102
  const paragraph = document.createElement('p');
@@ -122,31 +105,41 @@ function getContent(parent: HTMLElement): HTMLElement | undefined {
122
105
 
123
106
  wrapper.append(paragraph);
124
107
 
125
- return wrapper;
108
+ return {
109
+ attributes,
110
+ element: wrapper,
111
+ };
126
112
  }
127
113
 
128
- function getTop(parent: DOMRect, content: DOMRect): number {
129
- let top = parent.top - content.height - margin;
114
+ function getElements(attribute: string | null): HTMLElement[] {
115
+ const ids = attribute?.split(/\s+/) ?? [];
116
+ const elements: HTMLElement[] = [];
130
117
 
131
- if (top - margin >= 0) {
132
- return top;
133
- }
118
+ for (const id of ids) {
119
+ if (isNullableOrWhitespace(id)) {
120
+ continue;
121
+ }
122
+
123
+ const element = document.querySelector(`#${id}`);
134
124
 
135
- top = parent.bottom + margin;
125
+ if (element instanceof HTMLElement) {
126
+ const cloned = element.cloneNode(true) as HTMLElement;
136
127
 
137
- if (top + content.height + margin <= window.innerHeight) {
138
- return top;
128
+ cloned.hidden = false;
129
+
130
+ elements.push(cloned);
131
+ }
139
132
  }
140
133
 
141
- return parent.top + (parent.height - content.height) / 2;
134
+ return elements;
142
135
  }
143
136
 
144
- function getWrapper(parent: HTMLElement): HTMLElement {
137
+ function getWrapper(anchor: HTMLElement): HTMLElement {
145
138
  const wrapper = document.createElement('div');
146
139
 
147
140
  wrapper.setAttribute(`${selector}-content`, '');
148
141
 
149
- wrapper.className = parent.getAttribute(`${selector}-class`) ?? '';
142
+ wrapper.className = anchor.getAttribute(`${selector}-class`) ?? '';
150
143
  wrapper.hidden = true;
151
144
  wrapper.id = `${selector}-${++index}`;
152
145
  wrapper.role = 'tooltip';
@@ -155,39 +148,13 @@ function getWrapper(parent: HTMLElement): HTMLElement {
155
148
  return wrapper;
156
149
  }
157
150
 
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
- }
151
+ function initialize(tooltip: Tooltip): void {
152
+ const {abortController, floatable} = tooltip;
153
+ const {anchor, content} = floatable;
183
154
 
184
- return {
185
- left,
186
- };
187
- }
155
+ anchor.insertAdjacentElement('afterend', content);
188
156
 
189
- function initialize(tooltip: Tooltip): void {
190
- const elements = [tooltip.parent, tooltip.element];
157
+ const elements = [anchor, content];
191
158
 
192
159
  for (const element of elements) {
193
160
  for (const event of activateEvents) {
@@ -195,10 +162,10 @@ function initialize(tooltip: Tooltip): void {
195
162
  element,
196
163
  event,
197
164
  () => {
198
- activate(tooltip);
165
+ activate(floatable, active, 250);
199
166
  },
200
167
  {
201
- signal: tooltip.abortController.signal,
168
+ signal: abortController.signal,
202
169
  },
203
170
  );
204
171
  }
@@ -208,10 +175,10 @@ function initialize(tooltip: Tooltip): void {
208
175
  element,
209
176
  event,
210
177
  () => {
211
- deactivate(tooltip);
178
+ deactivate(floatable, active, 250);
212
179
  },
213
180
  {
214
- signal: tooltip.abortController.signal,
181
+ signal: abortController.signal,
215
182
  },
216
183
  );
217
184
  }
@@ -223,53 +190,24 @@ function removeTooltip(element: HTMLElement): void {
223
190
  tooltips.delete(element);
224
191
  }
225
192
 
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();
193
+ function reset(anchor: HTMLElement, attributes: Attributes): void {
194
+ console.log('reset', anchor, attributes);
250
195
 
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);
196
+ if (attributes.ariaDescribedby == null) {
197
+ anchor.removeAttribute('aria-describedby');
198
+ } else {
199
+ anchor.setAttribute('aria-describedby', attributes.ariaDescribedby);
257
200
  }
258
-
259
- tooltip.frame = requestAnimationFrame(run);
260
201
  }
261
202
 
262
203
  //
263
204
 
264
- const active = new Set<Tooltip>();
205
+ const active = new Set<Floatable>();
265
206
 
266
207
  const activateEvents = ['focus', 'mouseenter', 'touchstart'];
267
208
 
268
209
  const deactivateEvents = ['blur', 'mouseleave'];
269
210
 
270
- const margin =
271
- Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) / 4;
272
-
273
211
  const selector = 'oui-tooltip';
274
212
 
275
213
  const tooltips = new WeakMap<HTMLElement, Tooltip>();
@@ -282,8 +220,8 @@ attributable(selector, addTooltip, removeTooltip);
282
220
 
283
221
  on(document, 'keydown', event => {
284
222
  if (event.key === 'Escape') {
285
- for (const tooltip of active) {
286
- deactivate(tooltip, 0);
223
+ for (const floatable of active) {
224
+ deactivate(floatable, active);
287
225
  }
288
226
  }
289
227
  });
@@ -0,0 +1,16 @@
1
+ // Generated by dts-bundle-generator v9.5.1
2
+
3
+ export declare class Floatable {
4
+ anchor: HTMLElement;
5
+ content: HTMLElement;
6
+ activate: number | undefined;
7
+ active: boolean;
8
+ deactivate: number | undefined;
9
+ frame: DOMHighResTimeStamp | undefined;
10
+ constructor(anchor: HTMLElement, content: HTMLElement);
11
+ destroy(): void;
12
+ }
13
+ export declare function activate(floatable: Floatable, active: Set<Floatable>, time?: number): void;
14
+ export declare function deactivate(floatable: Floatable, active: Set<Floatable>, time?: number): void;
15
+
16
+ export {};
@@ -0,0 +1,12 @@
1
+ export declare class Floatable {
2
+ anchor: HTMLElement;
3
+ content: HTMLElement;
4
+ activate: number | undefined;
5
+ active: boolean;
6
+ deactivate: number | undefined;
7
+ frame: DOMHighResTimeStamp | undefined;
8
+ constructor(anchor: HTMLElement, content: HTMLElement);
9
+ destroy(): void;
10
+ }
11
+ export declare function activate(floatable: Floatable, active: Set<Floatable>, time?: number): void;
12
+ export declare function deactivate(floatable: Floatable, active: Set<Floatable>, time?: number): void;