Skip to content

Commit

Permalink
toasts through anchoring
Browse files Browse the repository at this point in the history
  • Loading branch information
LexSwed committed Jul 30, 2024
1 parent f6a1a95 commit 4d233de
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 50 deletions.
36 changes: 26 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"storybook-solidjs": "^1.0.0-beta.2",
"storybook-solidjs-vite": "^1.0.0-beta.2",
"tailwindcss": "^3.4.7",
"valibot": "^0.36.0",
"valibot": "^0.37.0",
"vite": "^5.3.5",
"vite-plugin-solid": "^2.10.2",
"vite-svg-sprite-wrapper": "^1.3.3"
Expand Down
35 changes: 21 additions & 14 deletions packages/ui/src/toast/toast.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,52 @@
.list {
view-transition-name: nou-toast-list;
transition: all 0.4s ease-in-out allow-discrete;
position: relative;
position: fixed;
min-width: theme(spacing.64);

&:is(:hover, :focus) {
& > .toast {
top: calc(anchor(bottom) + 1rem);
inset-block-start: anchor(end);
translate: 0;
scale: 1;
}
}
& > .toast:nth-child(n + 5) {
transition-delay: 400ms;
}
&:not(:hover, :focus, .expanded) > .toast:nth-child(n + 5) {
@apply animate-out hidden fill-mode-forwards fade-out translate-y-2;
}
}

.toast {
min-width: 0;
view-transition-class: nou-toast;
pointer-events: auto;
opacity: 1;
transition: 0.4s ease-out allow-discrete;
transition-property: inset-block, transform, display;
position: fixed;
transition:
all 0.2s ease-out allow-discrete,
translate 0.2s ease-out,
scale 0.2s ease-out;
position: absolute;
inset-block-start: anchor(start);
inset-inline-start: anchor(end);
/* justify-self: anchor-center; */
/* use padding instead of positioning to allow hover to remain stable for multiple toasts */
padding: theme("spacing[1]");

&:nth-of-type(1) {
z-index: 4;
justify-self: auto;
}
&:nth-of-type(2) {
z-index: 3;
transform: scale(0.95) translateY(1rem);
translate: 0 2rem;
scale: 0.9;
}
&:nth-of-type(3) {
z-index: 2;
transform: scale(0.9) translateY(2rem);
translate: 0 4rem;
scale: 0.85;
}
&:nth-of-type(4) {
z-index: 1;
transform: scale(0.85) translateY(3rem);
translate: 0 6rem;
scale: 0.8;
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/toast/toast.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const ToastExample = () => {
<>
<Button
onClick={async () => {
counter = counter * 8;
counter = counter + 1;
toast(() => <Toast class="max-w-80">#{counter} toast</Toast>);
}}
>
Expand Down
74 changes: 52 additions & 22 deletions packages/ui/src/toast/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createSingletonRoot } from "@solid-primitives/rootless";
import {
type Accessor,
type ComponentProps,
Index,
For,
type JSX,
type ResolvedChildren,
children,
Expand All @@ -18,6 +18,7 @@ import {
import { Card } from "../card";

import { tw } from "../tw";
import { composeEventHandlers } from "../utils";
import css from "./toast.module.css";
/**
* TODO:
Expand All @@ -39,6 +40,9 @@ const Toast = (ownProps: ToastProps) => {
tabIndex={0}
{...props}
class={tw("allow-discrete border border-on-background/5 shadow-popover", props.class)}
onClick={composeEventHandlers(props.onClick, (e) => {
(e.currentTarget as HTMLElement).dispatchEvent(new ToastDismissEvent());
})}
>
{props.children}
</Card>
Expand All @@ -47,6 +51,8 @@ const Toast = (ownProps: ToastProps) => {

interface ToastEntry {
id: string;
anchorName: string;
positionAnchor?: string;
element: Accessor<ResolvedChildren>;
}

Expand All @@ -57,14 +63,30 @@ const useToastsController = createSingletonRoot(() => {
items,
add: (element: Accessor<ResolvedChildren>) => {
const id = createUniqueId();
setItems((rendered) => [{ id, element }, ...rendered]);
setItems((rendered) => {
const newItem = { id, element, anchorName: `--nou-toast-anchor-${id}` };
const oldTopItem = rendered.at(0);
if (oldTopItem) {
const positionAnchor = `--nou-toast-anchor-${id}`;
// biome-ignore lint/style/noParameterAssign: ignore
rendered = rendered.with(0, {
...oldTopItem,
get positionAnchor() {
return positionAnchor;
},
});
}
return [newItem, ...rendered];
});
},
};
});
function Toaster(props: { label: string }) {
const toaster = useToastsController();
const [ref, setRef] = createSignal<HTMLElement | null>(null);
const hasToasts = createMemo(() => toaster.items().length > 0);
const id = createUniqueId();
const listAnchorName = `--nou-toast-anchor-${id}`;

createEffect(() => {
const root = ref();
Expand All @@ -81,40 +103,31 @@ function Toaster(props: { label: string }) {
// Popover is used to ensure that if notification is triggered from a dialog or any
// top layer element, the toast appears on top of it
popover="manual"
id="nou-ui-toaster"
class="pointer-events-none fixed top-12 mx-auto my-0 overflow-visible bg-transparent p-0"
role="region"
aria-label={props.label}
ref={setRef}
>
<ol
class={tw(
css.list,
"-m-4 pointer-events-auto relative items-center gap-2 p-4 empty:hidden",
)}
tabIndex={-1}
>
<Index each={toaster.items()}>
{(entry, i) => {
let positionAnchor: string | undefined = undefined;
console.log(i, `--nou-toast-anchor-${toaster.items().at(i - 1)?.id}`);
if (i > 0) {
positionAnchor = `--nou-toast-anchor-${toaster.items().at(i - 1)?.id}`;
}

<div style={{ "anchor-name": listAnchorName }} class="absolute">
<div>Hello</div>
</div>
<ol class={css.list} tabIndex={-1}>
<For each={toaster.items()}>
{(entry) => {
return (
<li
class={tw(css.toast)}
style={{
"anchor-name": `--nou-toast-anchor-${entry().id}`,
"position-anchor": positionAnchor,
"anchor-name": entry.anchorName,
"position-anchor": entry.positionAnchor ?? listAnchorName,
}}
// on:toast-dismiss={(e) => toaster.removeById(entry().id)}
>
{entry().element()}
{entry.element()}
</li>
);
}}
</Index>
</For>
</ol>
</div>
);
Expand All @@ -136,3 +149,20 @@ function useToaster() {
}

export { useToaster, Toast, Toaster };

class ToastDismissEvent extends Event {
constructor() {
super("toast-dismiss", { bubbles: true });
}
}

declare module "solid-js" {
namespace JSX {
interface CustomEvents {
"toast-dismiss": ToastDismissEvent;
}
interface CustomCaptureEvents {
"toast-dismiss": ToastDismissEvent;
}
}
}
4 changes: 2 additions & 2 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@
"solid-motionone": "^1.0.0",
"styled-qr-code": "^1.0.0",
"temporal-polyfill": "^0.2.5",
"valibot": "^0.36.0",
"valibot": "^0.37.0",
"vinxi": "0.4.1"
},
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@nou/config": "^1.0.0",
"@types/better-sqlite3": "^7.6.11",
"dotenv": "^16.4.5",
"drizzle-kit": "^0.23.0",
"drizzle-kit": "^0.23.1",
"tailwindcss": "^3.4.7",
"tsx": "^4.16.2",
"vite": "^5.3.5",
Expand Down

0 comments on commit 4d233de

Please sign in to comment.