@llui/components 0.0.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/LICENSE +21 -0
- package/README.md +143 -0
- package/dist/components/accordion.d.ts +115 -0
- package/dist/components/accordion.d.ts.map +1 -0
- package/dist/components/accordion.js +138 -0
- package/dist/components/alert-dialog.d.ts +45 -0
- package/dist/components/alert-dialog.d.ts.map +1 -0
- package/dist/components/alert-dialog.js +12 -0
- package/dist/components/angle-slider.d.ts +121 -0
- package/dist/components/angle-slider.d.ts.map +1 -0
- package/dist/components/angle-slider.js +145 -0
- package/dist/components/async-list.d.ts +104 -0
- package/dist/components/async-list.d.ts.map +1 -0
- package/dist/components/async-list.js +117 -0
- package/dist/components/avatar.d.ts +58 -0
- package/dist/components/avatar.d.ts.map +1 -0
- package/dist/components/avatar.js +43 -0
- package/dist/components/carousel.d.ts +128 -0
- package/dist/components/carousel.d.ts.map +1 -0
- package/dist/components/carousel.js +131 -0
- package/dist/components/cascade-select.d.ts +95 -0
- package/dist/components/cascade-select.d.ts.map +1 -0
- package/dist/components/cascade-select.js +100 -0
- package/dist/components/checkbox.d.ts +74 -0
- package/dist/components/checkbox.d.ts.map +1 -0
- package/dist/components/checkbox.js +73 -0
- package/dist/components/clipboard.d.ts +72 -0
- package/dist/components/clipboard.d.ts.map +1 -0
- package/dist/components/clipboard.js +73 -0
- package/dist/components/collapsible.d.ts +64 -0
- package/dist/components/collapsible.d.ts.map +1 -0
- package/dist/components/collapsible.js +51 -0
- package/dist/components/color-picker.d.ts +125 -0
- package/dist/components/color-picker.d.ts.map +1 -0
- package/dist/components/color-picker.js +169 -0
- package/dist/components/combobox.d.ts +163 -0
- package/dist/components/combobox.d.ts.map +1 -0
- package/dist/components/combobox.js +345 -0
- package/dist/components/context-menu.d.ts +105 -0
- package/dist/components/context-menu.d.ts.map +1 -0
- package/dist/components/context-menu.js +177 -0
- package/dist/components/date-input.d.ts +117 -0
- package/dist/components/date-input.d.ts.map +1 -0
- package/dist/components/date-input.js +149 -0
- package/dist/components/date-picker.d.ts +142 -0
- package/dist/components/date-picker.d.ts.map +1 -0
- package/dist/components/date-picker.js +294 -0
- package/dist/components/dialog.d.ts +152 -0
- package/dist/components/dialog.d.ts.map +1 -0
- package/dist/components/dialog.js +140 -0
- package/dist/components/drawer.d.ts +106 -0
- package/dist/components/drawer.d.ts.map +1 -0
- package/dist/components/drawer.js +136 -0
- package/dist/components/editable.d.ts +92 -0
- package/dist/components/editable.d.ts.map +1 -0
- package/dist/components/editable.js +112 -0
- package/dist/components/file-upload.d.ts +251 -0
- package/dist/components/file-upload.d.ts.map +1 -0
- package/dist/components/file-upload.js +324 -0
- package/dist/components/floating-panel.d.ts +171 -0
- package/dist/components/floating-panel.d.ts.map +1 -0
- package/dist/components/floating-panel.js +198 -0
- package/dist/components/hover-card.d.ts +85 -0
- package/dist/components/hover-card.d.ts.map +1 -0
- package/dist/components/hover-card.js +128 -0
- package/dist/components/image-cropper.d.ts +129 -0
- package/dist/components/image-cropper.d.ts.map +1 -0
- package/dist/components/image-cropper.js +208 -0
- package/dist/components/index.d.ts +109 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +54 -0
- package/dist/components/listbox.d.ts +98 -0
- package/dist/components/listbox.d.ts.map +1 -0
- package/dist/components/listbox.js +174 -0
- package/dist/components/marquee.d.ts +84 -0
- package/dist/components/marquee.d.ts.map +1 -0
- package/dist/components/marquee.js +73 -0
- package/dist/components/menu.d.ts +131 -0
- package/dist/components/menu.d.ts.map +1 -0
- package/dist/components/menu.js +262 -0
- package/dist/components/navigation-menu.d.ts +111 -0
- package/dist/components/navigation-menu.d.ts.map +1 -0
- package/dist/components/navigation-menu.js +102 -0
- package/dist/components/number-input.d.ts +106 -0
- package/dist/components/number-input.d.ts.map +1 -0
- package/dist/components/number-input.js +178 -0
- package/dist/components/pagination.d.ts +113 -0
- package/dist/components/pagination.d.ts.map +1 -0
- package/dist/components/pagination.js +135 -0
- package/dist/components/password-input.d.ts +64 -0
- package/dist/components/password-input.d.ts.map +1 -0
- package/dist/components/password-input.js +52 -0
- package/dist/components/pin-input.d.ts +89 -0
- package/dist/components/pin-input.d.ts.map +1 -0
- package/dist/components/pin-input.js +139 -0
- package/dist/components/popover.d.ts +116 -0
- package/dist/components/popover.d.ts.map +1 -0
- package/dist/components/popover.js +146 -0
- package/dist/components/presence.d.ts +71 -0
- package/dist/components/presence.d.ts.map +1 -0
- package/dist/components/presence.js +57 -0
- package/dist/components/progress.d.ts +74 -0
- package/dist/components/progress.d.ts.map +1 -0
- package/dist/components/progress.js +80 -0
- package/dist/components/qr-code.d.ts +114 -0
- package/dist/components/qr-code.d.ts.map +1 -0
- package/dist/components/qr-code.js +108 -0
- package/dist/components/radio-group.d.ts +89 -0
- package/dist/components/radio-group.d.ts.map +1 -0
- package/dist/components/radio-group.js +161 -0
- package/dist/components/rating-group.d.ts +88 -0
- package/dist/components/rating-group.d.ts.map +1 -0
- package/dist/components/rating-group.js +122 -0
- package/dist/components/scroll-area.d.ts +124 -0
- package/dist/components/scroll-area.d.ts.map +1 -0
- package/dist/components/scroll-area.js +152 -0
- package/dist/components/select.d.ts +161 -0
- package/dist/components/select.d.ts.map +1 -0
- package/dist/components/select.js +333 -0
- package/dist/components/signature-pad.d.ts +138 -0
- package/dist/components/signature-pad.d.ts.map +1 -0
- package/dist/components/signature-pad.js +142 -0
- package/dist/components/slider.d.ts +117 -0
- package/dist/components/slider.d.ts.map +1 -0
- package/dist/components/slider.js +210 -0
- package/dist/components/splitter.d.ts +87 -0
- package/dist/components/splitter.d.ts.map +1 -0
- package/dist/components/splitter.js +119 -0
- package/dist/components/steps.d.ts +104 -0
- package/dist/components/steps.d.ts.map +1 -0
- package/dist/components/steps.js +133 -0
- package/dist/components/switch.d.ts +66 -0
- package/dist/components/switch.d.ts.map +1 -0
- package/dist/components/switch.js +59 -0
- package/dist/components/tabs.d.ts +146 -0
- package/dist/components/tabs.d.ts.map +1 -0
- package/dist/components/tabs.js +244 -0
- package/dist/components/tags-input.d.ts +118 -0
- package/dist/components/tags-input.d.ts.map +1 -0
- package/dist/components/tags-input.js +168 -0
- package/dist/components/time-picker.d.ts +121 -0
- package/dist/components/time-picker.d.ts.map +1 -0
- package/dist/components/time-picker.js +147 -0
- package/dist/components/timer.d.ts +131 -0
- package/dist/components/timer.d.ts.map +1 -0
- package/dist/components/timer.js +117 -0
- package/dist/components/toast.d.ts +119 -0
- package/dist/components/toast.d.ts.map +1 -0
- package/dist/components/toast.js +102 -0
- package/dist/components/toc.d.ts +119 -0
- package/dist/components/toc.d.ts.map +1 -0
- package/dist/components/toc.js +107 -0
- package/dist/components/toggle-group.d.ts +80 -0
- package/dist/components/toggle-group.d.ts.map +1 -0
- package/dist/components/toggle-group.js +93 -0
- package/dist/components/toggle.d.ts +47 -0
- package/dist/components/toggle.d.ts.map +1 -0
- package/dist/components/toggle.js +41 -0
- package/dist/components/tooltip.d.ts +92 -0
- package/dist/components/tooltip.d.ts.map +1 -0
- package/dist/components/tooltip.js +147 -0
- package/dist/components/tour.d.ts +145 -0
- package/dist/components/tour.d.ts.map +1 -0
- package/dist/components/tour.js +133 -0
- package/dist/components/tree-view.d.ts +216 -0
- package/dist/components/tree-view.d.ts.map +1 -0
- package/dist/components/tree-view.js +293 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/patterns/confirm-dialog.d.ts +92 -0
- package/dist/patterns/confirm-dialog.d.ts.map +1 -0
- package/dist/patterns/confirm-dialog.js +92 -0
- package/dist/patterns/index.d.ts +3 -0
- package/dist/patterns/index.d.ts.map +1 -0
- package/dist/patterns/index.js +1 -0
- package/dist/utils/anatomy.d.ts +40 -0
- package/dist/utils/anatomy.d.ts.map +1 -0
- package/dist/utils/anatomy.js +41 -0
- package/dist/utils/aria-hidden.d.ts +12 -0
- package/dist/utils/aria-hidden.d.ts.map +1 -0
- package/dist/utils/aria-hidden.js +72 -0
- package/dist/utils/dismissable.d.ts +25 -0
- package/dist/utils/dismissable.d.ts.map +1 -0
- package/dist/utils/dismissable.js +65 -0
- package/dist/utils/dom.d.ts +8 -0
- package/dist/utils/dom.d.ts.map +1 -0
- package/dist/utils/dom.js +21 -0
- package/dist/utils/floating.d.ts +44 -0
- package/dist/utils/floating.d.ts.map +1 -0
- package/dist/utils/floating.js +44 -0
- package/dist/utils/focus-trap.d.ts +18 -0
- package/dist/utils/focus-trap.d.ts.map +1 -0
- package/dist/utils/focus-trap.js +85 -0
- package/dist/utils/focusables.d.ts +6 -0
- package/dist/utils/focusables.d.ts.map +1 -0
- package/dist/utils/focusables.js +65 -0
- package/dist/utils/index.d.ts +18 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +10 -0
- package/dist/utils/interact-outside.d.ts +26 -0
- package/dist/utils/interact-outside.d.ts.map +1 -0
- package/dist/utils/interact-outside.js +46 -0
- package/dist/utils/remove-scroll.d.ts +8 -0
- package/dist/utils/remove-scroll.d.ts.map +1 -0
- package/dist/utils/remove-scroll.js +37 -0
- package/dist/utils/tree-collection.d.ts +61 -0
- package/dist/utils/tree-collection.d.ts.map +1 -0
- package/dist/utils/tree-collection.js +137 -0
- package/dist/utils/typeahead.d.ts +49 -0
- package/dist/utils/typeahead.d.ts.map +1 -0
- package/dist/utils/typeahead.js +81 -0
- package/package.json +282 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hide sibling subtrees from assistive tech while an overlay is open.
|
|
3
|
+
*
|
|
4
|
+
* Walks from `target` up to the document root, applying `aria-hidden="true"`
|
|
5
|
+
* and `inert` to every sibling at each level. Previous attribute values are
|
|
6
|
+
* recorded and restored on cleanup.
|
|
7
|
+
*
|
|
8
|
+
* Nested calls are supported — each layer only touches elements that haven't
|
|
9
|
+
* been claimed by a higher layer (tracked via a WeakMap reference count).
|
|
10
|
+
*/
|
|
11
|
+
const ownership = new WeakMap();
|
|
12
|
+
const snapshots = new WeakMap();
|
|
13
|
+
export function setAriaHiddenOutside(target) {
|
|
14
|
+
if (typeof document === 'undefined')
|
|
15
|
+
return () => { };
|
|
16
|
+
const claimed = [];
|
|
17
|
+
walkSiblings(target, (sibling) => {
|
|
18
|
+
const count = ownership.get(sibling) ?? 0;
|
|
19
|
+
if (count === 0) {
|
|
20
|
+
snapshots.set(sibling, {
|
|
21
|
+
ariaHidden: sibling.getAttribute('aria-hidden'),
|
|
22
|
+
inert: sibling.getAttribute('inert'),
|
|
23
|
+
});
|
|
24
|
+
sibling.setAttribute('aria-hidden', 'true');
|
|
25
|
+
sibling.setAttribute('inert', '');
|
|
26
|
+
}
|
|
27
|
+
ownership.set(sibling, count + 1);
|
|
28
|
+
claimed.push(sibling);
|
|
29
|
+
});
|
|
30
|
+
return () => {
|
|
31
|
+
for (const el of claimed) {
|
|
32
|
+
const count = (ownership.get(el) ?? 1) - 1;
|
|
33
|
+
if (count <= 0) {
|
|
34
|
+
ownership.delete(el);
|
|
35
|
+
const snap = snapshots.get(el);
|
|
36
|
+
snapshots.delete(el);
|
|
37
|
+
if (snap) {
|
|
38
|
+
if (snap.ariaHidden === null)
|
|
39
|
+
el.removeAttribute('aria-hidden');
|
|
40
|
+
else
|
|
41
|
+
el.setAttribute('aria-hidden', snap.ariaHidden);
|
|
42
|
+
if (snap.inert === null)
|
|
43
|
+
el.removeAttribute('inert');
|
|
44
|
+
else
|
|
45
|
+
el.setAttribute('inert', snap.inert);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
ownership.set(el, count);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function walkSiblings(target, visit) {
|
|
55
|
+
let node = target;
|
|
56
|
+
while (node && node !== document.body && node !== document.documentElement) {
|
|
57
|
+
const parent = node.parentElement;
|
|
58
|
+
if (!parent)
|
|
59
|
+
break;
|
|
60
|
+
const siblings = Array.from(parent.children);
|
|
61
|
+
for (const child of siblings) {
|
|
62
|
+
if (child !== node && !shouldSkip(child)) {
|
|
63
|
+
visit(child);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
node = parent;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function shouldSkip(el) {
|
|
70
|
+
const tag = el.tagName.toLowerCase();
|
|
71
|
+
return tag === 'script' || tag === 'style' || tag === 'link' || tag === 'meta' || tag === 'title';
|
|
72
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ElementSource } from './dom';
|
|
2
|
+
/**
|
|
3
|
+
* Reason a dismissable layer was closed.
|
|
4
|
+
*/
|
|
5
|
+
export type DismissSource = 'escape' | 'outside';
|
|
6
|
+
export interface DismissableOptions {
|
|
7
|
+
/** The layer element (e.g. a dialog content or popover). */
|
|
8
|
+
element: ElementSource;
|
|
9
|
+
/** Trigger / anchor elements that should not count as outside interactions. */
|
|
10
|
+
ignore?: ElementSource;
|
|
11
|
+
/** Called when the user dismisses the layer. */
|
|
12
|
+
onDismiss: (source: DismissSource, event: Event) => void;
|
|
13
|
+
/** Disable outside-click dismissal (default: false). */
|
|
14
|
+
disableOutside?: boolean;
|
|
15
|
+
/** Disable Escape-key dismissal (default: false). */
|
|
16
|
+
disableEscape?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Register a dismissable layer. Handles Escape (topmost only) and
|
|
20
|
+
* outside-click. Returns a cleanup that removes the layer from the stack.
|
|
21
|
+
*/
|
|
22
|
+
export declare function pushDismissable(opts: DismissableOptions): () => void;
|
|
23
|
+
/** @internal — for tests */
|
|
24
|
+
export declare function _dismissableStackSize(): number;
|
|
25
|
+
//# sourceMappingURL=dismissable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dismissable.d.ts","sourceRoot":"","sources":["../../src/utils/dismissable.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAE1C;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,SAAS,CAAA;AAEhD,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,OAAO,EAAE,aAAa,CAAA;IACtB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,aAAa,CAAA;IACtB,gDAAgD;IAChD,SAAS,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IACxD,wDAAwD;IACxD,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,qDAAqD;IACrD,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAgCD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,IAAI,CA6BpE;AAED,4BAA4B;AAC5B,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { watchInteractOutside } from './interact-outside';
|
|
2
|
+
// Global stack — topmost layer gets to process events first. Only the
|
|
3
|
+
// topmost claims the escape key.
|
|
4
|
+
const stack = [];
|
|
5
|
+
let keyListenerAttached = false;
|
|
6
|
+
function handleKeydown(event) {
|
|
7
|
+
if (event.key !== 'Escape')
|
|
8
|
+
return;
|
|
9
|
+
const top = stack[stack.length - 1];
|
|
10
|
+
if (!top)
|
|
11
|
+
return;
|
|
12
|
+
const claimed = top.handleEscape(event);
|
|
13
|
+
if (claimed)
|
|
14
|
+
event.stopPropagation();
|
|
15
|
+
}
|
|
16
|
+
function ensureKeyListener() {
|
|
17
|
+
if (keyListenerAttached || typeof document === 'undefined')
|
|
18
|
+
return;
|
|
19
|
+
document.addEventListener('keydown', handleKeydown, true);
|
|
20
|
+
keyListenerAttached = true;
|
|
21
|
+
}
|
|
22
|
+
function maybeRemoveKeyListener() {
|
|
23
|
+
if (stack.length > 0 || !keyListenerAttached || typeof document === 'undefined')
|
|
24
|
+
return;
|
|
25
|
+
document.removeEventListener('keydown', handleKeydown, true);
|
|
26
|
+
keyListenerAttached = false;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Register a dismissable layer. Handles Escape (topmost only) and
|
|
30
|
+
* outside-click. Returns a cleanup that removes the layer from the stack.
|
|
31
|
+
*/
|
|
32
|
+
export function pushDismissable(opts) {
|
|
33
|
+
ensureKeyListener();
|
|
34
|
+
const layer = {
|
|
35
|
+
element: opts.element,
|
|
36
|
+
handleEscape(event) {
|
|
37
|
+
if (opts.disableEscape)
|
|
38
|
+
return false;
|
|
39
|
+
opts.onDismiss('escape', event);
|
|
40
|
+
return true;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
stack.push(layer);
|
|
44
|
+
let cleanupOutside = null;
|
|
45
|
+
if (!opts.disableOutside) {
|
|
46
|
+
cleanupOutside = watchInteractOutside({
|
|
47
|
+
element: opts.element,
|
|
48
|
+
ignore: opts.ignore,
|
|
49
|
+
shouldDispatch: () => stack[stack.length - 1] === layer,
|
|
50
|
+
onInteractOutside: (event) => opts.onDismiss('outside', event),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return () => {
|
|
54
|
+
const idx = stack.indexOf(layer);
|
|
55
|
+
if (idx !== -1)
|
|
56
|
+
stack.splice(idx, 1);
|
|
57
|
+
if (cleanupOutside)
|
|
58
|
+
cleanupOutside();
|
|
59
|
+
maybeRemoveKeyListener();
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** @internal — for tests */
|
|
63
|
+
export function _dismissableStackSize() {
|
|
64
|
+
return stack.length;
|
|
65
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared DOM helpers used by interaction utilities.
|
|
3
|
+
*/
|
|
4
|
+
export type ElementSource<T extends Element = Element> = T | T[] | (() => T | T[] | null);
|
|
5
|
+
export declare function resolveElements<T extends Element>(source: ElementSource<T>): T[];
|
|
6
|
+
export declare function containsOrEquals(container: Element, target: Node | null): boolean;
|
|
7
|
+
export declare function isInAnyElement(target: Node | null, elements: Element[]): boolean;
|
|
8
|
+
//# sourceMappingURL=dom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../../src/utils/dom.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,CAAA;AAEzF,wBAAgB,eAAe,CAAC,CAAC,SAAS,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAIhF;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO,CAGjF;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,CAKhF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared DOM helpers used by interaction utilities.
|
|
3
|
+
*/
|
|
4
|
+
export function resolveElements(source) {
|
|
5
|
+
const resolved = typeof source === 'function' ? source() : source;
|
|
6
|
+
if (!resolved)
|
|
7
|
+
return [];
|
|
8
|
+
return Array.isArray(resolved) ? resolved : [resolved];
|
|
9
|
+
}
|
|
10
|
+
export function containsOrEquals(container, target) {
|
|
11
|
+
if (!target)
|
|
12
|
+
return false;
|
|
13
|
+
return container === target || container.contains(target);
|
|
14
|
+
}
|
|
15
|
+
export function isInAnyElement(target, elements) {
|
|
16
|
+
for (const el of elements) {
|
|
17
|
+
if (containsOrEquals(el, target))
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type Placement } from '@floating-ui/dom';
|
|
2
|
+
/**
|
|
3
|
+
* Thin wrapper around `@floating-ui/dom` for anchored positioning. Used by
|
|
4
|
+
* popover, tooltip, menu, and any other component that attaches a floating
|
|
5
|
+
* element to an anchor.
|
|
6
|
+
*
|
|
7
|
+
* Returns a cleanup function that removes scroll/resize listeners and stops
|
|
8
|
+
* position updates.
|
|
9
|
+
*/
|
|
10
|
+
export type { Placement };
|
|
11
|
+
export interface FloatingOptions {
|
|
12
|
+
/** The reference element (trigger/anchor). */
|
|
13
|
+
anchor: Element;
|
|
14
|
+
/** The floating element (content). */
|
|
15
|
+
floating: HTMLElement;
|
|
16
|
+
/** Preferred placement (default: 'bottom'). */
|
|
17
|
+
placement?: Placement;
|
|
18
|
+
/** Gap between anchor and floating, in px (default: 0). */
|
|
19
|
+
offset?: number;
|
|
20
|
+
/** Flip to opposite side when there isn't enough room (default: true). */
|
|
21
|
+
flip?: boolean;
|
|
22
|
+
/** Shift along axis to stay in view (default: padding 8 unless false). */
|
|
23
|
+
shift?: boolean | {
|
|
24
|
+
padding?: number;
|
|
25
|
+
};
|
|
26
|
+
/** Optional arrow element to position. */
|
|
27
|
+
arrow?: HTMLElement;
|
|
28
|
+
/** Notify after each position computation. */
|
|
29
|
+
onUpdate?: (data: {
|
|
30
|
+
x: number;
|
|
31
|
+
y: number;
|
|
32
|
+
placement: Placement;
|
|
33
|
+
arrow?: {
|
|
34
|
+
x?: number;
|
|
35
|
+
y?: number;
|
|
36
|
+
};
|
|
37
|
+
}) => void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Position `floating` relative to `anchor` with live updates on scroll/resize.
|
|
41
|
+
* Applies `left` + `top` styles to the floating element. Returns a cleanup.
|
|
42
|
+
*/
|
|
43
|
+
export declare function attachFloating(opts: FloatingOptions): () => void;
|
|
44
|
+
//# sourceMappingURL=floating.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floating.d.ts","sourceRoot":"","sources":["../../src/utils/floating.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EAEf,MAAM,kBAAkB,CAAA;AAEzB;;;;;;;GAOG;AAEH,YAAY,EAAE,SAAS,EAAE,CAAA;AAEzB,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,MAAM,EAAE,OAAO,CAAA;IACf,sCAAsC;IACtC,QAAQ,EAAE,WAAW,CAAA;IACrB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0EAA0E;IAC1E,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,0EAA0E;IAC1E,KAAK,CAAC,EAAE,OAAO,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACtC,0CAA0C;IAC1C,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE;QAChB,CAAC,EAAE,MAAM,CAAA;QACT,CAAC,EAAE,MAAM,CAAA;QACT,SAAS,EAAE,SAAS,CAAA;QACpB,KAAK,CAAC,EAAE;YAAE,CAAC,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KACnC,KAAK,IAAI,CAAA;CACX;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,IAAI,CAgDhE"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { computePosition, autoUpdate, offset as offsetMw, flip as flipMw, shift as shiftMw, arrow as arrowMw, } from '@floating-ui/dom';
|
|
2
|
+
/**
|
|
3
|
+
* Position `floating` relative to `anchor` with live updates on scroll/resize.
|
|
4
|
+
* Applies `left` + `top` styles to the floating element. Returns a cleanup.
|
|
5
|
+
*/
|
|
6
|
+
export function attachFloating(opts) {
|
|
7
|
+
const { anchor, floating, placement = 'bottom', offset = 0, flip = true, shift = true, arrow, onUpdate, } = opts;
|
|
8
|
+
const middleware = [];
|
|
9
|
+
if (offset > 0)
|
|
10
|
+
middleware.push(offsetMw(offset));
|
|
11
|
+
if (flip)
|
|
12
|
+
middleware.push(flipMw());
|
|
13
|
+
if (shift !== false) {
|
|
14
|
+
const padding = typeof shift === 'object' ? (shift.padding ?? 8) : 8;
|
|
15
|
+
middleware.push(shiftMw({ padding }));
|
|
16
|
+
}
|
|
17
|
+
if (arrow)
|
|
18
|
+
middleware.push(arrowMw({ element: arrow }));
|
|
19
|
+
floating.style.position = 'absolute';
|
|
20
|
+
floating.style.top = '0';
|
|
21
|
+
floating.style.left = '0';
|
|
22
|
+
const update = () => {
|
|
23
|
+
void computePosition(anchor, floating, { placement, middleware }).then(({ x, y, placement: actual, middlewareData }) => {
|
|
24
|
+
floating.style.transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`;
|
|
25
|
+
floating.dataset.placement = actual;
|
|
26
|
+
if (arrow && middlewareData.arrow) {
|
|
27
|
+
const { x: ax, y: ay } = middlewareData.arrow;
|
|
28
|
+
if (ax != null)
|
|
29
|
+
arrow.style.left = `${ax}px`;
|
|
30
|
+
if (ay != null)
|
|
31
|
+
arrow.style.top = `${ay}px`;
|
|
32
|
+
}
|
|
33
|
+
onUpdate?.({
|
|
34
|
+
x,
|
|
35
|
+
y,
|
|
36
|
+
placement: actual,
|
|
37
|
+
arrow: middlewareData.arrow
|
|
38
|
+
? { x: middlewareData.arrow.x, y: middlewareData.arrow.y }
|
|
39
|
+
: undefined,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
return autoUpdate(anchor, floating, update);
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ElementSource } from './dom';
|
|
2
|
+
export interface FocusTrapOptions {
|
|
3
|
+
/** The container whose focusable descendants form the trap. */
|
|
4
|
+
container: ElementSource;
|
|
5
|
+
/** Element to focus when the trap activates. Defaults to first focusable. */
|
|
6
|
+
initialFocus?: Element | (() => Element | null);
|
|
7
|
+
/** Restore focus to the previously active element on release (default: true). */
|
|
8
|
+
restoreFocus?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Push a focus trap onto the stack. Tab/Shift+Tab will cycle within the
|
|
12
|
+
* container's focusable descendants. Returns a cleanup that removes the
|
|
13
|
+
* trap and (optionally) restores focus to the element active before push.
|
|
14
|
+
*/
|
|
15
|
+
export declare function pushFocusTrap(opts: FocusTrapOptions): () => void;
|
|
16
|
+
/** @internal — tests only */
|
|
17
|
+
export declare function _focusTrapStackSize(): number;
|
|
18
|
+
//# sourceMappingURL=focus-trap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focus-trap.d.ts","sourceRoot":"","sources":["../../src/utils/focus-trap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAA;AAG3D,MAAM,WAAW,gBAAgB;IAC/B,+DAA+D;IAC/D,SAAS,EAAE,aAAa,CAAA;IACxB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,GAAG,IAAI,CAAC,CAAA;IAC/C,iFAAiF;IACjF,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AAsDD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,IAAI,CA2BhE;AAED,6BAA6B;AAC7B,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { resolveElements } from './dom';
|
|
2
|
+
import { getFocusables } from './focusables';
|
|
3
|
+
const stack = [];
|
|
4
|
+
let keyListenerAttached = false;
|
|
5
|
+
function handleKeydown(event) {
|
|
6
|
+
if (event.key !== 'Tab')
|
|
7
|
+
return;
|
|
8
|
+
const top = stack[stack.length - 1];
|
|
9
|
+
if (!top)
|
|
10
|
+
return;
|
|
11
|
+
const containers = resolveElements(top.container);
|
|
12
|
+
if (containers.length === 0)
|
|
13
|
+
return;
|
|
14
|
+
// Combine focusables from all container elements.
|
|
15
|
+
const focusables = [];
|
|
16
|
+
for (const c of containers)
|
|
17
|
+
focusables.push(...getFocusables(c));
|
|
18
|
+
if (focusables.length === 0) {
|
|
19
|
+
event.preventDefault();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const first = focusables[0];
|
|
23
|
+
const last = focusables[focusables.length - 1];
|
|
24
|
+
const active = document.activeElement;
|
|
25
|
+
const isInside = active ? containers.some((c) => c.contains(active)) : false;
|
|
26
|
+
if (event.shiftKey) {
|
|
27
|
+
if (!isInside || active === first) {
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
last.focus();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
if (!isInside || active === last) {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
first.focus();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function ensureListener() {
|
|
40
|
+
if (keyListenerAttached || typeof document === 'undefined')
|
|
41
|
+
return;
|
|
42
|
+
document.addEventListener('keydown', handleKeydown, true);
|
|
43
|
+
keyListenerAttached = true;
|
|
44
|
+
}
|
|
45
|
+
function maybeRemoveListener() {
|
|
46
|
+
if (stack.length > 0 || !keyListenerAttached || typeof document === 'undefined')
|
|
47
|
+
return;
|
|
48
|
+
document.removeEventListener('keydown', handleKeydown, true);
|
|
49
|
+
keyListenerAttached = false;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Push a focus trap onto the stack. Tab/Shift+Tab will cycle within the
|
|
53
|
+
* container's focusable descendants. Returns a cleanup that removes the
|
|
54
|
+
* trap and (optionally) restores focus to the element active before push.
|
|
55
|
+
*/
|
|
56
|
+
export function pushFocusTrap(opts) {
|
|
57
|
+
const restoreFocus = opts.restoreFocus !== false;
|
|
58
|
+
const previouslyFocused = restoreFocus ? document.activeElement : null;
|
|
59
|
+
ensureListener();
|
|
60
|
+
const trap = { container: opts.container };
|
|
61
|
+
stack.push(trap);
|
|
62
|
+
// Move focus inside the trap
|
|
63
|
+
const containers = resolveElements(opts.container);
|
|
64
|
+
const initial = typeof opts.initialFocus === 'function' ? opts.initialFocus() : (opts.initialFocus ?? null);
|
|
65
|
+
if (initial && initial instanceof HTMLElement) {
|
|
66
|
+
initial.focus();
|
|
67
|
+
}
|
|
68
|
+
else if (containers.length > 0) {
|
|
69
|
+
const focusables = getFocusables(containers[0]);
|
|
70
|
+
focusables[0]?.focus();
|
|
71
|
+
}
|
|
72
|
+
return () => {
|
|
73
|
+
const idx = stack.indexOf(trap);
|
|
74
|
+
if (idx !== -1)
|
|
75
|
+
stack.splice(idx, 1);
|
|
76
|
+
maybeRemoveListener();
|
|
77
|
+
if (restoreFocus && previouslyFocused && typeof previouslyFocused.focus === 'function') {
|
|
78
|
+
previouslyFocused.focus();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** @internal — tests only */
|
|
83
|
+
export function _focusTrapStackSize() {
|
|
84
|
+
return stack.length;
|
|
85
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focusables.d.ts","sourceRoot":"","sources":["../../src/utils/focusables.ts"],"names":[],"mappings":"AAAA;;GAEG;AAqBH,wBAAgB,WAAW,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAShD;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,OAAO,GAAG,WAAW,EAAE,CAO/D"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find focusable descendants within a container.
|
|
3
|
+
*/
|
|
4
|
+
// Matches elements that can receive keyboard focus. Excludes elements with
|
|
5
|
+
// `tabindex=-1` (programmatically focusable but not tab-reachable) and
|
|
6
|
+
// elements inside `inert` subtrees.
|
|
7
|
+
const FOCUSABLE_SELECTOR = [
|
|
8
|
+
'a[href]',
|
|
9
|
+
'area[href]',
|
|
10
|
+
'button:not([disabled])',
|
|
11
|
+
'input:not([disabled]):not([type="hidden"])',
|
|
12
|
+
'select:not([disabled])',
|
|
13
|
+
'textarea:not([disabled])',
|
|
14
|
+
'iframe',
|
|
15
|
+
'object',
|
|
16
|
+
'embed',
|
|
17
|
+
'audio[controls]',
|
|
18
|
+
'video[controls]',
|
|
19
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
20
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
21
|
+
].join(',');
|
|
22
|
+
export function isFocusable(el) {
|
|
23
|
+
if (!(el instanceof HTMLElement))
|
|
24
|
+
return false;
|
|
25
|
+
if (el.hasAttribute('disabled'))
|
|
26
|
+
return false;
|
|
27
|
+
if (el.getAttribute('aria-hidden') === 'true')
|
|
28
|
+
return false;
|
|
29
|
+
if (el.hidden)
|
|
30
|
+
return false;
|
|
31
|
+
// Check tabindex
|
|
32
|
+
const tabIndex = el.getAttribute('tabindex');
|
|
33
|
+
if (tabIndex !== null && parseInt(tabIndex, 10) < 0)
|
|
34
|
+
return false;
|
|
35
|
+
return el.matches(FOCUSABLE_SELECTOR);
|
|
36
|
+
}
|
|
37
|
+
export function getFocusables(container) {
|
|
38
|
+
const nodes = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const n of nodes) {
|
|
41
|
+
if (isVisible(n) && !isInsideInert(n, container))
|
|
42
|
+
out.push(n);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function isVisible(el) {
|
|
47
|
+
if (el.hidden)
|
|
48
|
+
return false;
|
|
49
|
+
// offsetParent is null for display:none (not position:fixed roots, but good enough)
|
|
50
|
+
// jsdom returns null for disconnected; skip this check there.
|
|
51
|
+
if (typeof el.offsetParent === 'undefined')
|
|
52
|
+
return true;
|
|
53
|
+
// For root elements with position:fixed, offsetParent can be null but they're visible.
|
|
54
|
+
// Use getClientRects as a fallback. jsdom returns empty rects, so accept those.
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
function isInsideInert(el, container) {
|
|
58
|
+
let current = el;
|
|
59
|
+
while (current && current !== container) {
|
|
60
|
+
if (current.hasAttribute('inert'))
|
|
61
|
+
return true;
|
|
62
|
+
current = current.parentElement;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { anatomy, resetAnatomyIdCounter } from './anatomy';
|
|
2
|
+
export type { Anatomy, AnatomyScope } from './anatomy';
|
|
3
|
+
export { watchInteractOutside } from './interact-outside';
|
|
4
|
+
export type { InteractOutsideOptions } from './interact-outside';
|
|
5
|
+
export { pushDismissable } from './dismissable';
|
|
6
|
+
export type { DismissableOptions, DismissSource } from './dismissable';
|
|
7
|
+
export { pushFocusTrap } from './focus-trap';
|
|
8
|
+
export type { FocusTrapOptions } from './focus-trap';
|
|
9
|
+
export { setAriaHiddenOutside } from './aria-hidden';
|
|
10
|
+
export { lockBodyScroll } from './remove-scroll';
|
|
11
|
+
export { getFocusables, isFocusable } from './focusables';
|
|
12
|
+
export type { ElementSource } from './dom';
|
|
13
|
+
export { attachFloating } from './floating';
|
|
14
|
+
export type { FloatingOptions, Placement } from './floating';
|
|
15
|
+
export { typeaheadAccumulate, typeaheadMatch, typeaheadMatchByItems, isTypeaheadKey, TYPEAHEAD_TIMEOUT_MS, } from './typeahead';
|
|
16
|
+
export { TreeCollection } from './tree-collection';
|
|
17
|
+
export type { TreeNode } from './tree-collection';
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAA;AAC1D,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAEtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAEhE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAC/C,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAEtE,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAEpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AACzD,YAAY,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAE1C,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE5D,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,qBAAqB,EACrB,cAAc,EACd,oBAAoB,GACrB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,YAAY,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { anatomy, resetAnatomyIdCounter } from './anatomy';
|
|
2
|
+
export { watchInteractOutside } from './interact-outside';
|
|
3
|
+
export { pushDismissable } from './dismissable';
|
|
4
|
+
export { pushFocusTrap } from './focus-trap';
|
|
5
|
+
export { setAriaHiddenOutside } from './aria-hidden';
|
|
6
|
+
export { lockBodyScroll } from './remove-scroll';
|
|
7
|
+
export { getFocusables, isFocusable } from './focusables';
|
|
8
|
+
export { attachFloating } from './floating';
|
|
9
|
+
export { typeaheadAccumulate, typeaheadMatch, typeaheadMatchByItems, isTypeaheadKey, TYPEAHEAD_TIMEOUT_MS, } from './typeahead';
|
|
10
|
+
export { TreeCollection } from './tree-collection';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ElementSource } from './dom';
|
|
2
|
+
export interface InteractOutsideOptions {
|
|
3
|
+
/** Element(s) that define the "inside" region. */
|
|
4
|
+
element: ElementSource;
|
|
5
|
+
/** Additional elements whose interactions should not count as outside (e.g. triggers). */
|
|
6
|
+
ignore?: ElementSource;
|
|
7
|
+
/** Called on pointerdown or focus outside the inside region. */
|
|
8
|
+
onInteractOutside: (event: Event) => void;
|
|
9
|
+
/**
|
|
10
|
+
* If provided, called first with the event. Return `false` to suppress the
|
|
11
|
+
* outside callback (for an in-flight layer to claim the event).
|
|
12
|
+
*/
|
|
13
|
+
shouldDispatch?: (event: Event) => boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Watch for pointer or focus events outside a given element. Returns a
|
|
17
|
+
* cleanup function. Uses the capture phase so upstream `stopPropagation`
|
|
18
|
+
* calls cannot hide events.
|
|
19
|
+
*
|
|
20
|
+
* - pointerdown (or mousedown/touchstart fallback) triggers "outside" if the
|
|
21
|
+
* target is not contained by `element` or `ignore`.
|
|
22
|
+
* - focusin triggers "outside" when focus moves outside the element, except
|
|
23
|
+
* when the new target is in `ignore`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function watchInteractOutside(opts: InteractOutsideOptions): () => void;
|
|
26
|
+
//# sourceMappingURL=interact-outside.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interact-outside.d.ts","sourceRoot":"","sources":["../../src/utils/interact-outside.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,KAAK,aAAa,EAAE,MAAM,OAAO,CAAA;AAE3E,MAAM,WAAW,sBAAsB;IACrC,kDAAkD;IAClD,OAAO,EAAE,aAAa,CAAA;IACtB,0FAA0F;IAC1F,MAAM,CAAC,EAAE,aAAa,CAAA;IACtB,gEAAgE;IAChE,iBAAiB,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IACzC;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAA;CAC3C;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,sBAAsB,GAAG,MAAM,IAAI,CA+B7E"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { resolveElements, isInAnyElement } from './dom';
|
|
2
|
+
/**
|
|
3
|
+
* Watch for pointer or focus events outside a given element. Returns a
|
|
4
|
+
* cleanup function. Uses the capture phase so upstream `stopPropagation`
|
|
5
|
+
* calls cannot hide events.
|
|
6
|
+
*
|
|
7
|
+
* - pointerdown (or mousedown/touchstart fallback) triggers "outside" if the
|
|
8
|
+
* target is not contained by `element` or `ignore`.
|
|
9
|
+
* - focusin triggers "outside" when focus moves outside the element, except
|
|
10
|
+
* when the new target is in `ignore`.
|
|
11
|
+
*/
|
|
12
|
+
export function watchInteractOutside(opts) {
|
|
13
|
+
if (typeof document === 'undefined')
|
|
14
|
+
return () => { };
|
|
15
|
+
const handlePointer = (event) => {
|
|
16
|
+
if (opts.shouldDispatch && !opts.shouldDispatch(event))
|
|
17
|
+
return;
|
|
18
|
+
const target = event.target;
|
|
19
|
+
const inside = resolveElements(opts.element);
|
|
20
|
+
if (isInAnyElement(target, inside))
|
|
21
|
+
return;
|
|
22
|
+
const ignore = opts.ignore ? resolveElements(opts.ignore) : [];
|
|
23
|
+
if (isInAnyElement(target, ignore))
|
|
24
|
+
return;
|
|
25
|
+
opts.onInteractOutside(event);
|
|
26
|
+
};
|
|
27
|
+
const handleFocus = (event) => {
|
|
28
|
+
if (opts.shouldDispatch && !opts.shouldDispatch(event))
|
|
29
|
+
return;
|
|
30
|
+
const target = event.target;
|
|
31
|
+
const inside = resolveElements(opts.element);
|
|
32
|
+
if (isInAnyElement(target, inside))
|
|
33
|
+
return;
|
|
34
|
+
const ignore = opts.ignore ? resolveElements(opts.ignore) : [];
|
|
35
|
+
if (isInAnyElement(target, ignore))
|
|
36
|
+
return;
|
|
37
|
+
opts.onInteractOutside(event);
|
|
38
|
+
};
|
|
39
|
+
// Prefer pointerdown (unified); fall back already covered by pointer events.
|
|
40
|
+
document.addEventListener('pointerdown', handlePointer, true);
|
|
41
|
+
document.addEventListener('focusin', handleFocus, true);
|
|
42
|
+
return () => {
|
|
43
|
+
document.removeEventListener('pointerdown', handlePointer, true);
|
|
44
|
+
document.removeEventListener('focusin', handleFocus, true);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock body scroll while an overlay is open, preserving scrollbar width to
|
|
3
|
+
* avoid layout shift. Reference-counted so nested locks compose cleanly.
|
|
4
|
+
*/
|
|
5
|
+
export declare function lockBodyScroll(): () => void;
|
|
6
|
+
/** @internal — tests only */
|
|
7
|
+
export declare function _scrollLockCount(): number;
|
|
8
|
+
//# sourceMappingURL=remove-scroll.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remove-scroll.d.ts","sourceRoot":"","sources":["../../src/utils/remove-scroll.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAUH,wBAAgB,cAAc,IAAI,MAAM,IAAI,CAyB3C;AAED,6BAA6B;AAC7B,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock body scroll while an overlay is open, preserving scrollbar width to
|
|
3
|
+
* avoid layout shift. Reference-counted so nested locks compose cleanly.
|
|
4
|
+
*/
|
|
5
|
+
let lockCount = 0;
|
|
6
|
+
let snapshot = null;
|
|
7
|
+
export function lockBodyScroll() {
|
|
8
|
+
if (typeof document === 'undefined')
|
|
9
|
+
return () => { };
|
|
10
|
+
lockCount++;
|
|
11
|
+
if (lockCount === 1) {
|
|
12
|
+
const body = document.body;
|
|
13
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
14
|
+
snapshot = {
|
|
15
|
+
bodyOverflow: body.style.overflow,
|
|
16
|
+
bodyPaddingRight: body.style.paddingRight,
|
|
17
|
+
};
|
|
18
|
+
body.style.overflow = 'hidden';
|
|
19
|
+
if (scrollbarWidth > 0) {
|
|
20
|
+
const current = parseInt(getComputedStyle(body).paddingRight || '0', 10) || 0;
|
|
21
|
+
body.style.paddingRight = `${current + scrollbarWidth}px`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return () => {
|
|
25
|
+
lockCount = Math.max(0, lockCount - 1);
|
|
26
|
+
if (lockCount === 0 && snapshot) {
|
|
27
|
+
const body = document.body;
|
|
28
|
+
body.style.overflow = snapshot.bodyOverflow;
|
|
29
|
+
body.style.paddingRight = snapshot.bodyPaddingRight;
|
|
30
|
+
snapshot = null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** @internal — tests only */
|
|
35
|
+
export function _scrollLockCount() {
|
|
36
|
+
return lockCount;
|
|
37
|
+
}
|