@14ch/svelte-ui 0.0.64 → 0.0.66

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;
@@ -12,7 +12,7 @@
12
12
  import 'dayjs/locale/es';
13
13
  import 'dayjs/locale/zh-cn';
14
14
  import IconButton from './IconButton.svelte';
15
- import { onMount } from 'svelte';
15
+ import { onMount, onDestroy } from 'svelte';
16
16
  import { t } from '../i18n';
17
17
  import type { BivariantValueHandler } from '../types/callbackHandlers';
18
18
 
@@ -151,6 +151,10 @@
151
151
  }
152
152
  });
153
153
 
154
+ onDestroy(() => {
155
+ document.removeEventListener('keydown', handleKeyDown);
156
+ });
157
+
154
158
  // =========================================================================
155
159
  // Effects
156
160
  // =========================================================================
@@ -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;
@@ -12,8 +12,11 @@
12
12
  *
13
13
  * This prevents state synchronization bugs and ensures consistent behavior.
14
14
  */
15
+ import { tick } from 'svelte';
15
16
  import type { Snippet } from 'svelte';
17
+ import type { CloseReason } from '../types/propOptions';
16
18
  import { announceOpenClose } from '../utils/accessibility';
19
+ import { waitForAnimations } from '../utils/animation';
17
20
 
18
21
  // =========================================================================
19
22
  // Props, States & Constants
@@ -47,6 +50,11 @@
47
50
  ariaLabel?: string;
48
51
  ariaLabelledby?: string;
49
52
  ariaDescribedby?: string;
53
+
54
+ // イベントハンドラー
55
+ onOpen?: () => void;
56
+ /** Called after the close animation completes, with the reason it was closed. */
57
+ onClose?: (reason: CloseReason) => void;
50
58
  };
51
59
 
52
60
  let {
@@ -72,12 +80,19 @@
72
80
  // ARIA/アクセシビリティ
73
81
  ariaLabel,
74
82
  ariaLabelledby,
75
- ariaDescribedby
83
+ ariaDescribedby,
84
+
85
+ // イベントハンドラー
86
+ onOpen = () => {}, // No params for type inference
87
+ onClose = () => {}
76
88
  }: ModalProps = $props();
77
89
 
78
90
  let dialogRef: HTMLDialogElement;
79
91
  let containerRef: HTMLDivElement;
80
92
  let previousActiveElement: HTMLElement | null = null;
93
+ // isOpen は bind でも変わるため、実際に開いているかは内部で持つ
94
+ let isVisible = false;
95
+ let pendingCloseReason: CloseReason | null = null;
81
96
 
82
97
  // =========================================================================
83
98
  // Effects
@@ -89,7 +104,7 @@
89
104
  if (!closeIfClickOutside) return;
90
105
  if (!containerRef || !event.target) return;
91
106
  if (!containerRef.contains(event.target as Node)) {
92
- close();
107
+ close(undefined, 'outside');
93
108
  }
94
109
  };
95
110
 
@@ -110,7 +125,7 @@
110
125
  // クローズは抑止し、fade-out アニメーション付きの close() を使う。
111
126
  const handleCancel = (event: Event) => {
112
127
  event.preventDefault();
113
- close();
128
+ close(undefined, 'escape');
114
129
  };
115
130
 
116
131
  dialogRef.addEventListener('cancel', handleCancel);
@@ -172,13 +187,15 @@
172
187
  // =========================================================================
173
188
  /** Opens the modal. */
174
189
  export const open = (title?: string): void => {
175
- if (!dialogRef) return;
190
+ if (!dialogRef || isVisible) return;
176
191
 
192
+ isVisible = true;
177
193
  isOpen = true;
178
194
  previousActiveElement = document.activeElement as HTMLElement;
179
195
 
196
+ // 閉じるアニメーションの途中で開き直した場合は close を取り消す
197
+ pendingCloseReason = null;
180
198
  dialogRef.classList.add('fade-in');
181
- dialogRef.removeEventListener('animationend', closeEnd);
182
199
  dialogRef.showModal();
183
200
 
184
201
  setTimeout(() => {
@@ -186,20 +203,33 @@
186
203
  dialogRef?.focus();
187
204
  }
188
205
  announceOpenClose(componentType, true, title || ariaLabel || '');
206
+ onOpen?.();
189
207
  }, 0);
190
208
  };
191
209
 
192
210
  /** Closes the modal. */
193
- export const close = (title?: string): void => {
194
- if (!dialogRef) return;
211
+ export const close = (title?: string, reason: CloseReason = 'explicit'): void => {
212
+ if (!dialogRef || !isVisible) return;
195
213
 
214
+ isVisible = false;
196
215
  isOpen = false;
216
+ pendingCloseReason = reason;
197
217
  dialogRef.classList.add('fade-out');
198
- dialogRef.addEventListener('animationend', closeEnd, { once: true });
218
+ void finishClose();
199
219
 
200
220
  announceOpenClose(componentType, false, title || ariaLabel || '');
201
221
  };
202
222
 
223
+ /** 閉じるアニメーションの完了を待って後始末する。 */
224
+ const finishClose = async (): Promise<void> => {
225
+ await tick();
226
+ await waitForAnimations(dialogRef);
227
+
228
+ // 閉じるアニメーションの途中で開き直した場合は何もしない
229
+ if (pendingCloseReason === null) return;
230
+ closeEnd();
231
+ };
232
+
203
233
  /** @internal Completes the close animation. Called automatically. */
204
234
  export const closeEnd = (): void => {
205
235
  if (!dialogRef) return;
@@ -211,11 +241,18 @@
211
241
  previousActiveElement.focus();
212
242
  }
213
243
  previousActiveElement = null;
244
+
245
+ // アニメーション完了後に onClose を呼ぶ
246
+ if (pendingCloseReason) {
247
+ const reason = pendingCloseReason;
248
+ pendingCloseReason = null;
249
+ onClose?.(reason);
250
+ }
214
251
  };
215
252
 
216
253
  /** Toggles between open and closed. */
217
254
  export const toggle = (title?: string): void => {
218
- if (isOpen) {
255
+ if (isVisible) {
219
256
  close(title);
220
257
  } else {
221
258
  open(title);
@@ -1,16 +1,5 @@
1
- /**
2
- * 🚨 CRITICAL: DO NOT MANAGE MODAL STATE FROM PARENT COMPONENTS
3
- *
4
- * This Modal component manages its own open/closed state internally.
5
- * Parent components must NEVER create their own Modal state variables.
6
- *
7
- * ❌ WRONG: let isModalOpen = $state(false)
8
- * ✅ RIGHT: Use modalRef.open(), modalRef.close(), modalRef.toggle()
9
- * ✅ RIGHT: Use onOpen/onClose callbacks for side effects
10
- *
11
- * This prevents state synchronization bugs and ensures consistent behavior.
12
- */
13
1
  import type { Snippet } from 'svelte';
2
+ import type { CloseReason } from '../types/propOptions';
14
3
  export type ModalProps = {
15
4
  children?: Snippet;
16
5
  /** Internal label used for accessibility announcements. @default 'Modal' */
@@ -29,10 +18,13 @@ export type ModalProps = {
29
18
  ariaLabel?: string;
30
19
  ariaLabelledby?: string;
31
20
  ariaDescribedby?: string;
21
+ onOpen?: () => void;
22
+ /** Called after the close animation completes, with the reason it was closed. */
23
+ onClose?: (reason: CloseReason) => void;
32
24
  };
33
25
  declare const Modal: import("svelte").Component<ModalProps, {
34
26
  /** Opens the modal. */ open: (title?: string) => void;
35
- /** Closes the modal. */ close: (title?: string) => void;
27
+ /** Closes the modal. */ close: (title?: string, reason?: CloseReason) => void;
36
28
  /** @internal Completes the close animation. Called automatically. */ closeEnd: () => void;
37
29
  /** Toggles between open and closed. */ toggle: (title?: string) => void;
38
30
  }, "isOpen">;
@@ -18,6 +18,7 @@
18
18
  import { onDestroy, tick, onMount } from 'svelte';
19
19
  import { isMobileDevice, disableBodyScroll } from '../utils/mobile';
20
20
  import { announceOpenClose } from '../utils/accessibility';
21
+ import { waitForAnimations } from '../utils/animation';
21
22
  import { popupManager } from '../utils/popupManager';
22
23
  import type { CloseReason, PopupPosition } from '../types/propOptions';
23
24
 
@@ -106,6 +107,10 @@
106
107
  let isMobile: boolean = $state(false);
107
108
  let bodyScrollCleanup: (() => void) | undefined = $state();
108
109
  let pendingCloseReason: CloseReason = 'explicit';
110
+ let openTimer: ReturnType<typeof setTimeout> | undefined;
111
+ // open()/close() のたびに進め、進行中の終了処理が古い場合に破棄するための番号
112
+ let closeSequence = 0;
113
+ let isClosing = false;
109
114
 
110
115
  // =========================================================================
111
116
  // Lifecycle
@@ -115,6 +120,9 @@
115
120
  });
116
121
 
117
122
  onDestroy(() => {
123
+ cancelPendingOpen();
124
+ closeSequence++;
125
+ isClosing = false;
118
126
  removeEventListenersToClose();
119
127
  removeKeyboardListener();
120
128
  cleanupMobileFeatures();
@@ -346,16 +354,25 @@
346
354
 
347
355
  const closeOnResize = () => close();
348
356
 
357
+ const handleClickOutside = (event: MouseEvent) => {
358
+ if (!(event.target instanceof Node)) return;
359
+ if (popupRef?.contains(event.target)) return;
360
+ if (anchorElement?.contains(event.target)) return;
361
+ close('outside');
362
+ };
363
+
349
364
  const addEventListenersToClose = () => {
350
365
  if (typeof window === 'undefined' || typeof document === 'undefined') return;
351
366
  window.addEventListener('resize', closeOnResize);
352
367
  document.addEventListener('scroll', handleScroll, true);
368
+ document.addEventListener('click', handleClickOutside, true);
353
369
  };
354
370
 
355
371
  const removeEventListenersToClose = () => {
356
372
  if (typeof window === 'undefined' || typeof document === 'undefined') return;
357
373
  window.removeEventListener('resize', closeOnResize);
358
374
  document.removeEventListener('scroll', handleScroll, true);
375
+ document.removeEventListener('click', handleClickOutside, true);
359
376
  };
360
377
 
361
378
  const addKeyboardListener = () => {
@@ -403,6 +420,9 @@
403
420
  };
404
421
 
405
422
  const closeEnd = () => {
423
+ if (!isClosing) return;
424
+ isClosing = false;
425
+
406
426
  popupRef?.hidePopover();
407
427
 
408
428
  if (popupRef) {
@@ -421,29 +441,19 @@
421
441
  onClose?.(reason);
422
442
  };
423
443
 
424
- const clickOutside = (element: HTMLElement, callbackFunction: () => void) => {
425
- function onClick(event: MouseEvent) {
426
- if (!(event.target instanceof Node)) return;
444
+ /** 閉じるアニメーションの完了を待って後始末する。 */
445
+ const finishClose = async (sequence: number) => {
446
+ await tick();
447
+ await waitForAnimations(popupRef);
427
448
 
428
- const isInsidePopup = element.contains(event.target);
429
- const isInsideAnchor = anchorElement && anchorElement.contains(event.target);
430
- const isOutside = !isInsidePopup && !isInsideAnchor;
449
+ if (sequence !== closeSequence) return;
450
+ closeEnd();
451
+ };
431
452
 
432
- if (isOutside) {
433
- callbackFunction();
434
- }
435
- }
436
- setTimeout(() => {
437
- document.body.addEventListener('click', onClick);
438
- }, 1);
439
- return {
440
- update(newCallbackFunction: () => void) {
441
- callbackFunction = newCallbackFunction;
442
- },
443
- destroy() {
444
- document.body.removeEventListener('click', onClick);
445
- }
446
- };
453
+ const cancelPendingOpen = () => {
454
+ if (openTimer === undefined) return;
455
+ clearTimeout(openTimer);
456
+ openTimer = undefined;
447
457
  };
448
458
 
449
459
  /** Opens the popup. */
@@ -451,10 +461,17 @@
451
461
  // 他の開いているPopupをすべて閉じる
452
462
  popupManager.closeOthers(close);
453
463
 
464
+ // 閉じるアニメーションの途中で開き直した場合は close を取り消す
465
+ closeSequence++;
466
+ isClosing = false;
467
+ pendingCloseReason = 'explicit';
468
+
454
469
  previousActiveElement = document.activeElement as HTMLElement;
455
470
 
456
- setTimeout(async () => {
457
- popupRef?.removeEventListener('animationend', closeEnd);
471
+ cancelPendingOpen();
472
+ openTimer = setTimeout(async () => {
473
+ openTimer = undefined;
474
+ if (!popupRef?.isConnected) return;
458
475
 
459
476
  if (shouldUseFullscreen && popupRef) {
460
477
  // ボトムシート: showPopover()の前に位置を画面下部に設定
@@ -504,17 +521,22 @@
504
521
 
505
522
  /** Closes the popup. */
506
523
  export const close = (reason: CloseReason = 'explicit') => {
524
+ cancelPendingOpen();
525
+ // 開いていないPopupのcloseは、close reasonを汚さないよう何もしない
526
+ if (!isOpen && !isClosing && !popupRef?.matches(':popover-open')) return;
527
+
507
528
  if (pendingCloseReason === 'explicit') {
508
529
  pendingCloseReason = reason;
509
530
  }
510
531
  isOpen = false;
532
+ isClosing = true;
511
533
  removeEventListenersToClose();
512
534
  removeKeyboardListener();
513
535
  cleanupMobileFeatures();
514
536
  popupManager.unregister(close);
515
537
  // アニメーション完了後に閉じる(fullscreen/通常共通)
516
- popupRef?.addEventListener('animationend', closeEnd, { once: true });
517
538
  // onCloseはアニメーション完了後に呼ぶ(closeEndで実行)
539
+ void finishClose(++closeSequence);
518
540
  };
519
541
 
520
542
  /** Toggles between open and closed. */
@@ -552,9 +574,6 @@
552
574
  aria-modal={undefined}
553
575
  id={popupId}
554
576
  data-testid="popup"
555
- use:clickOutside={() => {
556
- if (isOpen) close('outside');
557
- }}
558
577
  >
559
578
  {@render children()}
560
579
  </div>
@@ -1,9 +1,10 @@
1
1
  <!-- SnackbarItem.svelte -->
2
2
 
3
3
  <script lang="ts">
4
- import { onMount } from 'svelte';
4
+ import { onMount, onDestroy, tick } from 'svelte';
5
5
  import type { Snippet } from 'svelte';
6
6
  import { snackbar } from '../utils/snackbar.svelte';
7
+ import { waitForAnimations } from '../utils/animation';
7
8
  import type { IconVariant } from '../types/icon';
8
9
  import type { SnackbarType, SnackbarVariant, SnackbarPosition } from '../types/propOptions';
9
10
  import Icon from './Icon.svelte';
@@ -89,15 +90,23 @@
89
90
  onMount(() => {
90
91
  if (duration > 0) {
91
92
  timeoutId = setTimeout(() => {
93
+ timeoutId = null;
92
94
  handleClose();
93
95
  }, duration);
94
96
  }
95
97
  });
96
98
 
99
+ onDestroy(() => {
100
+ if (timeoutId !== null) {
101
+ clearTimeout(timeoutId);
102
+ timeoutId = null;
103
+ }
104
+ });
105
+
97
106
  // =========================================================================
98
107
  // Methods
99
108
  // =========================================================================
100
- const handleClose = () => {
109
+ const handleClose = async () => {
101
110
  // アニメーション開始前に、他のSnackbarの位置を測定
102
111
  if (snackbarRef) {
103
112
  // 現在の要素の位置と親コンテナを取得
@@ -119,21 +128,17 @@
119
128
  );
120
129
 
121
130
  // CSSカスタムプロパティが確実に設定されるまで少し待つ
122
- requestAnimationFrame(() => {
123
- visible = false;
124
- });
125
-
126
- // animationendイベントでSnackbar削除
127
- snackbarRef.addEventListener(
128
- 'animationend',
129
- () => {
130
- snackbar.remove(id);
131
- },
132
- { once: true }
133
- );
131
+ await new Promise((resolve) => requestAnimationFrame(resolve));
132
+ visible = false;
133
+
134
+ // アニメーション完了後にSnackbar削除
135
+ await tick();
136
+ await waitForAnimations(snackbarRef);
134
137
  } else {
135
138
  visible = false;
136
139
  }
140
+
141
+ snackbar.remove(id);
137
142
  };
138
143
 
139
144
  const handleAction = () => {
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';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Animation utilities
3
+ */
4
+ /**
5
+ * 要素で実行中のアニメーションの完了を待つ。
6
+ *
7
+ * animationend イベントに依存すると、アニメーションが動かない状況
8
+ * (アニメーション無効化・要素が非表示・テスト環境など)でイベントが発火せず、
9
+ * 閉じる処理の後始末が永久に実行されないままになる。実際の Animation を待つことで、
10
+ * アニメーションが無い場合は即座に解決する。
11
+ */
12
+ export declare const waitForAnimations: (element: Element | null | undefined) => Promise<void>;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Animation utilities
3
+ */
4
+ /**
5
+ * 要素で実行中のアニメーションの完了を待つ。
6
+ *
7
+ * animationend イベントに依存すると、アニメーションが動かない状況
8
+ * (アニメーション無効化・要素が非表示・テスト環境など)でイベントが発火せず、
9
+ * 閉じる処理の後始末が永久に実行されないままになる。実際の Animation を待つことで、
10
+ * アニメーションが無い場合は即座に解決する。
11
+ */
12
+ export const waitForAnimations = async (element) => {
13
+ if (!element?.isConnected)
14
+ return;
15
+ const animations = element.getAnimations();
16
+ if (animations.length === 0)
17
+ return;
18
+ await Promise.allSettled(animations.map((animation) => animation.finished));
19
+ };
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.64",
5
+ "version": "0.0.66",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "svelte",