ropav 0.11.0 → 0.12.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/dist/components/autocomplete/autocomplete-clear-button.js +1 -1
- package/dist/components/autocomplete/autocomplete-filter.d.ts +1 -1
- package/dist/components/autocomplete/autocomplete-root.js +3 -0
- package/dist/components/checkbox/checkbox-root.js +4 -1
- package/dist/components/radio/radio-root.js +4 -1
- package/dist/components/scroll-shadow/scroll-shadow-root.js +2 -10
- package/dist/components/scroll-shadow/use-scroll-shadow.d.ts +10 -0
- package/dist/components/scroll-shadow/use-scroll-shadow.js +34 -2
- package/dist/components/select/index.d.ts +3 -2
- package/dist/components/select/index.js +2 -1
- package/dist/components/select/select-clear-button.d.ts +21 -0
- package/dist/components/select/select-clear-button.js +66 -0
- package/dist/components/select/select-root.d.ts +2 -1
- package/dist/components/select/select-root.js +15 -1
- package/dist/components/select/select.context.d.ts +11 -0
- package/dist/components/select/select.types.d.ts +3 -0
- package/dist/components/switch/switch-root.js +4 -1
- package/dist/components/table/table-row.js +3 -2
- package/dist/components/table/use-grid-keyboard.js +2 -3
- package/dist/components/table/use-table-collection.d.ts +0 -11
- package/dist/components/table/use-table-collection.js +1 -17
- package/dist/components/tabs/tabs-root.js +5 -1
- package/dist/components/tabs/tabs.types.d.ts +6 -0
- package/dist/components/textarea/textarea-root.js +20 -0
- package/dist/components/textarea/textarea.types.d.ts +21 -0
- package/dist/components/toast/index.d.ts +1 -1
- package/dist/components/toast/index.js +3 -3
- package/dist/components/toast/toast-default-content.js +26 -4
- package/dist/components/toast/toast-indicator.js +8 -2
- package/dist/components/toast/toast-provider.js +53 -5
- package/dist/components/toast/toast-queue.d.ts +60 -5
- package/dist/components/toast/toast-queue.js +85 -27
- package/dist/components/toast/toast-root.js +67 -14
- package/dist/components/toast/toast.constants.d.ts +17 -1
- package/dist/components/toast/toast.constants.js +18 -2
- package/dist/components/toast/toast.context.d.ts +6 -0
- package/dist/components/toast/toast.types.d.ts +19 -0
- package/dist/composables/use-list-keyboard.d.ts +1 -1
- package/dist/composables/use-list-keyboard.js +4 -5
- package/dist/composables/use-measured-height.d.ts +18 -10
- package/dist/composables/use-measured-height.js +53 -15
- package/dist/composables/use-overlay-position.js +7 -4
- package/dist/composables/use-select-state.d.ts +8 -0
- package/dist/composables/use-select-state.js +5 -0
- package/dist/composables/use-select.d.ts +7 -0
- package/dist/composables/use-select.js +6 -0
- package/dist/composables/use-textarea-autosize.d.ts +55 -0
- package/dist/composables/use-textarea-autosize.js +189 -0
- package/dist/composables/use-toast-region.d.ts +18 -4
- package/dist/composables/use-toast-region.js +128 -6
- package/dist/composables/use-toast.d.ts +1 -1
- package/dist/composables/use-toast.js +4 -3
- package/dist/composables/use-typeahead.js +3 -0
- package/dist/index.js +5 -4
- package/dist/ropav.min.css +1 -1
- package/dist/utils/focus.d.ts +13 -0
- package/dist/utils/focus.js +18 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -8,7 +8,7 @@ import toast_indicator_default from "./toast-indicator.js";
|
|
|
8
8
|
import toast_renderable_default from "./toast-renderable.js";
|
|
9
9
|
import toast_root_default from "./toast-root.js";
|
|
10
10
|
import toast_title_default from "./toast-title.js";
|
|
11
|
-
import { computed, createComponent, createIf, defineVaporComponent, extend, unref } from "vue";
|
|
11
|
+
import { computed, createComponent, createIf, defineVaporComponent, extend, shallowRef, unref, watch } from "vue";
|
|
12
12
|
//#region src/components/toast/toast-default-content.vue?vue&type=script&setup=true&vapor=true&lang.ts
|
|
13
13
|
var toast_default_content_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PURE__*/ defineVaporComponent({
|
|
14
14
|
__name: "toast-default-content",
|
|
@@ -21,6 +21,19 @@ var toast_default_content_vue_vue_type_script_setup_true_vapor_true_lang_default
|
|
|
21
21
|
* because there is no room for a row.
|
|
22
22
|
*/
|
|
23
23
|
const isMobile = useMediaQuery("(max-width: 768px)");
|
|
24
|
+
/**
|
|
25
|
+
* Which of the three indicators is showing. Tracked here rather than inside the indicator because
|
|
26
|
+
* the three are separate branches: settling a promise toast unmounts the spinner's instance and
|
|
27
|
+
* mounts a fresh one, which would have no memory of what it replaced.
|
|
28
|
+
*/
|
|
29
|
+
const indicatorKind = computed(() => {
|
|
30
|
+
if (content.value.isLoading === true) return "loading";
|
|
31
|
+
return content.value.indicator === void 0 ? "default" : "custom";
|
|
32
|
+
});
|
|
33
|
+
const hasSwappedIndicator = shallowRef(false);
|
|
34
|
+
watch(indicatorKind, () => {
|
|
35
|
+
hasSwappedIndicator.value = true;
|
|
36
|
+
});
|
|
24
37
|
const action = computed(() => {
|
|
25
38
|
const value = content.value.actionProps;
|
|
26
39
|
return value?.label == null ? void 0 : value;
|
|
@@ -38,18 +51,27 @@ var toast_default_content_vue_vue_type_script_setup_true_vapor_true_lang_default
|
|
|
38
51
|
return [
|
|
39
52
|
createIf(() => content.value.indicator !== null, () => {
|
|
40
53
|
return createIf(() => content.value.isLoading === true, () => {
|
|
41
|
-
return createComponent(toast_indicator_default, {
|
|
54
|
+
return createComponent(toast_indicator_default, {
|
|
55
|
+
"is-swapped": () => hasSwappedIndicator.value,
|
|
56
|
+
variant: () => content.value.variant
|
|
57
|
+
}, () => {
|
|
42
58
|
return createComponent(spinner_root_default, {
|
|
43
59
|
color: "current",
|
|
44
60
|
size: "sm"
|
|
45
61
|
});
|
|
46
62
|
});
|
|
47
63
|
}, () => createIf(() => content.value.indicator !== void 0, () => {
|
|
48
|
-
return createComponent(toast_indicator_default, {
|
|
64
|
+
return createComponent(toast_indicator_default, {
|
|
65
|
+
"is-swapped": () => hasSwappedIndicator.value,
|
|
66
|
+
variant: () => content.value.variant
|
|
67
|
+
}, () => {
|
|
49
68
|
return createComponent(toast_renderable_default, { value: () => content.value.indicator });
|
|
50
69
|
});
|
|
51
70
|
}, () => {
|
|
52
|
-
return createComponent(toast_indicator_default, {
|
|
71
|
+
return createComponent(toast_indicator_default, {
|
|
72
|
+
"is-swapped": () => hasSwappedIndicator.value,
|
|
73
|
+
variant: () => content.value.variant
|
|
74
|
+
});
|
|
53
75
|
}, 517), 261);
|
|
54
76
|
}),
|
|
55
77
|
createComponent(toast_content_default, null, extend(() => {
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import { dataAttr } from "../../utils/assertion.js";
|
|
1
2
|
import danger_icon_default from "../icons/danger-icon.js";
|
|
2
3
|
import info_icon_default from "../icons/info-icon.js";
|
|
3
4
|
import success_icon_default from "../icons/success-icon.js";
|
|
4
5
|
import warning_icon_default from "../icons/warning-icon.js";
|
|
5
6
|
import { useToastItemContext } from "./toast.context.js";
|
|
6
|
-
import { computed, createDynamicComponent, createSlot, defineVaporComponent, renderEffect, setClass, setInsertionState, template } from "vue";
|
|
7
|
+
import { computed, createDynamicComponent, createSlot, defineVaporComponent, renderEffect, setAttr, setClass, setInsertionState, template, unref } from "vue";
|
|
7
8
|
//#region src/components/toast/toast-indicator.vue?vue&type=script&setup=true&vapor=true&lang.ts
|
|
8
9
|
var t0 = template("<div data-slot=toast-indicator>", 1);
|
|
9
10
|
var toast_indicator_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PURE__*/ defineVaporComponent({
|
|
10
11
|
__name: "toast-indicator",
|
|
11
12
|
props: {
|
|
12
13
|
class: {},
|
|
14
|
+
isSwapped: { type: Boolean },
|
|
13
15
|
variant: {}
|
|
14
16
|
},
|
|
15
17
|
setup(__props) {
|
|
@@ -25,7 +27,11 @@ var toast_indicator_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@
|
|
|
25
27
|
}
|
|
26
28
|
});
|
|
27
29
|
const n3 = t0();
|
|
28
|
-
renderEffect(() =>
|
|
30
|
+
renderEffect(() => {
|
|
31
|
+
const _props = props;
|
|
32
|
+
setClass(n3, ["rp-toast__indicator", _props.class]);
|
|
33
|
+
setAttr(n3, "data-swapped", unref(dataAttr)(_props.isSwapped));
|
|
34
|
+
});
|
|
29
35
|
setInsertionState(n3);
|
|
30
36
|
createSlot("default", null, () => {
|
|
31
37
|
return createDynamicComponent(() => defaultIcon.value, { "data-slot": "toast-default-icon" });
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { dataAttr } from "../../utils/assertion.js";
|
|
1
2
|
import { composeSlotClassName } from "../../utils/compose.js";
|
|
2
3
|
import { useLocale } from "../../composables/use-locale.js";
|
|
3
4
|
import { provideToastRegionContext } from "./toast.context.js";
|
|
5
|
+
import { DEFAULT_SCALE_FACTOR, EXIT_FALLBACK } from "./toast.constants.js";
|
|
4
6
|
import { useToastRegion } from "../../composables/use-toast-region.js";
|
|
5
7
|
import toast_default_content_default from "./toast-default-content.js";
|
|
6
|
-
import { DEFAULT_SCALE_FACTOR } from "./toast.constants.js";
|
|
7
8
|
import { toastQueue, useToastQueue } from "./toast-queue.js";
|
|
8
|
-
import { VaporTeleport, child, computed, createComponent, createFor, createIf, createSlot, defineVaporComponent, on, renderEffect, setDynamicProps, setInsertionState, setStyle, setTemplateRefBinding, shallowRef, template, unref } from "vue";
|
|
9
|
+
import { VaporTeleport, child, computed, createComponent, createFor, createIf, createSlot, defineVaporComponent, on, onScopeDispose, renderEffect, setDynamicProps, setInsertionState, setStyle, setTemplateRefBinding, shallowRef, template, unref, watch } from "vue";
|
|
9
10
|
import { toastVariants } from "@ropav/styles";
|
|
10
11
|
//#region src/components/toast/toast-provider.vue?vue&type=script&setup=true&vapor=true&lang.ts
|
|
11
12
|
var t0 = template("<li>");
|
|
@@ -16,6 +17,8 @@ var toast_provider_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@_
|
|
|
16
17
|
ariaLabel: {},
|
|
17
18
|
class: {},
|
|
18
19
|
gap: { default: 12 },
|
|
20
|
+
hotkey: {},
|
|
21
|
+
isExpanded: { type: Boolean },
|
|
19
22
|
maxVisibleToasts: {},
|
|
20
23
|
placement: { default: "bottom" },
|
|
21
24
|
portalContainer: {},
|
|
@@ -27,6 +30,43 @@ var toast_provider_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@_
|
|
|
27
30
|
const props = __props;
|
|
28
31
|
const queue = computed(() => props.queue ?? toastQueue);
|
|
29
32
|
const { visibleToasts } = useToastQueue(queue);
|
|
33
|
+
/**
|
|
34
|
+
* Toasts the queue has let go of that are still on screen playing their exit.
|
|
35
|
+
*
|
|
36
|
+
* Held here rather than in the queue because it is a rendering concern: the queue's job ends when
|
|
37
|
+
* a toast is closed, and a caller reading `visibleToasts` should not be told about one that is
|
|
38
|
+
* only still there to finish animating.
|
|
39
|
+
*/
|
|
40
|
+
const exiting = shallowRef([]);
|
|
41
|
+
/**
|
|
42
|
+
* Timeouts that let a held toast go even if it never reports its exit.
|
|
43
|
+
*
|
|
44
|
+
* A backstop rather than the length of the animation, which the toast itself waits out: the
|
|
45
|
+
* provider's slot is the caller's to fill, and a slot that renders its own markup instead of a
|
|
46
|
+
* `ToastRoot` has nothing that would ever report. Without this, every closed toast would stay in
|
|
47
|
+
* the list for the life of the page.
|
|
48
|
+
*/
|
|
49
|
+
const exitFallbacks = /* @__PURE__ */ new Map();
|
|
50
|
+
const release = (key) => {
|
|
51
|
+
clearTimeout(exitFallbacks.get(key));
|
|
52
|
+
exitFallbacks.delete(key);
|
|
53
|
+
exiting.value = exiting.value.filter((entry) => entry.key !== key);
|
|
54
|
+
};
|
|
55
|
+
watch(visibleToasts, (next, previous) => {
|
|
56
|
+
if (!previous) return;
|
|
57
|
+
const live = new Set(next.map((entry) => entry.key));
|
|
58
|
+
const gone = previous.filter((entry) => !live.has(entry.key));
|
|
59
|
+
if (gone.length === 0) return;
|
|
60
|
+
exiting.value = [...exiting.value, ...gone];
|
|
61
|
+
for (const entry of gone) exitFallbacks.set(entry.key, setTimeout(() => release(entry.key), EXIT_FALLBACK));
|
|
62
|
+
});
|
|
63
|
+
onScopeDispose(() => {
|
|
64
|
+
for (const timeout of exitFallbacks.values()) clearTimeout(timeout);
|
|
65
|
+
exitFallbacks.clear();
|
|
66
|
+
});
|
|
67
|
+
const exitingKeys = computed(() => new Set(exiting.value.map((entry) => entry.key)));
|
|
68
|
+
/** Exiting toasts sit after the live ones; each one carries the slot it had through its own style. */
|
|
69
|
+
const renderedToasts = computed(() => [...visibleToasts.value, ...exiting.value]);
|
|
30
70
|
const element = shallowRef(null);
|
|
31
71
|
const setElement = (next) => {
|
|
32
72
|
element.value = next ?? null;
|
|
@@ -34,6 +74,8 @@ var toast_provider_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@_
|
|
|
34
74
|
const region = useToastRegion({
|
|
35
75
|
ariaLabel: () => props.ariaLabel,
|
|
36
76
|
elementRef: element,
|
|
77
|
+
hotkey: () => props.hotkey,
|
|
78
|
+
isExpanded: () => props.isExpanded,
|
|
37
79
|
onPauseAll: () => queue.value.pauseAll(),
|
|
38
80
|
onResumeAll: () => queue.value.resumeAll(),
|
|
39
81
|
visibleToasts
|
|
@@ -49,9 +91,12 @@ var toast_provider_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@_
|
|
|
49
91
|
const heights = shallowRef({});
|
|
50
92
|
provideToastRegionContext({
|
|
51
93
|
close: (key) => queue.value.close(key),
|
|
94
|
+
exitingKeys,
|
|
52
95
|
gap: computed(() => props.gap),
|
|
53
96
|
heightsByKey: computed(() => heights.value),
|
|
97
|
+
isExpanded: region.isExpanded,
|
|
54
98
|
maxVisibleToasts: computed(() => props.maxVisibleToasts ?? queue.value.maxVisibleToasts ?? 3),
|
|
99
|
+
onExitFinished: release,
|
|
55
100
|
onToastHeightChange: (key, height) => {
|
|
56
101
|
if (heights.value[key] === height) return;
|
|
57
102
|
heights.value = {
|
|
@@ -82,21 +127,23 @@ var toast_provider_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@_
|
|
|
82
127
|
* semantics without taking part in the layout the stylesheet builds out of absolute positioning.
|
|
83
128
|
*/
|
|
84
129
|
const CONTENTS_STYLE = { display: "contents" };
|
|
85
|
-
return createIf(() =>
|
|
130
|
+
return createIf(() => renderedToasts.value.length > 0, () => {
|
|
86
131
|
return createComponent(VaporTeleport, { to: () => target.value }, () => {
|
|
87
132
|
const n9 = t1();
|
|
88
133
|
const n8 = child(n9);
|
|
89
134
|
renderEffect(() => {
|
|
135
|
+
const _region = unref(region);
|
|
90
136
|
setDynamicProps(n9, [{
|
|
91
137
|
class: styles.value,
|
|
138
|
+
"data-expanded": unref(dataAttr)(_region.isExpanded.value),
|
|
92
139
|
"data-slot": "toast-region",
|
|
93
140
|
dir: unref(locale).direction,
|
|
94
141
|
style: regionStyle.value
|
|
95
|
-
},
|
|
142
|
+
}, _region.regionAttrs.value]);
|
|
96
143
|
setStyle(n8, CONTENTS_STYLE);
|
|
97
144
|
});
|
|
98
145
|
setInsertionState(n8);
|
|
99
|
-
createFor(() =>
|
|
146
|
+
createFor(() => renderedToasts.value, (_for_item0) => {
|
|
100
147
|
const n7 = t0();
|
|
101
148
|
renderEffect(() => setStyle(n7, CONTENTS_STYLE));
|
|
102
149
|
setInsertionState(n7);
|
|
@@ -112,6 +159,7 @@ var toast_provider_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@_
|
|
|
112
159
|
on(n9, "focusout", (e) => unref(region).onFocusout(e));
|
|
113
160
|
on(n9, "pointerenter", (e) => unref(region).onPointerenter(e));
|
|
114
161
|
on(n9, "pointerleave", (e) => unref(region).onPointerleave(e));
|
|
162
|
+
on(n9, "pointermove", (e) => unref(region).onPointermove(e));
|
|
115
163
|
setTemplateRefBinding(n9, () => setElement);
|
|
116
164
|
return n9;
|
|
117
165
|
}, true);
|
|
@@ -12,6 +12,7 @@ export declare class Timer {
|
|
|
12
12
|
private startTime;
|
|
13
13
|
private timerId;
|
|
14
14
|
constructor(callback: () => void, delay: number);
|
|
15
|
+
/** Starts the clock again on `delay`, whether or not it was already running. */
|
|
15
16
|
reset(delay: number): void;
|
|
16
17
|
pause(): void;
|
|
17
18
|
resume(): void;
|
|
@@ -20,7 +21,7 @@ export declare class Timer {
|
|
|
20
21
|
* The order toasts are shown in, and the clocks that close them.
|
|
21
22
|
*
|
|
22
23
|
* Ported from `react-stately`'s `ToastQueue`, merged with the wrapper that normally sits around
|
|
23
|
-
* it.
|
|
24
|
+
* it. There those are two classes because the wrapper had a primitive to wrap; writing both
|
|
24
25
|
* here means keeping them apart would be two paths doing one job. What the merge keeps from the
|
|
25
26
|
* wrapper: the default timeout, the serialized view transition, and `maxVisibleToasts` as a hint
|
|
26
27
|
* the region reads rather than a truncation.
|
|
@@ -37,10 +38,22 @@ export declare class ToastQueue<T = ToastContentValue> {
|
|
|
37
38
|
/** Every toast currently held, newest first. */
|
|
38
39
|
visibleToasts: QueuedToast<T>[];
|
|
39
40
|
constructor(options?: ToastQueueOptions);
|
|
40
|
-
/** Forgets any transitions still queued. For a test, since a chain outlives a mount. */
|
|
41
|
-
readonly resetTransitions: () => void;
|
|
42
41
|
/** Adds a toast and returns its key. */
|
|
43
42
|
add(content: T, options?: ToastOptions): string;
|
|
43
|
+
/**
|
|
44
|
+
* Replaces a toast's content where it stands, keeping its key and its place in the stack.
|
|
45
|
+
* Returns `false` for a key the queue no longer holds.
|
|
46
|
+
*
|
|
47
|
+
* Options the caller leaves out are kept rather than resolved: a toast asked to stay until
|
|
48
|
+
* something closes it must not pick up the default life because its message changed. That is
|
|
49
|
+
* why this reads the keys that are *present* instead of their values — an explicit `undefined`
|
|
50
|
+
* is a caller clearing a handler, and an absent key is a caller not mentioning one.
|
|
51
|
+
*
|
|
52
|
+
* The entry is replaced rather than mutated so the toast re-renders. The list is shallow, so a
|
|
53
|
+
* toast whose fields changed underneath it is the same object as far as anything watching is
|
|
54
|
+
* concerned; a new one keyed the same patches the node it already has.
|
|
55
|
+
*/
|
|
56
|
+
update(key: string, content: T, options?: ToastOptions): boolean;
|
|
44
57
|
/** Closes one toast. Notifies even for a key that is no longer held. */
|
|
45
58
|
close(key: string): void;
|
|
46
59
|
/** Drops every toast at once. Deliberately does not run their `onClose` — nothing closed them. */
|
|
@@ -51,8 +64,42 @@ export declare class ToastQueue<T = ToastContentValue> {
|
|
|
51
64
|
resumeAll(): void;
|
|
52
65
|
/** Subscribes to changes in the visible toasts. Returns the unsubscribe. */
|
|
53
66
|
subscribe(fn: () => void): () => void;
|
|
54
|
-
private
|
|
67
|
+
private notify;
|
|
55
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* A queue's own chain of view transitions, so its updates animate one after another. Hand the
|
|
71
|
+
* `wrapUpdate` it returns to a queue to animate that queue's toasts this way.
|
|
72
|
+
*
|
|
73
|
+
* Not what a queue does by default, and the reason is measured rather than assumed: a chain is
|
|
74
|
+
* serial, so under a burst each toast waits for every transition queued ahead of it — the fifth
|
|
75
|
+
* toast of five added 80ms apart took 1.5s to appear, against 0.37s when the stack animates
|
|
76
|
+
* itself. A transition also replaces the toast with a snapshot that answers no hit test, so the
|
|
77
|
+
* close and action buttons of the frontmost toast are unreachable for as long as it runs. Neither
|
|
78
|
+
* shows up as dropped frames; both are plainly visible to someone using the thing.
|
|
79
|
+
*
|
|
80
|
+
* It stays because it is the only way to get a genuine cross-fade between two different toasts,
|
|
81
|
+
* and because a caller who wants it should not have to rebuild the chain below. Each toast names
|
|
82
|
+
* itself for the transition through `view-transition-name`, which is what makes the capture
|
|
83
|
+
* per-toast; without it the document root would be the only captured element and every toast
|
|
84
|
+
* would cross-fade the whole page.
|
|
85
|
+
*
|
|
86
|
+
* The View Transitions API allows one active transition per document: starting a second while the
|
|
87
|
+
* first is still animating aborts the first, which surfaces as a rejection on `ready`. Each new
|
|
88
|
+
* transition is appended to the *end* of the chain rather than attached to whichever is currently
|
|
89
|
+
* active, so three mutations inside one microtask stay in order — which is what `toast.promise`
|
|
90
|
+
* does while the loading toast's own entry is still in flight.
|
|
91
|
+
*
|
|
92
|
+
* **One chain per queue, not one per document.** Sharing a single chain across every queue looks
|
|
93
|
+
* more faithful to an API that only runs one transition at a time, and it is wrong: with several
|
|
94
|
+
* regions on a page, a burst of toasts in one of them puts every other region's toast behind the
|
|
95
|
+
* whole burst, so a toast added elsewhere does not appear until seconds later. Independent chains
|
|
96
|
+
* let a second region interrupt instead — the superseded transition is skipped, which the catch
|
|
97
|
+
* below already handles, which is why a chain belongs to one queue and is built per caller.
|
|
98
|
+
*/
|
|
99
|
+
export declare const createViewTransitionUpdate: () => {
|
|
100
|
+
reset: () => void;
|
|
101
|
+
wrapUpdate: (fn: () => void) => void;
|
|
102
|
+
};
|
|
56
103
|
export interface UseToastQueueReturn<T = ToastContentValue> {
|
|
57
104
|
/** The toasts the queue currently holds, newest first. */
|
|
58
105
|
visibleToasts: ComputedRef<QueuedToast<T>[]>;
|
|
@@ -80,6 +127,14 @@ export interface ToastFunction {
|
|
|
80
127
|
promise: <T>(promise: (() => Promise<T>) | Promise<T>, options: ToastPromiseOptions<T>) => string;
|
|
81
128
|
resumeAll: () => void;
|
|
82
129
|
success: (message: ToastRenderable, options?: Omit<ToastAddOptions, "variant">) => string;
|
|
130
|
+
/**
|
|
131
|
+
* Replaces a toast's message where it stands, and adds one if that toast has already gone.
|
|
132
|
+
* Returns the key either way.
|
|
133
|
+
*
|
|
134
|
+
* `timeout` and `onClose` are kept when they are not mentioned; everything else describes the
|
|
135
|
+
* new content and is replaced.
|
|
136
|
+
*/
|
|
137
|
+
update: (key: string, message: ToastRenderable, options?: ToastAddOptions) => string;
|
|
83
138
|
warning: (message: ToastRenderable, options?: Omit<ToastAddOptions, "variant">) => string;
|
|
84
139
|
}
|
|
85
140
|
export declare const createToastFunction: (queue: ToastQueue<ToastContentValue>) => ToastFunction;
|
|
@@ -93,5 +148,5 @@ export declare const toastQueue: ToastQueue<ToastContentValue>;
|
|
|
93
148
|
export declare const toast: ToastFunction;
|
|
94
149
|
/** How many toasts the default queue holds. For a test asserting it did not leak into the next. */
|
|
95
150
|
export declare const getQueuedToastCount: () => number;
|
|
96
|
-
/** Empties the default queue
|
|
151
|
+
/** Empties the default queue. For a test, since the queue outlives a mount. */
|
|
97
152
|
export declare const resetToastQueue: () => void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "./toast.constants.js";
|
|
1
|
+
import { DEFAULT_TOAST_TIMEOUT } from "./toast.constants.js";
|
|
2
2
|
import { computed, nextTick, onScopeDispose, shallowRef, toValue, watch } from "vue";
|
|
3
3
|
//#region src/components/toast/toast-queue.ts
|
|
4
4
|
/**
|
|
@@ -16,7 +16,9 @@ var Timer = class {
|
|
|
16
16
|
this.callback = callback;
|
|
17
17
|
this.remaining = delay;
|
|
18
18
|
}
|
|
19
|
+
/** Starts the clock again on `delay`, whether or not it was already running. */
|
|
19
20
|
reset(delay) {
|
|
21
|
+
this.pause();
|
|
20
22
|
this.remaining = delay;
|
|
21
23
|
this.resume();
|
|
22
24
|
}
|
|
@@ -27,7 +29,7 @@ var Timer = class {
|
|
|
27
29
|
this.remaining -= Date.now() - this.startTime;
|
|
28
30
|
}
|
|
29
31
|
resume() {
|
|
30
|
-
if (this.remaining <= 0) return;
|
|
32
|
+
if (this.timerId != null || this.remaining <= 0) return;
|
|
31
33
|
this.startTime = Date.now();
|
|
32
34
|
this.timerId = setTimeout(() => {
|
|
33
35
|
this.timerId = null;
|
|
@@ -40,7 +42,7 @@ var Timer = class {
|
|
|
40
42
|
* The order toasts are shown in, and the clocks that close them.
|
|
41
43
|
*
|
|
42
44
|
* Ported from `react-stately`'s `ToastQueue`, merged with the wrapper that normally sits around
|
|
43
|
-
* it.
|
|
45
|
+
* it. There those are two classes because the wrapper had a primitive to wrap; writing both
|
|
44
46
|
* here means keeping them apart would be two paths doing one job. What the merge keeps from the
|
|
45
47
|
* wrapper: the default timeout, the serialized view transition, and `maxVisibleToasts` as a hint
|
|
46
48
|
* the region reads rather than a truncation.
|
|
@@ -58,12 +60,8 @@ var ToastQueue = class {
|
|
|
58
60
|
visibleToasts = [];
|
|
59
61
|
constructor(options = {}) {
|
|
60
62
|
this.maxVisibleToasts = options.maxVisibleToasts;
|
|
61
|
-
|
|
62
|
-
this.resetTransitions = transitions.reset;
|
|
63
|
-
this.wrapUpdate = options.wrapUpdate ?? transitions.wrapUpdate;
|
|
63
|
+
this.wrapUpdate = options.wrapUpdate ?? ((fn) => fn());
|
|
64
64
|
}
|
|
65
|
-
/** Forgets any transitions still queued. For a test, since a chain outlives a mount. */
|
|
66
|
-
resetTransitions;
|
|
67
65
|
/** Adds a toast and returns its key. */
|
|
68
66
|
add(content, options = {}) {
|
|
69
67
|
const key = `_${Math.random().toString(36).slice(2)}`;
|
|
@@ -75,9 +73,40 @@ var ToastQueue = class {
|
|
|
75
73
|
timeout,
|
|
76
74
|
timer: timeout ? new Timer(() => this.close(key), timeout) : void 0
|
|
77
75
|
});
|
|
78
|
-
this.
|
|
76
|
+
this.notify("add");
|
|
79
77
|
return key;
|
|
80
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Replaces a toast's content where it stands, keeping its key and its place in the stack.
|
|
81
|
+
* Returns `false` for a key the queue no longer holds.
|
|
82
|
+
*
|
|
83
|
+
* Options the caller leaves out are kept rather than resolved: a toast asked to stay until
|
|
84
|
+
* something closes it must not pick up the default life because its message changed. That is
|
|
85
|
+
* why this reads the keys that are *present* instead of their values — an explicit `undefined`
|
|
86
|
+
* is a caller clearing a handler, and an absent key is a caller not mentioning one.
|
|
87
|
+
*
|
|
88
|
+
* The entry is replaced rather than mutated so the toast re-renders. The list is shallow, so a
|
|
89
|
+
* toast whose fields changed underneath it is the same object as far as anything watching is
|
|
90
|
+
* concerned; a new one keyed the same patches the node it already has.
|
|
91
|
+
*/
|
|
92
|
+
update(key, content, options = {}) {
|
|
93
|
+
const index = this.queue.findIndex((toast) => toast.key === key);
|
|
94
|
+
if (index < 0) return false;
|
|
95
|
+
const previous = this.queue[index];
|
|
96
|
+
const next = {
|
|
97
|
+
...previous,
|
|
98
|
+
content
|
|
99
|
+
};
|
|
100
|
+
if ("onClose" in options) next.onClose = options.onClose;
|
|
101
|
+
if ("timeout" in options) {
|
|
102
|
+
previous.timer?.pause();
|
|
103
|
+
next.timeout = options.timeout;
|
|
104
|
+
next.timer = options.timeout ? new Timer(() => this.close(key), options.timeout) : void 0;
|
|
105
|
+
}
|
|
106
|
+
this.queue[index] = next;
|
|
107
|
+
this.notify("add");
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
81
110
|
/** Closes one toast. Notifies even for a key that is no longer held. */
|
|
82
111
|
close(key) {
|
|
83
112
|
const index = this.queue.findIndex((toast) => toast.key === key);
|
|
@@ -85,12 +114,12 @@ var ToastQueue = class {
|
|
|
85
114
|
this.queue[index].onClose?.();
|
|
86
115
|
this.queue.splice(index, 1);
|
|
87
116
|
}
|
|
88
|
-
this.
|
|
117
|
+
this.notify("remove");
|
|
89
118
|
}
|
|
90
119
|
/** Drops every toast at once. Deliberately does not run their `onClose` — nothing closed them. */
|
|
91
120
|
clear() {
|
|
92
121
|
this.queue = [];
|
|
93
|
-
this.
|
|
122
|
+
this.notify("clear");
|
|
94
123
|
}
|
|
95
124
|
/** Stops the clocks of the toasts on screen. */
|
|
96
125
|
pauseAll() {
|
|
@@ -107,7 +136,7 @@ var ToastQueue = class {
|
|
|
107
136
|
this.subscriptions.delete(fn);
|
|
108
137
|
};
|
|
109
138
|
}
|
|
110
|
-
|
|
139
|
+
notify(action) {
|
|
111
140
|
this.visibleToasts = [...this.queue];
|
|
112
141
|
this.wrapUpdate(() => {
|
|
113
142
|
for (const fn of [...this.subscriptions]) fn();
|
|
@@ -115,7 +144,21 @@ var ToastQueue = class {
|
|
|
115
144
|
}
|
|
116
145
|
};
|
|
117
146
|
/**
|
|
118
|
-
* A queue's own chain of view transitions, so its updates animate one after another.
|
|
147
|
+
* A queue's own chain of view transitions, so its updates animate one after another. Hand the
|
|
148
|
+
* `wrapUpdate` it returns to a queue to animate that queue's toasts this way.
|
|
149
|
+
*
|
|
150
|
+
* Not what a queue does by default, and the reason is measured rather than assumed: a chain is
|
|
151
|
+
* serial, so under a burst each toast waits for every transition queued ahead of it — the fifth
|
|
152
|
+
* toast of five added 80ms apart took 1.5s to appear, against 0.37s when the stack animates
|
|
153
|
+
* itself. A transition also replaces the toast with a snapshot that answers no hit test, so the
|
|
154
|
+
* close and action buttons of the frontmost toast are unreachable for as long as it runs. Neither
|
|
155
|
+
* shows up as dropped frames; both are plainly visible to someone using the thing.
|
|
156
|
+
*
|
|
157
|
+
* It stays because it is the only way to get a genuine cross-fade between two different toasts,
|
|
158
|
+
* and because a caller who wants it should not have to rebuild the chain below. Each toast names
|
|
159
|
+
* itself for the transition through `view-transition-name`, which is what makes the capture
|
|
160
|
+
* per-toast; without it the document root would be the only captured element and every toast
|
|
161
|
+
* would cross-fade the whole page.
|
|
119
162
|
*
|
|
120
163
|
* The View Transitions API allows one active transition per document: starting a second while the
|
|
121
164
|
* first is still animating aborts the first, which surfaces as a rejection on `ready`. Each new
|
|
@@ -128,7 +171,7 @@ var ToastQueue = class {
|
|
|
128
171
|
* regions on a page, a burst of toasts in one of them puts every other region's toast behind the
|
|
129
172
|
* whole burst, so a toast added elsewhere does not appear until seconds later. Independent chains
|
|
130
173
|
* let a second region interrupt instead — the superseded transition is skipped, which the catch
|
|
131
|
-
* below already handles, which is why
|
|
174
|
+
* below already handles, which is why a chain belongs to one queue and is built per caller.
|
|
132
175
|
*/
|
|
133
176
|
var createViewTransitionUpdate = () => {
|
|
134
177
|
let chain = Promise.resolve();
|
|
@@ -185,17 +228,19 @@ var useToastQueue = (queue) => {
|
|
|
185
228
|
*/
|
|
186
229
|
var resolve = (value, input) => typeof value === "function" ? value(input) : value;
|
|
187
230
|
var createToastFunction = (queue) => {
|
|
188
|
-
const
|
|
231
|
+
const contentOf = (message, options) => ({
|
|
189
232
|
actionProps: options.actionProps,
|
|
190
233
|
description: options.description,
|
|
191
234
|
indicator: options.indicator,
|
|
192
235
|
isLoading: options.isLoading,
|
|
193
236
|
title: message,
|
|
194
237
|
variant: options.variant ?? "default"
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
238
|
+
});
|
|
239
|
+
const deferred = (onClose) => onClose ? () => {
|
|
240
|
+
requestAnimationFrame(() => onClose());
|
|
241
|
+
} : void 0;
|
|
242
|
+
const add = (message, options = {}) => queue.add(contentOf(message, options), {
|
|
243
|
+
onClose: deferred(options.onClose),
|
|
199
244
|
timeout: options.timeout
|
|
200
245
|
});
|
|
201
246
|
const withVariant = (variant) => (message, options = {}) => add(message, {
|
|
@@ -212,6 +257,14 @@ var createToastFunction = (queue) => {
|
|
|
212
257
|
toastFn.resumeAll = () => queue.resumeAll();
|
|
213
258
|
toastFn.success = withVariant("success");
|
|
214
259
|
toastFn.warning = withVariant("warning");
|
|
260
|
+
toastFn.update = (key, message, options = {}) => {
|
|
261
|
+
const inherited = {
|
|
262
|
+
..."onClose" in options ? { onClose: deferred(options.onClose) } : null,
|
|
263
|
+
..."timeout" in options ? { timeout: options.timeout } : null
|
|
264
|
+
};
|
|
265
|
+
if (queue.update(key, contentOf(message, options), inherited)) return key;
|
|
266
|
+
return add(message, options);
|
|
267
|
+
};
|
|
215
268
|
toastFn.promise = (promise, options) => {
|
|
216
269
|
const pending = typeof promise === "function" ? promise() : promise;
|
|
217
270
|
const loadingKey = queue.add({
|
|
@@ -220,11 +273,17 @@ var createToastFunction = (queue) => {
|
|
|
220
273
|
variant: "default"
|
|
221
274
|
}, { timeout: 0 });
|
|
222
275
|
pending.then((data) => {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
276
|
+
toastFn.update(loadingKey, resolve(options.success, data), {
|
|
277
|
+
isLoading: false,
|
|
278
|
+
timeout: DEFAULT_TOAST_TIMEOUT,
|
|
279
|
+
variant: "success"
|
|
280
|
+
});
|
|
281
|
+
}).catch((error) => {
|
|
282
|
+
toastFn.update(loadingKey, resolve(options.error, error), {
|
|
283
|
+
isLoading: false,
|
|
284
|
+
timeout: DEFAULT_TOAST_TIMEOUT,
|
|
285
|
+
variant: "danger"
|
|
286
|
+
});
|
|
228
287
|
});
|
|
229
288
|
return loadingKey;
|
|
230
289
|
};
|
|
@@ -240,10 +299,9 @@ var toastQueue = new ToastQueue();
|
|
|
240
299
|
var toast = createToastFunction(toastQueue);
|
|
241
300
|
/** How many toasts the default queue holds. For a test asserting it did not leak into the next. */
|
|
242
301
|
var getQueuedToastCount = () => toastQueue.visibleToasts.length;
|
|
243
|
-
/** Empties the default queue
|
|
302
|
+
/** Empties the default queue. For a test, since the queue outlives a mount. */
|
|
244
303
|
var resetToastQueue = () => {
|
|
245
304
|
toastQueue.clear();
|
|
246
|
-
toastQueue.resetTransitions();
|
|
247
305
|
};
|
|
248
306
|
//#endregion
|
|
249
|
-
export { Timer, ToastQueue, createToastFunction, getQueuedToastCount, resetToastQueue, toast, toastQueue, useToastQueue };
|
|
307
|
+
export { Timer, ToastQueue, createToastFunction, createViewTransitionUpdate, getQueuedToastCount, resetToastQueue, toast, toastQueue, useToastQueue };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { dataAttr } from "../../utils/assertion.js";
|
|
2
2
|
import { provideSurfaceContext } from "../surface/surface.context.js";
|
|
3
|
+
import { createAnimationSettleWaiter } from "../../utils/animation-settled.js";
|
|
3
4
|
import { provideToastItemContext, useToastRegionContext } from "./toast.context.js";
|
|
4
5
|
import { useMeasuredHeight } from "../../composables/use-measured-height.js";
|
|
5
6
|
import { useToast } from "../../composables/use-toast.js";
|
|
6
|
-
import { computed, createSlot, defineVaporComponent, onScopeDispose, renderEffect, setDynamicProps, setInsertionState, setTemplateRefBinding, shallowRef, template, unref, watch } from "vue";
|
|
7
|
+
import { computed, createSlot, defineVaporComponent, onMounted, onScopeDispose, renderEffect, setDynamicProps, setInsertionState, setTemplateRefBinding, shallowRef, template, unref, watch } from "vue";
|
|
7
8
|
//#region src/components/toast/toast-root.vue?vue&type=script&setup=true&vapor=true&lang.ts
|
|
8
9
|
var t0 = template("<div>", 1);
|
|
9
10
|
var toast_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PURE__*/ defineVaporComponent({
|
|
@@ -24,10 +25,53 @@ var toast_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PUR
|
|
|
24
25
|
};
|
|
25
26
|
const placement = computed(() => props.placement ?? region.placement.value);
|
|
26
27
|
const scaleFactor = computed(() => props.scaleFactor ?? region.scaleFactor.value);
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
const isExiting = computed(() => region.exitingKeys.value.has(props.toast.key));
|
|
29
|
+
/**
|
|
30
|
+
* The place this toast holds in the stack. Index 0 is the newest, which is the one in front.
|
|
31
|
+
*
|
|
32
|
+
* A toast on its way out keeps the slot it had while the ones behind it renumber and close up,
|
|
33
|
+
* so the stack reflows around it instead of waiting for it to finish.
|
|
34
|
+
*/
|
|
35
|
+
const lastIndex = shallowRef(0);
|
|
36
|
+
const index = computed(() => {
|
|
37
|
+
if (isExiting.value) return lastIndex.value;
|
|
38
|
+
return Math.max(region.visibleToasts.value.findIndex((entry) => entry.key === props.toast.key), 0);
|
|
39
|
+
});
|
|
40
|
+
watch(index, (next) => {
|
|
41
|
+
if (!isExiting.value) lastIndex.value = next;
|
|
42
|
+
}, { immediate: true });
|
|
29
43
|
const isFrontmost = computed(() => index.value <= 0);
|
|
30
|
-
const isHidden = computed(() => index.value >= region.maxVisibleToasts.value);
|
|
44
|
+
const isHidden = computed(() => !isExiting.value && index.value >= region.maxVisibleToasts.value);
|
|
45
|
+
/**
|
|
46
|
+
* Rendered from the very first frame rather than set once mounted.
|
|
47
|
+
*
|
|
48
|
+
* A transition needs a style to leave, so the offset has to be the element's *first* resolved
|
|
49
|
+
* style. Written after the element already exists, the browser transitions towards the offset and
|
|
50
|
+
* the flip below then drags it back — which reads as a toast that twitches instead of arriving.
|
|
51
|
+
*/
|
|
52
|
+
const isEntering = shallowRef(true);
|
|
53
|
+
onMounted(() => {
|
|
54
|
+
let inner = 0;
|
|
55
|
+
const outer = requestAnimationFrame(() => {
|
|
56
|
+
inner = requestAnimationFrame(() => {
|
|
57
|
+
isEntering.value = false;
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
onScopeDispose(() => {
|
|
61
|
+
cancelAnimationFrame(outer);
|
|
62
|
+
cancelAnimationFrame(inner);
|
|
63
|
+
}, true);
|
|
64
|
+
});
|
|
65
|
+
/**
|
|
66
|
+
* The exit is waited for rather than timed, so its length lives in the stylesheet — the one place
|
|
67
|
+
* that knows it — and no number here can fall out of step with it.
|
|
68
|
+
*/
|
|
69
|
+
const exit = createAnimationSettleWaiter(() => element.value);
|
|
70
|
+
watch(isExiting, (exiting) => {
|
|
71
|
+
if (!exiting) return;
|
|
72
|
+
exit.whenSettled(() => region.onExitFinished(props.toast.key));
|
|
73
|
+
});
|
|
74
|
+
onScopeDispose(exit.cancel, true);
|
|
31
75
|
const { height } = useMeasuredHeight(element);
|
|
32
76
|
/**
|
|
33
77
|
* Every toast reports its height up, because a toast that is *not* frontmost is clipped to the
|
|
@@ -64,32 +108,41 @@ var toast_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__PUR
|
|
|
64
108
|
}));
|
|
65
109
|
const style = computed(() => {
|
|
66
110
|
const visible = region.visibleToasts.value;
|
|
111
|
+
const heights = region.heightsByKey.value;
|
|
67
112
|
const frontKey = visible[0]?.key;
|
|
68
|
-
const frontHeight = (frontKey === void 0 ? void 0 :
|
|
69
|
-
|
|
70
|
-
|
|
113
|
+
const frontHeight = (frontKey === void 0 ? void 0 : heights[frontKey]) ?? height.value ?? 0;
|
|
114
|
+
let heightsBefore = 0;
|
|
115
|
+
for (let position = 0; position < index.value; position += 1) {
|
|
116
|
+
const key = visible[position]?.key;
|
|
117
|
+
heightsBefore += (key === void 0 ? void 0 : heights[key]) ?? frontHeight;
|
|
118
|
+
}
|
|
119
|
+
const gap = index.value * region.gap.value;
|
|
71
120
|
return {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
121
|
+
...frontHeight ? { "--front-height": `${frontHeight}px` } : null,
|
|
122
|
+
"--offset-collapsed": `${gap}px`,
|
|
123
|
+
"--offset-expanded": `${heightsBefore + gap}px`,
|
|
124
|
+
"--scale-collapsed": `${1 - index.value * scaleFactor.value}`,
|
|
76
125
|
viewTransitionName: `rp-toast-${props.toast.key.replace(/[^a-zA-Z0-9]/g, "-")}`,
|
|
77
|
-
zIndex: visible.length - index.value,
|
|
78
|
-
...
|
|
126
|
+
zIndex: isExiting.value ? 0 : visible.length - index.value,
|
|
127
|
+
...height.value === void 0 ? null : { "--toast-height": `${height.value}px` }
|
|
79
128
|
};
|
|
80
129
|
});
|
|
81
130
|
const n1 = t0();
|
|
82
131
|
renderEffect(() => {
|
|
83
132
|
const _dataAttr = unref(dataAttr);
|
|
133
|
+
const _isExiting = isExiting.value;
|
|
84
134
|
const _isFrontmost = isFrontmost.value;
|
|
85
135
|
setDynamicProps(n1, [{
|
|
86
136
|
class: styles.value,
|
|
137
|
+
"data-entering": _dataAttr(isEntering.value),
|
|
138
|
+
"data-exiting": _dataAttr(_isExiting),
|
|
139
|
+
"data-expanded": _dataAttr(unref(region).isExpanded.value),
|
|
87
140
|
"data-frontmost": _dataAttr(_isFrontmost),
|
|
88
141
|
"data-hidden": _dataAttr(isHidden.value),
|
|
89
142
|
"data-index": index.value,
|
|
90
143
|
"data-slot": "toast",
|
|
91
144
|
style: style.value,
|
|
92
|
-
tabindex: _isFrontmost ? 0 : -1
|
|
145
|
+
tabindex: _isFrontmost && !_isExiting ? 0 : -1
|
|
93
146
|
}, unref(api).toastAttrs.value]);
|
|
94
147
|
});
|
|
95
148
|
setInsertionState(n1);
|