@geoffcox/sterling-svelte 2.0.2 → 2.0.3

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 (131) hide show
  1. package/README.md +18 -0
  2. package/dist/@types/clickOutside.d.ts +15 -0
  3. package/dist/Button.svelte +29 -0
  4. package/dist/Button.svelte.d.ts +8 -0
  5. package/dist/Callout.svelte +243 -0
  6. package/dist/Callout.svelte.d.ts +14 -0
  7. package/dist/Callout.types.d.ts +11 -0
  8. package/dist/Callout.types.js +1 -0
  9. package/dist/Checkbox.svelte +62 -0
  10. package/dist/Checkbox.svelte.d.ts +9 -0
  11. package/dist/Dialog.svelte +201 -0
  12. package/dist/Dialog.svelte.d.ts +14 -0
  13. package/dist/Dropdown.svelte +159 -0
  14. package/dist/Dropdown.svelte.d.ts +23 -0
  15. package/dist/Input.svelte +80 -0
  16. package/dist/Input.svelte.d.ts +11 -0
  17. package/dist/Label.constants.d.ts +2 -0
  18. package/dist/Label.constants.js +2 -0
  19. package/dist/Label.svelte +135 -0
  20. package/dist/Label.svelte.d.ts +17 -0
  21. package/dist/Link.svelte +31 -0
  22. package/dist/Link.svelte.d.ts +11 -0
  23. package/dist/List.constants.d.ts +1 -0
  24. package/dist/List.constants.js +1 -0
  25. package/dist/List.svelte +258 -0
  26. package/dist/List.svelte.d.ts +19 -0
  27. package/dist/List.types.d.ts +5 -0
  28. package/dist/List.types.js +1 -0
  29. package/dist/ListItem.svelte +64 -0
  30. package/dist/ListItem.svelte.d.ts +12 -0
  31. package/dist/Menu.svelte +105 -0
  32. package/dist/Menu.svelte.d.ts +12 -0
  33. package/dist/MenuBar.constants.d.ts +1 -0
  34. package/dist/MenuBar.constants.js +1 -0
  35. package/dist/MenuBar.svelte +144 -0
  36. package/dist/MenuBar.svelte.d.ts +12 -0
  37. package/dist/MenuBar.types.d.ts +4 -0
  38. package/dist/MenuBar.types.js +1 -0
  39. package/dist/MenuButton.svelte +156 -0
  40. package/dist/MenuButton.svelte.d.ts +20 -0
  41. package/dist/MenuItem.constants.d.ts +2 -0
  42. package/dist/MenuItem.constants.js +2 -0
  43. package/dist/MenuItem.svelte +431 -0
  44. package/dist/MenuItem.svelte.d.ts +22 -0
  45. package/dist/MenuItem.types.d.ts +20 -0
  46. package/dist/MenuItem.types.js +1 -0
  47. package/dist/MenuItem.utils.d.ts +7 -0
  48. package/dist/MenuItem.utils.js +36 -0
  49. package/dist/MenuSeparator.svelte +11 -0
  50. package/dist/MenuSeparator.svelte.d.ts +5 -0
  51. package/dist/Pagination.svelte +267 -0
  52. package/dist/Pagination.svelte.d.ts +4 -0
  53. package/dist/Pagination.types.d.ts +24 -0
  54. package/dist/Pagination.types.js +1 -0
  55. package/dist/Popover.constants.d.ts +1 -0
  56. package/dist/Popover.constants.js +14 -0
  57. package/dist/Popover.svelte +175 -0
  58. package/dist/Popover.svelte.d.ts +14 -0
  59. package/dist/Popover.types.d.ts +4 -0
  60. package/dist/Popover.types.js +1 -0
  61. package/dist/Portal.constants.d.ts +2 -0
  62. package/dist/Portal.constants.js +2 -0
  63. package/dist/Portal.types.d.ts +3 -0
  64. package/dist/Portal.types.js +1 -0
  65. package/dist/Progress.constants.d.ts +1 -0
  66. package/dist/Progress.constants.js +1 -0
  67. package/dist/Progress.svelte +61 -0
  68. package/dist/Progress.svelte.d.ts +11 -0
  69. package/dist/Progress.types.d.ts +4 -0
  70. package/dist/Progress.types.js +1 -0
  71. package/dist/Radio.svelte +65 -0
  72. package/dist/Radio.svelte.d.ts +12 -0
  73. package/dist/Select.svelte +262 -0
  74. package/dist/Select.svelte.d.ts +26 -0
  75. package/dist/Slider.svelte +182 -0
  76. package/dist/Slider.svelte.d.ts +18 -0
  77. package/dist/Switch.svelte +92 -0
  78. package/dist/Switch.svelte.d.ts +21 -0
  79. package/dist/Tab.svelte +66 -0
  80. package/dist/Tab.svelte.d.ts +11 -0
  81. package/dist/TabList.constants.d.ts +1 -0
  82. package/dist/TabList.constants.js +1 -0
  83. package/dist/TabList.svelte +253 -0
  84. package/dist/TabList.svelte.d.ts +17 -0
  85. package/dist/TabList.types.d.ts +5 -0
  86. package/dist/TabList.types.js +1 -0
  87. package/dist/TextArea.constants.d.ts +1 -0
  88. package/dist/TextArea.constants.js +1 -0
  89. package/dist/TextArea.svelte +116 -0
  90. package/dist/TextArea.svelte.d.ts +18 -0
  91. package/dist/TextArea.types.d.ts +4 -0
  92. package/dist/TextArea.types.js +1 -0
  93. package/dist/Tooltip.svelte +95 -0
  94. package/dist/Tooltip.svelte.d.ts +10 -0
  95. package/dist/Tree.constants.d.ts +1 -0
  96. package/dist/Tree.constants.js +1 -0
  97. package/dist/Tree.svelte +81 -0
  98. package/dist/Tree.svelte.d.ts +14 -0
  99. package/dist/Tree.types.d.ts +5 -0
  100. package/dist/Tree.types.js +1 -0
  101. package/dist/TreeChevron.svelte +39 -0
  102. package/dist/TreeChevron.svelte.d.ts +8 -0
  103. package/dist/TreeItem.constants.d.ts +1 -0
  104. package/dist/TreeItem.constants.js +1 -0
  105. package/dist/TreeItem.svelte +396 -0
  106. package/dist/TreeItem.svelte.d.ts +22 -0
  107. package/dist/TreeItem.types.d.ts +4 -0
  108. package/dist/TreeItem.types.js +1 -0
  109. package/dist/actions/applyLightDarkMode.d.ts +10 -0
  110. package/dist/actions/applyLightDarkMode.js +36 -0
  111. package/dist/actions/clickOutside.d.ts +15 -0
  112. package/dist/actions/clickOutside.js +28 -0
  113. package/dist/actions/extraClass.d.ts +9 -0
  114. package/dist/actions/extraClass.js +15 -0
  115. package/dist/actions/forwardEvents.d.ts +12 -0
  116. package/dist/actions/forwardEvents.js +32 -0
  117. package/dist/actions/portal.d.ts +8 -0
  118. package/dist/actions/portal.js +19 -0
  119. package/dist/actions/trapKeyboardFocus.d.ts +3 -0
  120. package/dist/actions/trapKeyboardFocus.js +52 -0
  121. package/dist/idGenerator.d.ts +5 -0
  122. package/dist/idGenerator.js +11 -0
  123. package/dist/index.d.ts +60 -0
  124. package/dist/index.js +55 -0
  125. package/dist/mediaQueries/prefersColorSchemeDark.d.ts +1 -0
  126. package/dist/mediaQueries/prefersColorSchemeDark.js +14 -0
  127. package/dist/mediaQueries/prefersReducedMotion.d.ts +1 -0
  128. package/dist/mediaQueries/prefersReducedMotion.js +14 -0
  129. package/dist/mediaQueries/usingKeyboard.d.ts +1 -0
  130. package/dist/mediaQueries/usingKeyboard.js +17 -0
  131. package/package.json +8 -7
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # sterling-svelte
2
+
3
+ A modern, accessible, and lightweight component library for Svelte.
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ npm install @geoffcox/sterling-svelte
9
+ ```
10
+
11
+ ## Documentation
12
+
13
+ The project builds the documentation for the library as a SvelteKit application.
14
+ See the published version of the [documentation](https://geoffcox.github.io/docs/sterling-svelte/).
15
+
16
+ ## Repository
17
+
18
+ https://github.com/GeoffCox/sterling-svelte
@@ -0,0 +1,15 @@
1
+ import type { EventHandler } from 'svelte/elements';
2
+
3
+ export type ClickOutsideEventDetail = {
4
+ mouseEvent: MouseEvent;
5
+ };
6
+
7
+ export type ClickOutsideEvent = CustomEvent<ClickOutsideEventDetail>;
8
+
9
+ declare module 'svelte/elements' {
10
+ export interface DOMAttributes<T extends EventTarget> {
11
+ 'on:click_outside'?: EventHandler<ClickOutsideEvent, T>;
12
+ }
13
+ }
14
+
15
+ export {}; // ensure this is not an ambient module, else types will be overridden instead of augmented
@@ -0,0 +1,29 @@
1
+ <svelte:options runes={true} />
2
+
3
+ <script lang="ts">
4
+ import type { HTMLButtonAttributes } from 'svelte/elements';
5
+
6
+ type Props = HTMLButtonAttributes;
7
+
8
+ let { children, class: _class, ...rest }: Props = $props();
9
+
10
+ let buttonRef: HTMLButtonElement;
11
+
12
+ export const click = () => {
13
+ buttonRef?.click();
14
+ };
15
+
16
+ export const blur = () => {
17
+ buttonRef?.blur();
18
+ };
19
+
20
+ export const focus = (options?: FocusOptions) => {
21
+ buttonRef?.focus(options);
22
+ };
23
+ </script>
24
+
25
+ <button bind:this={buttonRef} class={['sterling-button', _class]} type="button" {...rest}>
26
+ {#if children}
27
+ {@render children()}
28
+ {/if}
29
+ </button>
@@ -0,0 +1,8 @@
1
+ import type { HTMLButtonAttributes } from 'svelte/elements';
2
+ declare const Button: import("svelte").Component<HTMLButtonAttributes, {
3
+ click: () => void;
4
+ blur: () => void;
5
+ focus: (options?: FocusOptions) => void;
6
+ }, "">;
7
+ type Button = ReturnType<typeof Button>;
8
+ export default Button;
@@ -0,0 +1,243 @@
1
+ <svelte:options runes={true} />
2
+
3
+ <script lang="ts">
4
+ import {
5
+ arrow,
6
+ autoUpdate,
7
+ computePosition,
8
+ flip,
9
+ offset,
10
+ type ComputePositionReturn,
11
+ type Placement
12
+ } from '@floating-ui/dom';
13
+ import { getContext, tick } from 'svelte';
14
+ import type { HTMLAttributes, KeyboardEventHandler } from 'svelte/elements';
15
+ import { fade, type FadeParams, type TransitionConfig } from 'svelte/transition';
16
+ import { portal } from './actions/portal';
17
+ import { prefersReducedMotion } from './mediaQueries/prefersReducedMotion';
18
+ import type { PopoverPlacement } from './Popover.types';
19
+ import { STERLING_PORTAL_CONTEXT_ID, STERLING_PORTAL_HOST_ID } from './Portal.constants';
20
+ import type { PortalContext } from './Portal.types';
21
+
22
+ type Props = HTMLAttributes<HTMLDivElement> & {
23
+ conditionalRender?: boolean | null;
24
+ crossAxisOffset?: number;
25
+ mainAxisOffset?: number;
26
+ open?: boolean | null;
27
+ placement?: PopoverPlacement;
28
+ portalHost?: HTMLElement;
29
+ reference?: HTMLElement | null;
30
+ };
31
+
32
+ let {
33
+ children,
34
+ conditionalRender = $bindable(true),
35
+ crossAxisOffset = $bindable(0),
36
+ mainAxisOffset = $bindable(0),
37
+ open = $bindable(false),
38
+ placement = $bindable('top-start'),
39
+ portalHost,
40
+ reference,
41
+ class: _class,
42
+ ...rest
43
+ }: Props = $props();
44
+
45
+ let popupRef: HTMLDivElement | undefined = $state(undefined);
46
+ let arrowRef: HTMLDivElement | undefined = $state(undefined);
47
+ let popupPosition: Partial<ComputePositionReturn> = $state({ x: 0, y: 0 });
48
+ let floatingUIPlacement = $derived(placement as Placement);
49
+ let bodyHeight = $state(0);
50
+ let resizeObserver: ResizeObserver | undefined = undefined;
51
+
52
+ const portalContext = getContext<PortalContext>(STERLING_PORTAL_CONTEXT_ID) || {
53
+ portalHost: undefined
54
+ };
55
+
56
+ const ensurePortalHost = async () => {
57
+ await tick();
58
+
59
+ // use the host set from context, usually set from a Dialog
60
+ let host = portalContext.portalHost;
61
+
62
+ // use or create the sterling portal host
63
+ if (!host && globalThis?.document) {
64
+ host = globalThis.document.querySelector(`#${STERLING_PORTAL_HOST_ID}`) as HTMLElement;
65
+
66
+ // fallback to creating the sterling portal host
67
+ if (!host) {
68
+ host = globalThis.document.createElement('div');
69
+ host.id = STERLING_PORTAL_HOST_ID;
70
+ host.style.overflow = 'visible';
71
+ globalThis.document.body.append(host);
72
+ }
73
+ }
74
+
75
+ portalHost = host;
76
+ };
77
+
78
+ let middleware = $derived([
79
+ offset({ mainAxis: mainAxisOffset, crossAxis: crossAxisOffset }),
80
+ flip(),
81
+ arrowRef && arrow({ element: arrowRef, padding: 8 })
82
+ ]);
83
+
84
+ const computeCalloutPosition = async () => {
85
+ if (reference && popupRef) {
86
+ popupPosition = await computePosition(reference, popupRef, {
87
+ placement: floatingUIPlacement,
88
+ middleware
89
+ });
90
+ } else {
91
+ popupPosition = { x: 0, y: 0 };
92
+ }
93
+ };
94
+
95
+ // whenever a positioned element is portaled it needs resubscription to auto-update
96
+ let cleanupAutoUpdate = () => {};
97
+
98
+ const autoUpdateCalloutPosition = () => {
99
+ cleanupAutoUpdate();
100
+ cleanupAutoUpdate = () => {};
101
+ if (reference && popupRef) {
102
+ cleanupAutoUpdate = autoUpdate(reference, popupRef, computeCalloutPosition);
103
+ }
104
+ };
105
+
106
+ $effect(() => {
107
+ autoUpdateCalloutPosition();
108
+ return () => {
109
+ cleanupAutoUpdate();
110
+ cleanupAutoUpdate = () => {};
111
+ };
112
+ });
113
+
114
+ $effect(() => {
115
+ bodyHeight;
116
+ reference;
117
+ computeCalloutPosition();
118
+ });
119
+
120
+ // ----- Arrow ----- //
121
+
122
+ const getArrowPlacementStyle = (position?: Partial<ComputePositionReturn>) => {
123
+ if (position?.placement && arrowRef) {
124
+ switch (position.placement) {
125
+ case 'top':
126
+ case 'top-start':
127
+ case 'top-end':
128
+ return (
129
+ `bottom: -${arrowRef.offsetWidth}px;` + `transform:translate(0, -50%) rotate(135deg);`
130
+ );
131
+ case 'right':
132
+ case 'right-start':
133
+ case 'right-end':
134
+ return (
135
+ `left: -${arrowRef.offsetWidth}px;` + `transform:translate(50%, 0) rotate(225deg);`
136
+ );
137
+ case 'bottom':
138
+ case 'bottom-start':
139
+ case 'bottom-end':
140
+ return `top: -${arrowRef.offsetWidth}px;` + `transform:translate(0, 50%) rotate(-45deg);`;
141
+ case 'left':
142
+ case 'left-start':
143
+ case 'left-end':
144
+ return (
145
+ `right: -${arrowRef.offsetWidth}px;` + `transform:translate(-50%, 0) rotate(45deg);`
146
+ );
147
+ }
148
+ }
149
+ return '';
150
+ };
151
+
152
+ const getArrowOffsetStyle = (position?: Partial<ComputePositionReturn>) => {
153
+ const arrow = position?.middlewareData?.arrow;
154
+ if (arrow) {
155
+ if (arrow.x !== null && arrow.x !== undefined) {
156
+ return `left: ${arrow.x}px;`;
157
+ } else if (arrow.y !== null && arrow.y !== undefined) {
158
+ return `top: ${arrow.y}px;`;
159
+ }
160
+ }
161
+ return '';
162
+ };
163
+
164
+ let arrowStyle = $derived(
165
+ getArrowPlacementStyle(popupPosition) + getArrowOffsetStyle(popupPosition)
166
+ );
167
+
168
+ // ----- Animation ----- //
169
+
170
+ const fadeNoOp = (node: Element, params?: FadeParams): TransitionConfig => {
171
+ return { delay: 0, duration: 0 };
172
+ };
173
+
174
+ let fadeMotion = $derived(!$prefersReducedMotion ? fade : fadeNoOp);
175
+
176
+ // ----- EventHandlers ----- //
177
+
178
+ const onKeydown: KeyboardEventHandler<HTMLDivElement> = (event) => {
179
+ if (event.key === 'Escape') {
180
+ open = false;
181
+ }
182
+ rest.onkeydown?.(event);
183
+ };
184
+
185
+ $effect(() => {
186
+ ensurePortalHost();
187
+
188
+ resizeObserver = new ResizeObserver((entries) => {
189
+ bodyHeight = entries[0].target.clientHeight;
190
+ });
191
+
192
+ // start observing a DOM node
193
+ resizeObserver.observe(document.body);
194
+
195
+ return () => {
196
+ resizeObserver?.unobserve(document.body);
197
+ resizeObserver?.disconnect();
198
+ resizeObserver = undefined;
199
+ };
200
+ });
201
+
202
+ ensurePortalHost();
203
+ </script>
204
+
205
+ {#if open || !conditionalRender}
206
+ <div
207
+ use:portal={{ target: portalHost }}
208
+ class="sterling-callout-portal"
209
+ transition:fadeMotion|global={{ duration: 250 }}
210
+ >
211
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
212
+ <div
213
+ bind:this={popupRef}
214
+ class={['sterling-callout', _class]}
215
+ class:open
216
+ class:top={popupPosition.placement === 'top'}
217
+ class:top-start={popupPosition.placement === 'top-start'}
218
+ class:top-end={popupPosition.placement === 'top-end'}
219
+ class:right={popupPosition.placement === 'right'}
220
+ class:right-start={popupPosition.placement === 'right-start'}
221
+ class:right-end={popupPosition.placement === 'right-end'}
222
+ class:bottom={popupPosition.placement === 'bottom'}
223
+ class:bottom-start={popupPosition.placement === 'bottom-start'}
224
+ class:bottom-end={popupPosition.placement === 'bottom-end'}
225
+ class:left={popupPosition.placement === 'left'}
226
+ class:left-start={popupPosition.placement === 'left-start'}
227
+ class:left-end={popupPosition.placement === 'left-end'}
228
+ role="tooltip"
229
+ {...rest}
230
+ onkeydown={onKeydown}
231
+ style="left:{popupPosition.x}px; top:{popupPosition.y}px"
232
+ >
233
+ {#if children}
234
+ {#if typeof children === 'string'}
235
+ <div class="callout-text">{children}</div>
236
+ {:else}
237
+ {@render children()}
238
+ {/if}
239
+ {/if}
240
+ <div class="arrow" bind:this={arrowRef} style={arrowStyle}></div>
241
+ </div>
242
+ </div>
243
+ {/if}
@@ -0,0 +1,14 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import type { PopoverPlacement } from './Popover.types';
3
+ type Props = HTMLAttributes<HTMLDivElement> & {
4
+ conditionalRender?: boolean | null;
5
+ crossAxisOffset?: number;
6
+ mainAxisOffset?: number;
7
+ open?: boolean | null;
8
+ placement?: PopoverPlacement;
9
+ portalHost?: HTMLElement;
10
+ reference?: HTMLElement | null;
11
+ };
12
+ declare const Callout: import("svelte").Component<Props, {}, "conditionalRender" | "crossAxisOffset" | "mainAxisOffset" | "open" | "placement">;
13
+ type Callout = ReturnType<typeof Callout>;
14
+ export default Callout;
@@ -0,0 +1,11 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import type { PopoverPlacement } from './Popover.types';
3
+ export type CalloutProps = HTMLAttributes<HTMLDivElement> & {
4
+ conditionalRender?: boolean | null;
5
+ crossAxisOffset?: number;
6
+ mainAxisOffset?: number;
7
+ open?: boolean | null;
8
+ placement?: PopoverPlacement;
9
+ portalHost?: HTMLElement;
10
+ reference?: HTMLElement | null;
11
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
1
+ <svelte:options runes={true} />
2
+
3
+ <script lang="ts">
4
+ import type { HTMLInputAttributes } from 'svelte/elements';
5
+ import { usingKeyboard } from './mediaQueries/usingKeyboard';
6
+
7
+ const uuid = $props.id();
8
+
9
+ type Props = HTMLInputAttributes;
10
+
11
+ let {
12
+ id,
13
+ children,
14
+ checked = $bindable(false),
15
+ class: _class,
16
+ disabled = $bindable(false),
17
+ ...rest
18
+ }: Props = $props();
19
+
20
+ let inputRef: HTMLInputElement;
21
+
22
+ $effect(() => {
23
+ if (children && id === undefined) {
24
+ id = `Checkbox-${uuid}`;
25
+ }
26
+ });
27
+
28
+ // ----- Methods ----- //
29
+
30
+ export const blur = () => {
31
+ inputRef?.blur();
32
+ };
33
+
34
+ export const click = () => {
35
+ inputRef?.click();
36
+ };
37
+
38
+ export const focus = (options?: FocusOptions) => {
39
+ inputRef?.focus(options);
40
+ };
41
+ </script>
42
+
43
+ <!--
44
+ @component
45
+ A styled HTML input type=checkbox element.
46
+ -->
47
+ <div
48
+ class={['sterling-checkbox', _class]}
49
+ class:checked
50
+ class:disabled
51
+ class:using-keyboard={$usingKeyboard}
52
+ >
53
+ <div class="container">
54
+ <input bind:this={inputRef} bind:checked {disabled} {id} type="checkbox" {...rest} />
55
+ <div class="indicator"></div>
56
+ </div>
57
+ {#if children}
58
+ <label for={id}>
59
+ {@render children()}
60
+ </label>
61
+ {/if}
62
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ /** A styled HTML input type=checkbox element. */
3
+ declare const Checkbox: import("svelte").Component<HTMLInputAttributes, {
4
+ blur: () => void;
5
+ click: () => void;
6
+ focus: (options?: FocusOptions) => void;
7
+ }, "disabled" | "checked">;
8
+ type Checkbox = ReturnType<typeof Checkbox>;
9
+ export default Checkbox;
@@ -0,0 +1,201 @@
1
+ <svelte:options runes={true} />
2
+
3
+ <script lang="ts">
4
+ import { onMount, setContext, tick, type Snippet } from 'svelte';
5
+ import type { FormEventHandler, HTMLDialogAttributes } from 'svelte/elements';
6
+ import { writable } from 'svelte/store';
7
+ import Button from './Button.svelte';
8
+ import { STERLING_PORTAL_CONTEXT_ID } from './Portal.constants';
9
+ import type { PortalContext } from './Portal.types';
10
+
11
+ const dialogFadeDuration = 250;
12
+
13
+ type Props = HTMLDialogAttributes & {
14
+ backdropCloses?: boolean | null | undefined;
15
+ body?: Snippet;
16
+ content?: Snippet;
17
+ footer?: Snippet;
18
+ header?: Snippet;
19
+ returnValue?: string;
20
+ headerTitle?: string | Snippet;
21
+ };
22
+
23
+ let {
24
+ backdropCloses = false,
25
+ open = $bindable(false),
26
+ body,
27
+ class: _class,
28
+ content,
29
+ footer,
30
+ header,
31
+ returnValue = $bindable(''),
32
+ headerTitle,
33
+ ...rest
34
+ }: Props = $props();
35
+
36
+ let dialogRef: HTMLDialogElement;
37
+ let contentRef: HTMLDivElement;
38
+ let formRef: HTMLFormElement;
39
+
40
+ // svelte-ignore non_reactive_update
41
+ let closing = false;
42
+
43
+ let portalHost: HTMLElement | undefined = $state(undefined);
44
+
45
+ let portalContext: PortalContext = {
46
+ get portalHost() {
47
+ return portalHost;
48
+ },
49
+ set portalHost(host: HTMLElement | undefined) {
50
+ portalHost = host;
51
+ }
52
+ };
53
+
54
+ // ----- Context ----- //
55
+ setContext<PortalContext>(STERLING_PORTAL_CONTEXT_ID, portalContext);
56
+
57
+ // ----- Event Handlers ----- //
58
+
59
+ const onDocumentClick = (event: MouseEvent) => {
60
+ // as tracking clicks outside the dialog is only active while the dialog is open
61
+ // the clickOutside action is not used and reimplemented here.
62
+ const targetNode = event?.target as Node;
63
+ // the content must be used as the container because dialog::backdrop is considered
64
+ // part of the HTMLDialogElement
65
+ if (targetNode && !contentRef?.contains(targetNode) && backdropCloses) {
66
+ open = false;
67
+ }
68
+ };
69
+
70
+ const showDialog = () => {
71
+ if (dialogRef?.open === false) {
72
+ // when open, track clicks outside the dialog (use capture = true)
73
+ document.addEventListener('click', onDocumentClick, true);
74
+ returnValue = '';
75
+ dialogRef.showModal();
76
+ }
77
+ open = true;
78
+ };
79
+
80
+ const closeDialog = async () => {
81
+ if (dialogRef?.open === true) {
82
+ // when closed, stop tracking clicks outside the dialog
83
+ document.removeEventListener('click', onDocumentClick);
84
+
85
+ // to allow time for the CSS transition animation, closing the dialog is delayed
86
+ closing = true;
87
+ await tick();
88
+ setTimeout(() => {
89
+ dialogRef.close(returnValue);
90
+ open = false;
91
+ closing = false;
92
+ }, dialogFadeDuration);
93
+ } else {
94
+ open = false;
95
+ }
96
+ };
97
+
98
+ const updateDialog = (open: boolean | null | undefined) => {
99
+ if (open) {
100
+ showDialog();
101
+ } else {
102
+ closeDialog();
103
+ }
104
+ };
105
+
106
+ const onCancel = (event: Event) => {
107
+ // Cancelling a dialog instantly hides the dialog.
108
+ // To allow animation with closeDialog, this event is canceled.
109
+ event.preventDefault();
110
+ event.stopPropagation();
111
+ returnValue = '';
112
+ closeDialog();
113
+ return false;
114
+ };
115
+
116
+ const onSubmit: FormEventHandler<HTMLFormElement> = (event) => {
117
+ // Submitting a form instantly hides the dialog.
118
+ // The dialog.close event is not cancellable, but form.submit is.
119
+ // To allow animation with closeDialog, this event is canceled.
120
+ // The form is resubmitted after the dialog closes to ensure the form is in the correct state.
121
+ const anyEvent = event as unknown as any;
122
+ if (anyEvent?.submitter.type === 'submit') {
123
+ if (dialogRef.open) {
124
+ const eventSubmitter = anyEvent?.submitter;
125
+ returnValue = eventSubmitter?.value ?? '';
126
+ closeDialog();
127
+ setTimeout(() => {
128
+ formRef.requestSubmit(eventSubmitter);
129
+ }, dialogFadeDuration);
130
+ event.preventDefault();
131
+ return false;
132
+ }
133
+ } else {
134
+ event.preventDefault();
135
+ return false;
136
+ }
137
+ };
138
+
139
+ $effect(() => {
140
+ updateDialog(open);
141
+ });
142
+
143
+ onMount(() => {
144
+ updateDialog(open);
145
+
146
+ // Use the dialog for any element portals
147
+ portalContext.portalHost = dialogRef;
148
+
149
+ dialogRef.addEventListener('cancel', onCancel);
150
+
151
+ return () => {
152
+ dialogRef?.removeEventListener('cancel', onCancel);
153
+
154
+ portalContext.portalHost = undefined;
155
+ };
156
+ });
157
+ </script>
158
+
159
+ <dialog
160
+ bind:this={dialogRef}
161
+ class={['sterling-dialog', _class]}
162
+ class:open
163
+ class:closing
164
+ {...rest}
165
+ >
166
+ <form method="dialog" bind:this={formRef} onsubmit={onSubmit}>
167
+ <div class="content" bind:this={contentRef}>
168
+ {#if content}
169
+ {@render content()}
170
+ {:else}
171
+ <div class="header">
172
+ {#if header}
173
+ {@render header()}
174
+ {:else}
175
+ <div class="title">
176
+ {#if headerTitle}
177
+ {#if typeof headerTitle === 'string'}
178
+ {headerTitle}
179
+ {:else}
180
+ {@render headerTitle()}
181
+ {/if}
182
+ {/if}
183
+ </div>
184
+ <div class="close">
185
+ <Button class="circular tool" onclick={() => closeDialog()}>
186
+ <div class="close-x"></div>
187
+ </Button>
188
+ </div>
189
+ {/if}
190
+ </div>
191
+ <div class="body">
192
+ {@render body?.()}
193
+ </div>
194
+ <div class="separator"></div>
195
+ <div class="footer">
196
+ {@render footer?.()}
197
+ </div>
198
+ {/if}
199
+ </div>
200
+ </form>
201
+ </dialog>
@@ -0,0 +1,14 @@
1
+ import { type Snippet } from 'svelte';
2
+ import type { HTMLDialogAttributes } from 'svelte/elements';
3
+ type Props = HTMLDialogAttributes & {
4
+ backdropCloses?: boolean | null | undefined;
5
+ body?: Snippet;
6
+ content?: Snippet;
7
+ footer?: Snippet;
8
+ header?: Snippet;
9
+ returnValue?: string;
10
+ headerTitle?: string | Snippet;
11
+ };
12
+ declare const Dialog: import("svelte").Component<Props, {}, "open" | "returnValue">;
13
+ type Dialog = ReturnType<typeof Dialog>;
14
+ export default Dialog;