Skip to content

Commit 98b8077

Browse files
committedAug 23, 2024·
feat: add snackbar component
1 parent 4e648c7 commit 98b8077

File tree

8 files changed

+275
-6
lines changed

8 files changed

+275
-6
lines changed
 

‎apps/demo/src/app/layout.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import "material-symbols";
2-
// import "@lit-labs/ssr-react/enable-lit-ssr.js";
3-
import GitHubButton from "../components/GitHubButton";
42

53
import type { Metadata } from "next";
64
import { Roboto } from "next/font/google";
75
import "./globals.css";
6+
import SnackbarClientProvider from "./providers";
87

98
// @TODO: Get static fonts to work somehow, to prevent FOUC
109
const roboto = Roboto({
@@ -38,6 +37,7 @@ export default function RootLayout({
3837
</head>
3938
<body>
4039
<main className="bg-[#FDF7FF] max-h-screen w-full">
40+
<SnackbarClientProvider />
4141
<div className="flex flex-col justify-center items-center md:grid md:grid-cols-[1fr_1fr] lg:grid-cols-[auto_1fr_1fr] md:h-screen">
4242
{children}
4343
</div>

‎apps/demo/src/app/page.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import Slider from "material-web-components-react/slider";
3232
import Switch from "material-web-components-react/switch";
3333
import Tabs, { PrimaryTab } from "material-web-components-react/tabs";
3434
import TextField from "material-web-components-react/text-field";
35+
import {snackbar} from "material-web-components-react/snackbar";
3536

3637
import Stack from "material-web-components-react/stack";
3738

@@ -558,8 +559,15 @@ export default function Home() {
558559
</ComponentDemo>
559560
<ComponentDemo title={"Ripple"}>
560561
<div className="w-[320px] h-[120px] px-10 py-8 flex flex-row gap-3 items-center justify-center">
561-
<div className="relative rounded-lg flex flex-row gap-10 justify-center items-center w-[200px] h-[100px]">
562-
Tap me for effect
562+
<div className="relative rounded-lg flex flex-row gap-10 justify-center items-center w-[200px] h-[100px]" onClick={() => {
563+
// @ts-expect-error
564+
const snackbarId = snackbar.show("Tapped on an element.", {
565+
actionText: "Close me",
566+
onAction: () => snackbar.dismiss(snackbarId),
567+
className: 'bg-[#313033] text-[#F4EFF4]'
568+
})
569+
}}>
570+
Tap me for ripple effect (also for a snackbar!)
563571
<Ripple></Ripple>
564572
</div>
565573
</div>

‎apps/demo/src/app/providers.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"use client";
2+
3+
import {SnackbarProvider} from "material-web-components-react/snackbar";
4+
5+
const SnackbarClientProvider = (props: any) => <SnackbarProvider {...props} />
6+
7+
export default SnackbarClientProvider

‎apps/demo/tailwind.config.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,29 @@ const config: Config = {
1414
pattern: /text-(xs|sm|md|lg|xl|2xl|3xl)/,
1515
},
1616
{
17-
pattern: /p(y|t|x|b)-(0|2|3)/,
17+
pattern: /rounded-(xs|sm|md|lg|xl|2xl|3xl)/,
18+
},
19+
{
20+
pattern: /p(y|t|x|b|l|r)-(.)/,
21+
},
22+
{
23+
pattern: /gap-(y|t|x|b)-(.)/,
1824
},
1925
{
2026
pattern: /flex-(.)/,
2127
},
28+
{
29+
pattern: /bg-(.)/,
30+
},
31+
{
32+
pattern: /bg-[#313033]/,
33+
},
34+
{
35+
pattern: /min-(w|h)-(.)/,
36+
},
37+
{
38+
pattern: /max-(w|h)-(.)/,
39+
},
2240
],
2341
theme: {
2442
extend: {

‎packages/ui/README.md

+18-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,24 @@ To use Material Web Components for React as a **library** in your project, run:
1717
npm install material-web-components-react
1818
```
1919

20-
## Documentation
20+
## Usage
21+
22+
Here's a general example of how the components can be used:
23+
24+
```tsx
25+
import React from 'react';
26+
import Button from 'material-web-components-react/button';
27+
28+
function Example() {
29+
return (
30+
<div>
31+
<Button>Click me</Button>
32+
</div>
33+
);
34+
}
35+
```
36+
37+
For a detailed reference on usage, you might want to check out the source code of the [NextJS demo](./apps/demo/src/app/page.tsx). It's simple!
2138

2239
Under the hood, this library simply uses the official [@material/web](https://github.com/material-components/material-web/) components. Visit [the official Material Web Components docs](https://github.com/material-components/material-web/blob/main/docs/intro.md) to learn how to use those components. The props remain the same!
2340

‎packages/ui/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
"autoprefixer": "^10.4.19",
5555
"lit": "^3.2.0",
5656
"react": "^18.2.0",
57+
"react-hot-toast": "^2.4.1",
58+
"react-swipeable": "^7.0.1",
5759
"tailwind-merge": "^2.4.0",
5860
"tailwindcss": "^3.4.1",
5961
"tslib": "^2.6.3",

‎packages/ui/src/snackbar/index.tsx

+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
"use client";
2+
import React, { ReactNode, useState } from "react";
3+
4+
import Button from "../button/index.js";
5+
import Icon from "../icon/index.js";
6+
import IconButton from "../icon-button/index.js";
7+
8+
import { ToastOptions, toast } from "react-hot-toast";
9+
import { useSwipeable } from "react-swipeable";
10+
import { twMerge } from "tailwind-merge";
11+
12+
import { Toaster, ToasterProps } from "react-hot-toast";
13+
14+
export type SnackbarProviderProps = ToasterProps
15+
export const SnackbarProvider = (props: SnackbarProviderProps) => {
16+
return (
17+
<>
18+
<Toaster {...props} />
19+
{props.children}
20+
</>
21+
)
22+
}
23+
24+
export type SnackbarProps = {
25+
icon?: React.ReactNode;
26+
variant?: string;
27+
children: React.ReactNode;
28+
showClose?: boolean;
29+
onAction?: () => void;
30+
onClose?: () => void;
31+
actionText?: string;
32+
};
33+
34+
export const Snackbar = ({
35+
icon,
36+
variant,
37+
children,
38+
showClose,
39+
onAction,
40+
onClose,
41+
actionText,
42+
}: SnackbarProps) => {
43+
const [translateX, setTranslateX] = useState(0);
44+
const [isSwiped, setIsSwiped] = useState(false);
45+
46+
// Handle swipe
47+
const handlers = useSwipeable({
48+
onSwiping: (eventData) => {
49+
setTranslateX(eventData.deltaX); // Update the translateX value dynamically
50+
if (Math.abs(eventData.deltaX) > 100) {
51+
setIsSwiped(true); // Swipe threshold for dismissing
52+
} else {
53+
setIsSwiped(false); // Swipe threshold for dismissing
54+
}
55+
},
56+
onSwiped: (eventData) => {
57+
if (Math.abs(eventData.deltaX) > 100) {
58+
setIsSwiped(true); // Swipe threshold for dismissing
59+
setTimeout(() => {
60+
onClose?.();
61+
}, 300); // Delay to allow animation to complete
62+
} else {
63+
setTranslateX(0); // Reset if swipe is not sufficient
64+
setIsSwiped(false); // Swipe threshold for dismissing
65+
}
66+
},
67+
preventScrollOnSwipe: true,
68+
trackTouch: true,
69+
trackMouse: false,
70+
});
71+
72+
const _showClose = showClose || !actionText;
73+
const _variant = variant
74+
? variant
75+
: (actionText?.length ?? 0) > 10 || actionText?.includes(" ")
76+
? "extended"
77+
: "standard";
78+
const useLongerAction = _variant === "extended";
79+
80+
return (
81+
<div
82+
{...handlers}
83+
style={{
84+
transform: `translateX(${translateX}px)`, // Dynamically apply the translation
85+
opacity: isSwiped ? 1 - Math.abs(translateX / (384 / 2)) : 1, // Fade out when dismissed
86+
transition:
87+
translateX === 0
88+
? "all 0.5s"
89+
: "transform 0.02s linear, opacity 0.1s linear",
90+
}}
91+
className={twMerge(
92+
"relative font-regular flex items-start gap-x-5 justify-evenly rounded-md bg-[#313033] py-4 pl-5 pr-3 text-[#F4EFF4] shadow-lg text-sm min-w-80 sm:min-w-96 max-w-96 w-screen transition-all",
93+
useLongerAction ? "flex-col" : "flex-row",
94+
isSwiped ? "opacity-0" : "opacity-100"
95+
)}
96+
>
97+
{icon && <span className="text-white">{icon}</span>}
98+
<span
99+
className={twMerge("flex flex-1 flex-wrap", useLongerAction && "pr-10")}
100+
>
101+
{children}
102+
</span>
103+
<div
104+
className={twMerge(
105+
useLongerAction ? "w-full flex-1" : "w-fit",
106+
"flex flex-row justify-end items-center gap-3"
107+
)}
108+
>
109+
{actionText && (
110+
<Button
111+
onClick={() => {
112+
onAction?.();
113+
onClose?.();
114+
}}
115+
variant="text"
116+
id="action"
117+
>
118+
{actionText}
119+
</Button>
120+
)}
121+
</div>
122+
{_showClose && (
123+
<IconButton
124+
className={twMerge(useLongerAction && "absolute top-2 right-2")}
125+
onClick={onClose}
126+
>
127+
<Icon>close</Icon>
128+
</IconButton>
129+
)}
130+
</div>
131+
);
132+
};
133+
134+
const showSnackbar = (
135+
message: string | ReactNode,
136+
options?: ToastOptions & Partial<SnackbarProps>
137+
) => {
138+
const _options = options;
139+
const { icon, style = {}, ...toastOptions } = _options || {};
140+
const toastId = toast(
141+
<Snackbar
142+
showClose={_options?.showClose}
143+
onClose={() => {
144+
toast.dismiss(toastId);
145+
}}
146+
{...toastOptions}
147+
>
148+
{message}
149+
</Snackbar>,
150+
{
151+
position: "bottom-center",
152+
style: {
153+
padding: "0rem 1rem",
154+
margin: 0,
155+
boxShadow: "none",
156+
background: "transparent",
157+
display: "flex",
158+
justifyContent: "center",
159+
alignItems: "center",
160+
...style,
161+
},
162+
icon,
163+
...toastOptions,
164+
}
165+
);
166+
167+
return toastId;
168+
};
169+
170+
const snackbar: typeof toast & {
171+
show: (
172+
message: string | ReactNode,
173+
options?: ToastOptions & Partial<SnackbarProps>
174+
) => void;
175+
} = toast;
176+
snackbar.show = showSnackbar;
177+
178+
export { snackbar };

‎pnpm-lock.yaml

+39
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)