diff --git a/apps/docs/src/examples/toast.tsx b/apps/docs/src/examples/toast.tsx index 21082ba4..6fd7c1e9 100644 --- a/apps/docs/src/examples/toast.tsx +++ b/apps/docs/src/examples/toast.tsx @@ -1,4 +1,5 @@ import { Toast, toaster } from "@kobalte/core"; +import { Accessor } from "solid-js"; import { CrossIcon } from "../components"; import style from "./toast.module.css"; @@ -114,3 +115,47 @@ export function MultipleRegionsExample() { ); } + +function ToastTimeExampleToastBody() { + const { remainingTime, remainingFraction, elapsedTime, elapsedFraction } = + Toast.useToastTime(); + const format = (time: Accessor, fraction: Accessor) => ( + <> + {(time() / 1000).toFixed(1)}s ({Math.round(fraction() * 100)}%) + + ); + return ( + <> +
+
+ + Remaining time: {format(remainingTime, remainingFraction)} + + + Elapsed time: {format(elapsedTime, elapsedFraction)} + +
+ + + +
+ + + ); +} + +export function ToastTimeExample() { + const showToast = () => { + toaster.show((props) => ( + + + + )); + }; + + return ( + + ); +} diff --git a/apps/docs/src/routes/docs/core/components/toast.mdx b/apps/docs/src/routes/docs/core/components/toast.mdx index db350f57..f609257a 100644 --- a/apps/docs/src/routes/docs/core/components/toast.mdx +++ b/apps/docs/src/routes/docs/core/components/toast.mdx @@ -1,5 +1,5 @@ import { Preview, TabsSnippets, Callout, Kbd } from "../../../../components"; -import { BasicExample, MultipleRegionsExample } from "../../../../examples/toast"; +import { BasicExample, MultipleRegionsExample, ToastTimeExample } from "../../../../examples/toast"; # Toast @@ -554,6 +554,52 @@ Inside your application, use add your custom region: +### The `useToastTime` primitive + +`Toast.useToastTime` allows components to access the remaining time usually displayed by `Toast.ProgressFillTrack`. +It should be used within `Toast.Root`, e. g. in a component that replaces `Toast.ProgressTrack` and `Toast.ProgressFill`. + +```tsx +import { Toast, toaster } from "@kobalte/core"; + +function ToastBody() { + const { remainingTime, remainingFraction, elapsedTime, elapsedFraction } = Toast.useToastTime(); + const format = (time: Accessor, fraction: Accessor) => ( + <> + {(time() / 1000).toFixed(1)}s ({Math.round(fraction() * 100)}%) + + ); + return ( + <> +
+
+ + Remaining time: {format(remainingTime, remainingFraction)} + + + Elapsed time: {format(elapsedTime, elapsedFraction)} + +
+ + + +
+ + + ); +} + +toaster.show(props => ( + + + +)); +``` + + + + + ## API reference ### toaster @@ -613,6 +659,17 @@ Inside your application, use add your custom region: | --kb-toast-swipe-end-x | The offset end position of the toast after horizontally swiping. | | --kb-toast-swipe-end-y | The offset end position of the toast after vertically swiping. | +### Toast.useToastTime + +The `Toast.useToastTime` primitive returns the following properties. All of the properties ignore paused time. + +| Name | Description | +| :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| elapsedTime | `Accessor`
The time this toast has been open in milliseconds.
Range: `0` to `duration` of the toast, increasing over time. | +| elapsedFraction | `Accessor`
The fraction of this toast's duration it has been open.
Range: `0` to `1`, increasing over time. | +| remainingTime | `Accessor`
The time this toast will been open before it is closed in milliseconds.
Range: `0` to `duration` of the toast, decreasing over time. | +| remainingFraction | `Accessor`
The fraction of this toast's duration it will been open before it is closed.
Range: `0` to `1`, decreasing over time. | + ## Rendered elements | Component | Default rendered element | diff --git a/packages/core/src/toast/index.tsx b/packages/core/src/toast/index.tsx index b2df9d16..1b9ceb6a 100644 --- a/packages/core/src/toast/index.tsx +++ b/packages/core/src/toast/index.tsx @@ -45,6 +45,7 @@ import { type ToastPromiseState, type ToastSwipeDirection, } from "./types"; +import { type ToastTime, useToastTime } from "./use-toast-time"; export type { ToastCloseButtonOptions, @@ -66,6 +67,7 @@ export type { ToastRootOptions, ToastRootProps, ToastSwipeDirection, + ToastTime, ToastTitleOptions, ToastTitleProps, }; @@ -79,4 +81,5 @@ export { Region, Root, Title, + useToastTime, }; diff --git a/packages/core/src/toast/toast-progress-fill.tsx b/packages/core/src/toast/toast-progress-fill.tsx index d87ca03d..eb0a2b93 100644 --- a/packages/core/src/toast/toast-progress-fill.tsx +++ b/packages/core/src/toast/toast-progress-fill.tsx @@ -1,15 +1,8 @@ import { OverrideComponentProps } from "@kobalte/utils"; -import { - JSX, - createEffect, - createSignal, - onCleanup, - splitProps, -} from "solid-js"; +import { JSX, splitProps } from "solid-js"; import { AsChildProp, Polymorphic } from "../polymorphic"; -import { useToastContext } from "./toast-context"; -import { useToastRegionContext } from "./toast-region-context"; +import { useToastTime } from "./use-toast-time"; export interface ToastProgressFillOptions extends AsChildProp { /** The HTML styles attribute (object form only). */ @@ -24,32 +17,10 @@ export interface ToastProgressFillProps * Used to visually show the fill of `Toast.ProgressTrack`. */ export function ToastProgressFill(props: ToastProgressFillProps) { - const rootContext = useToastRegionContext(); - const context = useToastContext(); - const [local, others] = splitProps(props, ["style"]); - const [lifeTime, setLifeTime] = createSignal(100); - let totalElapsedTime = 0; - - createEffect(() => { - if (rootContext.isPaused() || context.isPersistent()) { - return; - } - - const intervalId = setInterval(() => { - const elapsedTime = - new Date().getTime() - context.closeTimerStartTime() + totalElapsedTime; - - const life = Math.trunc(100 - (elapsedTime / context.duration()) * 100); - setLifeTime(life < 0 ? 0 : life); - }); - - onCleanup(() => { - totalElapsedTime += new Date().getTime() - context.closeTimerStartTime(); - clearInterval(intervalId); - }); - }); + const { remainingFraction } = useToastTime(); + const lifeTime = () => Math.trunc(remainingFraction() * 100); return ( ; + /** + * The fraction of this toast's duration it has been open, not accounting for paused time. + * Range: `0` to `1`, increasing over time. + */ + elapsedFraction: Accessor; + /** + * The time this toast will been open before it is closed in milliseconds, not accounting for paused time. + * Range: `0` to `duration` of the toast, decreasing over time. + */ + remainingTime: Accessor; + /** + * The fraction of this toast's duration it will been open before it is closed, not accounting for paused time. + * Range: `0` to `1`, decreasing over time. + */ + remainingFraction: Accessor; +} + +/** + * Compute times and fractions regarding this toast's elapsed and remaining time open. + * + * @example + * + * ```js + * const { + * elapsedTime, + * elapsedFraction, + * remainingTime, + * remainingFraction, + * } = useToastTime(); + * ``` + */ +export function useToastTime(): ToastTime { + const rootContext = useToastRegionContext(); + const context = useToastContext(); + + const [elapsedTime, setElapsedTime] = createSignal(0); + const remainingTime = () => { + const duration = context.duration(); + return clamp(duration - elapsedTime(), 0, duration); + }; + const elapsedFraction = () => clamp(elapsedTime() / context.duration(), 0, 1); + const remainingFraction = () => 1 - elapsedFraction(); + + const timeSinceStart = () => + new Date().getTime() - context.closeTimerStartTime(); + + let totalElapsedTime = 0; + + createEffect(() => { + if (rootContext.isPaused() || context.isPersistent()) { + return; + } + + const intervalId = setInterval(() => { + setElapsedTime(timeSinceStart() + totalElapsedTime); + }); + + onCleanup(() => { + totalElapsedTime += timeSinceStart(); + clearInterval(intervalId); + }); + }); + + return { + elapsedTime, + elapsedFraction, + remainingTime, + remainingFraction, + }; +}