@14ch/svelte-ui 0.0.63 → 0.0.65

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.
@@ -5,6 +5,7 @@
5
5
  import Button from './Button.svelte';
6
6
  import { convertToHtml } from '../utils/formatText';
7
7
  import type { SvelteComponent } from 'svelte';
8
+ import type { CloseReason } from '../types/propOptions';
8
9
 
9
10
  // =========================================================================
10
11
  // Props, States & Constants
@@ -35,7 +36,11 @@
35
36
 
36
37
  // イベントハンドラー
37
38
  onSubmit?: () => void;
39
+ /** Called on cancel, including dismissal via Escape or a backdrop click. */
38
40
  onCancel?: () => void;
41
+ onOpen?: () => void;
42
+ /** Called after the close animation completes, with the reason it was closed. */
43
+ onClose?: (reason: CloseReason) => void;
39
44
  };
40
45
 
41
46
  let {
@@ -60,7 +65,9 @@
60
65
 
61
66
  // イベントハンドラー
62
67
  onSubmit = () => {}, // No params for type inference
63
- onCancel = () => {} // No params for type inference
68
+ onCancel = () => {}, // No params for type inference
69
+ onOpen = () => {},
70
+ onClose = () => {}
64
71
  }: ConfirmDialogProps = $props();
65
72
 
66
73
  let dialogRef: SvelteComponent | undefined = $state();
@@ -78,6 +85,14 @@
78
85
  close();
79
86
  };
80
87
 
88
+ const handleClose = (reason: CloseReason): void => {
89
+ // Escape / 背景クリックでの離脱もキャンセルとして扱う
90
+ if (reason !== 'explicit') {
91
+ onCancel();
92
+ }
93
+ onClose(reason);
94
+ };
95
+
81
96
  /** Opens the dialog. */
82
97
  export const open = (): void => {
83
98
  isOpen = true;
@@ -102,6 +117,8 @@
102
117
  {scrollable}
103
118
  {closeIfClickOutside}
104
119
  {focusFirstOnOpen}
120
+ {onOpen}
121
+ onClose={handleClose}
105
122
  id={id ? `${id}-dialog` : undefined}
106
123
  >
107
124
  <div class="confirm-dialog-message">
@@ -1,3 +1,4 @@
1
+ import type { CloseReason } from '../types/propOptions';
1
2
  export type ConfirmDialogProps = {
2
3
  title?: string;
3
4
  description?: string;
@@ -15,7 +16,11 @@ export type ConfirmDialogProps = {
15
16
  closeIfClickOutside?: boolean;
16
17
  focusFirstOnOpen?: boolean;
17
18
  onSubmit?: () => void;
19
+ /** Called on cancel, including dismissal via Escape or a backdrop click. */
18
20
  onCancel?: () => void;
21
+ onOpen?: () => void;
22
+ /** Called after the close animation completes, with the reason it was closed. */
23
+ onClose?: (reason: CloseReason) => void;
19
24
  };
20
25
  declare const ConfirmDialog: import("svelte").Component<ConfirmDialogProps, {
21
26
  /** Opens the dialog. */ open: () => void;
@@ -14,6 +14,7 @@
14
14
  * This prevents state synchronization bugs and ensures consistent behavior.
15
15
  */
16
16
  import type { Snippet } from 'svelte';
17
+ import type { CloseReason } from '../types/propOptions';
17
18
  import Modal from './Modal.svelte';
18
19
  import { getStyleFromNumber } from '../utils/style';
19
20
 
@@ -56,6 +57,11 @@
56
57
  // ARIA/アクセシビリティ
57
58
  ariaLabel?: string;
58
59
  ariaDescribedby?: string;
60
+
61
+ // イベントハンドラー
62
+ onOpen?: () => void;
63
+ /** Called after the close animation completes, with the reason it was closed. */
64
+ onClose?: (reason: CloseReason) => void;
59
65
  };
60
66
 
61
67
  let {
@@ -85,7 +91,11 @@
85
91
 
86
92
  // ARIA/アクセシビリティ
87
93
  ariaLabel,
88
- ariaDescribedby
94
+ ariaDescribedby,
95
+
96
+ // イベントハンドラー
97
+ onOpen = () => {}, // No params for type inference
98
+ onClose = () => {}
89
99
  }: DialogProps = $props();
90
100
 
91
101
  let modalRef: Modal;
@@ -145,6 +155,8 @@
145
155
  {closeIfClickOutside}
146
156
  {restoreFocus}
147
157
  {focusFirstOnOpen}
158
+ {onOpen}
159
+ {onClose}
148
160
  componentType="Dialog"
149
161
  {ariaLabel}
150
162
  {ariaLabelledby}
@@ -11,6 +11,7 @@
11
11
  * This prevents state synchronization bugs and ensures consistent behavior.
12
12
  */
13
13
  import type { Snippet } from 'svelte';
14
+ import type { CloseReason } from '../types/propOptions';
14
15
  export type DialogProps = {
15
16
  header?: Snippet;
16
17
  children?: Snippet;
@@ -36,6 +37,9 @@ export type DialogProps = {
36
37
  focusFirstOnOpen?: boolean;
37
38
  ariaLabel?: string;
38
39
  ariaDescribedby?: string;
40
+ onOpen?: () => void;
41
+ /** Called after the close animation completes, with the reason it was closed. */
42
+ onClose?: (reason: CloseReason) => void;
39
43
  };
40
44
  declare const Dialog: import("svelte").Component<DialogProps, {
41
45
  /** Opens the dialog. */ open: () => void;
@@ -14,6 +14,7 @@
14
14
  * This prevents state synchronization bugs and ensures consistent behavior.
15
15
  */
16
16
  import type { Snippet } from 'svelte';
17
+ import type { CloseReason } from '../types/propOptions';
17
18
  import Modal from './Modal.svelte';
18
19
  import { getStyleFromNumber } from '../utils/style';
19
20
 
@@ -58,6 +59,11 @@
58
59
  // ARIA/アクセシビリティ
59
60
  ariaLabel?: string;
60
61
  ariaDescribedby?: string;
62
+
63
+ // イベントハンドラー
64
+ onOpen?: () => void;
65
+ /** Called after the close animation completes, with the reason it was closed. */
66
+ onClose?: (reason: CloseReason) => void;
61
67
  };
62
68
 
63
69
  let {
@@ -88,7 +94,11 @@
88
94
 
89
95
  // ARIA/アクセシビリティ
90
96
  ariaLabel = 'Drawer',
91
- ariaDescribedby
97
+ ariaDescribedby,
98
+
99
+ // イベントハンドラー
100
+ onOpen = () => {}, // No params for type inference
101
+ onClose = () => {}
92
102
  }: DrawerProps = $props();
93
103
 
94
104
  let modalRef: Modal;
@@ -159,6 +169,8 @@
159
169
  {closeIfClickOutside}
160
170
  {restoreFocus}
161
171
  {focusFirstOnOpen}
172
+ {onOpen}
173
+ {onClose}
162
174
  componentType="Drawer"
163
175
  {ariaLabel}
164
176
  {ariaLabelledby}
@@ -11,6 +11,7 @@
11
11
  * This prevents state synchronization bugs and ensures consistent behavior.
12
12
  */
13
13
  import type { Snippet } from 'svelte';
14
+ import type { CloseReason } from '../types/propOptions';
14
15
  export type DrawerProps = {
15
16
  header?: Snippet;
16
17
  children?: Snippet;
@@ -38,6 +39,9 @@ export type DrawerProps = {
38
39
  focusFirstOnOpen?: boolean;
39
40
  ariaLabel?: string;
40
41
  ariaDescribedby?: string;
42
+ onOpen?: () => void;
43
+ /** Called after the close animation completes, with the reason it was closed. */
44
+ onClose?: (reason: CloseReason) => void;
41
45
  };
42
46
  declare const Drawer: import("svelte").Component<DrawerProps, {
43
47
  /** Opens the drawer. */ open: () => void;
@@ -13,6 +13,7 @@
13
13
  * This prevents state synchronization bugs and ensures consistent behavior.
14
14
  */
15
15
  import type { Snippet } from 'svelte';
16
+ import type { CloseReason } from '../types/propOptions';
16
17
  import { announceOpenClose } from '../utils/accessibility';
17
18
 
18
19
  // =========================================================================
@@ -47,6 +48,11 @@
47
48
  ariaLabel?: string;
48
49
  ariaLabelledby?: string;
49
50
  ariaDescribedby?: string;
51
+
52
+ // イベントハンドラー
53
+ onOpen?: () => void;
54
+ /** Called after the close animation completes, with the reason it was closed. */
55
+ onClose?: (reason: CloseReason) => void;
50
56
  };
51
57
 
52
58
  let {
@@ -72,12 +78,19 @@
72
78
  // ARIA/アクセシビリティ
73
79
  ariaLabel,
74
80
  ariaLabelledby,
75
- ariaDescribedby
81
+ ariaDescribedby,
82
+
83
+ // イベントハンドラー
84
+ onOpen = () => {}, // No params for type inference
85
+ onClose = () => {}
76
86
  }: ModalProps = $props();
77
87
 
78
88
  let dialogRef: HTMLDialogElement;
79
89
  let containerRef: HTMLDivElement;
80
90
  let previousActiveElement: HTMLElement | null = null;
91
+ // isOpen は bind でも変わるため、実際に開いているかは内部で持つ
92
+ let isVisible = false;
93
+ let pendingCloseReason: CloseReason | null = null;
81
94
 
82
95
  // =========================================================================
83
96
  // Effects
@@ -89,7 +102,7 @@
89
102
  if (!closeIfClickOutside) return;
90
103
  if (!containerRef || !event.target) return;
91
104
  if (!containerRef.contains(event.target as Node)) {
92
- close();
105
+ close(undefined, 'outside');
93
106
  }
94
107
  };
95
108
 
@@ -103,18 +116,20 @@
103
116
  });
104
117
 
105
118
  $effect(() => {
106
- if (!isOpen || !dialogRef) return;
119
+ if (!dialogRef) return;
107
120
 
108
- const handleKeyDown = (event: KeyboardEvent) => {
109
- if (event.key === 'Escape') {
110
- close();
111
- }
121
+ // showModal() した dialog は最前面の1つだけに cancel が配送されるため、
122
+ // Escape で多重の Modal がまとめて閉じることを防げる。ネイティブの即時
123
+ // クローズは抑止し、fade-out アニメーション付きの close() を使う。
124
+ const handleCancel = (event: Event) => {
125
+ event.preventDefault();
126
+ close(undefined, 'escape');
112
127
  };
113
128
 
114
- document.addEventListener('keydown', handleKeyDown);
129
+ dialogRef.addEventListener('cancel', handleCancel);
115
130
 
116
131
  return () => {
117
- document.removeEventListener('keydown', handleKeyDown);
132
+ dialogRef.removeEventListener('cancel', handleCancel);
118
133
  };
119
134
  });
120
135
 
@@ -170,11 +185,14 @@
170
185
  // =========================================================================
171
186
  /** Opens the modal. */
172
187
  export const open = (title?: string): void => {
173
- if (!dialogRef) return;
188
+ if (!dialogRef || isVisible) return;
174
189
 
190
+ isVisible = true;
175
191
  isOpen = true;
176
192
  previousActiveElement = document.activeElement as HTMLElement;
177
193
 
194
+ // 閉じるアニメーションの途中で開き直した場合は close を取り消す
195
+ pendingCloseReason = null;
178
196
  dialogRef.classList.add('fade-in');
179
197
  dialogRef.removeEventListener('animationend', closeEnd);
180
198
  dialogRef.showModal();
@@ -184,14 +202,17 @@
184
202
  dialogRef?.focus();
185
203
  }
186
204
  announceOpenClose(componentType, true, title || ariaLabel || '');
205
+ onOpen?.();
187
206
  }, 0);
188
207
  };
189
208
 
190
209
  /** Closes the modal. */
191
- export const close = (title?: string): void => {
192
- if (!dialogRef) return;
210
+ export const close = (title?: string, reason: CloseReason = 'explicit'): void => {
211
+ if (!dialogRef || !isVisible) return;
193
212
 
213
+ isVisible = false;
194
214
  isOpen = false;
215
+ pendingCloseReason = reason;
195
216
  dialogRef.classList.add('fade-out');
196
217
  dialogRef.addEventListener('animationend', closeEnd, { once: true });
197
218
 
@@ -209,11 +230,18 @@
209
230
  previousActiveElement.focus();
210
231
  }
211
232
  previousActiveElement = null;
233
+
234
+ // アニメーション完了後に onClose を呼ぶ
235
+ if (pendingCloseReason) {
236
+ const reason = pendingCloseReason;
237
+ pendingCloseReason = null;
238
+ onClose?.(reason);
239
+ }
212
240
  };
213
241
 
214
242
  /** Toggles between open and closed. */
215
243
  export const toggle = (title?: string): void => {
216
- if (isOpen) {
244
+ if (isVisible) {
217
245
  close(title);
218
246
  } else {
219
247
  open(title);
@@ -11,6 +11,7 @@
11
11
  * This prevents state synchronization bugs and ensures consistent behavior.
12
12
  */
13
13
  import type { Snippet } from 'svelte';
14
+ import type { CloseReason } from '../types/propOptions';
14
15
  export type ModalProps = {
15
16
  children?: Snippet;
16
17
  /** Internal label used for accessibility announcements. @default 'Modal' */
@@ -29,10 +30,13 @@ export type ModalProps = {
29
30
  ariaLabel?: string;
30
31
  ariaLabelledby?: string;
31
32
  ariaDescribedby?: string;
33
+ onOpen?: () => void;
34
+ /** Called after the close animation completes, with the reason it was closed. */
35
+ onClose?: (reason: CloseReason) => void;
32
36
  };
33
37
  declare const Modal: import("svelte").Component<ModalProps, {
34
38
  /** Opens the modal. */ open: (title?: string) => void;
35
- /** Closes the modal. */ close: (title?: string) => void;
39
+ /** Closes the modal. */ close: (title?: string, reason?: CloseReason) => void;
36
40
  /** @internal Completes the close animation. Called automatically. */ closeEnd: () => void;
37
41
  /** Toggles between open and closed. */ toggle: (title?: string) => void;
38
42
  }, "isOpen">;
@@ -553,7 +553,7 @@
553
553
  id={popupId}
554
554
  data-testid="popup"
555
555
  use:clickOutside={() => {
556
- close('outside');
556
+ if (isOpen) close('outside');
557
557
  }}
558
558
  >
559
559
  {@render children()}
package/dist/index.d.ts CHANGED
@@ -85,7 +85,7 @@ export type { NavProps } from './components/Nav.svelte';
85
85
  export type { NavItemProps, NavItemSelectedVariant } from './components/NavItem.svelte';
86
86
  export type { TabProps } from './components/Tab.svelte';
87
87
  export type { TextareaProps } from './components/Textarea.svelte';
88
- export type { PopupPosition, SnackbarPosition, FabPosition, ButtonVariant, ButtonSize, IconPosition, ComponentSize, SnackbarType, SnackbarVariant, BadgeVariant, DatepickerMode, FocusStyle, NavVariant, ChildrenVariant, StepNavOrientation } from './types/propOptions';
88
+ export type { PopupPosition, SnackbarPosition, FabPosition, ButtonVariant, ButtonSize, IconPosition, ComponentSize, SnackbarType, SnackbarVariant, BadgeVariant, DatepickerMode, FocusStyle, NavVariant, ChildrenVariant, StepNavOrientation, CloseReason } from './types/propOptions';
89
89
  export type { MenuItem } from './types/menuItem';
90
90
  export type { StepItem } from './types/stepItem';
91
91
  export type { SegmentedControlItem } from './types/segmentedControlItem';
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@14ch/svelte-ui",
3
3
  "description": "Modern Svelte UI components library with TypeScript support",
4
4
  "private": false,
5
- "version": "0.0.63",
5
+ "version": "0.0.65",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "svelte",