Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions apps/docs/src/examples/toast.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -114,3 +115,47 @@ export function MultipleRegionsExample() {
</div>
);
}

function ToastTimeExampleToastBody() {
const { remainingTime, remainingFraction, elapsedTime, elapsedFraction } =
Toast.useToastTime();
const format = (time: Accessor<number>, fraction: Accessor<number>) => (
<>
{(time() / 1000).toFixed(1)}s ({Math.round(fraction() * 100)}%)
</>
);
return (
<>
<div class={style.toast__content}>
<div>
<Toast.Title class={style.toast__title}>
Remaining time: {format(remainingTime, remainingFraction)}
</Toast.Title>
<Toast.Description class={style.toast__description}>
Elapsed time: {format(elapsedTime, elapsedFraction)}
</Toast.Description>
</div>
<Toast.CloseButton class={style["toast__close-button"]}>
<CrossIcon />
</Toast.CloseButton>
</div>
<progress value={remainingFraction()} max={1} />
</>
);
}

export function ToastTimeExample() {
const showToast = () => {
toaster.show((props) => (
<Toast.Root toastId={props.toastId} class={style.toast}>
<ToastTimeExampleToastBody />
</Toast.Root>
));
};

return (
<button type="button" class="kb-button-primary" onClick={() => showToast()}>
Show toast
</button>
);
}
59 changes: 58 additions & 1 deletion apps/docs/src/routes/docs/core/components/toast.mdx
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -554,6 +554,52 @@ Inside your application, use add your custom region:
<MultipleRegionsExample />
</Preview>

### 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<number>, fraction: Accessor<number>) => (
<>
{(time() / 1000).toFixed(1)}s ({Math.round(fraction() * 100)}%)
</>
);
return (
<>
<div class="toast__content">
<div>
<Toast.Title class="toast__title">
Remaining time: {format(remainingTime, remainingFraction)}
</Toast.Title>
<Toast.Description class="toast__description">
Elapsed time: {format(elapsedTime, elapsedFraction)}
</Toast.Description>
</div>
<Toast.CloseButton class="toast__close-button">
<CrossIcon />
</Toast.CloseButton>
</div>
<progress value={remainingFraction()} max={1} />
</>
);
}

toaster.show(props => (
<Toast.Root toastId={props.toastId}>
<ToastBody />
</Toast.Root>
));
```

<Preview isRounded>
<ToastTimeExample />
</Preview>

## API reference

### toaster
Expand Down Expand Up @@ -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<number>` <br /> The time this toast has been open in milliseconds. <br /> Range: `0` to `duration` of the toast, increasing over time. |
| elapsedFraction | `Accessor<number>` <br /> The fraction of this toast's duration it has been open. <br /> Range: `0` to `1`, increasing over time. |
| remainingTime | `Accessor<number>` <br /> The time this toast will been open before it is closed in milliseconds. <br /> Range: `0` to `duration` of the toast, decreasing over time. |
| remainingFraction | `Accessor<number>` <br /> The fraction of this toast's duration it will been open before it is closed. <br /> Range: `0` to `1`, decreasing over time. |

## Rendered elements

| Component | Default rendered element |
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/toast/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
type ToastPromiseState,
type ToastSwipeDirection,
} from "./types";
import { type ToastTime, useToastTime } from "./use-toast-time";

export type {
ToastCloseButtonOptions,
Expand All @@ -66,6 +67,7 @@ export type {
ToastRootOptions,
ToastRootProps,
ToastSwipeDirection,
ToastTime,
ToastTitleOptions,
ToastTitleProps,
};
Expand All @@ -79,4 +81,5 @@ export {
Region,
Root,
Title,
useToastTime,
};
37 changes: 4 additions & 33 deletions packages/core/src/toast/toast-progress-fill.tsx
Original file line number Diff line number Diff line change
@@ -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). */
Expand All @@ -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 (
<Polymorphic
Expand Down
84 changes: 84 additions & 0 deletions packages/core/src/toast/use-toast-time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { clamp } from "@kobalte/utils";
import { Accessor, createEffect, createSignal, onCleanup } from "solid-js";
import { useToastContext } from "./toast-context";
import { useToastRegionContext } from "./toast-region-context";

/**
* Times and fractions regarding a toast's elapsed and remaining time open.
*/
export interface ToastTime {
/**
* The time this toast has been open in milliseconds, not accounting for paused time.
* Range: `0` to `duration` of the toast, increasing over time.
*/
elapsedTime: Accessor<number>;
/**
* 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<number>;
/**
* 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<number>;
/**
* 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<number>;
}

/**
* 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,
};
}