@neuravision/construct 2.0.3 → 2.1.1
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/CHANGELOG.md +272 -0
- package/README.md +148 -152
- package/behaviors/combobox.js +447 -0
- package/behaviors/drawer.js +18 -0
- package/behaviors/dropdown.js +296 -0
- package/behaviors/index.d.ts +333 -0
- package/behaviors/index.js +9 -0
- package/behaviors/internal/dialog-controller.js +351 -0
- package/behaviors/internal/dom.js +377 -0
- package/behaviors/internal/overlay.js +241 -0
- package/behaviors/internal/popup-layer.js +188 -0
- package/behaviors/modal.js +17 -0
- package/behaviors/popover.js +269 -0
- package/behaviors/select-menu.js +326 -0
- package/behaviors/tabs.js +230 -0
- package/behaviors/toggle-group.js +234 -0
- package/behaviors/tooltip.js +233 -0
- package/components/README.md +279 -83
- package/components/_shared.css +11 -10
- package/components/_shell-shared.css +20 -4
- package/components/app-shell-v2.css +21 -14
- package/components/app-shell.css +23 -14
- package/components/badge.css +15 -0
- package/components/button.css +103 -15
- package/components/card.css +53 -8
- package/components/checkbox.css +3 -2
- package/components/chip.css +1 -1
- package/components/datepicker.css +3 -1
- package/components/datum.css +204 -0
- package/components/index.css +5 -0
- package/components/meter.css +182 -0
- package/components/navbar.css +88 -3
- package/components/slider.css +2 -1
- package/components/status.css +107 -0
- package/components/switch.css +4 -3
- package/components/tooltip.css +4 -0
- package/docs/architecture.md +79 -0
- package/docs/behaviors.md +101 -0
- package/docs/fonts.md +70 -0
- package/docs/governance.md +85 -0
- package/docs/guidelines.md +42 -23
- package/examples/react/README.md +20 -0
- package/examples/react/SettingsModal.jsx +66 -0
- package/examples/react/construct.css +5 -0
- package/fonts.css +10 -0
- package/foundations.css +8 -1
- package/package.json +72 -21
- package/schemas/components.schema.json +107 -0
- package/schemas/examples/theme.example.json +27 -0
- package/schemas/primitives.schema.json +135 -0
- package/schemas/semantic.schema.json +223 -0
- package/schemas/theme.schema.json +63 -0
- package/scripts/build-tokens.mjs +1417 -0
- package/scripts/construct-theme.mjs +43 -0
- package/scripts/validate-tokens.mjs +25 -0
- package/standalone/accordion.css +3 -0
- package/standalone/alert.css +3 -0
- package/standalone/app-shell-v2.css +4 -0
- package/standalone/app-shell.css +4 -0
- package/standalone/avatar.css +3 -0
- package/standalone/badge.css +3 -0
- package/standalone/banner.css +3 -0
- package/standalone/breadcrumbs.css +3 -0
- package/standalone/button.css +3 -0
- package/standalone/card.css +3 -0
- package/standalone/chart.css +3 -0
- package/standalone/checkbox.css +3 -0
- package/standalone/chip.css +3 -0
- package/standalone/combobox.css +3 -0
- package/standalone/core.css +4 -0
- package/standalone/data-table.css +3 -0
- package/standalone/datepicker.css +3 -0
- package/standalone/datum.css +3 -0
- package/standalone/divider.css +3 -0
- package/standalone/drawer.css +3 -0
- package/standalone/dropdown.css +3 -0
- package/standalone/empty-state.css +3 -0
- package/standalone/field.css +3 -0
- package/standalone/file-upload.css +3 -0
- package/standalone/icon.css +3 -0
- package/standalone/input.css +3 -0
- package/standalone/list.css +3 -0
- package/standalone/meter.css +3 -0
- package/standalone/modal.css +3 -0
- package/standalone/navbar.css +3 -0
- package/standalone/pagination.css +3 -0
- package/standalone/popover.css +3 -0
- package/standalone/progress-bar.css +3 -0
- package/standalone/select-menu.css +3 -0
- package/standalone/select.css +3 -0
- package/standalone/sidebar.css +3 -0
- package/standalone/skeleton.css +3 -0
- package/standalone/skip-link.css +3 -0
- package/standalone/slider.css +3 -0
- package/standalone/spinner.css +3 -0
- package/standalone/status.css +3 -0
- package/standalone/switch.css +3 -0
- package/standalone/table.css +3 -0
- package/standalone/tabs.css +3 -0
- package/standalone/textarea.css +3 -0
- package/standalone/toast.css +3 -0
- package/standalone/toggle-group.css +3 -0
- package/standalone/toolbar.css +3 -0
- package/standalone/tooltip.css +3 -0
- package/standalone/tree.css +3 -0
- package/tokens/README.md +130 -126
- package/tokens/components.json +54 -0
- package/tokens/primitives.json +268 -0
- package/tokens/semantic.dark.json +93 -0
- package/tokens/semantic.high-contrast.json +93 -0
- package/tokens/semantic.light.json +178 -0
- package/tokens/tokens.css +532 -28
- package/tokens/tokens.js +335 -25
- package/tokens/tokens.json +764 -21
- package/tokens/tokens.ts +335 -25
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertChoice,
|
|
3
|
+
assertDescendants,
|
|
4
|
+
assertElement,
|
|
5
|
+
assertElements,
|
|
6
|
+
assertFunction,
|
|
7
|
+
assertUniqueElements,
|
|
8
|
+
boundaryEnabledIndex,
|
|
9
|
+
createAttributeStore,
|
|
10
|
+
createControllerLifecycle,
|
|
11
|
+
createDisposables,
|
|
12
|
+
dispatch,
|
|
13
|
+
focusElement,
|
|
14
|
+
getItemValue,
|
|
15
|
+
isDisabled,
|
|
16
|
+
nextEnabledIndex,
|
|
17
|
+
orientationDelta,
|
|
18
|
+
} from './internal/dom.js';
|
|
19
|
+
|
|
20
|
+
const controllers = new WeakMap();
|
|
21
|
+
|
|
22
|
+
function isNativeButton(element) {
|
|
23
|
+
return element.tagName === 'BUTTON' || (element.tagName === 'INPUT' && ['button', 'submit', 'reset'].includes(element.type));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeMultipleValue(value, name) {
|
|
27
|
+
if (value == null) return [];
|
|
28
|
+
if (typeof value === 'string' || typeof value[Symbol.iterator] !== 'function') {
|
|
29
|
+
throw new TypeError(`${name} must be a non-string iterable for a multiple toggle group.`);
|
|
30
|
+
}
|
|
31
|
+
return Array.from(value, String);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates a single- or multiple-value toggle group with roving tabindex.
|
|
36
|
+
*/
|
|
37
|
+
export function createToggleGroupController(options) {
|
|
38
|
+
if (!options || typeof options !== 'object') {
|
|
39
|
+
throw new TypeError('Toggle group controller options are required.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const root = assertElement(options.root, 'options.root');
|
|
43
|
+
const existing = controllers.get(root);
|
|
44
|
+
if (existing) return existing;
|
|
45
|
+
|
|
46
|
+
const items = options.items == null
|
|
47
|
+
? [...root.querySelectorAll('[data-ct-toggle], .ct-toggle-group__item')]
|
|
48
|
+
: assertElements(options.items, 'options.items');
|
|
49
|
+
if (!items.length) throw new TypeError('Toggle group controller requires at least one item.');
|
|
50
|
+
assertUniqueElements(items, 'options.items');
|
|
51
|
+
assertDescendants(items, root, 'options.items');
|
|
52
|
+
|
|
53
|
+
const type = assertChoice(options.type ?? 'single', ['single', 'multiple'], 'options.type');
|
|
54
|
+
const orientation = assertChoice(
|
|
55
|
+
options.orientation ?? root.getAttribute('aria-orientation') ?? 'horizontal',
|
|
56
|
+
['horizontal', 'vertical'],
|
|
57
|
+
'options.orientation',
|
|
58
|
+
);
|
|
59
|
+
assertFunction(options.onValueChange, 'options.onValueChange');
|
|
60
|
+
|
|
61
|
+
const attributes = createAttributeStore();
|
|
62
|
+
const lifecycle = createControllerLifecycle('Toggle group');
|
|
63
|
+
const disposables = createDisposables();
|
|
64
|
+
const values = items.map(getItemValue);
|
|
65
|
+
if (new Set(values).size !== values.length) {
|
|
66
|
+
throw new TypeError('Toggle group item values must be unique.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const loop = options.loop !== false;
|
|
70
|
+
const allowEmpty = options.allowEmpty !== false;
|
|
71
|
+
let selected = new Set();
|
|
72
|
+
|
|
73
|
+
if (options.value != null) {
|
|
74
|
+
const requested = type === 'multiple'
|
|
75
|
+
? new Set(normalizeMultipleValue(options.value, 'options.value'))
|
|
76
|
+
: new Set([String(options.value)]);
|
|
77
|
+
requested.forEach((value) => {
|
|
78
|
+
if (!values.includes(value)) throw new RangeError(`Unknown toggle group value: ${value}.`);
|
|
79
|
+
selected.add(value);
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
items.forEach((item, index) => {
|
|
83
|
+
if (item.getAttribute('aria-pressed') === 'true' || item.getAttribute('aria-checked') === 'true') {
|
|
84
|
+
if (type === 'multiple' || selected.size === 0) selected.add(values[index]);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let focusIndex = items.findIndex((item, index) => selected.has(values[index]) && !isDisabled(item));
|
|
90
|
+
if (focusIndex < 0) focusIndex = boundaryEnabledIndex(items);
|
|
91
|
+
if (focusIndex < 0) throw new TypeError('Toggle group controller requires at least one enabled item.');
|
|
92
|
+
if (!allowEmpty && selected.size === 0) selected.add(values[focusIndex]);
|
|
93
|
+
|
|
94
|
+
const rootRole = root.getAttribute('role') ?? 'group';
|
|
95
|
+
attributes.set(root, 'role', rootRole);
|
|
96
|
+
attributes.set(root, 'data-orientation', orientation);
|
|
97
|
+
if (rootRole === 'toolbar') attributes.set(root, 'aria-orientation', orientation);
|
|
98
|
+
items.forEach((item) => {
|
|
99
|
+
if (!isNativeButton(item)) attributes.set(item, 'role', 'button');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function currentValue() {
|
|
103
|
+
if (type === 'multiple') return values.filter((value) => selected.has(value));
|
|
104
|
+
return values.find((value) => selected.has(value)) ?? null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sync() {
|
|
108
|
+
items.forEach((item, index) => {
|
|
109
|
+
const pressed = selected.has(values[index]);
|
|
110
|
+
attributes.set(item, 'aria-pressed', String(pressed));
|
|
111
|
+
attributes.set(item, 'data-state', pressed ? 'on' : 'off');
|
|
112
|
+
attributes.set(item, 'tabindex', index === focusIndex && !isDisabled(item) ? '0' : '-1');
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function commit(nextSelected, reason, item = null) {
|
|
117
|
+
return lifecycle.run('value change', (checkpoint) => {
|
|
118
|
+
const previous = currentValue();
|
|
119
|
+
const next = type === 'multiple'
|
|
120
|
+
? values.filter((value) => nextSelected.has(value))
|
|
121
|
+
: values.find((value) => nextSelected.has(value)) ?? null;
|
|
122
|
+
if (JSON.stringify(previous) === JSON.stringify(next)) return false;
|
|
123
|
+
|
|
124
|
+
const detail = { controller, item, previousValue: previous, reason, value: next };
|
|
125
|
+
const allowed = dispatch(root, 'ct:beforevaluechange', detail, true);
|
|
126
|
+
checkpoint();
|
|
127
|
+
if (!allowed) return false;
|
|
128
|
+
|
|
129
|
+
selected = nextSelected;
|
|
130
|
+
sync();
|
|
131
|
+
options.onValueChange?.(detail);
|
|
132
|
+
checkpoint();
|
|
133
|
+
dispatch(root, 'ct:valuechange', detail);
|
|
134
|
+
checkpoint();
|
|
135
|
+
return true;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function toggle(target, reason = 'programmatic') {
|
|
140
|
+
lifecycle.assertAlive();
|
|
141
|
+
const index = typeof target === 'number' ? target : items.indexOf(target);
|
|
142
|
+
if (!Number.isInteger(index) || index < 0 || index >= items.length) {
|
|
143
|
+
throw new RangeError('Toggle target is not managed by this controller.');
|
|
144
|
+
}
|
|
145
|
+
const item = items[index];
|
|
146
|
+
if (isDisabled(item)) return false;
|
|
147
|
+
|
|
148
|
+
const value = values[index];
|
|
149
|
+
const next = new Set(selected);
|
|
150
|
+
if (type === 'multiple') {
|
|
151
|
+
if (next.has(value)) next.delete(value);
|
|
152
|
+
else next.add(value);
|
|
153
|
+
} else if (next.has(value)) {
|
|
154
|
+
if (!allowEmpty) return false;
|
|
155
|
+
next.clear();
|
|
156
|
+
} else {
|
|
157
|
+
next.clear();
|
|
158
|
+
next.add(value);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return commit(next, reason, item);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function setValue(value, reason = 'programmatic') {
|
|
165
|
+
lifecycle.assertAlive();
|
|
166
|
+
const requested = type === 'multiple'
|
|
167
|
+
? new Set(normalizeMultipleValue(value, 'value'))
|
|
168
|
+
: new Set(value == null ? [] : [String(value)]);
|
|
169
|
+
requested.forEach((entry) => {
|
|
170
|
+
if (!values.includes(entry)) throw new RangeError(`Unknown toggle group value: ${entry}.`);
|
|
171
|
+
});
|
|
172
|
+
if (!allowEmpty && requested.size === 0) return false;
|
|
173
|
+
return commit(requested, reason);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function onKeyDown(event, index) {
|
|
177
|
+
const delta = orientationDelta(event.key, orientation, root);
|
|
178
|
+
let nextIndex = -1;
|
|
179
|
+
if (delta) nextIndex = nextEnabledIndex(items, index, delta, loop);
|
|
180
|
+
else if (event.key === 'Home' || event.key === 'End') {
|
|
181
|
+
nextIndex = boundaryEnabledIndex(items, event.key === 'End');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (nextIndex >= 0) {
|
|
185
|
+
event.preventDefault();
|
|
186
|
+
focusIndex = nextIndex;
|
|
187
|
+
sync();
|
|
188
|
+
focusElement(items[nextIndex]);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!isNativeButton(items[index]) && (event.key === 'Enter' || event.key === ' ')) {
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
toggle(index, 'keyboard');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
items.forEach((item, index) => {
|
|
199
|
+
disposables.listen(item, 'click', (event) => {
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
if (!isDisabled(item)) toggle(index, 'pointer');
|
|
202
|
+
});
|
|
203
|
+
disposables.listen(item, 'focus', () => {
|
|
204
|
+
if (isDisabled(item)) return;
|
|
205
|
+
focusIndex = index;
|
|
206
|
+
sync();
|
|
207
|
+
});
|
|
208
|
+
disposables.listen(item, 'keydown', (event) => onKeyDown(event, index));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const controller = Object.freeze({
|
|
212
|
+
get root() {
|
|
213
|
+
return root;
|
|
214
|
+
},
|
|
215
|
+
get items() {
|
|
216
|
+
return [...items];
|
|
217
|
+
},
|
|
218
|
+
get value() {
|
|
219
|
+
return currentValue();
|
|
220
|
+
},
|
|
221
|
+
toggle,
|
|
222
|
+
setValue,
|
|
223
|
+
destroy() {
|
|
224
|
+
if (!lifecycle.destroy()) return;
|
|
225
|
+
disposables.dispose();
|
|
226
|
+
attributes.restore();
|
|
227
|
+
controllers.delete(root);
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
controllers.set(root, controller);
|
|
232
|
+
sync();
|
|
233
|
+
return controller;
|
|
234
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertElement,
|
|
3
|
+
assertFunction,
|
|
4
|
+
createAttributeStore,
|
|
5
|
+
createControllerLifecycle,
|
|
6
|
+
createDisposables,
|
|
7
|
+
dispatch,
|
|
8
|
+
ensureId,
|
|
9
|
+
eventTargetsOutside,
|
|
10
|
+
} from './internal/dom.js';
|
|
11
|
+
import { consumeTopOverlayEvent, pushOverlay } from './internal/overlay.js';
|
|
12
|
+
|
|
13
|
+
const controllers = new WeakMap();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a tooltip controller that works with keyboard focus, hover-capable
|
|
17
|
+
* pointers, Escape, and touch without depending on sticky hover state.
|
|
18
|
+
*/
|
|
19
|
+
export function createTooltipController(options) {
|
|
20
|
+
if (!options || typeof options !== 'object') {
|
|
21
|
+
throw new TypeError('Tooltip controller options are required.');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const trigger = assertElement(options.trigger, 'options.trigger');
|
|
25
|
+
const existing = controllers.get(trigger);
|
|
26
|
+
if (existing) return existing;
|
|
27
|
+
const tooltip = assertElement(options.tooltip, 'options.tooltip');
|
|
28
|
+
const root = assertElement(options.root ?? tooltip.closest('.ct-tooltip') ?? tooltip, 'options.root');
|
|
29
|
+
if (trigger.ownerDocument !== tooltip.ownerDocument || trigger.ownerDocument !== root.ownerDocument) {
|
|
30
|
+
throw new TypeError('Tooltip elements must belong to the same document.');
|
|
31
|
+
}
|
|
32
|
+
assertFunction(options.onOpen, 'options.onOpen');
|
|
33
|
+
assertFunction(options.onClose, 'options.onClose');
|
|
34
|
+
|
|
35
|
+
const attributes = createAttributeStore();
|
|
36
|
+
const lifecycle = createControllerLifecycle('Tooltip');
|
|
37
|
+
const disposables = createDisposables();
|
|
38
|
+
const document = trigger.ownerDocument;
|
|
39
|
+
const token = {};
|
|
40
|
+
const openDelay = options.openDelay ?? 400;
|
|
41
|
+
const closeDelay = options.closeDelay ?? 100;
|
|
42
|
+
if (!Number.isFinite(openDelay) || openDelay < 0 || !Number.isFinite(closeDelay) || closeDelay < 0) {
|
|
43
|
+
throw new RangeError('Tooltip delays must be non-negative finite numbers.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let controller;
|
|
47
|
+
let open = options.open ?? root.getAttribute('data-state') === 'open';
|
|
48
|
+
let timer;
|
|
49
|
+
let removeFromStack = null;
|
|
50
|
+
let tooltipHovered = false;
|
|
51
|
+
let triggerHovered = false;
|
|
52
|
+
let focused = false;
|
|
53
|
+
let touchOpened = false;
|
|
54
|
+
|
|
55
|
+
const tooltipId = ensureId(tooltip, 'ct-tooltip', attributes);
|
|
56
|
+
const describedBy = new Set((trigger.getAttribute('aria-describedby') ?? '').split(/\s+/).filter(Boolean));
|
|
57
|
+
describedBy.add(tooltipId);
|
|
58
|
+
attributes.set(trigger, 'aria-describedby', [...describedBy].join(' '));
|
|
59
|
+
attributes.set(tooltip, 'role', 'tooltip');
|
|
60
|
+
|
|
61
|
+
function sync() {
|
|
62
|
+
attributes.set(root, 'data-state', open ? 'open' : 'closed');
|
|
63
|
+
attributes.set(tooltip, 'hidden', open ? null : '');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function clearTimer() {
|
|
67
|
+
if (timer) clearTimeout(timer);
|
|
68
|
+
timer = undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function registerOverlay() {
|
|
72
|
+
return pushOverlay(document, token, {
|
|
73
|
+
focusScopes: [tooltip],
|
|
74
|
+
onOwnerClose: () => closeTooltip('owner-close'),
|
|
75
|
+
onOwnerDestroy: () => controller.destroy(),
|
|
76
|
+
trigger,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function openTooltip(reason = 'programmatic') {
|
|
81
|
+
return lifecycle.run('open', (checkpoint) => {
|
|
82
|
+
clearTimer();
|
|
83
|
+
if (open) return false;
|
|
84
|
+
const detail = { controller, reason };
|
|
85
|
+
const allowed = dispatch(tooltip, 'ct:beforeopen', detail, true);
|
|
86
|
+
checkpoint();
|
|
87
|
+
if (!allowed) return false;
|
|
88
|
+
|
|
89
|
+
open = true;
|
|
90
|
+
sync();
|
|
91
|
+
removeFromStack = registerOverlay();
|
|
92
|
+
try {
|
|
93
|
+
options.onOpen?.(detail);
|
|
94
|
+
checkpoint();
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (lifecycle.isAlive) {
|
|
97
|
+
open = false;
|
|
98
|
+
sync();
|
|
99
|
+
removeFromStack?.();
|
|
100
|
+
removeFromStack = null;
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
dispatch(tooltip, 'ct:open', detail);
|
|
105
|
+
checkpoint();
|
|
106
|
+
return true;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function closeTooltip(reason = 'programmatic') {
|
|
111
|
+
return lifecycle.run('close', (checkpoint) => {
|
|
112
|
+
clearTimer();
|
|
113
|
+
if (!open) return false;
|
|
114
|
+
const detail = { controller, reason };
|
|
115
|
+
const allowed = dispatch(tooltip, 'ct:beforeclose', detail, true);
|
|
116
|
+
checkpoint();
|
|
117
|
+
if (!allowed) return false;
|
|
118
|
+
|
|
119
|
+
open = false;
|
|
120
|
+
touchOpened = false;
|
|
121
|
+
sync();
|
|
122
|
+
removeFromStack?.();
|
|
123
|
+
removeFromStack = null;
|
|
124
|
+
options.onClose?.(detail);
|
|
125
|
+
checkpoint();
|
|
126
|
+
dispatch(tooltip, 'ct:close', detail);
|
|
127
|
+
checkpoint();
|
|
128
|
+
return true;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function scheduleOpen(reason, delay = openDelay) {
|
|
133
|
+
clearTimer();
|
|
134
|
+
timer = setTimeout(() => openTooltip(reason), delay);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function scheduleClose(reason, delay = closeDelay) {
|
|
138
|
+
clearTimer();
|
|
139
|
+
timer = setTimeout(() => closeTooltip(reason), delay);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
disposables.listen(trigger, 'pointerenter', (event) => {
|
|
143
|
+
if (event.pointerType !== 'touch') {
|
|
144
|
+
triggerHovered = true;
|
|
145
|
+
scheduleOpen('hover');
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
disposables.listen(trigger, 'pointerleave', (event) => {
|
|
149
|
+
if (event.pointerType !== 'touch') {
|
|
150
|
+
triggerHovered = false;
|
|
151
|
+
if (!tooltipHovered && !focused && !touchOpened) scheduleClose('hover');
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
disposables.listen(tooltip, 'pointerenter', (event) => {
|
|
155
|
+
if (event.pointerType !== 'touch') {
|
|
156
|
+
tooltipHovered = true;
|
|
157
|
+
clearTimer();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
disposables.listen(tooltip, 'pointerleave', (event) => {
|
|
161
|
+
if (event.pointerType !== 'touch') {
|
|
162
|
+
tooltipHovered = false;
|
|
163
|
+
if (!triggerHovered && !focused && !touchOpened) scheduleClose('hover');
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
disposables.listen(trigger, 'focus', () => {
|
|
167
|
+
focused = true;
|
|
168
|
+
scheduleOpen('focus', 0);
|
|
169
|
+
});
|
|
170
|
+
disposables.listen(trigger, 'blur', () => {
|
|
171
|
+
focused = false;
|
|
172
|
+
if (!triggerHovered && !tooltipHovered && !touchOpened) scheduleClose('blur', 0);
|
|
173
|
+
});
|
|
174
|
+
disposables.listen(trigger, 'pointerdown', (event) => {
|
|
175
|
+
if (event.pointerType === 'touch') {
|
|
176
|
+
if (touchOpened || open) {
|
|
177
|
+
touchOpened = false;
|
|
178
|
+
closeTooltip('touch');
|
|
179
|
+
} else {
|
|
180
|
+
touchOpened = openTooltip('touch') || open;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
disposables.listen(document, 'keydown', (event) => {
|
|
185
|
+
if (open && event.key === 'Escape' && consumeTopOverlayEvent(event, document, token)) {
|
|
186
|
+
event.preventDefault();
|
|
187
|
+
event.stopPropagation();
|
|
188
|
+
closeTooltip('escape');
|
|
189
|
+
}
|
|
190
|
+
}, true);
|
|
191
|
+
disposables.listen(document, 'pointerdown', (event) => {
|
|
192
|
+
if (
|
|
193
|
+
open &&
|
|
194
|
+
eventTargetsOutside(event, [trigger, tooltip]) &&
|
|
195
|
+
consumeTopOverlayEvent(event, document, token)
|
|
196
|
+
) {
|
|
197
|
+
touchOpened = false;
|
|
198
|
+
closeTooltip('outside');
|
|
199
|
+
}
|
|
200
|
+
}, true);
|
|
201
|
+
|
|
202
|
+
controller = Object.freeze({
|
|
203
|
+
get root() {
|
|
204
|
+
return root;
|
|
205
|
+
},
|
|
206
|
+
get trigger() {
|
|
207
|
+
return trigger;
|
|
208
|
+
},
|
|
209
|
+
get tooltip() {
|
|
210
|
+
return tooltip;
|
|
211
|
+
},
|
|
212
|
+
get isOpen() {
|
|
213
|
+
return open;
|
|
214
|
+
},
|
|
215
|
+
open: openTooltip,
|
|
216
|
+
close: closeTooltip,
|
|
217
|
+
destroy() {
|
|
218
|
+
if (!lifecycle.destroy()) return;
|
|
219
|
+
open = false;
|
|
220
|
+
clearTimer();
|
|
221
|
+
removeFromStack?.();
|
|
222
|
+
removeFromStack = null;
|
|
223
|
+
disposables.dispose();
|
|
224
|
+
attributes.restore();
|
|
225
|
+
controllers.delete(trigger);
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
controllers.set(trigger, controller);
|
|
230
|
+
sync();
|
|
231
|
+
if (open) removeFromStack = registerOverlay();
|
|
232
|
+
return controller;
|
|
233
|
+
}
|