@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.
Files changed (140) hide show
  1. package/dist/combobox/TODO.md +175 -0
  2. package/dist/combobox/button/combobox-button.svelte +57 -0
  3. package/dist/combobox/button/combobox-button.svelte.d.ts +9 -0
  4. package/dist/combobox/index.d.ts +14 -0
  5. package/dist/combobox/index.js +18 -0
  6. package/dist/combobox/index.parts.d.ts +10 -0
  7. package/dist/combobox/index.parts.js +11 -0
  8. package/dist/combobox/input/combobox-input.svelte +98 -0
  9. package/dist/combobox/input/combobox-input.svelte.d.ts +13 -0
  10. package/dist/combobox/item/combobox-item-implicit-text-test.svelte +21 -0
  11. package/dist/combobox/item/combobox-item-implicit-text-test.svelte.d.ts +3 -0
  12. package/dist/combobox/item/combobox-listboxitem.svelte +136 -0
  13. package/dist/combobox/item/combobox-listboxitem.svelte.d.ts +18 -0
  14. package/dist/combobox/item-indicator/combobox-item-indicator.svelte +63 -0
  15. package/dist/combobox/item-indicator/combobox-item-indicator.svelte.d.ts +17 -0
  16. package/dist/combobox/list/combobox-listbox.svelte +76 -0
  17. package/dist/combobox/list/combobox-listbox.svelte.d.ts +47 -0
  18. package/dist/combobox/popover/combobox-popover.svelte +69 -0
  19. package/dist/combobox/popover/combobox-popover.svelte.d.ts +12 -0
  20. package/dist/combobox/root/combobox-filtered-test.svelte +51 -0
  21. package/dist/combobox/root/combobox-filtered-test.svelte.d.ts +7 -0
  22. package/dist/combobox/root/combobox-multiselect-test.svelte +76 -0
  23. package/dist/combobox/root/combobox-multiselect-test.svelte.d.ts +13 -0
  24. package/dist/combobox/root/combobox-numeric-string-id-test.svelte +20 -0
  25. package/dist/combobox/root/combobox-numeric-string-id-test.svelte.d.ts +3 -0
  26. package/dist/combobox/root/combobox-test.svelte +43 -0
  27. package/dist/combobox/root/combobox-test.svelte.d.ts +9 -0
  28. package/dist/combobox/root/combobox.svelte +696 -0
  29. package/dist/combobox/root/combobox.svelte.d.ts +58 -0
  30. package/dist/combobox/root/context.d.ts +90 -0
  31. package/dist/combobox/root/context.js +15 -0
  32. package/dist/combobox/tag/combobox-tag.svelte +58 -0
  33. package/dist/combobox/tag/combobox-tag.svelte.d.ts +22 -0
  34. package/dist/combobox/tag/tag-context-provider.svelte +36 -0
  35. package/dist/combobox/tag/tag-context-provider.svelte.d.ts +14 -0
  36. package/dist/combobox/tag-remove/combobox-tag-remove.svelte +53 -0
  37. package/dist/combobox/tag-remove/combobox-tag-remove.svelte.d.ts +14 -0
  38. package/dist/combobox/tags/combobox-tags.svelte +50 -0
  39. package/dist/combobox/tags/combobox-tags.svelte.d.ts +20 -0
  40. package/dist/dialog/content/dialog-content.svelte +121 -0
  41. package/dist/dialog/content/dialog-content.svelte.d.ts +19 -0
  42. package/dist/dialog/index.d.ts +10 -0
  43. package/dist/dialog/index.js +15 -0
  44. package/dist/dialog/index.parts.d.ts +5 -0
  45. package/dist/dialog/index.parts.js +6 -0
  46. package/dist/dialog/overlay/dialog-overlay.svelte +39 -0
  47. package/dist/dialog/overlay/dialog-overlay.svelte.d.ts +12 -0
  48. package/dist/dialog/portal/dialog-portal.svelte +32 -0
  49. package/dist/dialog/portal/dialog-portal.svelte.d.ts +12 -0
  50. package/dist/dialog/root/context.d.ts +25 -0
  51. package/dist/dialog/root/context.js +8 -0
  52. package/dist/dialog/root/dialog-root.svelte +99 -0
  53. package/dist/dialog/root/dialog-root.svelte.d.ts +21 -0
  54. package/dist/dialog/root/dialog-stack.d.ts +32 -0
  55. package/dist/dialog/root/dialog-stack.js +55 -0
  56. package/dist/dialog/root/dialog-test.svelte +38 -0
  57. package/dist/dialog/root/dialog-test.svelte.d.ts +10 -0
  58. package/dist/dialog/root/dialog-with-combobox-test.svelte +61 -0
  59. package/dist/dialog/root/dialog-with-combobox-test.svelte.d.ts +7 -0
  60. package/dist/dialog/root/nested-dialog-test.svelte +63 -0
  61. package/dist/dialog/root/nested-dialog-test.svelte.d.ts +8 -0
  62. package/dist/dialog/root/types.d.ts +10 -0
  63. package/dist/dialog/root/types.js +1 -0
  64. package/dist/dialog/trigger/dialog-trigger.svelte +71 -0
  65. package/dist/dialog/trigger/dialog-trigger.svelte.d.ts +12 -0
  66. package/dist/hooks/use-virtual-focus.svelte.d.ts +55 -0
  67. package/dist/hooks/use-virtual-focus.svelte.js +201 -0
  68. package/dist/index.d.ts +13 -0
  69. package/dist/index.js +19 -0
  70. package/dist/input/index.d.ts +3 -0
  71. package/dist/input/index.js +3 -0
  72. package/dist/input/input.svelte +19 -0
  73. package/dist/input/input.svelte.d.ts +8 -0
  74. package/dist/label/index.d.ts +3 -0
  75. package/dist/label/index.js +3 -0
  76. package/dist/label/label.svelte +21 -0
  77. package/dist/label/label.svelte.d.ts +8 -0
  78. package/dist/listbox/index.d.ts +6 -0
  79. package/dist/listbox/index.js +10 -0
  80. package/dist/listbox/index.parts.d.ts +2 -0
  81. package/dist/listbox/index.parts.js +3 -0
  82. package/dist/listbox/item/listbox-item.svelte +186 -0
  83. package/dist/listbox/item/listbox-item.svelte.d.ts +34 -0
  84. package/dist/listbox/root/context.d.ts +73 -0
  85. package/dist/listbox/root/context.js +249 -0
  86. package/dist/listbox/root/listbox-numeric-id-test.svelte +18 -0
  87. package/dist/listbox/root/listbox-numeric-id-test.svelte.d.ts +3 -0
  88. package/dist/listbox/root/listbox-test.svelte +27 -0
  89. package/dist/listbox/root/listbox-test.svelte.d.ts +8 -0
  90. package/dist/listbox/root/listbox.svelte +146 -0
  91. package/dist/listbox/root/listbox.svelte.d.ts +54 -0
  92. package/dist/popover/content/popover-content-test.svelte +43 -0
  93. package/dist/popover/content/popover-content-test.svelte.d.ts +12 -0
  94. package/dist/popover/content/popover-content.svelte +167 -0
  95. package/dist/popover/content/popover-content.svelte.d.ts +38 -0
  96. package/dist/popover/index.d.ts +8 -0
  97. package/dist/popover/index.js +14 -0
  98. package/dist/popover/index.parts.d.ts +4 -0
  99. package/dist/popover/index.parts.js +5 -0
  100. package/dist/popover/root/context.d.ts +24 -0
  101. package/dist/popover/root/context.js +10 -0
  102. package/dist/popover/root/popover-root.svelte +87 -0
  103. package/dist/popover/root/popover-root.svelte.d.ts +20 -0
  104. package/dist/popover/root/popover-test.svelte +40 -0
  105. package/dist/popover/root/popover-test.svelte.d.ts +11 -0
  106. package/dist/popover/trigger/popover-trigger-button.svelte +42 -0
  107. package/dist/popover/trigger/popover-trigger-button.svelte.d.ts +12 -0
  108. package/dist/popover/trigger/popover-trigger-in-dialog-test.svelte +29 -0
  109. package/dist/popover/trigger/popover-trigger-in-dialog-test.svelte.d.ts +18 -0
  110. package/dist/popover/trigger/popover-trigger.svelte +71 -0
  111. package/dist/popover/trigger/popover-trigger.svelte.d.ts +12 -0
  112. package/dist/portal/index.d.ts +1 -0
  113. package/dist/portal/index.js +1 -0
  114. package/dist/portal/portal.svelte +44 -0
  115. package/dist/portal/portal.svelte.d.ts +10 -0
  116. package/dist/primitives/aria-hide-outside.d.ts +38 -0
  117. package/dist/primitives/aria-hide-outside.js +152 -0
  118. package/dist/primitives/click-outside.d.ts +26 -0
  119. package/dist/primitives/click-outside.js +66 -0
  120. package/dist/primitives/floating.d.ts +57 -0
  121. package/dist/primitives/floating.js +179 -0
  122. package/dist/primitives/focus-trap.d.ts +19 -0
  123. package/dist/primitives/focus-trap.js +102 -0
  124. package/dist/primitives/index.d.ts +6 -0
  125. package/dist/primitives/index.js +7 -0
  126. package/dist/primitives/keyboard-navigation.d.ts +88 -0
  127. package/dist/primitives/keyboard-navigation.js +274 -0
  128. package/dist/primitives/scroll-lock.d.ts +19 -0
  129. package/dist/primitives/scroll-lock.js +62 -0
  130. package/dist/test-mocks/app-environment.d.ts +7 -0
  131. package/dist/test-mocks/app-environment.js +7 -0
  132. package/dist/test-mocks/app-navigation.d.ts +11 -0
  133. package/dist/test-mocks/app-navigation.js +11 -0
  134. package/dist/test-mocks/app-stores.d.ts +16 -0
  135. package/dist/test-mocks/app-stores.js +18 -0
  136. package/dist/utils/cn.d.ts +2 -0
  137. package/dist/utils/cn.js +5 -0
  138. package/dist/utils/index.d.ts +1 -0
  139. package/dist/utils/index.js +1 -0
  140. package/package.json +99 -0
@@ -0,0 +1,12 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ /**
3
+ * Dialog.Overlay - The backdrop overlay behind the dialog.
4
+ * Must be used inside a Dialog.Portal.
5
+ */
6
+ type DialogOverlayProps = {
7
+ /** CSS class for the overlay. */
8
+ class?: string;
9
+ } & Omit<HTMLAttributes<HTMLDivElement>, 'class'>;
10
+ declare const DialogOverlay: import("svelte").Component<DialogOverlayProps, {}, "">;
11
+ type DialogOverlay = ReturnType<typeof DialogOverlay>;
12
+ export default DialogOverlay;
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { Portal } from '../../portal';
4
+ import { getDialogContext } from '../root/context';
5
+
6
+ /**
7
+ * Dialog.Portal - Renders children into a portal when dialog is open.
8
+ * Must be used inside a Dialog.Root.
9
+ */
10
+ type DialogPortalProps = {
11
+ /** Content to render in portal (Overlay and Content) */
12
+ children?: Snippet;
13
+ };
14
+
15
+ let { children }: DialogPortalProps = $props();
16
+
17
+ const ctx = getDialogContext();
18
+
19
+ if (!ctx) {
20
+ throw new Error('Dialog.Portal must be used inside a Dialog.Root');
21
+ }
22
+
23
+ const dialogCtx = ctx;
24
+ </script>
25
+
26
+ {#if dialogCtx.isOpen}
27
+ <Portal>
28
+ {#if children}
29
+ {@render children()}
30
+ {/if}
31
+ </Portal>
32
+ {/if}
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ /**
3
+ * Dialog.Portal - Renders children into a portal when dialog is open.
4
+ * Must be used inside a Dialog.Root.
5
+ */
6
+ type DialogPortalProps = {
7
+ /** Content to render in portal (Overlay and Content) */
8
+ children?: Snippet;
9
+ };
10
+ declare const DialogPortal: import("svelte").Component<DialogPortalProps, {}, "">;
11
+ type DialogPortal = ReturnType<typeof DialogPortal>;
12
+ export default DialogPortal;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Context shared between Dialog components (Root, Trigger, Content).
3
+ */
4
+ export type DialogContext = {
5
+ /** Whether the dialog is open */
6
+ isOpen: boolean;
7
+ /** Reference to the trigger element */
8
+ triggerRef: HTMLElement | null;
9
+ /** Set the trigger ref (used by Trigger component) */
10
+ setTriggerRef: (el: HTMLElement | null) => void;
11
+ /** Toggle dialog open state */
12
+ toggle: () => void;
13
+ /** Open the dialog */
14
+ open: () => void;
15
+ /** Close the dialog and return focus to trigger */
16
+ close: () => void;
17
+ /** Called when dialog open state changes */
18
+ onOpenChange: (open: boolean) => void;
19
+ /** Stack level for z-index calculation (set by Content) */
20
+ stackLevel: number;
21
+ /** Set the stack level (used by Content component) */
22
+ setStackLevel: (level: number) => void;
23
+ };
24
+ export declare function setDialogContext(ctx: DialogContext): void;
25
+ export declare function getDialogContext(): DialogContext | undefined;
@@ -0,0 +1,8 @@
1
+ import { setContext, getContext } from 'svelte';
2
+ const DIALOG_CONTEXT_KEY = Symbol('dialog');
3
+ export function setDialogContext(ctx) {
4
+ setContext(DIALOG_CONTEXT_KEY, ctx);
5
+ }
6
+ export function getDialogContext() {
7
+ return getContext(DIALOG_CONTEXT_KEY);
8
+ }
@@ -0,0 +1,99 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { setDialogContext, type DialogContext } from './context';
4
+ import type { DialogStateHelpers } from './types';
5
+
6
+ /**
7
+ * Dialog.Root - State management wrapper for Dialog components.
8
+ * Provides context for Trigger and Content children.
9
+ */
10
+ type DialogRootProps = {
11
+ /** Controlled open state. */
12
+ open?: boolean;
13
+ /** Initial open state for uncontrolled mode. */
14
+ defaultOpen?: boolean;
15
+ /** Callback when open state changes. */
16
+ onOpenChange?: (open: boolean) => void;
17
+ /** Reference to the trigger element. Can be set manually or via Dialog.Trigger. */
18
+ triggerRef?: HTMLElement | null;
19
+ /** Children snippet receives state helpers: { close, open, toggle, isOpen } */
20
+ children?: Snippet<[DialogStateHelpers]>;
21
+ };
22
+
23
+ let {
24
+ open: openProp = $bindable(),
25
+ defaultOpen = false,
26
+ onOpenChange,
27
+ triggerRef = $bindable<HTMLElement | null>(null),
28
+ children
29
+ }: DialogRootProps = $props();
30
+
31
+ // Use function to capture initial value only (not reactive)
32
+ let isOpenInternal = $state((() => defaultOpen)());
33
+
34
+ // Stack level for z-index calculation
35
+ let stackLevel = $state(0);
36
+
37
+ const isControlled = $derived(openProp !== undefined);
38
+ const isOpen = $derived(isControlled ? openProp! : isOpenInternal);
39
+
40
+ function setOpen(value: boolean) {
41
+ if (isControlled) {
42
+ onOpenChange?.(value);
43
+ } else {
44
+ isOpenInternal = value;
45
+ onOpenChange?.(value);
46
+ }
47
+ // Sync bindable prop
48
+ openProp = value;
49
+ }
50
+
51
+ function toggle() {
52
+ setOpen(!isOpen);
53
+ }
54
+
55
+ function openDialog() {
56
+ setOpen(true);
57
+ }
58
+
59
+ function closeDialog() {
60
+ setOpen(false);
61
+ triggerRef?.focus();
62
+ }
63
+
64
+ function setTriggerRef(el: HTMLElement | null) {
65
+ triggerRef = el;
66
+ }
67
+
68
+ function handleOpenChange(newOpen: boolean) {
69
+ setOpen(newOpen);
70
+ }
71
+
72
+ function setStackLevel(level: number) {
73
+ stackLevel = level;
74
+ }
75
+
76
+ const ctx: DialogContext = {
77
+ get isOpen() {
78
+ return isOpen;
79
+ },
80
+ get triggerRef() {
81
+ return triggerRef ?? null;
82
+ },
83
+ get stackLevel() {
84
+ return stackLevel;
85
+ },
86
+ setTriggerRef,
87
+ setStackLevel,
88
+ toggle,
89
+ open: openDialog,
90
+ close: closeDialog,
91
+ onOpenChange: handleOpenChange
92
+ };
93
+
94
+ setDialogContext(ctx);
95
+ </script>
96
+
97
+ {#if children}
98
+ {@render children({ close: closeDialog, open: openDialog, toggle, isOpen })}
99
+ {/if}
@@ -0,0 +1,21 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { DialogStateHelpers } from './types';
3
+ /**
4
+ * Dialog.Root - State management wrapper for Dialog components.
5
+ * Provides context for Trigger and Content children.
6
+ */
7
+ type DialogRootProps = {
8
+ /** Controlled open state. */
9
+ open?: boolean;
10
+ /** Initial open state for uncontrolled mode. */
11
+ defaultOpen?: boolean;
12
+ /** Callback when open state changes. */
13
+ onOpenChange?: (open: boolean) => void;
14
+ /** Reference to the trigger element. Can be set manually or via Dialog.Trigger. */
15
+ triggerRef?: HTMLElement | null;
16
+ /** Children snippet receives state helpers: { close, open, toggle, isOpen } */
17
+ children?: Snippet<[DialogStateHelpers]>;
18
+ };
19
+ declare const DialogRoot: import("svelte").Component<DialogRootProps, {}, "open" | "triggerRef">;
20
+ type DialogRoot = ReturnType<typeof DialogRoot>;
21
+ export default DialogRoot;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Global dialog stack for managing nested dialogs.
3
+ * Only the topmost dialog should respond to Escape and click-outside events.
4
+ */
5
+ /**
6
+ * Register a dialog when it opens.
7
+ * Returns the dialog ID and level for z-index calculation.
8
+ */
9
+ export declare function pushDialog(close: () => void): {
10
+ id: symbol;
11
+ level: number;
12
+ };
13
+ /**
14
+ * Unregister a dialog when it closes.
15
+ */
16
+ export declare function popDialog(id: symbol): void;
17
+ /**
18
+ * Check if this dialog is the topmost (should handle events).
19
+ */
20
+ export declare function isTopmostDialog(id: symbol): boolean;
21
+ /**
22
+ * Get the z-index for the overlay based on dialog level.
23
+ */
24
+ export declare function getOverlayZIndex(level: number): number;
25
+ /**
26
+ * Get the z-index for the content based on dialog level.
27
+ */
28
+ export declare function getContentZIndex(level: number): number;
29
+ /**
30
+ * Get the number of open dialogs.
31
+ */
32
+ export declare function getDialogCount(): number;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Global dialog stack for managing nested dialogs.
3
+ * Only the topmost dialog should respond to Escape and click-outside events.
4
+ */
5
+ const dialogStack = [];
6
+ /**
7
+ * Base z-index for dialogs. Each nested dialog increments by 10.
8
+ */
9
+ const BASE_Z_INDEX = 9990;
10
+ const Z_INDEX_INCREMENT = 10;
11
+ /**
12
+ * Register a dialog when it opens.
13
+ * Returns the dialog ID and level for z-index calculation.
14
+ */
15
+ export function pushDialog(close) {
16
+ const id = Symbol('dialog');
17
+ const level = dialogStack.length;
18
+ dialogStack.push({ id, close, level });
19
+ return { id, level };
20
+ }
21
+ /**
22
+ * Unregister a dialog when it closes.
23
+ */
24
+ export function popDialog(id) {
25
+ const index = dialogStack.findIndex((entry) => entry.id === id);
26
+ if (index !== -1) {
27
+ dialogStack.splice(index, 1);
28
+ }
29
+ }
30
+ /**
31
+ * Check if this dialog is the topmost (should handle events).
32
+ */
33
+ export function isTopmostDialog(id) {
34
+ if (dialogStack.length === 0)
35
+ return false;
36
+ return dialogStack[dialogStack.length - 1].id === id;
37
+ }
38
+ /**
39
+ * Get the z-index for the overlay based on dialog level.
40
+ */
41
+ export function getOverlayZIndex(level) {
42
+ return BASE_Z_INDEX + level * Z_INDEX_INCREMENT;
43
+ }
44
+ /**
45
+ * Get the z-index for the content based on dialog level.
46
+ */
47
+ export function getContentZIndex(level) {
48
+ return BASE_Z_INDEX + level * Z_INDEX_INCREMENT + 1;
49
+ }
50
+ /**
51
+ * Get the number of open dialogs.
52
+ */
53
+ export function getDialogCount() {
54
+ return dialogStack.length;
55
+ }
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import { Dialog } from '../index';
3
+
4
+ type Props = {
5
+ open?: boolean;
6
+ defaultOpen?: boolean;
7
+ shouldCloseOnInteractOutside?: boolean;
8
+ shouldCloseOnEscape?: boolean;
9
+ onOpenChange?: (open: boolean) => void;
10
+ };
11
+
12
+ let {
13
+ open,
14
+ defaultOpen = false,
15
+ shouldCloseOnInteractOutside = true,
16
+ shouldCloseOnEscape = true,
17
+ onOpenChange
18
+ }: Props = $props();
19
+ </script>
20
+
21
+ <Dialog.Root {open} {defaultOpen} {onOpenChange}>
22
+ {#snippet children({ close })}
23
+ <Dialog.Trigger>
24
+ <button type="button">Open Dialog</button>
25
+ </Dialog.Trigger>
26
+
27
+ <Dialog.Portal>
28
+ <Dialog.Overlay data-testid="dialog-overlay" />
29
+ <Dialog.Content class="dialog-content" {shouldCloseOnInteractOutside} {shouldCloseOnEscape}>
30
+ <div class="dialog-body">
31
+ <h3>Dialog Title</h3>
32
+ <p>Dialog content goes here.</p>
33
+ <button type="button" class="close-btn" onclick={close}>Close</button>
34
+ </div>
35
+ </Dialog.Content>
36
+ </Dialog.Portal>
37
+ {/snippet}
38
+ </Dialog.Root>
@@ -0,0 +1,10 @@
1
+ type Props = {
2
+ open?: boolean;
3
+ defaultOpen?: boolean;
4
+ shouldCloseOnInteractOutside?: boolean;
5
+ shouldCloseOnEscape?: boolean;
6
+ onOpenChange?: (open: boolean) => void;
7
+ };
8
+ declare const DialogTest: import("svelte").Component<Props, {}, "">;
9
+ type DialogTest = ReturnType<typeof DialogTest>;
10
+ export default DialogTest;
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ import { Dialog } from '../index';
3
+ import { ComboBox } from '../../combobox';
4
+
5
+ type Props = {
6
+ comboboxAutofocus?: boolean;
7
+ comboboxTrigger?: 'focus' | 'input' | 'press';
8
+ };
9
+
10
+ let { comboboxAutofocus = false, comboboxTrigger = 'press' }: Props = $props();
11
+
12
+ const countries = [
13
+ { id: 'ar', name: 'Argentina' },
14
+ { id: 'br', name: 'Brazil' },
15
+ { id: 'ca', name: 'Canada' }
16
+ ];
17
+
18
+ let selectedCountry = $state<string | number | undefined>();
19
+ </script>
20
+
21
+ <Dialog.Root>
22
+ {#snippet children({ close })}
23
+ <Dialog.Trigger>
24
+ <button type="button" data-testid="dialog-trigger">Open Dialog</button>
25
+ </Dialog.Trigger>
26
+
27
+ <Dialog.Portal>
28
+ <Dialog.Overlay />
29
+ <Dialog.Content class="dialog-content">
30
+ <h3>Select a Country</h3>
31
+ <ComboBox.Root trigger={comboboxTrigger} bind:value={selectedCountry}>
32
+ <div class="flex gap-1">
33
+ <ComboBox.Input
34
+ placeholder="Search countries..."
35
+ autofocus={comboboxAutofocus}
36
+ data-testid="combobox-input"
37
+ />
38
+ <ComboBox.Button data-testid="combobox-button" />
39
+ </div>
40
+ <ComboBox.Popover>
41
+ <ComboBox.List emptyPlaceholder="No countries found">
42
+ {#each countries as country (country.id)}
43
+ <ComboBox.Item
44
+ id={country.id}
45
+ textValue={country.name}
46
+ data-testid="combobox-item-{country.id}"
47
+ >
48
+ {country.name}
49
+ </ComboBox.Item>
50
+ {/each}
51
+ </ComboBox.List>
52
+ </ComboBox.Popover>
53
+ </ComboBox.Root>
54
+ <div style="margin-top: 16px;">
55
+ <button type="button" onclick={close} data-testid="close-button">Close</button>
56
+ </div>
57
+ <div data-testid="selected-value">{selectedCountry ?? ''}</div>
58
+ </Dialog.Content>
59
+ </Dialog.Portal>
60
+ {/snippet}
61
+ </Dialog.Root>
@@ -0,0 +1,7 @@
1
+ type Props = {
2
+ comboboxAutofocus?: boolean;
3
+ comboboxTrigger?: 'focus' | 'input' | 'press';
4
+ };
5
+ declare const DialogWithComboboxTest: import("svelte").Component<Props, {}, "">;
6
+ type DialogWithComboboxTest = ReturnType<typeof DialogWithComboboxTest>;
7
+ export default DialogWithComboboxTest;
@@ -0,0 +1,63 @@
1
+ <script lang="ts">
2
+ import { Dialog } from '../index';
3
+
4
+ type Props = {
5
+ onDialog1Close?: () => void;
6
+ onDialog2Close?: () => void;
7
+ onDialog3Close?: () => void;
8
+ };
9
+
10
+ let { onDialog1Close, onDialog2Close, onDialog3Close }: Props = $props();
11
+ </script>
12
+
13
+ <!-- Level 1 Dialog -->
14
+ <Dialog.Root onOpenChange={(open) => !open && onDialog1Close?.()}>
15
+ {#snippet children({ close })}
16
+ <Dialog.Trigger>
17
+ <button type="button" data-testid="trigger-1">Open Dialog 1</button>
18
+ </Dialog.Trigger>
19
+
20
+ <Dialog.Portal>
21
+ <Dialog.Overlay data-testid="overlay-1" />
22
+ <Dialog.Content data-testid="content-1" class="dialog-1">
23
+ <h3>Dialog 1</h3>
24
+ <button type="button" data-testid="close-1" onclick={close}>Close 1</button>
25
+
26
+ <!-- Level 2 Dialog (Nested) -->
27
+ <Dialog.Root onOpenChange={(open) => !open && onDialog2Close?.()}>
28
+ {#snippet children({ close: close2 })}
29
+ <Dialog.Trigger>
30
+ <button type="button" data-testid="trigger-2">Open Dialog 2</button>
31
+ </Dialog.Trigger>
32
+
33
+ <Dialog.Portal>
34
+ <Dialog.Overlay data-testid="overlay-2" />
35
+ <Dialog.Content data-testid="content-2" class="dialog-2">
36
+ <h3>Dialog 2</h3>
37
+ <button type="button" data-testid="close-2" onclick={close2}>Close 2</button>
38
+
39
+ <!-- Level 3 Dialog (Double Nested) -->
40
+ <Dialog.Root onOpenChange={(open) => !open && onDialog3Close?.()}>
41
+ {#snippet children({ close: close3 })}
42
+ <Dialog.Trigger>
43
+ <button type="button" data-testid="trigger-3">Open Dialog 3</button>
44
+ </Dialog.Trigger>
45
+
46
+ <Dialog.Portal>
47
+ <Dialog.Overlay data-testid="overlay-3" />
48
+ <Dialog.Content data-testid="content-3" class="dialog-3">
49
+ <h3>Dialog 3</h3>
50
+ <button type="button" data-testid="close-3" onclick={close3}>Close 3</button
51
+ >
52
+ </Dialog.Content>
53
+ </Dialog.Portal>
54
+ {/snippet}
55
+ </Dialog.Root>
56
+ </Dialog.Content>
57
+ </Dialog.Portal>
58
+ {/snippet}
59
+ </Dialog.Root>
60
+ </Dialog.Content>
61
+ </Dialog.Portal>
62
+ {/snippet}
63
+ </Dialog.Root>
@@ -0,0 +1,8 @@
1
+ type Props = {
2
+ onDialog1Close?: () => void;
3
+ onDialog2Close?: () => void;
4
+ onDialog3Close?: () => void;
5
+ };
6
+ declare const NestedDialogTest: import("svelte").Component<Props, {}, "">;
7
+ type NestedDialogTest = ReturnType<typeof NestedDialogTest>;
8
+ export default NestedDialogTest;
@@ -0,0 +1,10 @@
1
+ export type DialogStateHelpers = {
2
+ /** Close the dialog */
3
+ close: () => void;
4
+ /** Open the dialog */
5
+ open: () => void;
6
+ /** Toggle the dialog open state */
7
+ toggle: () => void;
8
+ /** Whether the dialog is currently open */
9
+ isOpen: boolean;
10
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { onMount, onDestroy } from 'svelte';
4
+ import { getDialogContext } from '../root/context';
5
+
6
+ /**
7
+ * Dialog.Trigger - Wrapper that auto-wires a trigger element.
8
+ * Must be used inside a Dialog.Root.
9
+ */
10
+ type DialogTriggerProps = {
11
+ /** Children (trigger button) */
12
+ children?: Snippet;
13
+ };
14
+
15
+ let { children }: DialogTriggerProps = $props();
16
+
17
+ const ctx = getDialogContext();
18
+
19
+ if (!ctx) {
20
+ throw new Error('Dialog.Trigger must be used inside a Dialog.Root');
21
+ }
22
+
23
+ // After the throw, TypeScript knows ctx is defined
24
+ const dialogCtx = ctx;
25
+
26
+ let wrapperRef: HTMLElement | null = $state(null);
27
+
28
+ function handleClick(event: MouseEvent) {
29
+ const target = event.target as HTMLElement;
30
+ const button = target.closest('button, [role="button"]') as HTMLElement | null;
31
+
32
+ if (button && wrapperRef?.contains(button)) {
33
+ // Set trigger ref if not already set
34
+ if (!dialogCtx.triggerRef) {
35
+ dialogCtx.setTriggerRef(button);
36
+ }
37
+ dialogCtx.toggle();
38
+ }
39
+ }
40
+
41
+ onMount(() => {
42
+ if (wrapperRef) {
43
+ // Find and set up the trigger button
44
+ const firstButton = wrapperRef.querySelector('button, [role="button"]') as HTMLElement | null;
45
+ if (firstButton) {
46
+ dialogCtx.setTriggerRef(firstButton);
47
+ firstButton.setAttribute('aria-haspopup', 'dialog');
48
+ }
49
+ }
50
+
51
+ // Add click listener imperatively to avoid a11y linter warnings
52
+ // Buttons inside handle keyboard events natively (Enter/Space trigger click)
53
+ wrapperRef?.addEventListener('click', handleClick);
54
+ });
55
+
56
+ onDestroy(() => {
57
+ wrapperRef?.removeEventListener('click', handleClick);
58
+ });
59
+
60
+ $effect(() => {
61
+ if (dialogCtx.triggerRef) {
62
+ dialogCtx.triggerRef.setAttribute('aria-expanded', String(dialogCtx.isOpen));
63
+ }
64
+ });
65
+ </script>
66
+
67
+ <div bind:this={wrapperRef} style="display: contents;">
68
+ {#if children}
69
+ {@render children()}
70
+ {/if}
71
+ </div>
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ /**
3
+ * Dialog.Trigger - Wrapper that auto-wires a trigger element.
4
+ * Must be used inside a Dialog.Root.
5
+ */
6
+ type DialogTriggerProps = {
7
+ /** Children (trigger button) */
8
+ children?: Snippet;
9
+ };
10
+ declare const DialogTrigger: import("svelte").Component<DialogTriggerProps, {}, "">;
11
+ type DialogTrigger = ReturnType<typeof DialogTrigger>;
12
+ export default DialogTrigger;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * useVirtualFocus - A reusable hook for virtual focus navigation.
3
+ *
4
+ * Implements the aria-activedescendant pattern for keyboard navigation
5
+ * in composite widgets like ComboBox, Menu, Select, TreeView, etc.
6
+ *
7
+ * Features:
8
+ * - Virtual focus state (focusedId)
9
+ * - Item registration/unregistration
10
+ * - Navigation (next, previous, first, last, pageUp, pageDown)
11
+ * - DOM order caching for performance
12
+ * - Scoped queries via containerRef
13
+ *
14
+ * @example
15
+ * ```svelte
16
+ * <script>
17
+ * import { useVirtualFocus } from './use-virtual-focus.svelte';
18
+ *
19
+ * const nav = useVirtualFocus({
20
+ * instanceId: 'my-list',
21
+ * containerRef: () => listboxElement
22
+ * });
23
+ *
24
+ * // Use nav.focusedId, nav.next(), nav.register(), etc.
25
+ * // PageSize is calculated automatically based on item count
26
+ * </script>
27
+ * ```
28
+ */
29
+ export type VirtualFocusOptions = {
30
+ /** Unique ID prefix for item identification */
31
+ instanceId: string;
32
+ /** Prefix for item IDs in the DOM (default: 'combobox-item') */
33
+ itemPrefix?: string;
34
+ /** Reference to container element for scoped DOM queries */
35
+ containerRef?: () => HTMLElement | null;
36
+ };
37
+ export type VirtualFocusReturn = {
38
+ readonly focusedId: string | number | null;
39
+ readonly itemIds: (string | number)[];
40
+ readonly itemLabels: Map<string | number, string>;
41
+ readonly pendingFocusDirection: 'first' | 'last' | null;
42
+ next: () => void;
43
+ previous: () => void;
44
+ first: () => void;
45
+ last: () => void;
46
+ pageUp: () => void;
47
+ pageDown: () => void;
48
+ register: (id: string | number, label: string) => void;
49
+ unregister: (id: string | number) => void;
50
+ setFocused: (id: string | number | null) => void;
51
+ setPendingDirection: (direction: 'first' | 'last' | null) => void;
52
+ reset: () => void;
53
+ invalidateCache: () => void;
54
+ };
55
+ export declare function useVirtualFocus(options: VirtualFocusOptions): VirtualFocusReturn;