@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/dist/css/tooltip.css +9 -0
- package/dist/css/value.css +1 -0
- package/dist/js/floatable.cjs +121 -0
- package/dist/js/floatable.js +121 -0
- package/dist/js/tooltip.cjs +84 -126
- package/dist/js/tooltip.js +84 -126
- package/package.json +3 -3
- package/src/css/tooltip.scss +7 -0
- package/src/js/floatable.ts +168 -0
- package/src/js/tooltip.ts +98 -160
- package/types/floatable.d.cts +16 -0
- package/types/floatable.d.ts +12 -0
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
class Tooltip {
|
|
17
|
+
abortController = new AbortController();
|
|
18
|
+
attributes: Attributes;
|
|
19
|
+
floatable: Floatable;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
constructor(anchor: HTMLElement, content: Content) {
|
|
22
|
+
this.attributes = content.attributes;
|
|
23
|
+
this.floatable = new Floatable(anchor, content.element);
|
|
23
24
|
|
|
24
|
-
|
|
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.
|
|
35
|
-
this.
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (tooltip.active) {
|
|
42
|
+
function addTooltip(anchor: HTMLElement): void {
|
|
43
|
+
if (tooltips.has(anchor)) {
|
|
46
44
|
return;
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
tooltip.active = true;
|
|
51
|
-
|
|
52
|
-
active.add(tooltip);
|
|
53
|
-
|
|
54
|
-
document.body.append(tooltip.element);
|
|
47
|
+
const content = getContent(anchor);
|
|
55
48
|
|
|
56
|
-
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
60
|
+
const wrapper = getWrapper(anchor);
|
|
79
61
|
|
|
80
|
-
|
|
62
|
+
const labelledElements = getElements(attributes.ariaLabelledBy);
|
|
81
63
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
64
|
+
if (labelledElements.length > 0) {
|
|
65
|
+
anchor.setAttribute('aria-labelledby', wrapper.id);
|
|
85
66
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const ids = (parent.getAttribute('aria-describedby') ?? '').split(/\s+/);
|
|
67
|
+
for (const element of labelledElements) {
|
|
68
|
+
wrapper.append(element);
|
|
69
|
+
}
|
|
90
70
|
|
|
91
|
-
|
|
71
|
+
return {
|
|
72
|
+
attributes,
|
|
73
|
+
element: wrapper,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
92
76
|
|
|
93
|
-
const
|
|
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 (
|
|
100
|
-
|
|
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
|
|
86
|
+
return {
|
|
87
|
+
attributes,
|
|
88
|
+
element: wrapper,
|
|
89
|
+
};
|
|
107
90
|
}
|
|
108
91
|
|
|
109
|
-
let content =
|
|
92
|
+
let content = anchor.getAttribute('aria-label');
|
|
110
93
|
|
|
111
94
|
if (isNullableOrWhitespace(content)) {
|
|
112
|
-
content =
|
|
95
|
+
content = anchor.getAttribute('aria-description');
|
|
96
|
+
}
|
|
113
97
|
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
108
|
+
return {
|
|
109
|
+
attributes,
|
|
110
|
+
element: wrapper,
|
|
111
|
+
};
|
|
126
112
|
}
|
|
127
113
|
|
|
128
|
-
function
|
|
129
|
-
|
|
114
|
+
function getElements(attribute: string | null): HTMLElement[] {
|
|
115
|
+
const ids = attribute?.split(/\s+/) ?? [];
|
|
116
|
+
const elements: HTMLElement[] = [];
|
|
130
117
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
118
|
+
for (const id of ids) {
|
|
119
|
+
if (isNullableOrWhitespace(id)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const element = document.querySelector(`#${id}`);
|
|
134
124
|
|
|
135
|
-
|
|
125
|
+
if (element instanceof HTMLElement) {
|
|
126
|
+
const cloned = element.cloneNode(true) as HTMLElement;
|
|
136
127
|
|
|
137
|
-
|
|
138
|
-
|
|
128
|
+
cloned.hidden = false;
|
|
129
|
+
|
|
130
|
+
elements.push(cloned);
|
|
131
|
+
}
|
|
139
132
|
}
|
|
140
133
|
|
|
141
|
-
return
|
|
134
|
+
return elements;
|
|
142
135
|
}
|
|
143
136
|
|
|
144
|
-
function getWrapper(
|
|
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 =
|
|
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
|
|
159
|
-
|
|
160
|
-
content
|
|
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
|
-
|
|
185
|
-
left,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
155
|
+
anchor.insertAdjacentElement('afterend', content);
|
|
188
156
|
|
|
189
|
-
|
|
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(
|
|
165
|
+
activate(floatable, active, 250);
|
|
199
166
|
},
|
|
200
167
|
{
|
|
201
|
-
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(
|
|
178
|
+
deactivate(floatable, active, 250);
|
|
212
179
|
},
|
|
213
180
|
{
|
|
214
|
-
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
|
|
227
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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<
|
|
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
|
|
286
|
-
deactivate(
|
|
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;
|