@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.
- package/dist/css/popover.css +15 -19
- package/dist/css/reset.css +16 -4
- package/dist/css/tooltip.css +23 -19
- package/dist/js/focus-trap/embedded.d.mts +2 -2
- package/dist/js/focus-trap/embedded.mjs +5 -1
- package/dist/js/focus-trap/standalone.mjs +2 -6
- package/dist/js/index.d.mts +1 -2
- package/dist/js/index.mjs +2 -2
- package/dist/js/internal/floatable.d.mts +12 -20
- package/dist/js/internal/floatable.mjs +32 -153
- package/dist/js/popover.d.mts +1 -20
- package/dist/js/popover.mjs +53 -82
- package/dist/js/tooltip.d.mts +1 -1
- package/dist/js/tooltip.mjs +101 -126
- package/package.json +6 -6
- package/src/css/mixins.scss +14 -12
- package/src/css/popover.scss +1 -9
- package/src/css/reset.scss +16 -4
- package/src/css/tooltip.scss +9 -7
- package/src/js/focus-trap/embedded.ts +6 -1
- package/src/js/focus-trap/standalone.ts +2 -7
- package/src/js/index.ts +2 -1
- package/src/js/internal/floatable.ts +44 -303
- package/src/js/popover.ts +69 -131
- package/src/js/tooltip.ts +135 -180
package/src/js/tooltip.ts
CHANGED
|
@@ -1,128 +1,98 @@
|
|
|
1
1
|
import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
|
|
2
|
+
import {createElement} from '@oscarpalmer/toretto/create';
|
|
2
3
|
import {on} from '@oscarpalmer/toretto/event';
|
|
3
4
|
import {findAncestor} from '@oscarpalmer/toretto/find';
|
|
4
5
|
import {attributable} from './internal/attributable';
|
|
5
|
-
import {Floatable} from './internal/floatable';
|
|
6
|
+
import {Floatable, type FloatableOptions} from './internal/floatable';
|
|
6
7
|
|
|
7
8
|
class Tooltip {
|
|
8
|
-
elements: HTMLElement[];
|
|
9
|
-
|
|
10
9
|
floatable: Floatable;
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
const content = getContent();
|
|
14
|
-
|
|
15
|
-
this.elements = elements;
|
|
16
|
-
|
|
17
|
-
this.floatable = new Floatable({
|
|
18
|
-
anchor,
|
|
19
|
-
content,
|
|
20
|
-
defaultPosition: 'above',
|
|
21
|
-
interactive: false,
|
|
22
|
-
positionAttribute: ATTRIBUTE_POSITION,
|
|
23
|
-
reusable: true,
|
|
24
|
-
onAfter: (active: boolean): void => {
|
|
25
|
-
if (active) {
|
|
26
|
-
content.innerHTML = '';
|
|
27
|
-
|
|
28
|
-
content.append(...this.elements);
|
|
29
|
-
}
|
|
11
|
+
timer: number | undefined;
|
|
30
12
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
13
|
+
constructor(
|
|
14
|
+
public anchor: HTMLElement,
|
|
15
|
+
public content: HTMLElement,
|
|
16
|
+
) {
|
|
17
|
+
instances.set(content, this);
|
|
34
18
|
|
|
35
|
-
|
|
19
|
+
anchor.insertAdjacentElement('afterend', content);
|
|
36
20
|
|
|
37
|
-
|
|
38
|
-
content.innerHTML = '';
|
|
21
|
+
this.floatable = new Floatable(anchor, content, options);
|
|
39
22
|
|
|
40
|
-
|
|
41
|
-
}, delayClose);
|
|
42
|
-
},
|
|
43
|
-
});
|
|
23
|
+
this.floatable.update();
|
|
44
24
|
}
|
|
45
25
|
|
|
46
26
|
destroy(): void {
|
|
47
|
-
|
|
27
|
+
instances.delete(this.content);
|
|
48
28
|
|
|
49
|
-
|
|
50
|
-
|
|
29
|
+
this.anchor = null as never;
|
|
30
|
+
this.content = null as never;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
51
33
|
|
|
52
|
-
|
|
53
|
-
|
|
34
|
+
function activate(event: Event, focus: boolean): void {
|
|
35
|
+
const anchor = findAncestor(event, SELECTOR) as HTMLElement | null;
|
|
36
|
+
const content = findAncestor(event, SELECTOR_CONTENT) as HTMLElement | null;
|
|
54
37
|
|
|
55
|
-
|
|
38
|
+
if (anchor == null && content == null) {
|
|
39
|
+
if (!focused) {
|
|
40
|
+
closeTooltip(active);
|
|
56
41
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
}
|
|
42
|
+
active = undefined;
|
|
43
|
+
}
|
|
61
44
|
|
|
62
|
-
function addTooltip(anchor: HTMLElement): void {
|
|
63
|
-
if (TOOLTIPS_ALL.has(anchor)) {
|
|
64
45
|
return;
|
|
65
46
|
}
|
|
66
47
|
|
|
67
|
-
const
|
|
48
|
+
const instance = instances.get((anchor ?? content)!);
|
|
68
49
|
|
|
69
|
-
if (
|
|
70
|
-
|
|
50
|
+
if (instance == null || instance === active) {
|
|
51
|
+
return;
|
|
71
52
|
}
|
|
72
|
-
}
|
|
73
53
|
|
|
74
|
-
|
|
75
|
-
const pending = [...TOOLTIPS_PENDING];
|
|
54
|
+
const shouldDelay = active == null;
|
|
76
55
|
|
|
77
|
-
|
|
78
|
-
if (item === current) {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
TOOLTIPS_PENDING.delete(item);
|
|
83
|
-
}
|
|
56
|
+
closeTooltip(active);
|
|
84
57
|
|
|
85
|
-
|
|
58
|
+
active = instance;
|
|
59
|
+
focused = focus;
|
|
86
60
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
61
|
+
instance.timer = setTimeout(
|
|
62
|
+
() => {
|
|
63
|
+
instance.content.showPopover({
|
|
64
|
+
source: instance.anchor,
|
|
65
|
+
});
|
|
91
66
|
|
|
92
|
-
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
instance.floatable.update();
|
|
93
69
|
|
|
94
|
-
|
|
95
|
-
|
|
70
|
+
instance.content.setAttribute(ATTRIBUTE_CONTENT_ACTIVE, '');
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
shouldDelay ? delay : 0,
|
|
74
|
+
);
|
|
96
75
|
}
|
|
97
76
|
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (elements.length > 0) {
|
|
102
|
-
return elements.map(element => element.cloneNode(true)) as HTMLElement[];
|
|
77
|
+
function closeTooltip(instance?: Tooltip): void {
|
|
78
|
+
if (instance == null) {
|
|
79
|
+
return;
|
|
103
80
|
}
|
|
104
81
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (elements.length > 0) {
|
|
108
|
-
return elements.map(element => element.cloneNode(true)) as HTMLElement[];
|
|
82
|
+
if (instance.timer != null) {
|
|
83
|
+
clearTimeout(instance.timer);
|
|
109
84
|
}
|
|
110
85
|
|
|
111
|
-
|
|
86
|
+
instance.content.hidePopover();
|
|
87
|
+
instance.content.removeAttribute(ATTRIBUTE_CONTENT_ACTIVE);
|
|
88
|
+
}
|
|
112
89
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
90
|
+
function createTooltip(anchor: HTMLElement): void {
|
|
91
|
+
const content = getContent(anchor);
|
|
116
92
|
|
|
117
|
-
if (
|
|
118
|
-
|
|
93
|
+
if (content != null) {
|
|
94
|
+
instances.set(anchor, new Tooltip(anchor, content));
|
|
119
95
|
}
|
|
120
|
-
|
|
121
|
-
const paragraph = document.createElement('p');
|
|
122
|
-
|
|
123
|
-
paragraph.textContent = content;
|
|
124
|
-
|
|
125
|
-
return [paragraph];
|
|
126
96
|
}
|
|
127
97
|
|
|
128
98
|
function findElements(attribute: string | null): HTMLElement[] {
|
|
@@ -148,109 +118,105 @@ function findElements(attribute: string | null): HTMLElement[] {
|
|
|
148
118
|
return elements;
|
|
149
119
|
}
|
|
150
120
|
|
|
151
|
-
function getContent(): HTMLElement {
|
|
152
|
-
|
|
153
|
-
|
|
121
|
+
function getContent(anchor: HTMLElement): HTMLElement | undefined {
|
|
122
|
+
const elements = getElements(anchor);
|
|
123
|
+
|
|
124
|
+
if (elements == null) {
|
|
125
|
+
return;
|
|
154
126
|
}
|
|
155
127
|
|
|
156
|
-
|
|
128
|
+
const content = createElement(CONTENT_TAGNAME, {
|
|
129
|
+
popover: 'hint',
|
|
130
|
+
role: 'tooltip',
|
|
131
|
+
tabIndex: -1,
|
|
132
|
+
});
|
|
157
133
|
|
|
158
|
-
|
|
134
|
+
content.setAttribute(ATTRIBUTE_CONTENT, '');
|
|
159
135
|
|
|
160
|
-
|
|
161
|
-
contentElement.tabIndex = -1;
|
|
136
|
+
content.append(...elements);
|
|
162
137
|
|
|
163
|
-
return
|
|
138
|
+
return content;
|
|
164
139
|
}
|
|
165
140
|
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
}
|
|
141
|
+
function getElements(anchor: HTMLElement): HTMLElement[] | undefined {
|
|
142
|
+
let elements = findElements(anchor.getAttribute(ARIA_LABELLEDBY));
|
|
169
143
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return;
|
|
144
|
+
if (elements.length > 0) {
|
|
145
|
+
return elements.map(element => element.cloneNode(true)) as HTMLElement[];
|
|
173
146
|
}
|
|
174
|
-
}
|
|
175
147
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
148
|
+
elements = findElements(anchor.getAttribute(ARIA_DESCRIBEDBY));
|
|
149
|
+
|
|
150
|
+
if (elements.length > 0) {
|
|
151
|
+
return elements.map(element => element.cloneNode(true)) as HTMLElement[];
|
|
179
152
|
}
|
|
180
|
-
}
|
|
181
153
|
|
|
182
|
-
|
|
183
|
-
onToggle(event, false);
|
|
184
|
-
}
|
|
154
|
+
let content = anchor.getAttribute(ARIA_LABEL);
|
|
185
155
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
cancelAnimationFrame(toggle);
|
|
156
|
+
if (isNullableOrWhitespace(content)) {
|
|
157
|
+
content = anchor.getAttribute(ARIA_DESCRIPTION);
|
|
189
158
|
}
|
|
190
159
|
|
|
191
|
-
if (
|
|
192
|
-
if (contentTimer != null) {
|
|
193
|
-
clearTimeout(contentTimer);
|
|
194
|
-
}
|
|
195
|
-
|
|
160
|
+
if (isNullableOrWhitespace(content)) {
|
|
196
161
|
return;
|
|
197
162
|
}
|
|
198
163
|
|
|
199
|
-
|
|
200
|
-
toggle = undefined;
|
|
201
|
-
|
|
202
|
-
const element = findAncestor(event, SELECTOR) as HTMLElement;
|
|
203
|
-
|
|
204
|
-
const tooltip = TOOLTIPS_ALL.get(element);
|
|
205
|
-
|
|
206
|
-
if (TOOLTIPS_ACTIVE.size > 0 || TOOLTIPS_PENDING.size > 0) {
|
|
207
|
-
closeTooltips(tooltip);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (tooltip == null || tooltip.floatable.active === activate) {
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
164
|
+
const paragraph = document.createElement('p');
|
|
213
165
|
|
|
214
|
-
|
|
215
|
-
clearTimeout(contentTimer);
|
|
166
|
+
paragraph.textContent = content;
|
|
216
167
|
|
|
217
|
-
|
|
168
|
+
return [paragraph];
|
|
169
|
+
}
|
|
218
170
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
171
|
+
function onAdd(element: HTMLElement): void {
|
|
172
|
+
if (!instances.has(element)) {
|
|
173
|
+
createTooltip(element);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
222
176
|
|
|
223
|
-
|
|
224
|
-
|
|
177
|
+
function onFocusin(event: Event): void {
|
|
178
|
+
activate(event, true);
|
|
179
|
+
}
|
|
225
180
|
|
|
226
|
-
|
|
181
|
+
function onFocusout(): void {
|
|
182
|
+
if (active == null) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
227
185
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}, delayOpen);
|
|
186
|
+
if (focusTimer != null) {
|
|
187
|
+
clearTimeout(focusTimer);
|
|
188
|
+
}
|
|
232
189
|
|
|
190
|
+
focusTimer = setTimeout(() => {
|
|
191
|
+
if (
|
|
192
|
+
active == null ||
|
|
193
|
+
document.activeElement == null ||
|
|
194
|
+
findAncestor(document.activeElement, SELECTOR) != null
|
|
195
|
+
) {
|
|
233
196
|
return;
|
|
234
197
|
}
|
|
235
198
|
|
|
236
|
-
|
|
199
|
+
closeTooltip(active);
|
|
237
200
|
|
|
238
|
-
|
|
201
|
+
active = undefined;
|
|
239
202
|
});
|
|
240
203
|
}
|
|
241
204
|
|
|
242
|
-
function
|
|
243
|
-
|
|
244
|
-
TOOLTIPS_ALL.get(element)?.destroy();
|
|
245
|
-
}
|
|
205
|
+
function onPointermove(event: Event): void {
|
|
206
|
+
activate(event, false);
|
|
246
207
|
}
|
|
247
208
|
|
|
248
|
-
function
|
|
249
|
-
|
|
250
|
-
|
|
209
|
+
function onRemove(element: HTMLElement): void {
|
|
210
|
+
instances.get(element)?.destroy();
|
|
211
|
+
|
|
212
|
+
instances.delete(element);
|
|
251
213
|
}
|
|
252
214
|
|
|
253
|
-
|
|
215
|
+
export function setTooltipDelay(value: number): void {
|
|
216
|
+
if (typeof value === 'number' && !Number.isNaN(value) && value >= 0) {
|
|
217
|
+
delay = value;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
254
220
|
|
|
255
221
|
const ARIA_DESCRIBEDBY = 'aria-describedby';
|
|
256
222
|
|
|
@@ -260,48 +226,37 @@ const ARIA_LABEL = 'aria-label';
|
|
|
260
226
|
|
|
261
227
|
const ARIA_LABELLEDBY = 'aria-labelledby';
|
|
262
228
|
|
|
229
|
+
const CONTENT_TAGNAME = 'div';
|
|
230
|
+
|
|
231
|
+
const EXPRESSION_WHITESPACE = /\s+/;
|
|
232
|
+
|
|
263
233
|
const ATTRIBUTE = 'oui-tooltip';
|
|
264
234
|
|
|
265
235
|
const ATTRIBUTE_CONTENT = `${ATTRIBUTE}-content`;
|
|
266
236
|
|
|
267
237
|
const ATTRIBUTE_CONTENT_ACTIVE = `${ATTRIBUTE_CONTENT}-active`;
|
|
268
238
|
|
|
269
|
-
const ATTRIBUTE_POSITION = `${ATTRIBUTE}-position`;
|
|
270
|
-
|
|
271
239
|
const SELECTOR = `[${ATTRIBUTE}]`;
|
|
272
240
|
|
|
273
241
|
const SELECTOR_CONTENT = `[${ATTRIBUTE_CONTENT}]`;
|
|
274
242
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
const TOOLTIPS_ACTIVE: Set<Tooltip> = new Set();
|
|
278
|
-
|
|
279
|
-
const TOOLTIPS_ALL: WeakMap<HTMLElement, Tooltip> = new WeakMap();
|
|
280
|
-
|
|
281
|
-
const TOOLTIPS_PENDING: Set<Tooltip> = new Set();
|
|
282
|
-
|
|
283
|
-
let contentElement: HTMLElement | undefined;
|
|
284
|
-
|
|
285
|
-
let contentTimer: number | undefined;
|
|
286
|
-
|
|
287
|
-
let delayClose = 250;
|
|
243
|
+
const instances = new WeakMap<HTMLElement, Tooltip>();
|
|
288
244
|
|
|
289
|
-
|
|
245
|
+
const options: FloatableOptions = {
|
|
246
|
+
attribute: 'oui-tooltip-position',
|
|
247
|
+
position: 'above',
|
|
248
|
+
};
|
|
290
249
|
|
|
291
|
-
let
|
|
250
|
+
let delay = 500;
|
|
292
251
|
|
|
293
|
-
|
|
252
|
+
let focused = false;
|
|
294
253
|
|
|
295
|
-
|
|
254
|
+
let active: Tooltip | undefined;
|
|
296
255
|
|
|
297
|
-
|
|
298
|
-
on(document, 'focusin', onActivate);
|
|
299
|
-
on(document, 'focusout', onDeactivate);
|
|
300
|
-
on(document, 'mousemove', onActivate);
|
|
301
|
-
on(window, 'resize', onChange);
|
|
302
|
-
on(document, 'scroll', onChange);
|
|
303
|
-
on(document, 'touchstart', onActivate);
|
|
256
|
+
let focusTimer: number | undefined;
|
|
304
257
|
|
|
305
|
-
|
|
258
|
+
on(document, 'focusin', onFocusin);
|
|
259
|
+
on(document, 'focusout', onFocusout);
|
|
260
|
+
on(document, 'pointermove', onPointermove);
|
|
306
261
|
|
|
307
|
-
|
|
262
|
+
attributable(ATTRIBUTE, onAdd, onRemove);
|