@ibobbyts/svelte-ui-utils 0.4.3 → 0.4.5

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 (38) hide show
  1. package/README.md +119 -14
  2. package/dist/data-table/FilterControl.svelte +30 -4
  3. package/dist/data-table/FilterControl.svelte.d.ts.map +1 -1
  4. package/dist/data-table/filter.d.ts +3 -1
  5. package/dist/data-table/filter.d.ts.map +1 -1
  6. package/dist/data-table/filter.js +4 -0
  7. package/dist/data-table/index.d.ts +1 -1
  8. package/dist/data-table/index.d.ts.map +1 -1
  9. package/dist/data-table/types.d.ts +41 -11
  10. package/dist/data-table/types.d.ts.map +1 -1
  11. package/dist/dialog/ConfirmDialog.svelte +2 -0
  12. package/dist/dialog/ConfirmDialog.svelte.d.ts +1 -0
  13. package/dist/dialog/ConfirmDialog.svelte.d.ts.map +1 -1
  14. package/dist/dialog/Dialog.svelte +164 -71
  15. package/dist/dialog/Dialog.svelte.d.ts +1 -0
  16. package/dist/dialog/Dialog.svelte.d.ts.map +1 -1
  17. package/dist/dropdown/Dropdown.svelte +470 -43
  18. package/dist/dropdown/Dropdown.svelte.d.ts +18 -1
  19. package/dist/dropdown/Dropdown.svelte.d.ts.map +1 -1
  20. package/dist/dropdown/DropdownMultiSelect.svelte +2 -0
  21. package/dist/dropdown/DropdownMultiSelect.svelte.d.ts +1 -0
  22. package/dist/dropdown/DropdownMultiSelect.svelte.d.ts.map +1 -1
  23. package/dist/dropdown/index.d.ts +1 -1
  24. package/dist/dropdown/index.d.ts.map +1 -1
  25. package/dist/dropdown/types.d.ts +24 -2
  26. package/dist/dropdown/types.d.ts.map +1 -1
  27. package/dist/dropdown-search/DropdownSearch.svelte +12 -10
  28. package/dist/dropdown-search/DropdownSearch.svelte.d.ts +2 -2
  29. package/dist/dropdown-search/DropdownSearch.svelte.d.ts.map +1 -1
  30. package/dist/dropdown-search/DropdownSearchMultiSelect.svelte +3 -3
  31. package/dist/dropdown-search/DropdownSearchMultiSelect.svelte.d.ts +2 -2
  32. package/dist/dropdown-search/index.d.ts +1 -1
  33. package/dist/dropdown-search/types.d.ts +10 -3
  34. package/dist/dropdown-search/types.d.ts.map +1 -1
  35. package/dist/pagination/Pagination.svelte +5 -4
  36. package/dist/pagination/Pagination.svelte.d.ts.map +1 -1
  37. package/dist/style.css +119 -0
  38. package/package.json +1 -1
@@ -1,5 +1,14 @@
1
1
  <svelte:options runes={false} />
2
2
 
3
+ <script context="module" lang="ts">
4
+ // Mount order mirrors the native top layer: the last registered element is
5
+ // the topmost dialog, and only it reacts to Escape, Tab, and backdrop clicks.
6
+ const openDialogElements: HTMLDialogElement[] = [];
7
+
8
+ const focusableSelector =
9
+ 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
10
+ </script>
11
+
3
12
  <script lang="ts">
4
13
  import { tick } from 'svelte';
5
14
  import type { DialogSize } from './types.js';
@@ -23,44 +32,91 @@
23
32
  export let countdownLabel = 'Dialog countdown';
24
33
  export let labelledBy: string | undefined = undefined;
25
34
  export let describedBy: string | undefined = undefined;
35
+ export let restoreFocusSelector: string | undefined = undefined;
26
36
  export let onClose: (() => void) | undefined = undefined;
27
37
 
28
- let dialogElement: HTMLElement | undefined;
38
+ let dialogElement: HTMLDialogElement | undefined;
29
39
  let titleId = `suu-dialog-title-${Math.random().toString(36).slice(2)}`;
30
40
  let descriptionId = `suu-dialog-description-${Math.random().toString(36).slice(2)}`;
31
41
  let wasOpen = false;
32
42
  let previouslyFocusedElement: HTMLElement | null = null;
43
+ let restoreFocusSelectorAtOpen: string | undefined;
44
+
45
+ // The dialog element is the full-viewport top layer surface; the visual
46
+ // backdrop and panel stay on their existing inner elements.
47
+ const dialogResetStyle =
48
+ 'width: 100%; height: 100%; max-width: none; max-height: none; margin: 0; padding: 0; border: none; background: transparent;';
33
49
 
34
50
  $: resolvedLabelledBy = labelledBy ?? (title ? titleId : undefined);
35
51
  $: resolvedDescribedBy =
36
52
  describedBy ?? (description ? descriptionId : undefined);
37
53
  $: backdropStyle =
38
54
  `--suu-dialog-backdrop-opacity: ${backdropOpacity}; --suu-dialog-backdrop-blur: ${backdropBlur};`;
39
- $: dialogStyle = padding ? `--suu-dialog-padding: ${padding};` : '';
40
55
  $: countdownStyle = `--suu-dialog-countdown-duration: ${Math.max(0, countdownDurationMs)}ms;`;
41
56
  $: if (open && !wasOpen) {
42
57
  wasOpen = true;
43
- void focusDialog();
44
- } else if (!open && wasOpen) {
45
- wasOpen = false;
46
- restoreFocus();
47
- }
48
-
49
- async function focusDialog() {
50
58
  if (typeof document !== 'undefined') {
51
59
  previouslyFocusedElement =
52
60
  document.activeElement instanceof HTMLElement
53
61
  ? document.activeElement
54
62
  : null;
63
+ restoreFocusSelectorAtOpen = restoreFocusSelector;
64
+ }
65
+ void focusDialog();
66
+ } else if (!open && wasOpen) {
67
+ wasOpen = false;
68
+ void restoreFocus();
69
+ }
70
+
71
+ function presentDialog(element: HTMLDialogElement) {
72
+ if (!openDialogElements.includes(element)) {
73
+ openDialogElements.push(element);
74
+ }
75
+ if (typeof element.showModal === 'function') {
76
+ if (!element.open) {
77
+ element.showModal();
78
+ }
79
+ } else if (!element.hasAttribute('open')) {
80
+ // jsdom does not implement showModal; the attribute keeps the element
81
+ // rendered as an open dialog there.
82
+ element.setAttribute('open', '');
55
83
  }
56
84
 
85
+ return {
86
+ destroy() {
87
+ const index = openDialogElements.indexOf(element);
88
+ if (index !== -1) {
89
+ openDialogElements.splice(index, 1);
90
+ }
91
+ },
92
+ };
93
+ }
94
+
95
+ async function focusDialog() {
57
96
  await tick();
58
97
  dialogElement?.focus();
59
98
  }
60
99
 
61
- function restoreFocus() {
62
- previouslyFocusedElement?.focus();
100
+ async function restoreFocus() {
101
+ const previousTarget = previouslyFocusedElement;
102
+ const selectorAtOpen = restoreFocusSelectorAtOpen;
63
103
  previouslyFocusedElement = null;
104
+ restoreFocusSelectorAtOpen = undefined;
105
+ if (!previousTarget && !selectorAtOpen) {
106
+ return;
107
+ }
108
+
109
+ await tick();
110
+ const replacementTarget = selectorAtOpen
111
+ ? document.querySelector(selectorAtOpen)
112
+ : null;
113
+ const target =
114
+ replacementTarget instanceof HTMLElement
115
+ ? replacementTarget
116
+ : previousTarget?.isConnected
117
+ ? previousTarget
118
+ : null;
119
+ target?.focus();
64
120
  }
65
121
 
66
122
  function requestClose() {
@@ -79,78 +135,115 @@
79
135
  requestClose();
80
136
  }
81
137
 
82
- function handleKeydown(event: KeyboardEvent) {
83
- if (open && event.key === 'Escape' && closeOnEscape) {
84
- event.preventDefault();
138
+ function handleCancel(event: Event) {
139
+ // Prevent the browser's own Escape close so the open prop stays the only
140
+ // source of truth; only the topmost dialog receives this event natively.
141
+ event.preventDefault();
142
+ if (closeOnEscape) {
85
143
  requestClose();
86
144
  }
87
145
  }
88
- </script>
89
146
 
90
- <svelte:window on:keydown={handleKeydown} />
147
+ function handleKeydown(event: KeyboardEvent) {
148
+ if (event.key !== 'Tab' || !dialogElement) {
149
+ return;
150
+ }
151
+ if (openDialogElements[openDialogElements.length - 1] !== dialogElement) {
152
+ return;
153
+ }
154
+
155
+ event.preventDefault();
156
+ const focusables = Array.from(
157
+ dialogElement.querySelectorAll<HTMLElement>(focusableSelector),
158
+ );
159
+ if (focusables.length === 0) {
160
+ dialogElement.focus();
161
+ return;
162
+ }
163
+
164
+ const activeElement =
165
+ document.activeElement instanceof HTMLElement
166
+ ? document.activeElement
167
+ : undefined;
168
+ const currentIndex = activeElement
169
+ ? focusables.indexOf(activeElement)
170
+ : -1;
171
+ const nextIndex = event.shiftKey
172
+ ? currentIndex <= 0
173
+ ? focusables.length - 1
174
+ : currentIndex - 1
175
+ : currentIndex === -1 || currentIndex === focusables.length - 1
176
+ ? 0
177
+ : currentIndex + 1;
178
+ focusables[nextIndex].focus();
179
+ }
180
+ </script>
91
181
 
92
182
  {#if open}
93
- <div
94
- class="suu-dialog-backdrop"
95
- data-dimmed={dimBackdrop ? 'true' : 'false'}
96
- data-blurred={blurBackdrop ? 'true' : 'false'}
97
- style={backdropStyle}
98
- role="presentation"
99
- on:mousedown={handleBackdropMouseDown}
183
+ <dialog
184
+ bind:this={dialogElement}
185
+ class="suu-dialog-root"
186
+ style={dialogResetStyle}
187
+ aria-labelledby={resolvedLabelledBy}
188
+ aria-describedby={resolvedDescribedBy}
189
+ tabindex="-1"
190
+ use:presentDialog
191
+ on:cancel={handleCancel}
192
+ on:keydown={handleKeydown}
100
193
  >
101
194
  <div
102
- bind:this={dialogElement}
103
- class={`suu-dialog suu-dialog--${size}`}
104
- style={dialogStyle}
105
- role="dialog"
106
- aria-modal="true"
107
- aria-labelledby={resolvedLabelledBy}
108
- aria-describedby={resolvedDescribedBy}
109
- tabindex="-1"
195
+ class="suu-dialog-backdrop"
196
+ data-dimmed={dimBackdrop ? 'true' : 'false'}
197
+ data-blurred={blurBackdrop ? 'true' : 'false'}
198
+ style={backdropStyle}
199
+ role="presentation"
200
+ on:mousedown={handleBackdropMouseDown}
110
201
  >
111
- {#if showCountdown && countdownDurationMs > 0}
112
- <div
113
- class="suu-dialog__countdown"
114
- style={countdownStyle}
115
- role="timer"
116
- aria-label={countdownLabel}
117
- ></div>
118
- {/if}
119
- <header class="suu-dialog__header">
120
- <div class="suu-dialog__heading">
121
- {#if title}
122
- <h2 class="suu-dialog__title" id={titleId}>{title}</h2>
123
- {/if}
124
- {#if description}
125
- <p class="suu-dialog__description" id={descriptionId}>
126
- {description}
127
- </p>
202
+ <div class={`suu-dialog suu-dialog--${size}`} style:--suu-dialog-padding={padding}>
203
+ {#if showCountdown && countdownDurationMs > 0}
204
+ <div
205
+ class="suu-dialog__countdown"
206
+ style={countdownStyle}
207
+ role="timer"
208
+ aria-label={countdownLabel}
209
+ ></div>
210
+ {/if}
211
+ <header class="suu-dialog__header">
212
+ <div class="suu-dialog__heading">
213
+ {#if title}
214
+ <h2 class="suu-dialog__title" id={titleId}>{title}</h2>
215
+ {/if}
216
+ {#if description}
217
+ <p class="suu-dialog__description" id={descriptionId}>
218
+ {description}
219
+ </p>
220
+ {/if}
221
+ </div>
222
+ {#if showCloseButton && dismissible}
223
+ <button
224
+ type="button"
225
+ class="suu-dialog__close"
226
+ aria-label={closeLabel}
227
+ title={closeLabel}
228
+ on:click={requestClose}
229
+ >
230
+ <svg viewBox="0 0 20 20" aria-hidden="true">
231
+ <path d="m6 6 8 8M14 6l-8 8"></path>
232
+ </svg>
233
+ </button>
128
234
  {/if}
235
+ </header>
236
+
237
+ <div class="suu-dialog__body">
238
+ <slot />
129
239
  </div>
130
- {#if showCloseButton && dismissible}
131
- <button
132
- type="button"
133
- class="suu-dialog__close"
134
- aria-label={closeLabel}
135
- title={closeLabel}
136
- on:click={requestClose}
137
- >
138
- <svg viewBox="0 0 20 20" aria-hidden="true">
139
- <path d="m6 6 8 8M14 6l-8 8"></path>
140
- </svg>
141
- </button>
142
- {/if}
143
- </header>
144
240
 
145
- <div class="suu-dialog__body">
146
- <slot />
241
+ {#if $$slots.footer}
242
+ <footer class="suu-dialog__footer">
243
+ <slot name="footer" />
244
+ </footer>
245
+ {/if}
147
246
  </div>
148
-
149
- {#if $$slots.footer}
150
- <footer class="suu-dialog__footer">
151
- <slot name="footer" />
152
- </footer>
153
- {/if}
154
247
  </div>
155
- </div>
248
+ </dialog>
156
249
  {/if}
@@ -37,6 +37,7 @@ declare const Dialog: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_PropsWit
37
37
  countdownLabel?: string;
38
38
  labelledBy?: string | undefined;
39
39
  describedBy?: string | undefined;
40
+ restoreFocusSelector?: string | undefined;
40
41
  onClose?: (() => void) | undefined;
41
42
  }, {
42
43
  default: {};
@@ -1 +1 @@
1
- {"version":3,"file":"Dialog.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dialog/Dialog.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAsI7C,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AACD,KAAK,gCAAgC,CAAC,KAAK,EAAE,KAAK,IAAI,KAAK,GACvD,CAAC,KAAK,SAAS;IAAE,OAAO,EAAE,GAAG,CAAA;CAAE,GACzB,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GACnC,GAAG,GACH;IAAE,QAAQ,CAAC,EAAE,GAAG,CAAA;CAAE,GAClB,EAAE,CAAC,CAAC;AAId,QAAA,MAAM,MAAM;;;;;WAfqpB,UAAU;cAAY,MAAM,GAAG,SAAS;;;;;;;sBAAsP,MAAM,GAAG,MAAM;;;;;iBAAmL,MAAM,GAAG,SAAS;kBAAgB,MAAM,GAAG,SAAS;cAAY,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS;;;;;;;;;cAexnC,CAAC;AAChF,KAAK,MAAM,GAAG,YAAY,CAAC,OAAO,MAAM,CAAC,CAAC;AAC5C,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"Dialog.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dialog/Dialog.svelte.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA2N7C,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AACD,KAAK,gCAAgC,CAAC,KAAK,EAAE,KAAK,IAAI,KAAK,GACvD,CAAC,KAAK,SAAS;IAAE,OAAO,EAAE,GAAG,CAAA;CAAE,GACzB,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GACnC,GAAG,GACH;IAAE,QAAQ,CAAC,EAAE,GAAG,CAAA;CAAE,GAClB,EAAE,CAAC,CAAC;AAId,QAAA,MAAM,MAAM;;;;;WAfksB,UAAU;cAAY,MAAM,GAAG,SAAS;;;;;;;sBAAsP,MAAM,GAAG,MAAM;;;;;iBAAmL,MAAM,GAAG,SAAS;kBAAgB,MAAM,GAAG,SAAS;2BAAyB,MAAM,GAAG,SAAS;cAAY,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS;;;;;;;;;cAehtC,CAAC;AAChF,KAAK,MAAM,GAAG,YAAY,CAAC,OAAO,MAAM,CAAC,CAAC;AAC5C,eAAe,MAAM,CAAC"}