@human-kit/svelte-components 1.0.0-alpha.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/combobox/TODO.md +175 -0
- package/dist/combobox/button/combobox-button.svelte +57 -0
- package/dist/combobox/button/combobox-button.svelte.d.ts +9 -0
- package/dist/combobox/index.d.ts +14 -0
- package/dist/combobox/index.js +18 -0
- package/dist/combobox/index.parts.d.ts +10 -0
- package/dist/combobox/index.parts.js +11 -0
- package/dist/combobox/input/combobox-input.svelte +98 -0
- package/dist/combobox/input/combobox-input.svelte.d.ts +13 -0
- package/dist/combobox/item/combobox-item-implicit-text-test.svelte +21 -0
- package/dist/combobox/item/combobox-item-implicit-text-test.svelte.d.ts +3 -0
- package/dist/combobox/item/combobox-listboxitem.svelte +136 -0
- package/dist/combobox/item/combobox-listboxitem.svelte.d.ts +18 -0
- package/dist/combobox/item-indicator/combobox-item-indicator.svelte +63 -0
- package/dist/combobox/item-indicator/combobox-item-indicator.svelte.d.ts +17 -0
- package/dist/combobox/list/combobox-listbox.svelte +76 -0
- package/dist/combobox/list/combobox-listbox.svelte.d.ts +47 -0
- package/dist/combobox/popover/combobox-popover.svelte +69 -0
- package/dist/combobox/popover/combobox-popover.svelte.d.ts +12 -0
- package/dist/combobox/root/combobox-filtered-test.svelte +51 -0
- package/dist/combobox/root/combobox-filtered-test.svelte.d.ts +7 -0
- package/dist/combobox/root/combobox-multiselect-test.svelte +76 -0
- package/dist/combobox/root/combobox-multiselect-test.svelte.d.ts +13 -0
- package/dist/combobox/root/combobox-numeric-string-id-test.svelte +20 -0
- package/dist/combobox/root/combobox-numeric-string-id-test.svelte.d.ts +3 -0
- package/dist/combobox/root/combobox-test.svelte +43 -0
- package/dist/combobox/root/combobox-test.svelte.d.ts +9 -0
- package/dist/combobox/root/combobox.svelte +696 -0
- package/dist/combobox/root/combobox.svelte.d.ts +58 -0
- package/dist/combobox/root/context.d.ts +90 -0
- package/dist/combobox/root/context.js +15 -0
- package/dist/combobox/tag/combobox-tag.svelte +58 -0
- package/dist/combobox/tag/combobox-tag.svelte.d.ts +22 -0
- package/dist/combobox/tag/tag-context-provider.svelte +36 -0
- package/dist/combobox/tag/tag-context-provider.svelte.d.ts +14 -0
- package/dist/combobox/tag-remove/combobox-tag-remove.svelte +53 -0
- package/dist/combobox/tag-remove/combobox-tag-remove.svelte.d.ts +14 -0
- package/dist/combobox/tags/combobox-tags.svelte +50 -0
- package/dist/combobox/tags/combobox-tags.svelte.d.ts +20 -0
- package/dist/dialog/content/dialog-content.svelte +121 -0
- package/dist/dialog/content/dialog-content.svelte.d.ts +19 -0
- package/dist/dialog/index.d.ts +10 -0
- package/dist/dialog/index.js +15 -0
- package/dist/dialog/index.parts.d.ts +5 -0
- package/dist/dialog/index.parts.js +6 -0
- package/dist/dialog/overlay/dialog-overlay.svelte +39 -0
- package/dist/dialog/overlay/dialog-overlay.svelte.d.ts +12 -0
- package/dist/dialog/portal/dialog-portal.svelte +32 -0
- package/dist/dialog/portal/dialog-portal.svelte.d.ts +12 -0
- package/dist/dialog/root/context.d.ts +25 -0
- package/dist/dialog/root/context.js +8 -0
- package/dist/dialog/root/dialog-root.svelte +99 -0
- package/dist/dialog/root/dialog-root.svelte.d.ts +21 -0
- package/dist/dialog/root/dialog-stack.d.ts +32 -0
- package/dist/dialog/root/dialog-stack.js +55 -0
- package/dist/dialog/root/dialog-test.svelte +38 -0
- package/dist/dialog/root/dialog-test.svelte.d.ts +10 -0
- package/dist/dialog/root/dialog-with-combobox-test.svelte +61 -0
- package/dist/dialog/root/dialog-with-combobox-test.svelte.d.ts +7 -0
- package/dist/dialog/root/nested-dialog-test.svelte +63 -0
- package/dist/dialog/root/nested-dialog-test.svelte.d.ts +8 -0
- package/dist/dialog/root/types.d.ts +10 -0
- package/dist/dialog/root/types.js +1 -0
- package/dist/dialog/trigger/dialog-trigger.svelte +71 -0
- package/dist/dialog/trigger/dialog-trigger.svelte.d.ts +12 -0
- package/dist/hooks/use-virtual-focus.svelte.d.ts +55 -0
- package/dist/hooks/use-virtual-focus.svelte.js +201 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +19 -0
- package/dist/input/index.d.ts +3 -0
- package/dist/input/index.js +3 -0
- package/dist/input/input.svelte +19 -0
- package/dist/input/input.svelte.d.ts +8 -0
- package/dist/label/index.d.ts +3 -0
- package/dist/label/index.js +3 -0
- package/dist/label/label.svelte +21 -0
- package/dist/label/label.svelte.d.ts +8 -0
- package/dist/listbox/index.d.ts +6 -0
- package/dist/listbox/index.js +10 -0
- package/dist/listbox/index.parts.d.ts +2 -0
- package/dist/listbox/index.parts.js +3 -0
- package/dist/listbox/item/listbox-item.svelte +186 -0
- package/dist/listbox/item/listbox-item.svelte.d.ts +34 -0
- package/dist/listbox/root/context.d.ts +73 -0
- package/dist/listbox/root/context.js +249 -0
- package/dist/listbox/root/listbox-numeric-id-test.svelte +18 -0
- package/dist/listbox/root/listbox-numeric-id-test.svelte.d.ts +3 -0
- package/dist/listbox/root/listbox-test.svelte +27 -0
- package/dist/listbox/root/listbox-test.svelte.d.ts +8 -0
- package/dist/listbox/root/listbox.svelte +146 -0
- package/dist/listbox/root/listbox.svelte.d.ts +54 -0
- package/dist/popover/content/popover-content-test.svelte +43 -0
- package/dist/popover/content/popover-content-test.svelte.d.ts +12 -0
- package/dist/popover/content/popover-content.svelte +167 -0
- package/dist/popover/content/popover-content.svelte.d.ts +38 -0
- package/dist/popover/index.d.ts +8 -0
- package/dist/popover/index.js +14 -0
- package/dist/popover/index.parts.d.ts +4 -0
- package/dist/popover/index.parts.js +5 -0
- package/dist/popover/root/context.d.ts +24 -0
- package/dist/popover/root/context.js +10 -0
- package/dist/popover/root/popover-root.svelte +87 -0
- package/dist/popover/root/popover-root.svelte.d.ts +20 -0
- package/dist/popover/root/popover-test.svelte +40 -0
- package/dist/popover/root/popover-test.svelte.d.ts +11 -0
- package/dist/popover/trigger/popover-trigger-button.svelte +42 -0
- package/dist/popover/trigger/popover-trigger-button.svelte.d.ts +12 -0
- package/dist/popover/trigger/popover-trigger-in-dialog-test.svelte +29 -0
- package/dist/popover/trigger/popover-trigger-in-dialog-test.svelte.d.ts +18 -0
- package/dist/popover/trigger/popover-trigger.svelte +71 -0
- package/dist/popover/trigger/popover-trigger.svelte.d.ts +12 -0
- package/dist/portal/index.d.ts +1 -0
- package/dist/portal/index.js +1 -0
- package/dist/portal/portal.svelte +44 -0
- package/dist/portal/portal.svelte.d.ts +10 -0
- package/dist/primitives/aria-hide-outside.d.ts +38 -0
- package/dist/primitives/aria-hide-outside.js +152 -0
- package/dist/primitives/click-outside.d.ts +26 -0
- package/dist/primitives/click-outside.js +66 -0
- package/dist/primitives/floating.d.ts +57 -0
- package/dist/primitives/floating.js +179 -0
- package/dist/primitives/focus-trap.d.ts +19 -0
- package/dist/primitives/focus-trap.js +102 -0
- package/dist/primitives/index.d.ts +6 -0
- package/dist/primitives/index.js +7 -0
- package/dist/primitives/keyboard-navigation.d.ts +88 -0
- package/dist/primitives/keyboard-navigation.js +274 -0
- package/dist/primitives/scroll-lock.d.ts +19 -0
- package/dist/primitives/scroll-lock.js +62 -0
- package/dist/test-mocks/app-environment.d.ts +7 -0
- package/dist/test-mocks/app-environment.js +7 -0
- package/dist/test-mocks/app-navigation.d.ts +11 -0
- package/dist/test-mocks/app-navigation.js +11 -0
- package/dist/test-mocks/app-stores.d.ts +16 -0
- package/dist/test-mocks/app-stores.js +18 -0
- package/dist/utils/cn.d.ts +2 -0
- package/dist/utils/cn.js +5 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +99 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { onMount, onDestroy, tick } from 'svelte';
|
|
4
|
+
import { browser } from '$app/environment';
|
|
5
|
+
|
|
6
|
+
type PortalProps = {
|
|
7
|
+
/** Target element or selector to render into. Defaults to document.body */
|
|
8
|
+
target?: string;
|
|
9
|
+
/** Content to render in the portal */
|
|
10
|
+
children?: Snippet;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let { target = 'body', children }: PortalProps = $props();
|
|
14
|
+
|
|
15
|
+
let wrapper: HTMLDivElement | undefined = $state();
|
|
16
|
+
|
|
17
|
+
onMount(async () => {
|
|
18
|
+
if (!browser || !wrapper) return;
|
|
19
|
+
|
|
20
|
+
await tick();
|
|
21
|
+
|
|
22
|
+
const targetEl = document.querySelector(target);
|
|
23
|
+
if (!targetEl) {
|
|
24
|
+
console.error(`Portal: target "${target}" not found`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
targetEl.appendChild(wrapper);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
onDestroy(() => {
|
|
32
|
+
// Wrapper will be automatically removed when component is destroyed
|
|
33
|
+
// because Svelte still controls it
|
|
34
|
+
if (wrapper && wrapper.parentNode) {
|
|
35
|
+
wrapper.parentNode.removeChild(wrapper);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<div bind:this={wrapper} style="display: contents;">
|
|
41
|
+
{#if children}
|
|
42
|
+
{@render children()}
|
|
43
|
+
{/if}
|
|
44
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type PortalProps = {
|
|
3
|
+
/** Target element or selector to render into. Defaults to document.body */
|
|
4
|
+
target?: string;
|
|
5
|
+
/** Content to render in the portal */
|
|
6
|
+
children?: Snippet;
|
|
7
|
+
};
|
|
8
|
+
declare const Portal: import("svelte").Component<PortalProps, {}, "">;
|
|
9
|
+
type Portal = ReturnType<typeof Portal>;
|
|
10
|
+
export default Portal;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hides all elements in the DOM tree outside of the given targets from screen readers
|
|
3
|
+
* and makes them inert. Based on React Aria's ariaHideOutside implementation.
|
|
4
|
+
*
|
|
5
|
+
* This works by walking the DOM from the body and marking all siblings
|
|
6
|
+
* of ancestors of the target elements as inert.
|
|
7
|
+
*/
|
|
8
|
+
interface HideOutsideResult {
|
|
9
|
+
/** Call this to restore the original state */
|
|
10
|
+
restore: () => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Hides all content outside of the target elements from assistive technologies
|
|
14
|
+
* and makes it non-interactive.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const { restore } = hideOutside([popoverRef]);
|
|
19
|
+
* // Later, when popover closes:
|
|
20
|
+
* restore();
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function hideOutside(targets: HTMLElement[]): HideOutsideResult;
|
|
24
|
+
/**
|
|
25
|
+
* Svelte action that hides all content outside of the element.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```svelte
|
|
29
|
+
* <div use:ariaHideOutside={enabled}>
|
|
30
|
+
* Modal content
|
|
31
|
+
* </div>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function ariaHideOutside(node: HTMLElement, enabled?: boolean): {
|
|
35
|
+
update(newEnabled: boolean): void;
|
|
36
|
+
destroy(): void;
|
|
37
|
+
};
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hides all elements in the DOM tree outside of the given targets from screen readers
|
|
3
|
+
* and makes them inert. Based on React Aria's ariaHideOutside implementation.
|
|
4
|
+
*
|
|
5
|
+
* This works by walking the DOM from the body and marking all siblings
|
|
6
|
+
* of ancestors of the target elements as inert.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Global hidden state tracker.
|
|
10
|
+
* Allows multiple overlapping hideOutside calls without restoring too early.
|
|
11
|
+
*/
|
|
12
|
+
const hiddenState = new Map();
|
|
13
|
+
function hideElement(element) {
|
|
14
|
+
const existing = hiddenState.get(element);
|
|
15
|
+
if (existing) {
|
|
16
|
+
existing.count += 1;
|
|
17
|
+
hiddenState.set(element, existing);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
hiddenState.set(element, {
|
|
21
|
+
count: 1,
|
|
22
|
+
hadInert: element.hasAttribute('inert'),
|
|
23
|
+
ariaHidden: element.getAttribute('aria-hidden')
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
element.setAttribute('inert', '');
|
|
27
|
+
element.setAttribute('aria-hidden', 'true');
|
|
28
|
+
}
|
|
29
|
+
function restoreElement(element) {
|
|
30
|
+
const existing = hiddenState.get(element);
|
|
31
|
+
if (!existing)
|
|
32
|
+
return;
|
|
33
|
+
if (existing.count > 1) {
|
|
34
|
+
existing.count -= 1;
|
|
35
|
+
hiddenState.set(element, existing);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!existing.hadInert) {
|
|
39
|
+
element.removeAttribute('inert');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
element.setAttribute('inert', '');
|
|
43
|
+
}
|
|
44
|
+
if (existing.ariaHidden === null) {
|
|
45
|
+
element.removeAttribute('aria-hidden');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
element.setAttribute('aria-hidden', existing.ariaHidden);
|
|
49
|
+
}
|
|
50
|
+
hiddenState.delete(element);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Hides all content outside of the target elements from assistive technologies
|
|
54
|
+
* and makes it non-interactive.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const { restore } = hideOutside([popoverRef]);
|
|
59
|
+
* // Later, when popover closes:
|
|
60
|
+
* restore();
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function hideOutside(targets) {
|
|
64
|
+
const affectedElements = new Set();
|
|
65
|
+
const targetSet = new Set(targets);
|
|
66
|
+
const targetAncestors = new Set();
|
|
67
|
+
for (const target of targets) {
|
|
68
|
+
let current = target.parentElement;
|
|
69
|
+
while (current) {
|
|
70
|
+
targetAncestors.add(current);
|
|
71
|
+
current = current.parentElement;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function walk(root) {
|
|
75
|
+
const children = root.children;
|
|
76
|
+
for (let i = 0; i < children.length; i++) {
|
|
77
|
+
const child = children[i];
|
|
78
|
+
const tagName = child.tagName;
|
|
79
|
+
if (tagName === 'SCRIPT' || tagName === 'STYLE' || tagName === 'LINK') {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (targetSet.has(child)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (targetAncestors.has(child)) {
|
|
86
|
+
walk(child);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
hideElement(child);
|
|
90
|
+
affectedElements.add(child);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (document.body) {
|
|
95
|
+
walk(document.body);
|
|
96
|
+
}
|
|
97
|
+
let restored = false;
|
|
98
|
+
return {
|
|
99
|
+
restore() {
|
|
100
|
+
if (restored)
|
|
101
|
+
return;
|
|
102
|
+
restored = true;
|
|
103
|
+
affectedElements.forEach((element) => {
|
|
104
|
+
restoreElement(element);
|
|
105
|
+
});
|
|
106
|
+
affectedElements.clear();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Svelte action that hides all content outside of the element.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```svelte
|
|
115
|
+
* <div use:ariaHideOutside={enabled}>
|
|
116
|
+
* Modal content
|
|
117
|
+
* </div>
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export function ariaHideOutside(node, enabled = true) {
|
|
121
|
+
let result = null;
|
|
122
|
+
function activate() {
|
|
123
|
+
requestAnimationFrame(() => {
|
|
124
|
+
if (node.isConnected) {
|
|
125
|
+
result = hideOutside([node]);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function deactivate() {
|
|
130
|
+
if (result) {
|
|
131
|
+
result.restore();
|
|
132
|
+
result = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (enabled) {
|
|
136
|
+
activate();
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
update(newEnabled) {
|
|
140
|
+
if (newEnabled && !enabled) {
|
|
141
|
+
activate();
|
|
142
|
+
}
|
|
143
|
+
else if (!newEnabled && enabled) {
|
|
144
|
+
deactivate();
|
|
145
|
+
}
|
|
146
|
+
enabled = newEnabled;
|
|
147
|
+
},
|
|
148
|
+
destroy() {
|
|
149
|
+
deactivate();
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Click outside primitive.
|
|
3
|
+
* Detects clicks outside of an element.
|
|
4
|
+
*/
|
|
5
|
+
export type ClickOutsideOptions = {
|
|
6
|
+
/** Callback when clicking outside. */
|
|
7
|
+
handler: () => void;
|
|
8
|
+
/** Whether the listener is enabled. */
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
/** Elements to ignore (clicks on these won't trigger). */
|
|
11
|
+
ignore?: (HTMLElement | null)[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Svelte action that detects clicks outside of an element.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```svelte
|
|
18
|
+
* <div use:clickOutside={{ handler: close, ignore: [triggerRef] }}>
|
|
19
|
+
* Popover content
|
|
20
|
+
* </div>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function clickOutside(node: HTMLElement, options: ClickOutsideOptions): {
|
|
24
|
+
update(newOptions: ClickOutsideOptions): void;
|
|
25
|
+
destroy(): void;
|
|
26
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Click outside primitive.
|
|
3
|
+
* Detects clicks outside of an element.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Check if an element is in a "top layer" (portal, dialog, popover, etc.)
|
|
7
|
+
* that was spawned from within the reference node.
|
|
8
|
+
* This prevents clicks on nested portals from triggering clickOutside.
|
|
9
|
+
*/
|
|
10
|
+
function isInTopLayer(target) {
|
|
11
|
+
if (!(target instanceof Element))
|
|
12
|
+
return false;
|
|
13
|
+
// Check if the element or any ancestor is marked as top-layer
|
|
14
|
+
// This includes our popovers, nested dialogs, and other portaled content
|
|
15
|
+
const topLayerElement = target.closest('[data-dialog-content], [role="dialog"]');
|
|
16
|
+
return topLayerElement !== null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Svelte action that detects clicks outside of an element.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```svelte
|
|
23
|
+
* <div use:clickOutside={{ handler: close, ignore: [triggerRef] }}>
|
|
24
|
+
* Popover content
|
|
25
|
+
* </div>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function clickOutside(node, options) {
|
|
29
|
+
let { handler, enabled = true, ignore = [] } = options;
|
|
30
|
+
function handleClick(event) {
|
|
31
|
+
if (!enabled)
|
|
32
|
+
return;
|
|
33
|
+
const target = event.target;
|
|
34
|
+
if (node.contains(target))
|
|
35
|
+
return;
|
|
36
|
+
for (const el of ignore) {
|
|
37
|
+
if (el && el.contains(target))
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Don't trigger if clicking on a top-layer element (portal content)
|
|
41
|
+
// This prevents closing when clicking on nested popovers/dialogs
|
|
42
|
+
if (isInTopLayer(target))
|
|
43
|
+
return;
|
|
44
|
+
handler();
|
|
45
|
+
}
|
|
46
|
+
if (enabled) {
|
|
47
|
+
document.addEventListener('mousedown', handleClick, true);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
update(newOptions) {
|
|
51
|
+
const wasEnabled = enabled;
|
|
52
|
+
handler = newOptions.handler;
|
|
53
|
+
enabled = newOptions.enabled ?? true;
|
|
54
|
+
ignore = newOptions.ignore ?? [];
|
|
55
|
+
if (enabled && !wasEnabled) {
|
|
56
|
+
document.addEventListener('mousedown', handleClick, true);
|
|
57
|
+
}
|
|
58
|
+
else if (!enabled && wasEnabled) {
|
|
59
|
+
document.removeEventListener('mousedown', handleClick, true);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
destroy() {
|
|
63
|
+
document.removeEventListener('mousedown', handleClick, true);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type Placement as FloatingPlacement } from '@floating-ui/dom';
|
|
2
|
+
/**
|
|
3
|
+
* Placement options for floating elements.
|
|
4
|
+
* Follows the specification with logical 'start'/'end' values.
|
|
5
|
+
*/
|
|
6
|
+
export type Placement = 'bottom' | 'bottom-start' | 'bottom-end' | 'top' | 'top-start' | 'top-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end';
|
|
7
|
+
/**
|
|
8
|
+
* Extended placement type that includes human-readable variants.
|
|
9
|
+
*/
|
|
10
|
+
export type ExtendedPlacement = Placement | 'bottom left' | 'bottom right' | 'top left' | 'top right' | 'left top' | 'left bottom' | 'right top' | 'right bottom' | 'start' | 'start top' | 'start bottom' | 'end' | 'end top' | 'end bottom';
|
|
11
|
+
/**
|
|
12
|
+
* Options for the floating element positioning.
|
|
13
|
+
*/
|
|
14
|
+
export type FloatingOptions = {
|
|
15
|
+
/** Offset along the main axis from the anchor. */
|
|
16
|
+
offset?: number;
|
|
17
|
+
/** Placement relative to the anchor element. */
|
|
18
|
+
placement?: ExtendedPlacement;
|
|
19
|
+
/** Whether to flip when there's insufficient space. */
|
|
20
|
+
shouldFlip?: boolean;
|
|
21
|
+
/** Boundary element for positioning constraints. */
|
|
22
|
+
boundaryElement?: Element | null;
|
|
23
|
+
/** Callback when position is updated. */
|
|
24
|
+
onPositionUpdate?: (x: number, y: number, placement: FloatingPlacement) => void;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Creates a Svelte action for positioning a floating element relative to an anchor.
|
|
28
|
+
*
|
|
29
|
+
* Exposes CSS custom properties on the floating element:
|
|
30
|
+
* - `--trigger-width`: The trigger element's width
|
|
31
|
+
* - `--trigger-height`: The trigger element's height
|
|
32
|
+
* - `--available-width`: Available width between trigger and viewport edge
|
|
33
|
+
* - `--available-height`: Available height between trigger and viewport edge
|
|
34
|
+
* - `--transform-origin`: Coordinates for animations (e.g., "center top")
|
|
35
|
+
*/
|
|
36
|
+
export declare function createFloating(anchorElement: HTMLElement | null, options?: FloatingOptions): (floatingElement: HTMLElement) => {
|
|
37
|
+
destroy(): void;
|
|
38
|
+
} | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Simple Svelte action for floating positioning.
|
|
41
|
+
* Use when you just need positioning without complex state management.
|
|
42
|
+
*
|
|
43
|
+
* Exposes CSS custom properties on the floating element:
|
|
44
|
+
* - `--trigger-width`: The trigger element's width
|
|
45
|
+
* - `--trigger-height`: The trigger element's height
|
|
46
|
+
* - `--available-width`: Available width between trigger and viewport edge
|
|
47
|
+
* - `--available-height`: Available height between trigger and viewport edge
|
|
48
|
+
* - `--transform-origin`: Coordinates for animations (e.g., "center top")
|
|
49
|
+
*/
|
|
50
|
+
export declare function floating(floatingElement: HTMLElement, options: {
|
|
51
|
+
anchor: HTMLElement | null;
|
|
52
|
+
} & FloatingOptions): {
|
|
53
|
+
update(newOptions: {
|
|
54
|
+
anchor: HTMLElement | null;
|
|
55
|
+
} & FloatingOptions): void;
|
|
56
|
+
destroy(): void;
|
|
57
|
+
} | undefined;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { computePosition, flip, shift, offset as offsetMiddleware, size, autoUpdate } from '@floating-ui/dom';
|
|
2
|
+
/**
|
|
3
|
+
* Converts extended placement syntax to Floating UI placement.
|
|
4
|
+
*/
|
|
5
|
+
function normalizeExtendedPlacement(placement) {
|
|
6
|
+
const mappings = {
|
|
7
|
+
bottom: 'bottom',
|
|
8
|
+
'bottom-start': 'bottom-start',
|
|
9
|
+
'bottom-end': 'bottom-end',
|
|
10
|
+
'bottom left': 'bottom-start',
|
|
11
|
+
'bottom right': 'bottom-end',
|
|
12
|
+
top: 'top',
|
|
13
|
+
'top-start': 'top-start',
|
|
14
|
+
'top-end': 'top-end',
|
|
15
|
+
'top left': 'top-start',
|
|
16
|
+
'top right': 'top-end',
|
|
17
|
+
left: 'left',
|
|
18
|
+
'left-start': 'left-start',
|
|
19
|
+
'left-end': 'left-end',
|
|
20
|
+
'left top': 'left-start',
|
|
21
|
+
'left bottom': 'left-end',
|
|
22
|
+
right: 'right',
|
|
23
|
+
'right-start': 'right-start',
|
|
24
|
+
'right-end': 'right-end',
|
|
25
|
+
'right top': 'right-start',
|
|
26
|
+
'right bottom': 'right-end',
|
|
27
|
+
start: 'left',
|
|
28
|
+
'start top': 'left-start',
|
|
29
|
+
'start bottom': 'left-end',
|
|
30
|
+
end: 'right',
|
|
31
|
+
'end top': 'right-start',
|
|
32
|
+
'end bottom': 'right-end'
|
|
33
|
+
};
|
|
34
|
+
return mappings[placement] || 'bottom';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Calculates the transform origin based on the final placement.
|
|
38
|
+
* Used for animations that should originate from the anchor point.
|
|
39
|
+
*/
|
|
40
|
+
function getTransformOrigin(placement) {
|
|
41
|
+
const [side, align] = placement.split('-');
|
|
42
|
+
const sideMap = {
|
|
43
|
+
top: 'bottom',
|
|
44
|
+
bottom: 'top',
|
|
45
|
+
left: 'right',
|
|
46
|
+
right: 'left'
|
|
47
|
+
};
|
|
48
|
+
const alignMap = {
|
|
49
|
+
start: 'left',
|
|
50
|
+
end: 'right'
|
|
51
|
+
};
|
|
52
|
+
const vertical = side === 'top' || side === 'bottom';
|
|
53
|
+
const oppositeSide = sideMap[side] || 'top';
|
|
54
|
+
if (vertical) {
|
|
55
|
+
const horizontalAlign = align ? alignMap[align] || 'center' : 'center';
|
|
56
|
+
return `${horizontalAlign} ${oppositeSide}`;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const verticalAlign = align ? (align === 'start' ? 'top' : 'bottom') : 'center';
|
|
60
|
+
return `${oppositeSide} ${verticalAlign}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Creates a Svelte action for positioning a floating element relative to an anchor.
|
|
65
|
+
*
|
|
66
|
+
* Exposes CSS custom properties on the floating element:
|
|
67
|
+
* - `--trigger-width`: The trigger element's width
|
|
68
|
+
* - `--trigger-height`: The trigger element's height
|
|
69
|
+
* - `--available-width`: Available width between trigger and viewport edge
|
|
70
|
+
* - `--available-height`: Available height between trigger and viewport edge
|
|
71
|
+
* - `--transform-origin`: Coordinates for animations (e.g., "center top")
|
|
72
|
+
*/
|
|
73
|
+
export function createFloating(anchorElement, options = {}) {
|
|
74
|
+
const { offset = 8, placement = 'bottom', shouldFlip = true, boundaryElement = null, onPositionUpdate } = options;
|
|
75
|
+
let cleanup = null;
|
|
76
|
+
function action(floatingElement) {
|
|
77
|
+
if (!anchorElement)
|
|
78
|
+
return;
|
|
79
|
+
const normalizedPlacement = normalizeExtendedPlacement(placement);
|
|
80
|
+
const middleware = [
|
|
81
|
+
offsetMiddleware(offset),
|
|
82
|
+
...(shouldFlip ? [flip({ boundary: boundaryElement || undefined })] : []),
|
|
83
|
+
shift({ boundary: boundaryElement || undefined }),
|
|
84
|
+
size({
|
|
85
|
+
apply({ rects, availableWidth, availableHeight, elements, placement }) {
|
|
86
|
+
const floatingEl = elements.floating;
|
|
87
|
+
floatingEl.style.setProperty('--trigger-width', `${rects.reference.width}px`);
|
|
88
|
+
floatingEl.style.setProperty('--trigger-height', `${rects.reference.height}px`);
|
|
89
|
+
floatingEl.style.setProperty('--available-width', `${availableWidth}px`);
|
|
90
|
+
floatingEl.style.setProperty('--available-height', `${availableHeight}px`);
|
|
91
|
+
floatingEl.style.setProperty('--transform-origin', getTransformOrigin(placement));
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
];
|
|
95
|
+
async function updatePosition() {
|
|
96
|
+
if (!anchorElement || !floatingElement)
|
|
97
|
+
return;
|
|
98
|
+
const { x, y, placement: finalPlacement } = await computePosition(anchorElement, floatingElement, {
|
|
99
|
+
placement: normalizedPlacement,
|
|
100
|
+
middleware
|
|
101
|
+
});
|
|
102
|
+
Object.assign(floatingElement.style, {
|
|
103
|
+
left: `${x}px`,
|
|
104
|
+
top: `${y}px`
|
|
105
|
+
});
|
|
106
|
+
onPositionUpdate?.(x, y, finalPlacement);
|
|
107
|
+
}
|
|
108
|
+
cleanup = autoUpdate(anchorElement, floatingElement, updatePosition);
|
|
109
|
+
return {
|
|
110
|
+
destroy() {
|
|
111
|
+
cleanup?.();
|
|
112
|
+
cleanup = null;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return action;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Simple Svelte action for floating positioning.
|
|
120
|
+
* Use when you just need positioning without complex state management.
|
|
121
|
+
*
|
|
122
|
+
* Exposes CSS custom properties on the floating element:
|
|
123
|
+
* - `--trigger-width`: The trigger element's width
|
|
124
|
+
* - `--trigger-height`: The trigger element's height
|
|
125
|
+
* - `--available-width`: Available width between trigger and viewport edge
|
|
126
|
+
* - `--available-height`: Available height between trigger and viewport edge
|
|
127
|
+
* - `--transform-origin`: Coordinates for animations (e.g., "center top")
|
|
128
|
+
*/
|
|
129
|
+
export function floating(floatingElement, options) {
|
|
130
|
+
const { anchor, ...floatingOptions } = options;
|
|
131
|
+
if (!anchor)
|
|
132
|
+
return;
|
|
133
|
+
const normalizedPlacement = normalizeExtendedPlacement(floatingOptions.placement || 'bottom');
|
|
134
|
+
const offset = floatingOptions.offset ?? 8;
|
|
135
|
+
const shouldFlip = floatingOptions.shouldFlip ?? true;
|
|
136
|
+
const boundaryElement = floatingOptions.boundaryElement || null;
|
|
137
|
+
const middleware = [
|
|
138
|
+
offsetMiddleware(offset),
|
|
139
|
+
...(shouldFlip ? [flip({ boundary: boundaryElement || undefined })] : []),
|
|
140
|
+
shift({ boundary: boundaryElement || undefined }),
|
|
141
|
+
size({
|
|
142
|
+
apply({ rects, availableWidth, availableHeight, elements, placement }) {
|
|
143
|
+
const floatingEl = elements.floating;
|
|
144
|
+
floatingEl.style.setProperty('--trigger-width', `${rects.reference.width}px`);
|
|
145
|
+
floatingEl.style.setProperty('--trigger-height', `${rects.reference.height}px`);
|
|
146
|
+
floatingEl.style.setProperty('--available-width', `${availableWidth}px`);
|
|
147
|
+
floatingEl.style.setProperty('--available-height', `${availableHeight}px`);
|
|
148
|
+
floatingEl.style.setProperty('--transform-origin', getTransformOrigin(placement));
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
];
|
|
152
|
+
let cleanup = null;
|
|
153
|
+
async function updatePosition() {
|
|
154
|
+
if (!anchor || !floatingElement)
|
|
155
|
+
return;
|
|
156
|
+
const { x, y, placement: finalPlacement } = await computePosition(anchor, floatingElement, {
|
|
157
|
+
placement: normalizedPlacement,
|
|
158
|
+
middleware,
|
|
159
|
+
strategy: 'fixed' // Use fixed strategy for portal-rendered elements
|
|
160
|
+
});
|
|
161
|
+
Object.assign(floatingElement.style, {
|
|
162
|
+
left: `${x}px`,
|
|
163
|
+
top: `${y}px`
|
|
164
|
+
});
|
|
165
|
+
floatingOptions.onPositionUpdate?.(x, y, finalPlacement);
|
|
166
|
+
}
|
|
167
|
+
cleanup = autoUpdate(anchor, floatingElement, updatePosition);
|
|
168
|
+
return {
|
|
169
|
+
update(newOptions) {
|
|
170
|
+
cleanup?.();
|
|
171
|
+
if (newOptions.anchor) {
|
|
172
|
+
cleanup = autoUpdate(newOptions.anchor, floatingElement, updatePosition);
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
destroy() {
|
|
176
|
+
cleanup?.();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus trap primitive.
|
|
3
|
+
* Traps keyboard focus within a container element.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Svelte action that traps focus within an element.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```svelte
|
|
10
|
+
* <div use:focusTrap={isOpen}>
|
|
11
|
+
* <button>First</button>
|
|
12
|
+
* <button>Last</button>
|
|
13
|
+
* </div>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function focusTrap(node: HTMLElement, enabled?: boolean): {
|
|
17
|
+
update(newEnabled: boolean): void;
|
|
18
|
+
destroy(): void;
|
|
19
|
+
};
|