@pzerelles/headlessui-svelte 2.0.0-next.1 → 2.1.1-next.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/dist/button/Button.svelte +65 -0
- package/dist/button/Button.svelte.d.ts +39 -0
- package/dist/button/index.d.ts +1 -0
- package/dist/button/index.js +1 -0
- package/dist/checkbox/Checkbox.svelte +60 -46
- package/dist/checkbox/Checkbox.svelte.d.ts +1 -1
- package/dist/close-button/CloseButton.svelte +10 -0
- package/dist/close-button/CloseButton.svelte.d.ts +25 -0
- package/dist/close-button/index.d.ts +1 -0
- package/dist/close-button/index.js +1 -0
- package/dist/combobox/Combobox.svelte +6 -0
- package/dist/combobox/Combobox.svelte.d.ts +50 -0
- package/dist/description/Description.svelte +50 -32
- package/dist/description/Description.svelte.d.ts +14 -5
- package/dist/field/Field.svelte +9 -9
- package/dist/fieldset/Fieldset.svelte +9 -9
- package/dist/hooks/document-overflow/adjust-scrollbar-padding.d.ts +2 -0
- package/dist/hooks/document-overflow/adjust-scrollbar-padding.js +18 -0
- package/dist/hooks/document-overflow/handle-ios-locking.d.ts +6 -0
- package/dist/hooks/document-overflow/handle-ios-locking.js +134 -0
- package/dist/hooks/document-overflow/overflow-store.d.ts +19 -0
- package/dist/hooks/document-overflow/overflow-store.js +76 -0
- package/dist/hooks/document-overflow/prevent-scroll.d.ts +2 -0
- package/dist/hooks/document-overflow/prevent-scroll.js +7 -0
- package/dist/hooks/document-overflow/use-document-overflow.svelte.d.ts +7 -0
- package/dist/hooks/document-overflow/use-document-overflow.svelte.js +27 -0
- package/dist/hooks/use-active-press.svelte.d.ts +14 -0
- package/dist/{actions/activePress.svelte.js → hooks/use-active-press.svelte.js} +33 -39
- package/dist/hooks/use-by-comparator.d.ts +2 -0
- package/dist/hooks/use-by-comparator.js +15 -0
- package/dist/hooks/use-controllable.svelte.d.ts +6 -0
- package/dist/hooks/use-controllable.svelte.js +34 -0
- package/dist/hooks/use-did-element-move.svelte.d.ts +6 -0
- package/dist/hooks/use-did-element-move.svelte.js +27 -0
- package/dist/hooks/use-disabled.d.ts +3 -0
- package/dist/hooks/use-disabled.js +9 -0
- package/dist/hooks/use-element-size.svelte.d.ts +7 -0
- package/dist/hooks/use-element-size.svelte.js +36 -0
- package/dist/hooks/use-flags.svelte.d.ts +8 -0
- package/dist/hooks/use-flags.svelte.js +18 -0
- package/dist/hooks/use-focus-ring.svelte.d.ts +10 -0
- package/dist/hooks/use-focus-ring.svelte.js +24 -0
- package/dist/hooks/use-hover.svelte.d.ts +26 -0
- package/dist/hooks/use-hover.svelte.js +124 -0
- package/dist/hooks/use-id.d.ts +1 -0
- package/dist/hooks/use-id.js +1 -0
- package/dist/hooks/use-inert-others.svelte.d.ts +32 -0
- package/dist/hooks/use-inert-others.svelte.js +114 -0
- package/dist/hooks/use-is-top-layer.svelte.d.ts +29 -0
- package/dist/hooks/use-is-top-layer.svelte.js +82 -0
- package/dist/hooks/use-on-disappear.svelte.d.ts +12 -0
- package/dist/hooks/use-on-disappear.svelte.js +38 -0
- package/dist/hooks/use-outside-click.svelte.d.ts +10 -0
- package/dist/hooks/use-outside-click.svelte.js +150 -0
- package/dist/hooks/use-reducer.d.ts +4 -0
- package/dist/hooks/use-reducer.js +11 -0
- package/dist/hooks/use-resolve-button-type.svelte.d.ts +10 -0
- package/dist/hooks/use-resolve-button-type.svelte.js +19 -0
- package/dist/hooks/use-scroll-lock.svelte.d.ts +5 -0
- package/dist/hooks/use-scroll-lock.svelte.js +24 -0
- package/dist/hooks/use-sync-refs.d.ts +7 -0
- package/dist/hooks/use-sync-refs.js +22 -0
- package/dist/hooks/use-text-value.svelte.d.ts +3 -0
- package/dist/hooks/use-text-value.svelte.js +20 -0
- package/dist/hooks/use-tracked-pointer.d.ts +4 -0
- package/dist/hooks/use-tracked-pointer.js +26 -0
- package/dist/hooks/use-transition.svelte.d.ts +20 -0
- package/dist/hooks/use-transition.svelte.js +252 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/internal/FocusSentinel.svelte +45 -0
- package/dist/internal/FocusSentinel.svelte.d.ts +17 -0
- package/dist/internal/FormFields.svelte +2 -4
- package/dist/internal/FormFields.svelte.d.ts +5 -6
- package/dist/internal/FormResolver.svelte +11 -16
- package/dist/internal/FormResolver.svelte.d.ts +2 -3
- package/dist/internal/Hidden.svelte +8 -8
- package/dist/internal/Hidden.svelte.d.ts +28 -19
- package/dist/internal/HoistFormFields.svelte.d.ts +1 -1
- package/dist/internal/Portal.svelte.d.ts +1 -1
- package/dist/internal/floating.svelte.d.ts +57 -0
- package/dist/internal/floating.svelte.js +477 -0
- package/dist/internal/frozen.svelte.d.ts +6 -0
- package/dist/internal/frozen.svelte.js +18 -0
- package/dist/internal/id.d.ts +8 -0
- package/dist/internal/id.js +11 -0
- package/dist/internal/open-closed.d.ts +14 -0
- package/dist/internal/open-closed.js +17 -0
- package/dist/internal/portal-force-root.svelte.d.ts +6 -0
- package/dist/internal/portal-force-root.svelte.js +11 -0
- package/dist/label/Label.svelte +53 -32
- package/dist/label/Label.svelte.d.ts +14 -5
- package/dist/legend/Legend.svelte.d.ts +1 -2
- package/dist/listbox/Listbox.svelte +451 -0
- package/dist/listbox/Listbox.svelte.d.ts +107 -0
- package/dist/listbox/ListboxButton.svelte +141 -0
- package/dist/listbox/ListboxButton.svelte.d.ts +41 -0
- package/dist/listbox/ListboxOption.svelte +138 -0
- package/dist/listbox/ListboxOption.svelte.d.ts +39 -0
- package/dist/listbox/ListboxOptions.svelte +267 -0
- package/dist/listbox/ListboxOptions.svelte.d.ts +39 -0
- package/dist/listbox/ListboxSelectedOption.svelte +25 -0
- package/dist/listbox/ListboxSelectedOption.svelte.d.ts +30 -0
- package/dist/listbox/index.d.ts +5 -0
- package/dist/listbox/index.js +5 -0
- package/dist/portal/InternalPortal.svelte +108 -0
- package/dist/portal/InternalPortal.svelte.d.ts +34 -0
- package/dist/portal/Portal.svelte +11 -0
- package/dist/portal/Portal.svelte.d.ts +23 -0
- package/dist/portal/PortalGroup.svelte +15 -0
- package/dist/portal/PortalGroup.svelte.d.ts +31 -0
- package/dist/switch/Switch.svelte +149 -0
- package/dist/switch/Switch.svelte.d.ts +44 -0
- package/dist/switch/SwitchGroup.svelte +38 -0
- package/dist/switch/SwitchGroup.svelte.d.ts +27 -0
- package/dist/switch/index.d.ts +2 -0
- package/dist/switch/index.js +2 -0
- package/dist/tabs/Button.svelte +65 -0
- package/dist/tabs/Button.svelte.d.ts +39 -0
- package/dist/tabs/Tab.svelte +161 -0
- package/dist/tabs/Tab.svelte.d.ts +36 -0
- package/dist/tabs/TabGroup.svelte +244 -0
- package/dist/tabs/TabGroup.svelte.d.ts +54 -0
- package/dist/tabs/TabList.svelte +18 -0
- package/dist/tabs/TabList.svelte.d.ts +28 -0
- package/dist/tabs/TabPanel.svelte +63 -0
- package/dist/tabs/TabPanel.svelte.d.ts +34 -0
- package/dist/tabs/TabPanels.svelte +13 -0
- package/dist/tabs/TabPanels.svelte.d.ts +27 -0
- package/dist/tabs/index.d.ts +5 -0
- package/dist/tabs/index.js +5 -0
- package/dist/test-utils/accessability-assertions.d.ts +271 -0
- package/dist/test-utils/accessability-assertions.js +1572 -0
- package/dist/test-utils/fake-pointer.d.ts +24 -0
- package/dist/test-utils/fake-pointer.js +48 -0
- package/dist/test-utils/interactions.d.ts +61 -0
- package/dist/test-utils/interactions.js +453 -0
- package/dist/test-utils/suppress-console-logs.d.ts +7 -0
- package/dist/test-utils/suppress-console-logs.js +17 -0
- package/dist/utils/StableCollection.svelte +43 -0
- package/dist/utils/StableCollection.svelte.d.ts +19 -0
- package/dist/utils/calculate-active-index.d.ts +25 -0
- package/dist/utils/calculate-active-index.js +74 -0
- package/dist/utils/close.d.ts +2 -0
- package/dist/utils/close.js +3 -0
- package/dist/utils/default-map.d.ts +5 -0
- package/dist/utils/default-map.js +15 -0
- package/dist/utils/disposables.d.ts +14 -12
- package/dist/utils/disposables.js +13 -10
- package/dist/utils/dom.d.ts +0 -2
- package/dist/utils/dom.js +2 -4
- package/dist/utils/env.d.ts +17 -0
- package/dist/utils/env.js +39 -0
- package/dist/utils/focus-management.d.ts +44 -0
- package/dist/utils/focus-management.js +242 -0
- package/dist/utils/focusVisible.svelte.d.ts +3 -3
- package/dist/utils/focusVisible.svelte.js +52 -41
- package/dist/utils/get-text-value.d.ts +1 -0
- package/dist/utils/get-text-value.js +71 -0
- package/dist/utils/id.d.ts +1 -1
- package/dist/utils/match.d.ts +1 -0
- package/dist/utils/match.js +13 -0
- package/dist/utils/once.d.ts +1 -0
- package/dist/utils/once.js +9 -0
- package/dist/utils/owner.d.ts +1 -0
- package/dist/utils/owner.js +8 -0
- package/dist/utils/platform.d.ts +2 -0
- package/dist/utils/platform.js +17 -0
- package/dist/utils/ref.svelte.d.ts +4 -0
- package/dist/utils/ref.svelte.js +4 -0
- package/dist/utils/render.d.ts +31 -0
- package/dist/utils/render.js +56 -0
- package/dist/utils/store.d.ts +11 -0
- package/dist/utils/store.js +20 -0
- package/dist/utils/types.d.ts +27 -0
- package/dist/utils/types.js +6 -0
- package/package.json +28 -21
- package/dist/actions/activePress.svelte.d.ts +0 -8
- package/dist/actions/focusRing.svelte.d.ts +0 -9
- package/dist/actions/focusRing.svelte.js +0 -34
- package/dist/utils/disabled.d.ts +0 -3
- package/dist/utils/disabled.js +0 -2
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
|
|
2
|
+
import { disposables } from "../utils/disposables.js";
|
|
3
|
+
let globalIgnoreEmulatedMouseEvents = false;
|
|
4
|
+
let hoverCount = 0;
|
|
5
|
+
const d = disposables();
|
|
6
|
+
function setGlobalIgnoreEmulatedMouseEvents() {
|
|
7
|
+
globalIgnoreEmulatedMouseEvents = true;
|
|
8
|
+
// Clear globalIgnoreEmulatedMouseEvents after a short timeout. iOS fires onPointerEnter
|
|
9
|
+
// with pointerType="mouse" immediately after onPointerUp and before onFocus. On other
|
|
10
|
+
// devices that don't have this quirk, we don't want to ignore a mouse hover sometime in
|
|
11
|
+
// the distant future because a user previously touched the element.
|
|
12
|
+
setTimeout(() => { });
|
|
13
|
+
}
|
|
14
|
+
function handleGlobalPointerEvent(e) {
|
|
15
|
+
if (e.pointerType === "touch") {
|
|
16
|
+
setGlobalIgnoreEmulatedMouseEvents();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function isHoverPointerType(pointerType) {
|
|
20
|
+
return pointerType === "mouse" || pointerType === "pen" || pointerType === "touch";
|
|
21
|
+
}
|
|
22
|
+
function setupGlobalTouchEvents() {
|
|
23
|
+
if (typeof document === "undefined" || !document)
|
|
24
|
+
return;
|
|
25
|
+
if (hoverCount === 0) {
|
|
26
|
+
if (typeof PointerEvent !== "undefined") {
|
|
27
|
+
d.addEventListener(document, "pointerup", handleGlobalPointerEvent);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
d.addEventListener(document, "touchend", setGlobalIgnoreEmulatedMouseEvents);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
hoverCount++;
|
|
34
|
+
return () => {
|
|
35
|
+
hoverCount--;
|
|
36
|
+
if (hoverCount === 0)
|
|
37
|
+
d.dispose();
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export const useHover = (options = {}) => {
|
|
41
|
+
const { disabled } = $derived(options);
|
|
42
|
+
const _state = $state({
|
|
43
|
+
isHovered: false,
|
|
44
|
+
ignoreEmulatedMouseEvents: false,
|
|
45
|
+
pointerType: "",
|
|
46
|
+
target: null,
|
|
47
|
+
});
|
|
48
|
+
$effect(() => setupGlobalTouchEvents());
|
|
49
|
+
$effect(() => {
|
|
50
|
+
if (disabled) {
|
|
51
|
+
_state.pointerType = "";
|
|
52
|
+
_state.target = null;
|
|
53
|
+
_state.isHovered = false;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
function triggerHoverStart(originalEvent, pointerType) {
|
|
57
|
+
_state.pointerType = pointerType ?? undefined;
|
|
58
|
+
const target = originalEvent.currentTarget;
|
|
59
|
+
if (!(target instanceof HTMLElement || target instanceof SVGElement))
|
|
60
|
+
return;
|
|
61
|
+
if (disabled || pointerType === "touch" || _state.isHovered || !target.contains(originalEvent.target)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
_state.isHovered = true;
|
|
65
|
+
_state.target = target;
|
|
66
|
+
}
|
|
67
|
+
function triggerHoverEnd(originalEvent, pointerType) {
|
|
68
|
+
_state.pointerType = "";
|
|
69
|
+
_state.target = null;
|
|
70
|
+
const currentTarget = originalEvent.currentTarget;
|
|
71
|
+
if (pointerType === "touch" || !_state.isHovered || !(currentTarget instanceof HTMLElement))
|
|
72
|
+
return;
|
|
73
|
+
_state.isHovered = false;
|
|
74
|
+
}
|
|
75
|
+
function handlePointerEnter(e) {
|
|
76
|
+
const pointerType = e.pointerType;
|
|
77
|
+
if (!isHoverPointerType(pointerType))
|
|
78
|
+
return;
|
|
79
|
+
if (globalIgnoreEmulatedMouseEvents && e.pointerType === "mouse")
|
|
80
|
+
return;
|
|
81
|
+
triggerHoverStart(e, pointerType);
|
|
82
|
+
}
|
|
83
|
+
function handlePointerLeave(e) {
|
|
84
|
+
const pointerType = e.pointerType;
|
|
85
|
+
const currentTarget = e.currentTarget;
|
|
86
|
+
if (disabled ||
|
|
87
|
+
!(currentTarget instanceof HTMLElement) ||
|
|
88
|
+
!currentTarget.contains(e.target) ||
|
|
89
|
+
!isHoverPointerType(pointerType)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
triggerHoverEnd(e, pointerType);
|
|
93
|
+
}
|
|
94
|
+
function handleTouchStart() {
|
|
95
|
+
_state.ignoreEmulatedMouseEvents = true;
|
|
96
|
+
}
|
|
97
|
+
function handleMouseEnter(e) {
|
|
98
|
+
if (!_state.ignoreEmulatedMouseEvents && !globalIgnoreEmulatedMouseEvents) {
|
|
99
|
+
triggerHoverStart(e, "mouse");
|
|
100
|
+
}
|
|
101
|
+
_state.ignoreEmulatedMouseEvents = false;
|
|
102
|
+
}
|
|
103
|
+
function handleMouseLeave(e) {
|
|
104
|
+
const currentTarget = e.currentTarget;
|
|
105
|
+
if (disabled || !(currentTarget instanceof HTMLElement) || !currentTarget.contains(e.target))
|
|
106
|
+
return;
|
|
107
|
+
triggerHoverEnd(e, "mouse");
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
get isHovered() {
|
|
111
|
+
return _state.isHovered;
|
|
112
|
+
},
|
|
113
|
+
hoverProps: typeof PointerEvent !== "undefined"
|
|
114
|
+
? {
|
|
115
|
+
onpointerenter: handlePointerEnter,
|
|
116
|
+
onpointerleave: handlePointerLeave,
|
|
117
|
+
}
|
|
118
|
+
: {
|
|
119
|
+
onmouseenter: handleMouseEnter,
|
|
120
|
+
onmouseleave: handleMouseLeave,
|
|
121
|
+
ontouchstart: handleTouchStart,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { htmlid as useId } from "../utils/id.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { htmlid as useId } from "../utils/id.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mark all elements on the page as inert, except for the ones that are allowed.
|
|
3
|
+
*
|
|
4
|
+
* We move up the tree from the allowed elements, and mark all their siblings as
|
|
5
|
+
* inert. If any of the children happens to be a parent of one of the elements,
|
|
6
|
+
* then that child will not be marked as inert.
|
|
7
|
+
*
|
|
8
|
+
* E.g.:
|
|
9
|
+
*
|
|
10
|
+
* ```html
|
|
11
|
+
* <body> <!-- Stop at body -->
|
|
12
|
+
* <header></header> <!-- Inert, sibling of parent -->
|
|
13
|
+
* <main> <!-- Not inert, parent of allowed element -->
|
|
14
|
+
* <div>Sidebar</div> <!-- Inert, sibling of parent -->
|
|
15
|
+
* <div> <!-- Not inert, parent of allowed element -->
|
|
16
|
+
* <listbox> <!-- Not inert, parent of allowed element -->
|
|
17
|
+
* <button></button> <!-- Not inert, allowed element -->
|
|
18
|
+
* <options></options> <!-- Not inert, allowed element -->
|
|
19
|
+
* </listbox>
|
|
20
|
+
* </div>
|
|
21
|
+
* </main>
|
|
22
|
+
* <footer></footer> <!-- Inert, sibling of parent -->
|
|
23
|
+
* </body>
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function useInertOthers(options: {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
elements?: {
|
|
29
|
+
allowed?: () => (HTMLElement | null)[];
|
|
30
|
+
disallowed?: () => (HTMLElement | null)[];
|
|
31
|
+
};
|
|
32
|
+
}): void;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { disposables } from "../utils/disposables.js";
|
|
2
|
+
import { getOwnerDocument } from "../utils/owner.js";
|
|
3
|
+
import { useIsTopLayer } from "./use-is-top-layer.svelte.js";
|
|
4
|
+
const originals = new Map();
|
|
5
|
+
const counts = new Map();
|
|
6
|
+
function markInert(element) {
|
|
7
|
+
// Increase count
|
|
8
|
+
let count = counts.get(element) ?? 0;
|
|
9
|
+
counts.set(element, count + 1);
|
|
10
|
+
// Already marked as inert, no need to do it again
|
|
11
|
+
if (count !== 0)
|
|
12
|
+
return () => markNotInert(element);
|
|
13
|
+
// Keep track of previous values, so that we can restore them when we are done
|
|
14
|
+
originals.set(element, {
|
|
15
|
+
"aria-hidden": element.getAttribute("aria-hidden"),
|
|
16
|
+
inert: element.inert,
|
|
17
|
+
});
|
|
18
|
+
// Mark as inert
|
|
19
|
+
element.setAttribute("aria-hidden", "true");
|
|
20
|
+
element.inert = true;
|
|
21
|
+
return () => markNotInert(element);
|
|
22
|
+
}
|
|
23
|
+
function markNotInert(element) {
|
|
24
|
+
// Decrease counts
|
|
25
|
+
let count = counts.get(element) ?? 1; // Should always exist
|
|
26
|
+
if (count === 1)
|
|
27
|
+
counts.delete(element); // We are the last one, so we can delete the count
|
|
28
|
+
else
|
|
29
|
+
counts.set(element, count - 1); // We are not the last one
|
|
30
|
+
// Not the last one, so we don't restore the original values (yet)
|
|
31
|
+
if (count !== 1)
|
|
32
|
+
return;
|
|
33
|
+
let original = originals.get(element);
|
|
34
|
+
if (!original)
|
|
35
|
+
return; // Should never happen
|
|
36
|
+
// Restore original values
|
|
37
|
+
if (original["aria-hidden"] === null)
|
|
38
|
+
element.removeAttribute("aria-hidden");
|
|
39
|
+
else
|
|
40
|
+
element.setAttribute("aria-hidden", original["aria-hidden"]);
|
|
41
|
+
element.inert = original.inert;
|
|
42
|
+
// Remove tracking of original values
|
|
43
|
+
originals.delete(element);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Mark all elements on the page as inert, except for the ones that are allowed.
|
|
47
|
+
*
|
|
48
|
+
* We move up the tree from the allowed elements, and mark all their siblings as
|
|
49
|
+
* inert. If any of the children happens to be a parent of one of the elements,
|
|
50
|
+
* then that child will not be marked as inert.
|
|
51
|
+
*
|
|
52
|
+
* E.g.:
|
|
53
|
+
*
|
|
54
|
+
* ```html
|
|
55
|
+
* <body> <!-- Stop at body -->
|
|
56
|
+
* <header></header> <!-- Inert, sibling of parent -->
|
|
57
|
+
* <main> <!-- Not inert, parent of allowed element -->
|
|
58
|
+
* <div>Sidebar</div> <!-- Inert, sibling of parent -->
|
|
59
|
+
* <div> <!-- Not inert, parent of allowed element -->
|
|
60
|
+
* <listbox> <!-- Not inert, parent of allowed element -->
|
|
61
|
+
* <button></button> <!-- Not inert, allowed element -->
|
|
62
|
+
* <options></options> <!-- Not inert, allowed element -->
|
|
63
|
+
* </listbox>
|
|
64
|
+
* </div>
|
|
65
|
+
* </main>
|
|
66
|
+
* <footer></footer> <!-- Inert, sibling of parent -->
|
|
67
|
+
* </body>
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export function useInertOthers(options) {
|
|
71
|
+
const { enabled, elements } = $derived(options);
|
|
72
|
+
const { allowed, disallowed } = $derived(elements ?? {});
|
|
73
|
+
let isTopLayer = useIsTopLayer({
|
|
74
|
+
get enabled() {
|
|
75
|
+
return enabled;
|
|
76
|
+
},
|
|
77
|
+
scope: "inert-others",
|
|
78
|
+
});
|
|
79
|
+
$effect(() => {
|
|
80
|
+
if (!isTopLayer.value)
|
|
81
|
+
return;
|
|
82
|
+
let d = disposables();
|
|
83
|
+
// Mark all disallowed elements as inert
|
|
84
|
+
for (let element of disallowed?.() ?? []) {
|
|
85
|
+
if (!element)
|
|
86
|
+
continue;
|
|
87
|
+
d.add(markInert(element));
|
|
88
|
+
}
|
|
89
|
+
// Mark all siblings of allowed elements (and parents) as inert
|
|
90
|
+
let allowedElements = allowed?.() ?? [];
|
|
91
|
+
for (let element of allowedElements) {
|
|
92
|
+
if (!element)
|
|
93
|
+
continue;
|
|
94
|
+
let ownerDocument = getOwnerDocument(element);
|
|
95
|
+
if (!ownerDocument)
|
|
96
|
+
continue;
|
|
97
|
+
let parent = element.parentElement;
|
|
98
|
+
while (parent && parent !== ownerDocument.body) {
|
|
99
|
+
// Mark all siblings as inert
|
|
100
|
+
for (let node of parent.children) {
|
|
101
|
+
// If the node contains any of the elements we should not mark it as inert
|
|
102
|
+
// because it would make the elements unreachable.
|
|
103
|
+
if (allowedElements.some((el) => node.contains(el)))
|
|
104
|
+
continue;
|
|
105
|
+
// Mark the node as inert
|
|
106
|
+
d.add(markInert(node));
|
|
107
|
+
}
|
|
108
|
+
// Move up the tree
|
|
109
|
+
parent = parent.parentElement;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return d.dispose;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A hook that returns whether the current node is on the top of the hierarchy,
|
|
3
|
+
* aka "top layer". Note: this does not use the native DOM "top-layer" but
|
|
4
|
+
* conceptually it's the same thing.
|
|
5
|
+
*
|
|
6
|
+
* The hierarchy is also shared across multiple components that use the same
|
|
7
|
+
* scope.
|
|
8
|
+
*
|
|
9
|
+
* This is useful to use in components and hooks that mutate the DOM or share
|
|
10
|
+
* some global state.
|
|
11
|
+
*
|
|
12
|
+
* A use case for this is to use this inside of a `useOutsideClick` hook where
|
|
13
|
+
* only the last rendered component should handle the outside click event.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* <Dialog>
|
|
17
|
+
* <Menu>
|
|
18
|
+
* <MenuButton></MenuButton> // Pressing escape on an open `Menu` should close the `Menu` and not the `Dialog`.
|
|
19
|
+
* // …
|
|
20
|
+
* </Menu>
|
|
21
|
+
* </Dialog>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function useIsTopLayer(options: {
|
|
25
|
+
readonly enabled: boolean;
|
|
26
|
+
readonly scope: string;
|
|
27
|
+
}): {
|
|
28
|
+
value: boolean;
|
|
29
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { DefaultMap } from "../utils/default-map.js";
|
|
2
|
+
import { createStore } from "../utils/store.js";
|
|
3
|
+
import { useId } from "./use-id.js";
|
|
4
|
+
/**
|
|
5
|
+
* Map of stable hierarchy stores based on a given scope.
|
|
6
|
+
*/
|
|
7
|
+
let hierarchyStores = new DefaultMap(() => createStore(() => [], {
|
|
8
|
+
ADD(id) {
|
|
9
|
+
if (this.includes(id))
|
|
10
|
+
return this;
|
|
11
|
+
return [...this, id];
|
|
12
|
+
},
|
|
13
|
+
REMOVE(id) {
|
|
14
|
+
let idx = this.indexOf(id);
|
|
15
|
+
if (idx === -1)
|
|
16
|
+
return this;
|
|
17
|
+
let copy = this.slice();
|
|
18
|
+
copy.splice(idx, 1);
|
|
19
|
+
return copy;
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
/**
|
|
23
|
+
* A hook that returns whether the current node is on the top of the hierarchy,
|
|
24
|
+
* aka "top layer". Note: this does not use the native DOM "top-layer" but
|
|
25
|
+
* conceptually it's the same thing.
|
|
26
|
+
*
|
|
27
|
+
* The hierarchy is also shared across multiple components that use the same
|
|
28
|
+
* scope.
|
|
29
|
+
*
|
|
30
|
+
* This is useful to use in components and hooks that mutate the DOM or share
|
|
31
|
+
* some global state.
|
|
32
|
+
*
|
|
33
|
+
* A use case for this is to use this inside of a `useOutsideClick` hook where
|
|
34
|
+
* only the last rendered component should handle the outside click event.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* <Dialog>
|
|
38
|
+
* <Menu>
|
|
39
|
+
* <MenuButton></MenuButton> // Pressing escape on an open `Menu` should close the `Menu` and not the `Dialog`.
|
|
40
|
+
* // …
|
|
41
|
+
* </Menu>
|
|
42
|
+
* </Dialog>
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function useIsTopLayer(options) {
|
|
46
|
+
const { enabled, scope } = $derived(options);
|
|
47
|
+
const hierarchyStore = hierarchyStores.get(scope);
|
|
48
|
+
const id = useId();
|
|
49
|
+
let hierarchy = $state(hierarchyStore.getSnapshot());
|
|
50
|
+
$effect(() => {
|
|
51
|
+
const unsubscribe = hierarchyStore.subscribe(() => {
|
|
52
|
+
hierarchy = hierarchyStore.getSnapshot();
|
|
53
|
+
});
|
|
54
|
+
return unsubscribe;
|
|
55
|
+
});
|
|
56
|
+
$effect(() => {
|
|
57
|
+
if (!enabled)
|
|
58
|
+
return;
|
|
59
|
+
hierarchyStore.dispatch("ADD", id);
|
|
60
|
+
return () => hierarchyStore.dispatch("REMOVE", id);
|
|
61
|
+
});
|
|
62
|
+
const value = $derived.by(() => {
|
|
63
|
+
if (!enabled)
|
|
64
|
+
return false;
|
|
65
|
+
let idx = hierarchy.indexOf(id);
|
|
66
|
+
let hierarchyLength = hierarchy.length;
|
|
67
|
+
// Not in the hierarchy yet
|
|
68
|
+
if (idx === -1) {
|
|
69
|
+
// Assume that it will be inserted at the end, then it means that the `idx`
|
|
70
|
+
// will be the length of the current hierarchy.
|
|
71
|
+
idx = hierarchyLength;
|
|
72
|
+
// Increase the hierarchy length as-if the node is already in the hierarchy.
|
|
73
|
+
hierarchyLength += 1;
|
|
74
|
+
}
|
|
75
|
+
return idx === hierarchyLength - 1;
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
get value() {
|
|
79
|
+
return value;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A hook to ensure that a callback is called when the element has disappeared
|
|
3
|
+
* from the screen.
|
|
4
|
+
*
|
|
5
|
+
* This can happen if you use Tailwind classes like: `hidden md:block`, once the
|
|
6
|
+
* viewport is smaller than `md` the element will disappear.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useOnDisappear(options: {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
ref: HTMLElement | null | undefined;
|
|
11
|
+
ondisappear: () => void;
|
|
12
|
+
}): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { disposables } from "../utils/disposables.js";
|
|
2
|
+
/**
|
|
3
|
+
* A hook to ensure that a callback is called when the element has disappeared
|
|
4
|
+
* from the screen.
|
|
5
|
+
*
|
|
6
|
+
* This can happen if you use Tailwind classes like: `hidden md:block`, once the
|
|
7
|
+
* viewport is smaller than `md` the element will disappear.
|
|
8
|
+
*/
|
|
9
|
+
export function useOnDisappear(options) {
|
|
10
|
+
const { enabled, ref, ondisappear } = $derived(options);
|
|
11
|
+
let listenerRef = (element) => {
|
|
12
|
+
let rect = element.getBoundingClientRect();
|
|
13
|
+
if (rect.x === 0 && rect.y === 0 && rect.width === 0 && rect.height === 0) {
|
|
14
|
+
ondisappear();
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
$effect(() => {
|
|
18
|
+
if (!enabled)
|
|
19
|
+
return;
|
|
20
|
+
const element = ref;
|
|
21
|
+
if (!element)
|
|
22
|
+
return;
|
|
23
|
+
let d = disposables();
|
|
24
|
+
// Try using ResizeObserver
|
|
25
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
26
|
+
let observer = new ResizeObserver(() => listenerRef(element));
|
|
27
|
+
observer.observe(element);
|
|
28
|
+
d.add(() => observer.disconnect());
|
|
29
|
+
}
|
|
30
|
+
// Try using IntersectionObserver
|
|
31
|
+
if (typeof IntersectionObserver !== "undefined") {
|
|
32
|
+
let observer = new IntersectionObserver(() => listenerRef(element));
|
|
33
|
+
observer.observe(element);
|
|
34
|
+
d.add(() => observer.disconnect());
|
|
35
|
+
}
|
|
36
|
+
return () => d.dispose();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MutableRefObject } from "../utils/ref.svelte.js";
|
|
2
|
+
type Container = MutableRefObject<HTMLElement | null> | HTMLElement | null;
|
|
3
|
+
type ContainerCollection = Container[] | Set<Container>;
|
|
4
|
+
type ContainerInput = Container | ContainerCollection;
|
|
5
|
+
export declare function useOutsideClick(options: {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
containers: ContainerInput | (() => ContainerInput);
|
|
8
|
+
cb: (event: MouseEvent | PointerEvent | FocusEvent | TouchEvent, target: HTMLElement) => void;
|
|
9
|
+
}): void;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { disposables } from "../utils/disposables.js";
|
|
2
|
+
import { FocusableMode, isFocusableElement } from "../utils/focus-management.js";
|
|
3
|
+
import { isMobile } from "../utils/platform.js";
|
|
4
|
+
import { useIsTopLayer } from "./use-is-top-layer.svelte.js";
|
|
5
|
+
// If the user moves their finger by ${MOVE_THRESHOLD_PX} pixels or more, we'll
|
|
6
|
+
// assume that they are scrolling and not clicking. This will prevent the click
|
|
7
|
+
// from being triggered when the user is scrolling.
|
|
8
|
+
//
|
|
9
|
+
// This also allows you to "cancel" the click by moving your finger more than
|
|
10
|
+
// the threshold in pixels in any direction.
|
|
11
|
+
const MOVE_THRESHOLD_PX = 30;
|
|
12
|
+
export function useOutsideClick(options) {
|
|
13
|
+
const { enabled, containers, cb } = $derived(options);
|
|
14
|
+
const isTopLayer = useIsTopLayer({
|
|
15
|
+
get enabled() {
|
|
16
|
+
return enabled;
|
|
17
|
+
},
|
|
18
|
+
scope: "outside-click",
|
|
19
|
+
});
|
|
20
|
+
const handleOutsideClick = (event, resolveTarget) => {
|
|
21
|
+
// Check whether the event got prevented already. This can happen if you
|
|
22
|
+
// use the useOutsideClick hook in both a Dialog and a Menu and the inner
|
|
23
|
+
// Menu "cancels" the default behavior so that only the Menu closes and
|
|
24
|
+
// not the Dialog (yet)
|
|
25
|
+
if (event.defaultPrevented)
|
|
26
|
+
return;
|
|
27
|
+
let target = resolveTarget(event);
|
|
28
|
+
if (target === null) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Ignore if the target doesn't exist in the DOM anymore
|
|
32
|
+
if (!target.getRootNode().contains(target))
|
|
33
|
+
return;
|
|
34
|
+
// Ignore if the target was removed from the DOM by the time the handler
|
|
35
|
+
// was called
|
|
36
|
+
if (!target.isConnected)
|
|
37
|
+
return;
|
|
38
|
+
let _containers = (function resolve(containers) {
|
|
39
|
+
if (typeof containers === "function") {
|
|
40
|
+
return resolve(containers());
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(containers)) {
|
|
43
|
+
return containers;
|
|
44
|
+
}
|
|
45
|
+
if (containers instanceof Set) {
|
|
46
|
+
return containers;
|
|
47
|
+
}
|
|
48
|
+
return [containers];
|
|
49
|
+
})(containers);
|
|
50
|
+
// Ignore if the target exists in one of the containers
|
|
51
|
+
for (let container of _containers) {
|
|
52
|
+
if (container === null)
|
|
53
|
+
continue;
|
|
54
|
+
let domNode = container instanceof HTMLElement ? container : container.current;
|
|
55
|
+
if (domNode?.contains(target)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// If the click crossed a shadow boundary, we need to check if the
|
|
59
|
+
// container is inside the tree by using `composedPath` to "pierce" the
|
|
60
|
+
// shadow boundary
|
|
61
|
+
if (event.composed && event.composedPath().includes(domNode)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// This allows us to check whether the event was defaultPrevented when you
|
|
66
|
+
// are nesting this inside a `<Dialog />` for example.
|
|
67
|
+
if (
|
|
68
|
+
// This check allows us to know whether or not we clicked on a
|
|
69
|
+
// "focusable" element like a button or an input. This is a backwards
|
|
70
|
+
// compatibility check so that you can open a <Menu /> and click on
|
|
71
|
+
// another <Menu /> which should close Menu A and open Menu B. We might
|
|
72
|
+
// revisit that so that you will require 2 clicks instead.
|
|
73
|
+
!isFocusableElement(target, FocusableMode.Loose) &&
|
|
74
|
+
// This could be improved, but the `Combobox.Button` adds tabIndex={-1}
|
|
75
|
+
// to make it unfocusable via the keyboard so that tabbing to the next
|
|
76
|
+
// item from the input doesn't first go to the button.
|
|
77
|
+
target.tabIndex !== -1) {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
}
|
|
80
|
+
return cb(event, target);
|
|
81
|
+
};
|
|
82
|
+
let initialClickTarget = $state(null);
|
|
83
|
+
const addEventListeners = (enabled) => {
|
|
84
|
+
if (!enabled || typeof document === "undefined" || typeof window === "undefined")
|
|
85
|
+
return;
|
|
86
|
+
const d = disposables();
|
|
87
|
+
d.addEventListener(document, "pointerdown", (event) => {
|
|
88
|
+
initialClickTarget = event.composedPath?.()?.[0] || event.target;
|
|
89
|
+
}, true);
|
|
90
|
+
d.addEventListener(document, "mousedown", (event) => {
|
|
91
|
+
initialClickTarget = event.composedPath?.()?.[0] || event.target;
|
|
92
|
+
}, true);
|
|
93
|
+
d.addEventListener(document, "click", (event) => {
|
|
94
|
+
if (isMobile())
|
|
95
|
+
return;
|
|
96
|
+
if (!initialClickTarget)
|
|
97
|
+
return;
|
|
98
|
+
handleOutsideClick(event, () => {
|
|
99
|
+
return initialClickTarget;
|
|
100
|
+
});
|
|
101
|
+
initialClickTarget = null;
|
|
102
|
+
},
|
|
103
|
+
// We will use the `capture` phase so that layers in between with `event.stopPropagation()`
|
|
104
|
+
// don't "cancel" this outside click check. E.g.: A `Menu` inside a `DialogPanel` if the `Menu`
|
|
105
|
+
// is open, and you click outside of it in the `DialogPanel` the `Menu` should close. However,
|
|
106
|
+
// the `DialogPanel` has a `onClick(e) { e.stopPropagation() }` which would cancel this.
|
|
107
|
+
true);
|
|
108
|
+
let startPosition = $state({ x: 0, y: 0 });
|
|
109
|
+
d.addEventListener(document, "touchstart", (event) => {
|
|
110
|
+
startPosition.x = event.touches[0].clientX;
|
|
111
|
+
startPosition.y = event.touches[0].clientY;
|
|
112
|
+
}, true);
|
|
113
|
+
d.addEventListener(document, "touchend", (event) => {
|
|
114
|
+
// If the user moves their finger by ${MOVE_THRESHOLD_PX} pixels or more,
|
|
115
|
+
// we'll assume that they are scrolling and not clicking.
|
|
116
|
+
let endPosition = { x: event.changedTouches[0].clientX, y: event.changedTouches[0].clientY };
|
|
117
|
+
if (Math.abs(endPosition.x - startPosition.x) >= MOVE_THRESHOLD_PX ||
|
|
118
|
+
Math.abs(endPosition.y - startPosition.y) >= MOVE_THRESHOLD_PX) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
return handleOutsideClick(event, () => {
|
|
122
|
+
if (event.target instanceof HTMLElement) {
|
|
123
|
+
return event.target;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
// We will use the `capture` phase so that layers in between with `event.stopPropagation()`
|
|
129
|
+
// don't "cancel" this outside click check. E.g.: A `Menu` inside a `DialogPanel` if the `Menu`
|
|
130
|
+
// is open, and you click outside of it in the `DialogPanel` the `Menu` should close. However,
|
|
131
|
+
// the `DialogPanel` has a `onClick(e) { e.stopPropagation() }` which would cancel this.
|
|
132
|
+
true);
|
|
133
|
+
// When content inside an iframe is clicked `window` will receive a blur event
|
|
134
|
+
// This can happen when an iframe _inside_ a window is clicked
|
|
135
|
+
// Or, if headless UI is *in* the iframe, when a content in a window containing that iframe is clicked
|
|
136
|
+
// In this case we care only about the first case so we check to see if the active element is the iframe
|
|
137
|
+
// If so this was because of a click, focus, or other interaction with the child iframe
|
|
138
|
+
// and we can consider it an "outside click"
|
|
139
|
+
d.addEventListener(window, "blur", (event) => {
|
|
140
|
+
return handleOutsideClick(event, () => {
|
|
141
|
+
return window.document.activeElement instanceof HTMLIFrameElement ? window.document.activeElement : null;
|
|
142
|
+
});
|
|
143
|
+
}, true);
|
|
144
|
+
return d.dispose;
|
|
145
|
+
};
|
|
146
|
+
$effect(() => {
|
|
147
|
+
initialClickTarget;
|
|
148
|
+
return addEventListeners(isTopLayer.value);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MutableRefObject } from "../utils/ref.svelte.js";
|
|
2
|
+
export declare function useResolveButtonType<TTag>(options: {
|
|
3
|
+
props: {
|
|
4
|
+
type?: string;
|
|
5
|
+
as?: TTag;
|
|
6
|
+
};
|
|
7
|
+
ref: MutableRefObject<HTMLElement | null | undefined>;
|
|
8
|
+
}): {
|
|
9
|
+
readonly type: string | undefined;
|
|
10
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { untrack } from "svelte";
|
|
2
|
+
function resolveType(props) {
|
|
3
|
+
if (props.type)
|
|
4
|
+
return props.type;
|
|
5
|
+
const tag = props.as ?? "button";
|
|
6
|
+
if (typeof tag === "string" && tag.toLowerCase() === "button")
|
|
7
|
+
return "button";
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
export function useResolveButtonType(options) {
|
|
11
|
+
const { props, ref } = $derived(options);
|
|
12
|
+
return {
|
|
13
|
+
get type() {
|
|
14
|
+
return ref.current && ref.current instanceof HTMLButtonElement && !ref.current.hasAttribute("type")
|
|
15
|
+
? "button"
|
|
16
|
+
: resolveType(props);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|