@pienter/ui 0.2.0 → 0.5.0
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/CHANGELOG.md +259 -0
- package/CONVENTIONS.md +297 -385
- package/README.md +3 -18
- package/components/feedback/toast/toast.css +1 -1
- package/components/form/combobox/Combobox.vue +21 -38
- package/components/form/combobox/combobox.css +1 -1
- package/components/form/form/Form.vue +1 -2
- package/components/form/label/label.css +1 -1
- package/components/form/number-field/NumberField.vue +1 -1
- package/components/form/number-field/number-field.css +1 -1
- package/components/form/radio-group/RadioGroup.vue +2 -5
- package/components/form/slider/slider.css +2 -3
- package/components/form/tags-input/tags-input.css +1 -2
- package/components/form/textarea/textarea.css +1 -1
- package/components/layout/accordion/Accordion.vue +6 -27
- package/components/layout/collapsible/Collapsible.vue +9 -19
- package/components/layout/table/table.css +3 -3
- package/components/navigation/pagination/Pagination.vue +1 -1
- package/components/navigation/pagination/PaginationFooter.vue +23 -0
- package/components/navigation/pagination/pagination.css +128 -29
- package/components/navigation/sidebar/Sidebar.vue +326 -84
- package/components/navigation/sidebar/SidebarMenuItem.vue +199 -0
- package/components/navigation/sidebar/context.ts +21 -0
- package/components/navigation/sidebar/sidebar.css +346 -78
- package/components/navigation/sidebar/types.ts +13 -1
- package/components/overlay/alert-dialog/AlertDialog.vue +10 -31
- package/components/overlay/command/Command.vue +10 -38
- package/components/overlay/command/command.css +1 -1
- package/components/overlay/dropdown-menu/DropdownMenu.vue +37 -62
- package/components/overlay/modal/Modal.vue +7 -28
- package/components/overlay/popover/Popover.vue +9 -31
- package/components/overlay/sheet/Sheet.vue +7 -28
- package/components/overlay/tooltip/Tooltip.vue +14 -19
- package/{utils/controllers/dialog.ts → composables/useDialog.ts} +90 -78
- package/composables/useDisclosure.ts +113 -0
- package/composables/useEventListener.ts +16 -0
- package/{utils/controllers/menu.ts → composables/useMenu.ts} +66 -102
- package/{utils/controllers/popover.ts → composables/usePopover.ts} +107 -120
- package/package.json +6 -38
- package/styles/0-settings/colors.css +8 -1
- package/styles/0-settings/layout.css +18 -0
- package/styles/0-settings/motion.css +2 -2
- package/styles/0-settings/spacing.css +3 -1
- package/utils/navigation/sidebar.ts +97 -0
- package/utils/validation/form.ts +6 -9
- package/components/action/button/Button.astro +0 -95
- package/components/action/button/IconButton.astro +0 -86
- package/components/display/avatar/Avatar.astro +0 -17
- package/components/display/avatar/AvatarStack.astro +0 -9
- package/components/display/badge/Badge.astro +0 -15
- package/components/display/empty/Empty.astro +0 -9
- package/components/display/icon/Icon.astro +0 -52
- package/components/feedback/alert/Alert.astro +0 -52
- package/components/feedback/progress/Progress.astro +0 -68
- package/components/feedback/skeleton/Skeleton.astro +0 -32
- package/components/feedback/spinner/Spinner.astro +0 -25
- package/components/feedback/toast/Toast.astro +0 -50
- package/components/form/checkbox/Checkbox.astro +0 -79
- package/components/form/date-input/DateInput.astro +0 -105
- package/components/form/form/Form.astro +0 -106
- package/components/form/input-otp/InputOTP.astro +0 -147
- package/components/form/label/Label.astro +0 -13
- package/components/form/number-field/NumberField.astro +0 -142
- package/components/form/radio-group/RadioGroup.astro +0 -105
- package/components/form/select/Select.astro +0 -105
- package/components/form/slider/Slider.astro +0 -205
- package/components/form/switch/Switch.astro +0 -75
- package/components/form/tags-input/TagsInput.astro +0 -153
- package/components/form/text-input/TextInput.astro +0 -84
- package/components/form/textarea/Textarea.astro +0 -86
- package/components/layout/card/Card.astro +0 -13
- package/components/layout/separator/Separator.astro +0 -31
- package/components/layout/table/Table.astro +0 -116
- package/components/navigation/breadcrumb/Breadcrumb.astro +0 -36
- package/components/navigation/navbar/Navbar.astro +0 -62
- package/components/navigation/sidebar/Sidebar.astro +0 -132
- package/components/navigation/sidebar/SidebarItemRender.astro +0 -83
- package/components/navigation/sidebar/SidebarItemRender.vue +0 -98
- package/components/overlay/alert-dialog/AlertDialog.astro +0 -112
- package/components/overlay/modal/Modal.astro +0 -66
- package/utils/controllers/disclosure.ts +0 -117
- package/utils/controllers/form.ts +0 -524
- package/utils/controllers/index.ts +0 -39
- package/utils/controllers/number-field.ts +0 -103
- package/utils/controllers/otp.ts +0 -252
- package/utils/controllers/sidebar.ts +0 -610
- package/utils/controllers/slider.ts +0 -336
- package/utils/controllers/tags-input.ts +0 -255
- package/utils/controllers/toast.ts +0 -426
|
@@ -1,426 +0,0 @@
|
|
|
1
|
-
import { announce } from '../a11y/live-region.js';
|
|
2
|
-
import { icons, type IconName } from '../../icons/index.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Toast tones (parity with Toast.vue / Toast.astro). Drives the
|
|
6
|
-
* `data-tone` attribute on the rendered toast and the default icon
|
|
7
|
-
* selection inside `renderToastEl`.
|
|
8
|
-
*/
|
|
9
|
-
export type ToastTone = 'brand' | 'success' | 'warning' | 'danger';
|
|
10
|
-
|
|
11
|
-
export type ToastPosition =
|
|
12
|
-
| 'bottom-right'
|
|
13
|
-
| 'bottom-center'
|
|
14
|
-
| 'bottom-left'
|
|
15
|
-
| 'top-right'
|
|
16
|
-
| 'top-center'
|
|
17
|
-
| 'top-left';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Options accepted by `toast.show(opts)` and `toast.queue` items.
|
|
21
|
-
*
|
|
22
|
-
* `duration` is in milliseconds; `0` disables auto-dismiss (the
|
|
23
|
-
* toast stays until dismissed manually or via `toast.dismiss(id)`).
|
|
24
|
-
* `action` adds a single action button alongside the close button;
|
|
25
|
-
* clicking it invokes `onClick` and then dismisses the toast.
|
|
26
|
-
*/
|
|
27
|
-
export interface ToastOptions {
|
|
28
|
-
title: string;
|
|
29
|
-
message?: string;
|
|
30
|
-
tone?: ToastTone;
|
|
31
|
-
/** Milliseconds. `0` disables auto-dismiss. Default `defaultDuration`. */
|
|
32
|
-
duration?: number;
|
|
33
|
-
action?: { label: string; onClick: () => void };
|
|
34
|
-
/** Optional icon name override; defaults to the tone-mapped icon. */
|
|
35
|
-
icon?: IconName;
|
|
36
|
-
/** Optional ID for manual dismiss; auto-generated if omitted. */
|
|
37
|
-
id?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface ToastConfig {
|
|
41
|
-
/** Maximum visible toasts; queued toasts wait until a slot opens. Default `3`. */
|
|
42
|
-
maxVisible?: number;
|
|
43
|
-
/** Default auto-dismiss duration in ms. Default `5000`. */
|
|
44
|
-
defaultDuration?: number;
|
|
45
|
-
/** Container position. v1 ships `bottom-right` only; others reserved. */
|
|
46
|
-
position?: ToastPosition;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface ActiveToast {
|
|
50
|
-
id: string;
|
|
51
|
-
el: HTMLElement;
|
|
52
|
-
duration: number;
|
|
53
|
-
timerId: number | null;
|
|
54
|
-
remaining: number;
|
|
55
|
-
startedAt: number;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const toneIconMap: Record<ToastTone, IconName> = {
|
|
59
|
-
brand: 'info',
|
|
60
|
-
success: 'circle-check',
|
|
61
|
-
warning: 'alert-triangle',
|
|
62
|
-
danger: 'circle-alert',
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const config: Required<ToastConfig> = {
|
|
66
|
-
maxVisible: 3,
|
|
67
|
-
defaultDuration: 5000,
|
|
68
|
-
position: 'bottom-right',
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const queued: ToastOptions[] = [];
|
|
72
|
-
const active: ActiveToast[] = [];
|
|
73
|
-
let regionEl: HTMLElement | null = null;
|
|
74
|
-
let nextIdCounter = 0;
|
|
75
|
-
|
|
76
|
-
function generateToastId(): string {
|
|
77
|
-
nextIdCounter += 1;
|
|
78
|
-
return `v-toast-${Date.now().toString(36)}-${nextIdCounter}`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function escapeHtml(value: string): string {
|
|
82
|
-
return value
|
|
83
|
-
.replace(/&/g, '&')
|
|
84
|
-
.replace(/</g, '<')
|
|
85
|
-
.replace(/>/g, '>')
|
|
86
|
-
.replace(/"/g, '"')
|
|
87
|
-
.replace(/'/g, ''');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Lazily create the global region container on first `show()`. The
|
|
92
|
-
* region is `role="region" aria-label="Notifications"` per the
|
|
93
|
-
* locked CONVENTIONS rule — distinct from the global ARIA live
|
|
94
|
-
* region (which is a separate visually-hidden node managed by
|
|
95
|
-
* `utils/a11y/live-region.ts:announce()`). The two responsibilities
|
|
96
|
-
* are intentionally separate: the region is the visual container,
|
|
97
|
-
* the live region is the SR announcement channel.
|
|
98
|
-
*/
|
|
99
|
-
function ensureRegion(): HTMLElement {
|
|
100
|
-
if (regionEl && document.body.contains(regionEl)) return regionEl;
|
|
101
|
-
const el = document.createElement('div');
|
|
102
|
-
el.id = 'v-toast-region';
|
|
103
|
-
el.className = 'v-toast-region';
|
|
104
|
-
el.setAttribute('role', 'region');
|
|
105
|
-
el.setAttribute('aria-label', 'Notifications');
|
|
106
|
-
el.setAttribute('data-position', config.position);
|
|
107
|
-
document.body.appendChild(el);
|
|
108
|
-
regionEl = el;
|
|
109
|
-
return el;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function buildToastInnerHTML(opts: ToastOptions): string {
|
|
113
|
-
const iconName: IconName =
|
|
114
|
-
opts.icon ?? (opts.tone ? toneIconMap[opts.tone] : 'info');
|
|
115
|
-
const iconSvg = icons[iconName];
|
|
116
|
-
const xSvg = icons['x'];
|
|
117
|
-
return [
|
|
118
|
-
`<span class="v-toast__icon" aria-hidden="true">${iconSvg}</span>`,
|
|
119
|
-
'<div class="v-toast__body">',
|
|
120
|
-
`<p class="v-toast__title">${escapeHtml(opts.title)}</p>`,
|
|
121
|
-
opts.message
|
|
122
|
-
? `<p class="v-toast__msg">${escapeHtml(opts.message)}</p>`
|
|
123
|
-
: '',
|
|
124
|
-
'</div>',
|
|
125
|
-
opts.action
|
|
126
|
-
? `<button class="v-toast__action" type="button">${escapeHtml(opts.action.label)}</button>`
|
|
127
|
-
: '',
|
|
128
|
-
`<button class="v-toast__close" type="button" aria-label="Close">${xSvg}</button>`,
|
|
129
|
-
].join('');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Render the toast wrapper element matching Toast.vue / Toast.astro
|
|
134
|
-
* markup so the same `toast.css` styles apply. Wires close + action
|
|
135
|
-
* + hover/focus pause/resume listeners. The element is returned
|
|
136
|
-
* un-mounted; callers append into the region.
|
|
137
|
-
*/
|
|
138
|
-
function renderToastEl(opts: ToastOptions, id: string): HTMLElement {
|
|
139
|
-
const el = document.createElement('div');
|
|
140
|
-
el.className = 'v-toast';
|
|
141
|
-
if (opts.tone) el.setAttribute('data-tone', opts.tone);
|
|
142
|
-
el.setAttribute('data-toast-id', id);
|
|
143
|
-
// No `role` here — `announce()` already covers SR announcement, and
|
|
144
|
-
// double-announcing (visual `role="status"` + live region) is the
|
|
145
|
-
// common Toast a11y bug. See components/feedback/toast/AUDIT.md.
|
|
146
|
-
el.innerHTML = buildToastInnerHTML(opts);
|
|
147
|
-
|
|
148
|
-
el.querySelector<HTMLButtonElement>('.v-toast__close')?.addEventListener(
|
|
149
|
-
'click',
|
|
150
|
-
() => {
|
|
151
|
-
dismissByEl(el);
|
|
152
|
-
},
|
|
153
|
-
);
|
|
154
|
-
if (opts.action) {
|
|
155
|
-
el.querySelector<HTMLButtonElement>(
|
|
156
|
-
'.v-toast__action',
|
|
157
|
-
)?.addEventListener('click', () => {
|
|
158
|
-
opts.action!.onClick();
|
|
159
|
-
dismissByEl(el);
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
el.addEventListener('mouseenter', () => pauseEl(el));
|
|
163
|
-
el.addEventListener('mouseleave', () => resumeEl(el));
|
|
164
|
-
el.addEventListener('focusin', () => pauseEl(el));
|
|
165
|
-
el.addEventListener('focusout', () => resumeEl(el));
|
|
166
|
-
el.addEventListener('keydown', (event) => {
|
|
167
|
-
if (event.key === 'Escape') {
|
|
168
|
-
event.preventDefault();
|
|
169
|
-
dismissByEl(el);
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
return el;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function findActiveByEl(el: HTMLElement): ActiveToast | undefined {
|
|
177
|
-
return active.find((a) => a.el === el);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function findActiveById(id: string): ActiveToast | undefined {
|
|
181
|
-
return active.find((a) => a.id === id);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function startTimer(entry: ActiveToast): void {
|
|
185
|
-
if (entry.duration <= 0) return; // persistent
|
|
186
|
-
if (entry.timerId !== null) return; // already running
|
|
187
|
-
entry.startedAt = Date.now();
|
|
188
|
-
entry.timerId = window.setTimeout(() => {
|
|
189
|
-
entry.timerId = null;
|
|
190
|
-
dismissEntry(entry);
|
|
191
|
-
}, entry.remaining);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function pauseTimer(entry: ActiveToast): void {
|
|
195
|
-
if (entry.timerId === null) return;
|
|
196
|
-
window.clearTimeout(entry.timerId);
|
|
197
|
-
entry.timerId = null;
|
|
198
|
-
const elapsed = Date.now() - entry.startedAt;
|
|
199
|
-
entry.remaining = Math.max(0, entry.remaining - elapsed);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function pauseEl(el: HTMLElement): void {
|
|
203
|
-
const entry = findActiveByEl(el);
|
|
204
|
-
if (entry) pauseTimer(entry);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function resumeEl(el: HTMLElement): void {
|
|
208
|
-
const entry = findActiveByEl(el);
|
|
209
|
-
if (entry) startTimer(entry);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function dismissEntry(entry: ActiveToast): void {
|
|
213
|
-
if (entry.timerId !== null) {
|
|
214
|
-
window.clearTimeout(entry.timerId);
|
|
215
|
-
entry.timerId = null;
|
|
216
|
-
}
|
|
217
|
-
if (entry.el.parentNode) {
|
|
218
|
-
entry.el.parentNode.removeChild(entry.el);
|
|
219
|
-
}
|
|
220
|
-
const idx = active.indexOf(entry);
|
|
221
|
-
if (idx !== -1) active.splice(idx, 1);
|
|
222
|
-
drainQueue();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function dismissByEl(el: HTMLElement): void {
|
|
226
|
-
const entry = findActiveByEl(el);
|
|
227
|
-
if (entry) dismissEntry(entry);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function drainQueue(): void {
|
|
231
|
-
while (active.length < config.maxVisible && queued.length > 0) {
|
|
232
|
-
const next = queued.shift()!;
|
|
233
|
-
showNow(next);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function showNow(opts: ToastOptions): string {
|
|
238
|
-
const region = ensureRegion();
|
|
239
|
-
const id = opts.id ?? generateToastId();
|
|
240
|
-
const duration = opts.duration ?? config.defaultDuration;
|
|
241
|
-
const el = renderToastEl(opts, id);
|
|
242
|
-
region.appendChild(el);
|
|
243
|
-
|
|
244
|
-
const entry: ActiveToast = {
|
|
245
|
-
id,
|
|
246
|
-
el,
|
|
247
|
-
duration,
|
|
248
|
-
timerId: null,
|
|
249
|
-
remaining: duration,
|
|
250
|
-
startedAt: Date.now(),
|
|
251
|
-
};
|
|
252
|
-
active.push(entry);
|
|
253
|
-
|
|
254
|
-
// Announce via the global SR live region. Danger tone is assertive;
|
|
255
|
-
// everything else polite. The announcement uses title (+ message).
|
|
256
|
-
const announceText = opts.message
|
|
257
|
-
? `${opts.title}. ${opts.message}`
|
|
258
|
-
: opts.title;
|
|
259
|
-
announce(announceText, opts.tone === 'danger' ? 'assertive' : 'polite');
|
|
260
|
-
|
|
261
|
-
startTimer(entry);
|
|
262
|
-
return id;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Module-level singleton API per the singleton-controller exception
|
|
267
|
-
* documented in CONVENTIONS § Controller mount-on-element pattern.
|
|
268
|
-
*
|
|
269
|
-
* Toast manages a single global queue + region; consumers call
|
|
270
|
-
* `toast.show({ tone, title, message })` rather than rendering the
|
|
271
|
-
* `<Toast>` component themselves. Returns the assigned id so callers
|
|
272
|
-
* can dismiss programmatically via `toast.dismiss(id)`.
|
|
273
|
-
*
|
|
274
|
-
* `toast.configure({ maxVisible, defaultDuration, position })` mutates
|
|
275
|
-
* the module-level config; call once on app init if the defaults
|
|
276
|
-
* (`maxVisible: 3`, `defaultDuration: 5000`, `position: 'bottom-right'`)
|
|
277
|
-
* need overriding. v1 only ships `bottom-right`; other positions are
|
|
278
|
-
* accepted by the type but not yet styled.
|
|
279
|
-
*/
|
|
280
|
-
export const toast = {
|
|
281
|
-
show(opts: ToastOptions): string {
|
|
282
|
-
if (active.length >= config.maxVisible) {
|
|
283
|
-
const queuedOpts = { ...opts, id: opts.id ?? generateToastId() };
|
|
284
|
-
queued.push(queuedOpts);
|
|
285
|
-
return queuedOpts.id;
|
|
286
|
-
}
|
|
287
|
-
return showNow(opts);
|
|
288
|
-
},
|
|
289
|
-
|
|
290
|
-
dismiss(id: string): void {
|
|
291
|
-
const entry = findActiveById(id);
|
|
292
|
-
if (entry) {
|
|
293
|
-
dismissEntry(entry);
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
const queueIdx = queued.findIndex((q) => q.id === id);
|
|
297
|
-
if (queueIdx !== -1) queued.splice(queueIdx, 1);
|
|
298
|
-
},
|
|
299
|
-
|
|
300
|
-
dismissAll(): void {
|
|
301
|
-
queued.length = 0;
|
|
302
|
-
// Snapshot active because dismissEntry mutates the array.
|
|
303
|
-
const snapshot = [...active];
|
|
304
|
-
for (const entry of snapshot) dismissEntry(entry);
|
|
305
|
-
},
|
|
306
|
-
|
|
307
|
-
configure(c: ToastConfig): void {
|
|
308
|
-
if (typeof c.maxVisible === 'number' && c.maxVisible > 0) {
|
|
309
|
-
config.maxVisible = Math.floor(c.maxVisible);
|
|
310
|
-
}
|
|
311
|
-
if (typeof c.defaultDuration === 'number' && c.defaultDuration >= 0) {
|
|
312
|
-
config.defaultDuration = c.defaultDuration;
|
|
313
|
-
}
|
|
314
|
-
if (c.position) {
|
|
315
|
-
config.position = c.position;
|
|
316
|
-
if (regionEl) regionEl.setAttribute('data-position', c.position);
|
|
317
|
-
}
|
|
318
|
-
drainQueue();
|
|
319
|
-
},
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Per-instance escape hatch for consumers who render a `<Toast>`
|
|
324
|
-
* declaratively (Astro `<Toast>` or `<Toast>` Vue component) and
|
|
325
|
-
* want the controller to manage close-button wiring, action wiring,
|
|
326
|
-
* hover/focus pause-resume, and auto-dismiss timing. Returns a
|
|
327
|
-
* teardown function per the canonical mount signature.
|
|
328
|
-
*
|
|
329
|
-
* Most consumers should use the module-level `toast.show()` API
|
|
330
|
-
* instead. This is a fallback for cases where the toast DOM is
|
|
331
|
-
* authored declaratively and the controller layers behavior on top.
|
|
332
|
-
*
|
|
333
|
-
* Note: `mountToast(el)` does NOT add `el` to the global `active`
|
|
334
|
-
* queue, because the consumer owns the DOM placement (it isn't in
|
|
335
|
-
* the controller's region container). The lifecycle is local to
|
|
336
|
-
* `el` only — close button, action button, hover/focus pause-resume,
|
|
337
|
-
* auto-dismiss timer (if duration provided), Escape-to-dismiss on
|
|
338
|
-
* the rendered element. Calling `dismiss()` removes `el` from its
|
|
339
|
-
* parent.
|
|
340
|
-
*/
|
|
341
|
-
export function mountToast(
|
|
342
|
-
el: HTMLElement,
|
|
343
|
-
opts: { duration?: number; onDismiss?: () => void } = {},
|
|
344
|
-
): () => void {
|
|
345
|
-
const duration = opts.duration ?? 0; // default to no auto-dismiss for declarative usage
|
|
346
|
-
let timerId: number | null = null;
|
|
347
|
-
let remaining = duration;
|
|
348
|
-
let startedAt = Date.now();
|
|
349
|
-
let dismissed = false;
|
|
350
|
-
|
|
351
|
-
function dismiss(): void {
|
|
352
|
-
if (dismissed) return;
|
|
353
|
-
dismissed = true;
|
|
354
|
-
if (timerId !== null) {
|
|
355
|
-
window.clearTimeout(timerId);
|
|
356
|
-
timerId = null;
|
|
357
|
-
}
|
|
358
|
-
el.removeEventListener('mouseenter', pause);
|
|
359
|
-
el.removeEventListener('mouseleave', resume);
|
|
360
|
-
el.removeEventListener('focusin', pause);
|
|
361
|
-
el.removeEventListener('focusout', resume);
|
|
362
|
-
closeBtn?.removeEventListener('click', onClose);
|
|
363
|
-
actionBtn?.removeEventListener('click', onAction);
|
|
364
|
-
el.removeEventListener('keydown', onKeydown);
|
|
365
|
-
if (el.parentNode) el.parentNode.removeChild(el);
|
|
366
|
-
opts.onDismiss?.();
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function start(): void {
|
|
370
|
-
if (duration <= 0 || timerId !== null) return;
|
|
371
|
-
startedAt = Date.now();
|
|
372
|
-
timerId = window.setTimeout(() => {
|
|
373
|
-
timerId = null;
|
|
374
|
-
dismiss();
|
|
375
|
-
}, remaining);
|
|
376
|
-
}
|
|
377
|
-
function pause(): void {
|
|
378
|
-
if (timerId === null) return;
|
|
379
|
-
window.clearTimeout(timerId);
|
|
380
|
-
timerId = null;
|
|
381
|
-
remaining = Math.max(0, remaining - (Date.now() - startedAt));
|
|
382
|
-
}
|
|
383
|
-
function resume(): void {
|
|
384
|
-
start();
|
|
385
|
-
}
|
|
386
|
-
function onClose(): void {
|
|
387
|
-
dismiss();
|
|
388
|
-
}
|
|
389
|
-
function onAction(): void {
|
|
390
|
-
dismiss();
|
|
391
|
-
}
|
|
392
|
-
function onKeydown(event: KeyboardEvent): void {
|
|
393
|
-
if (event.key === 'Escape') {
|
|
394
|
-
event.preventDefault();
|
|
395
|
-
dismiss();
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const closeBtn = el.querySelector<HTMLButtonElement>('.v-toast__close');
|
|
400
|
-
const actionBtn = el.querySelector<HTMLButtonElement>('.v-toast__action');
|
|
401
|
-
closeBtn?.addEventListener('click', onClose);
|
|
402
|
-
actionBtn?.addEventListener('click', onAction);
|
|
403
|
-
el.addEventListener('mouseenter', pause);
|
|
404
|
-
el.addEventListener('mouseleave', resume);
|
|
405
|
-
el.addEventListener('focusin', pause);
|
|
406
|
-
el.addEventListener('focusout', resume);
|
|
407
|
-
el.addEventListener('keydown', onKeydown);
|
|
408
|
-
|
|
409
|
-
start();
|
|
410
|
-
|
|
411
|
-
return () => {
|
|
412
|
-
if (dismissed) return;
|
|
413
|
-
if (timerId !== null) {
|
|
414
|
-
window.clearTimeout(timerId);
|
|
415
|
-
timerId = null;
|
|
416
|
-
}
|
|
417
|
-
el.removeEventListener('mouseenter', pause);
|
|
418
|
-
el.removeEventListener('mouseleave', resume);
|
|
419
|
-
el.removeEventListener('focusin', pause);
|
|
420
|
-
el.removeEventListener('focusout', resume);
|
|
421
|
-
closeBtn?.removeEventListener('click', onClose);
|
|
422
|
-
actionBtn?.removeEventListener('click', onAction);
|
|
423
|
-
el.removeEventListener('keydown', onKeydown);
|
|
424
|
-
dismissed = true;
|
|
425
|
-
};
|
|
426
|
-
}
|