@spaethtech/svelte-ui 0.4.1-dev.13.042fac9 → 0.4.1-dev.14.d54aa31

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.
@@ -101,9 +101,10 @@ examples):
101
101
  - **Date / time:** `DatePicker` `Calendar` `TimePicker` `TimeSpinner` `TimeRangeInput` `DateTimeInput`
102
102
  - **Data:** `DataTable` `Query` — driven by the headless layer at **`@spaethtech/svelte-ui/data`**
103
103
  (query-language parser/AST, `createGrid`, `DataGrid<T>`, `DataSet`).
104
- - **Overlays:** `Dialog` (pure modal shell — BYO content; `width`/`height` accept any CSS value)
105
- · `ConfirmDialog` (title + message + Confirm/Cancel, built on Dialog) · `Popup` `Menu` ·
106
- `tooltip` (a Svelte `use:` action) · `anchored` (positioning engine)
104
+ - **Overlays:** `Dialog` (pure modal shell — BYO content; `width`/`height` take any CSS value;
105
+ `onShow(ctx)`/`onHide(ctx)` control focus set `ctx.autoFocus` to a selector/element/`false`,
106
+ `ctx.restoreFocus` likewise) · `ConfirmDialog` (title + message + Confirm/Cancel, built on Dialog)
107
+ · `Popup` `Menu` · `tooltip` (a Svelte `use:` action) · `anchored` (positioning engine)
107
108
  - **Navigation:** `TabStrip` `SideBarMenu`
108
109
  - **Feedback:** `Alert` `Banner` `Badge` `Toaster` + `toast`
109
110
  - **Layout:** `Card` `CardHeader` `CardBody` `CardFooter`
@@ -4,7 +4,7 @@
4
4
  import CardHeader from "./CardHeader.svelte";
5
5
  import CardFooter from "./CardFooter.svelte";
6
6
  import Button from "./Button.svelte";
7
- import type { Snippet } from "svelte";
7
+ import type { ComponentProps, Snippet } from "svelte";
8
8
  import type { Size } from "../types/sizes.js";
9
9
  import type { Responsive } from "../types/responsive.js";
10
10
 
@@ -32,6 +32,9 @@
32
32
  /** Forwarded to the Dialog shell — any CSS value (default: content-sized, capped at 32rem). */
33
33
  width?: string;
34
34
  height?: string;
35
+ /** Forwarded to the Dialog shell — open/close hooks with the `autoFocus`/`restoreFocus` knobs. */
36
+ onShow?: ComponentProps<typeof Dialog>["onShow"];
37
+ onHide?: ComponentProps<typeof Dialog>["onHide"];
35
38
  /** Custom body between header and footer (replaces `message`). */
36
39
  children?: Snippet;
37
40
  }
@@ -51,6 +54,8 @@
51
54
  dismissible = true,
52
55
  width,
53
56
  height,
57
+ onShow,
58
+ onHide,
54
59
  children,
55
60
  }: Props = $props();
56
61
 
@@ -69,6 +74,8 @@
69
74
  {dismissible}
70
75
  {width}
71
76
  {height}
77
+ {onShow}
78
+ {onHide}
72
79
  onClose={onCancel}
73
80
  aria-label={title}
74
81
  >
@@ -1,4 +1,5 @@
1
- import type { Snippet } from "svelte";
1
+ import Dialog from "./Dialog.svelte";
2
+ import type { ComponentProps, Snippet } from "svelte";
2
3
  import type { Size } from "../types/sizes.js";
3
4
  import type { Responsive } from "../types/responsive.js";
4
5
  /**
@@ -25,6 +26,9 @@ interface Props {
25
26
  /** Forwarded to the Dialog shell — any CSS value (default: content-sized, capped at 32rem). */
26
27
  width?: string;
27
28
  height?: string;
29
+ /** Forwarded to the Dialog shell — open/close hooks with the `autoFocus`/`restoreFocus` knobs. */
30
+ onShow?: ComponentProps<typeof Dialog>["onShow"];
31
+ onHide?: ComponentProps<typeof Dialog>["onHide"];
28
32
  /** Custom body between header and footer (replaces `message`). */
29
33
  children?: Snippet;
30
34
  }
@@ -11,7 +11,27 @@
11
11
  *
12
12
  * `width` / `height` accept any CSS length: `"80%"`, `"80vw"`, `"60vh"`, `"400px"`, … (both default
13
13
  * to content size, capped to the viewport minus a 1rem inset so the panel never runs off-screen).
14
+ *
15
+ * `onShow` / `onHide` fire on open / close. Each hands you a mutable focus knob preset to the
16
+ * default, so a handler that ignores it keeps built-in behavior:
17
+ * onShow(ctx): ctx.autoFocus is the default focusable selector — set a selector/element to focus
18
+ * something specific, or `false` to skip (focus stays on the panel).
19
+ * onHide(ctx): ctx.restoreFocus is the element focused before open — set `false` to skip
20
+ * restoration, or another element to focus on close.
14
21
  */
22
+ interface DialogShowContext {
23
+ /** The panel element (already mounted). */
24
+ panel: HTMLElement;
25
+ /** What to focus on open — preset to the default focusable selector. Set a CSS selector or an
26
+ * element to focus something specific, or `false` to skip autofocus (focus stays on the panel). */
27
+ autoFocus: string | HTMLElement | false;
28
+ }
29
+ interface DialogHideContext {
30
+ /** Where focus returns on close — preset to the element focused before the dialog opened. Set
31
+ * `false` to skip restoration, or another element to focus instead. */
32
+ restoreFocus: HTMLElement | null | false;
33
+ }
34
+
15
35
  interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "class"> {
16
36
  /** Whether the dialog is shown (bindable). */
17
37
  isOpen: boolean;
@@ -23,6 +43,12 @@
23
43
  dismissible?: boolean;
24
44
  /** Called when dismissed via backdrop / Escape (isOpen is also set false). */
25
45
  onClose?: () => void;
46
+ /** Fires on open (after mount). `ctx.autoFocus` (default = the focusable selector) controls what
47
+ * gets focused; mutate it to retarget or set `false` to skip. */
48
+ onShow?: (ctx: DialogShowContext) => void;
49
+ /** Fires on close. `ctx.restoreFocus` (default = the pre-open element) controls focus return;
50
+ * mutate it to retarget or set `false` to skip. */
51
+ onHide?: (ctx: DialogHideContext) => void;
26
52
  /** Extra classes on the panel wrapper. */
27
53
  class?: string;
28
54
  children?: Snippet;
@@ -34,6 +60,8 @@
34
60
  height,
35
61
  dismissible = true,
36
62
  onClose,
63
+ onShow,
64
+ onHide,
37
65
  class: cls = "",
38
66
  children,
39
67
  ...rest
@@ -66,16 +94,27 @@
66
94
  };
67
95
  });
68
96
 
69
- // Focus management: on open, remember what was focused and move focus into the panel; on close,
70
- // restore it. (Tab-wrapping is handled in onKeydown.)
97
+ // Focus management. On open: remember what was focused, fire `onShow`, then focus per
98
+ // `ctx.autoFocus` (default = first focusable). On close: fire `onHide`, then restore per
99
+ // `ctx.restoreFocus` (default = the pre-open element). (Tab-wrapping is handled in onKeydown.)
71
100
  $effect(() => {
72
101
  if (!isOpen) return;
73
102
  previouslyFocused = document.activeElement as HTMLElement | null;
74
103
  queueMicrotask(() => {
75
- const first = panelEl?.querySelector<HTMLElement>(FOCUSABLE);
76
- (first ?? panelEl)?.focus();
104
+ if (!panelEl) return;
105
+ const ctx: DialogShowContext = { panel: panelEl, autoFocus: FOCUSABLE };
106
+ onShow?.(ctx);
107
+ let target: HTMLElement | null;
108
+ if (ctx.autoFocus === false) target = panelEl;
109
+ else if (ctx.autoFocus instanceof HTMLElement) target = ctx.autoFocus;
110
+ else target = panelEl.querySelector<HTMLElement>(ctx.autoFocus) ?? panelEl;
111
+ target?.focus();
77
112
  });
78
- return () => previouslyFocused?.focus?.();
113
+ return () => {
114
+ const ctx: DialogHideContext = { restoreFocus: previouslyFocused };
115
+ onHide?.(ctx);
116
+ if (ctx.restoreFocus) ctx.restoreFocus.focus?.();
117
+ };
79
118
  });
80
119
 
81
120
  const panelStyle = $derived(
@@ -9,7 +9,26 @@ import type { HTMLAttributes } from "svelte/elements";
9
9
  *
10
10
  * `width` / `height` accept any CSS length: `"80%"`, `"80vw"`, `"60vh"`, `"400px"`, … (both default
11
11
  * to content size, capped to the viewport minus a 1rem inset so the panel never runs off-screen).
12
+ *
13
+ * `onShow` / `onHide` fire on open / close. Each hands you a mutable focus knob preset to the
14
+ * default, so a handler that ignores it keeps built-in behavior:
15
+ * onShow(ctx): ctx.autoFocus is the default focusable selector — set a selector/element to focus
16
+ * something specific, or `false` to skip (focus stays on the panel).
17
+ * onHide(ctx): ctx.restoreFocus is the element focused before open — set `false` to skip
18
+ * restoration, or another element to focus on close.
12
19
  */
20
+ interface DialogShowContext {
21
+ /** The panel element (already mounted). */
22
+ panel: HTMLElement;
23
+ /** What to focus on open — preset to the default focusable selector. Set a CSS selector or an
24
+ * element to focus something specific, or `false` to skip autofocus (focus stays on the panel). */
25
+ autoFocus: string | HTMLElement | false;
26
+ }
27
+ interface DialogHideContext {
28
+ /** Where focus returns on close — preset to the element focused before the dialog opened. Set
29
+ * `false` to skip restoration, or another element to focus instead. */
30
+ restoreFocus: HTMLElement | null | false;
31
+ }
13
32
  interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "class"> {
14
33
  /** Whether the dialog is shown (bindable). */
15
34
  isOpen: boolean;
@@ -21,6 +40,12 @@ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "class"> {
21
40
  dismissible?: boolean;
22
41
  /** Called when dismissed via backdrop / Escape (isOpen is also set false). */
23
42
  onClose?: () => void;
43
+ /** Fires on open (after mount). `ctx.autoFocus` (default = the focusable selector) controls what
44
+ * gets focused; mutate it to retarget or set `false` to skip. */
45
+ onShow?: (ctx: DialogShowContext) => void;
46
+ /** Fires on close. `ctx.restoreFocus` (default = the pre-open element) controls focus return;
47
+ * mutate it to retarget or set `false` to skip. */
48
+ onHide?: (ctx: DialogHideContext) => void;
24
49
  /** Extra classes on the panel wrapper. */
25
50
  class?: string;
26
51
  children?: Snippet;
@@ -148,8 +148,13 @@ dialog.
148
148
  - **Location**: `src/lib/components/Dialog.svelte`
149
149
  - **Props**: `isOpen` (bindable), `width` (any CSS value — `'80%'`, `'80vw'`, `'400px'`; default
150
150
  auto, capped at `min(32rem, …)`), `height` (any CSS value; default auto), `dismissible` (default
151
- `true`), `onClose`, `class`, `children` (snippet). Extra attributes (e.g. `aria-label`) spread onto
152
- the panel.
151
+ `true`), `onClose`, `onShow`, `onHide`, `class`, `children` (snippet). Extra attributes (e.g.
152
+ `aria-label`) spread onto the panel.
153
+ - **Focus control**: `onShow(ctx)` fires on open — `ctx.autoFocus` is preset to the default focusable
154
+ selector; set it to a **CSS selector / element** to focus something specific, or **`false`** to
155
+ skip (focus stays on the panel). `onHide(ctx)` mirrors it with `ctx.restoreFocus` (preset to the
156
+ pre-open element; set `false` to skip restoring focus, or another element). A handler that ignores
157
+ the knob keeps the built-in behavior.
153
158
  - **Features**: portals to `<body>`, background scroll-lock (scrollbar-width compensated), backdrop
154
159
  + Escape dismiss, **focus trap** (initial focus in, Tab wraps, focus restored on close),
155
160
  `role="dialog"` + `aria-modal`.
@@ -162,7 +167,8 @@ shell.
162
167
  - **Location**: `src/lib/components/ConfirmDialog.svelte`
163
168
  - **Props**: `isOpen` (bindable), `title`, `message`, `confirmText`, `cancelText`, `onConfirm`,
164
169
  `onCancel`, `variant` (`'normal' | 'danger'`), `size`, `showConfirm`, `showCancel`, `dismissible`,
165
- `width`/`height` (forwarded to the shell), `children` (snippet — replaces `message` as the body).
170
+ `width`/`height`/`onShow`/`onHide` (forwarded to the shell), `children` (snippet — replaces
171
+ `message` as the body).
166
172
 
167
173
  ### Popup
168
174
 
package/docs/usage.md CHANGED
@@ -273,6 +273,21 @@ with `width` / `height` (any CSS value).
273
273
  </Dialog>
274
274
  ```
275
275
 
276
+ **Controlling focus.** The shell focuses the first focusable element on open and restores focus on
277
+ close. Override via `onShow` / `onHide` — each hands you a mutable knob (default preserved if you
278
+ ignore it):
279
+
280
+ ```svelte
281
+ <Dialog
282
+ bind:isOpen={open}
283
+ onShow={(ctx) => (ctx.autoFocus = "[data-focus-me]")}
284
+ onHide={(ctx) => (ctx.restoreFocus = false)}
285
+ >
286
+ <!-- ctx.autoFocus: CSS selector | element | false (skip). Default = first focusable. -->
287
+ <!-- ctx.restoreFocus: element | false (skip). Default = the element focused before open. -->
288
+ </Dialog>
289
+ ```
290
+
276
291
  ### Badge
277
292
 
278
293
  ```svelte
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.4.1-dev.13.042fac9",
3
+ "version": "0.4.1-dev.14.d54aa31",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"