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