ropav 0.11.1 → 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.
Files changed (54) hide show
  1. package/dist/components/autocomplete/autocomplete-clear-button.js +1 -1
  2. package/dist/components/autocomplete/autocomplete-root.js +3 -0
  3. package/dist/components/checkbox/checkbox-root.js +4 -1
  4. package/dist/components/radio/radio-root.js +4 -1
  5. package/dist/components/scroll-shadow/scroll-shadow-root.js +2 -10
  6. package/dist/components/scroll-shadow/use-scroll-shadow.d.ts +10 -0
  7. package/dist/components/scroll-shadow/use-scroll-shadow.js +34 -2
  8. package/dist/components/select/index.d.ts +3 -2
  9. package/dist/components/select/index.js +2 -1
  10. package/dist/components/select/select-clear-button.d.ts +21 -0
  11. package/dist/components/select/select-clear-button.js +66 -0
  12. package/dist/components/select/select-root.d.ts +2 -1
  13. package/dist/components/select/select-root.js +15 -1
  14. package/dist/components/select/select.context.d.ts +11 -0
  15. package/dist/components/select/select.types.d.ts +3 -0
  16. package/dist/components/switch/switch-root.js +4 -1
  17. package/dist/components/table/table-row.js +3 -2
  18. package/dist/components/table/use-grid-keyboard.js +2 -3
  19. package/dist/components/table/use-table-collection.d.ts +0 -11
  20. package/dist/components/table/use-table-collection.js +1 -17
  21. package/dist/components/tabs/tabs-root.js +5 -1
  22. package/dist/components/tabs/tabs.types.d.ts +6 -0
  23. package/dist/components/toast/index.d.ts +1 -1
  24. package/dist/components/toast/index.js +3 -3
  25. package/dist/components/toast/toast-default-content.js +26 -4
  26. package/dist/components/toast/toast-indicator.js +8 -2
  27. package/dist/components/toast/toast-provider.js +53 -5
  28. package/dist/components/toast/toast-queue.d.ts +60 -5
  29. package/dist/components/toast/toast-queue.js +85 -27
  30. package/dist/components/toast/toast-root.js +67 -14
  31. package/dist/components/toast/toast.constants.d.ts +17 -1
  32. package/dist/components/toast/toast.constants.js +18 -2
  33. package/dist/components/toast/toast.context.d.ts +6 -0
  34. package/dist/components/toast/toast.types.d.ts +19 -0
  35. package/dist/composables/use-list-keyboard.d.ts +1 -1
  36. package/dist/composables/use-list-keyboard.js +4 -5
  37. package/dist/composables/use-measured-height.d.ts +18 -10
  38. package/dist/composables/use-measured-height.js +53 -15
  39. package/dist/composables/use-overlay-position.js +7 -4
  40. package/dist/composables/use-select-state.d.ts +8 -0
  41. package/dist/composables/use-select-state.js +5 -0
  42. package/dist/composables/use-select.d.ts +7 -0
  43. package/dist/composables/use-select.js +6 -0
  44. package/dist/composables/use-toast-region.d.ts +18 -4
  45. package/dist/composables/use-toast-region.js +128 -6
  46. package/dist/composables/use-toast.d.ts +1 -1
  47. package/dist/composables/use-toast.js +4 -3
  48. package/dist/composables/use-typeahead.js +3 -0
  49. package/dist/index.js +5 -4
  50. package/dist/ropav.min.css +1 -1
  51. package/dist/utils/focus.d.ts +13 -0
  52. package/dist/utils/focus.js +18 -1
  53. package/dist/version.js +1 -1
  54. package/package.json +3 -3
@@ -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(() => unref(visibleToasts).length > 0, () => {
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
- }, unref(region).regionAttrs.value]);
142
+ }, _region.regionAttrs.value]);
96
143
  setStyle(n8, CONTENTS_STYLE);
97
144
  });
98
145
  setInsertionState(n8);
99
- createFor(() => unref(visibleToasts), (_for_item0) => {
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. Upstream those are two classes because the wrapper had a primitive to wrap; writing both
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 update;
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 and the transition chain. For a test, since both outlive a mount. */
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. Upstream those are two classes because the wrapper had a primitive to wrap; writing both
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
- const transitions = createViewTransitionUpdate();
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.update("add");
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.update("remove");
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.update("clear");
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
- update(action) {
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 the chain is scoped per queue inside the constructor.
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 add = (message, options = {}) => queue.add({
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
- onClose: options.onClose ? () => {
197
- requestAnimationFrame(() => options.onClose?.());
198
- } : void 0,
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
- queue.close(loadingKey);
224
- toastFn.success(resolve(options.success, data));
225
- }, (error) => {
226
- queue.close(loadingKey);
227
- toastFn.danger(resolve(options.error, error));
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 and the transition chain. For a test, since both outlive a mount. */
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
- /** Index 0 is the newest toast, which is the one in front of the stack. */
28
- const index = computed(() => region.visibleToasts.value.findIndex((entry) => entry.key === props.toast.key));
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 : region.heightsByKey.value[frontKey]) ?? height.value ?? 0;
69
- const offset = index.value * region.gap.value;
70
- const direction = placement.value?.startsWith("bottom") ? -1 : 1;
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
- opacity: isHidden.value ? 0 : 1,
73
- pointerEvents: isHidden.value ? "none" : "auto",
74
- scale: `${1 - index.value * scaleFactor.value}`,
75
- translate: `0 ${direction * offset}px 0`,
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
- ...frontHeight ? { "--front-height": `${frontHeight}px` } : null
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);
@@ -12,5 +12,21 @@ export declare const DEFAULT_MAX_VISIBLE_TOAST = 3;
12
12
  export declare const DEFAULT_SCALE_FACTOR = 0.05;
13
13
  /** Pixels wide, written out as `--toast-width` for the stylesheet to read. */
14
14
  export declare const DEFAULT_TOAST_WIDTH = 460;
15
- /** Milliseconds a toast lives for. Matches sonner's default. */
15
+ /** Milliseconds a toast lives for. */
16
16
  export declare const DEFAULT_TOAST_TIMEOUT = 4000;
17
+ /**
18
+ * Milliseconds after which a closed toast is dropped whether or not it reported its exit.
19
+ *
20
+ * Not the length of the exit, which the toast waits out against its own animations. This is the
21
+ * ceiling for a toast that can never report one — a slot the caller filled with something other
22
+ * than a `ToastRoot` — so a list of them cannot grow without end.
23
+ */
24
+ export declare const EXIT_FALLBACK = 2000;
25
+ /**
26
+ * The key combination that moves focus to the toast region.
27
+ *
28
+ * An entry naming one of the `KeyboardEvent` modifier booleans matches that modifier; anything
29
+ * else matches `event.code`, so what counts is the key in that position rather than the character
30
+ * the layout produces there. An empty list turns the shortcut off.
31
+ */
32
+ export declare const DEFAULT_HOTKEY: readonly string[];
@@ -13,7 +13,23 @@ var DEFAULT_MAX_VISIBLE_TOAST = 3;
13
13
  var DEFAULT_SCALE_FACTOR = .05;
14
14
  /** Pixels wide, written out as `--toast-width` for the stylesheet to read. */
15
15
  var DEFAULT_TOAST_WIDTH = 460;
16
- /** Milliseconds a toast lives for. Matches sonner's default. */
16
+ /** Milliseconds a toast lives for. */
17
17
  var DEFAULT_TOAST_TIMEOUT = 4e3;
18
+ /**
19
+ * Milliseconds after which a closed toast is dropped whether or not it reported its exit.
20
+ *
21
+ * Not the length of the exit, which the toast waits out against its own animations. This is the
22
+ * ceiling for a toast that can never report one — a slot the caller filled with something other
23
+ * than a `ToastRoot` — so a list of them cannot grow without end.
24
+ */
25
+ var EXIT_FALLBACK = 2e3;
26
+ /**
27
+ * The key combination that moves focus to the toast region.
28
+ *
29
+ * An entry naming one of the `KeyboardEvent` modifier booleans matches that modifier; anything
30
+ * else matches `event.code`, so what counts is the key in that position rather than the character
31
+ * the layout produces there. An empty list turns the shortcut off.
32
+ */
33
+ var DEFAULT_HOTKEY = ["altKey", "KeyT"];
18
34
  //#endregion
19
- export { DEFAULT_GAP, DEFAULT_MAX_VISIBLE_TOAST, DEFAULT_SCALE_FACTOR, DEFAULT_TOAST_TIMEOUT, DEFAULT_TOAST_WIDTH };
35
+ export { DEFAULT_GAP, DEFAULT_HOTKEY, DEFAULT_MAX_VISIBLE_TOAST, DEFAULT_SCALE_FACTOR, DEFAULT_TOAST_TIMEOUT, DEFAULT_TOAST_WIDTH, EXIT_FALLBACK };
@@ -12,10 +12,16 @@ import type { ComputedRef } from "vue";
12
12
  export interface ToastRegionContext {
13
13
  /** Closes one toast by key. */
14
14
  close: (key: string) => void;
15
+ /** Keys still mounted for their exit animation, which have left the queue already. */
16
+ exitingKeys: ComputedRef<ReadonlySet<string>>;
15
17
  gap: ComputedRef<number>;
16
18
  /** Every mounted toast's measured height, so a stacked toast can be clipped to the front one. */
17
19
  heightsByKey: ComputedRef<Record<string, number>>;
20
+ /** Whether the stack is opened out, so every toast shows at its own height. */
21
+ isExpanded: ComputedRef<boolean>;
18
22
  maxVisibleToasts: ComputedRef<number>;
23
+ /** Reported by a toast once its exit animation has finished, so it can be unmounted. */
24
+ onExitFinished: (key: string) => void;
19
25
  onToastHeightChange: (key: string, height: number) => void;
20
26
  onToastHeightRemove: (key: string) => void;
21
27
  placement: ComputedRef<ToastVariants["placement"]>;