@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.
- package/dist/components/ConfirmDialog.svelte +18 -1
- package/dist/components/ConfirmDialog.svelte.d.ts +5 -0
- package/dist/components/DatepickerCalendar.svelte +5 -1
- package/dist/components/Dialog.svelte +13 -1
- package/dist/components/Dialog.svelte.d.ts +4 -0
- package/dist/components/Drawer.svelte +13 -1
- package/dist/components/Drawer.svelte.d.ts +4 -0
- package/dist/components/Modal.svelte +46 -9
- package/dist/components/Modal.svelte.d.ts +5 -13
- package/dist/components/Popup.svelte +46 -27
- package/dist/components/SnackbarItem.svelte +19 -14
- package/dist/index.d.ts +1 -1
- package/dist/utils/animation.d.ts +12 -0
- package/dist/utils/animation.js +19 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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 (
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
444
|
+
/** 閉じるアニメーションの完了を待って後始末する。 */
|
|
445
|
+
const finishClose = async (sequence: number) => {
|
|
446
|
+
await tick();
|
|
447
|
+
await waitForAnimations(popupRef);
|
|
427
448
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
449
|
+
if (sequence !== closeSequence) return;
|
|
450
|
+
closeEnd();
|
|
451
|
+
};
|
|
431
452
|
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
457
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
snackbarRef
|
|
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
|
+
};
|