Skip to content

Commit

Permalink
feat: add snackbar component
Browse files Browse the repository at this point in the history
  • Loading branch information
SaadBazaz committed Aug 23, 2024
1 parent 4e648c7 commit 98b8077
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 6 deletions.
4 changes: 2 additions & 2 deletions apps/demo/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import "material-symbols";
// import "@lit-labs/ssr-react/enable-lit-ssr.js";
import GitHubButton from "../components/GitHubButton";

import type { Metadata } from "next";
import { Roboto } from "next/font/google";
import "./globals.css";
import SnackbarClientProvider from "./providers";

// @TODO: Get static fonts to work somehow, to prevent FOUC
const roboto = Roboto({
Expand Down Expand Up @@ -38,6 +37,7 @@ export default function RootLayout({
</head>
<body>
<main className="bg-[#FDF7FF] max-h-screen w-full">
<SnackbarClientProvider />
<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">
{children}
</div>
Expand Down
12 changes: 10 additions & 2 deletions apps/demo/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import Slider from "material-web-components-react/slider";
import Switch from "material-web-components-react/switch";
import Tabs, { PrimaryTab } from "material-web-components-react/tabs";
import TextField from "material-web-components-react/text-field";
import {snackbar} from "material-web-components-react/snackbar";

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

Expand Down Expand Up @@ -558,8 +559,15 @@ export default function Home() {
</ComponentDemo>
<ComponentDemo title={"Ripple"}>
<div className="w-[320px] h-[120px] px-10 py-8 flex flex-row gap-3 items-center justify-center">
<div className="relative rounded-lg flex flex-row gap-10 justify-center items-center w-[200px] h-[100px]">
Tap me for effect
<div className="relative rounded-lg flex flex-row gap-10 justify-center items-center w-[200px] h-[100px]" onClick={() => {
// @ts-expect-error
const snackbarId = snackbar.show("Tapped on an element.", {
actionText: "Close me",
onAction: () => snackbar.dismiss(snackbarId),
className: 'bg-[#313033] text-[#F4EFF4]'
})
}}>
Tap me for ripple effect (also for a snackbar!)
<Ripple></Ripple>
</div>
</div>
Expand Down
7 changes: 7 additions & 0 deletions apps/demo/src/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client";

import {SnackbarProvider} from "material-web-components-react/snackbar";

const SnackbarClientProvider = (props: any) => <SnackbarProvider {...props} />

export default SnackbarClientProvider
20 changes: 19 additions & 1 deletion apps/demo/tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,29 @@ const config: Config = {
pattern: /text-(xs|sm|md|lg|xl|2xl|3xl)/,
},
{
pattern: /p(y|t|x|b)-(0|2|3)/,
pattern: /rounded-(xs|sm|md|lg|xl|2xl|3xl)/,
},
{
pattern: /p(y|t|x|b|l|r)-(.)/,
},
{
pattern: /gap-(y|t|x|b)-(.)/,
},
{
pattern: /flex-(.)/,
},
{
pattern: /bg-(.)/,
},
{
pattern: /bg-[#313033]/,
},
{
pattern: /min-(w|h)-(.)/,
},
{
pattern: /max-(w|h)-(.)/,
},
],
theme: {
extend: {
Expand Down
19 changes: 18 additions & 1 deletion packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,24 @@ To use Material Web Components for React as a **library** in your project, run:
npm install material-web-components-react
```

## Documentation
## Usage

Here's a general example of how the components can be used:

```tsx
import React from 'react';
import Button from 'material-web-components-react/button';

function Example() {
return (
<div>
<Button>Click me</Button>
</div>
);
}
```

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!

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!

Expand Down
2 changes: 2 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
"autoprefixer": "^10.4.19",
"lit": "^3.2.0",
"react": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-swipeable": "^7.0.1",
"tailwind-merge": "^2.4.0",
"tailwindcss": "^3.4.1",
"tslib": "^2.6.3",
Expand Down
178 changes: 178 additions & 0 deletions packages/ui/src/snackbar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"use client";
import React, { ReactNode, useState } from "react";

import Button from "../button/index.js";
import Icon from "../icon/index.js";
import IconButton from "../icon-button/index.js";

import { ToastOptions, toast } from "react-hot-toast";
import { useSwipeable } from "react-swipeable";
import { twMerge } from "tailwind-merge";

import { Toaster, ToasterProps } from "react-hot-toast";

export type SnackbarProviderProps = ToasterProps
export const SnackbarProvider = (props: SnackbarProviderProps) => {
return (
<>
<Toaster {...props} />
{props.children}
</>
)
}

export type SnackbarProps = {
icon?: React.ReactNode;
variant?: string;
children: React.ReactNode;
showClose?: boolean;
onAction?: () => void;
onClose?: () => void;
actionText?: string;
};

export const Snackbar = ({
icon,
variant,
children,
showClose,
onAction,
onClose,
actionText,
}: SnackbarProps) => {
const [translateX, setTranslateX] = useState(0);
const [isSwiped, setIsSwiped] = useState(false);

// Handle swipe
const handlers = useSwipeable({
onSwiping: (eventData) => {
setTranslateX(eventData.deltaX); // Update the translateX value dynamically
if (Math.abs(eventData.deltaX) > 100) {
setIsSwiped(true); // Swipe threshold for dismissing
} else {
setIsSwiped(false); // Swipe threshold for dismissing
}
},
onSwiped: (eventData) => {
if (Math.abs(eventData.deltaX) > 100) {
setIsSwiped(true); // Swipe threshold for dismissing
setTimeout(() => {
onClose?.();
}, 300); // Delay to allow animation to complete
} else {
setTranslateX(0); // Reset if swipe is not sufficient
setIsSwiped(false); // Swipe threshold for dismissing
}
},
preventScrollOnSwipe: true,
trackTouch: true,
trackMouse: false,
});

const _showClose = showClose || !actionText;
const _variant = variant
? variant
: (actionText?.length ?? 0) > 10 || actionText?.includes(" ")
? "extended"
: "standard";
const useLongerAction = _variant === "extended";

return (
<div
{...handlers}
style={{
transform: `translateX(${translateX}px)`, // Dynamically apply the translation
opacity: isSwiped ? 1 - Math.abs(translateX / (384 / 2)) : 1, // Fade out when dismissed
transition:
translateX === 0
? "all 0.5s"
: "transform 0.02s linear, opacity 0.1s linear",
}}
className={twMerge(
"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",
useLongerAction ? "flex-col" : "flex-row",
isSwiped ? "opacity-0" : "opacity-100"
)}
>
{icon && <span className="text-white">{icon}</span>}
<span
className={twMerge("flex flex-1 flex-wrap", useLongerAction && "pr-10")}
>
{children}
</span>
<div
className={twMerge(
useLongerAction ? "w-full flex-1" : "w-fit",
"flex flex-row justify-end items-center gap-3"
)}
>
{actionText && (
<Button
onClick={() => {
onAction?.();
onClose?.();
}}
variant="text"
id="action"
>
{actionText}
</Button>
)}
</div>
{_showClose && (
<IconButton
className={twMerge(useLongerAction && "absolute top-2 right-2")}
onClick={onClose}
>
<Icon>close</Icon>
</IconButton>
)}
</div>
);
};

const showSnackbar = (
message: string | ReactNode,
options?: ToastOptions & Partial<SnackbarProps>
) => {
const _options = options;
const { icon, style = {}, ...toastOptions } = _options || {};
const toastId = toast(
<Snackbar
showClose={_options?.showClose}
onClose={() => {
toast.dismiss(toastId);
}}
{...toastOptions}
>
{message}
</Snackbar>,
{
position: "bottom-center",
style: {
padding: "0rem 1rem",
margin: 0,
boxShadow: "none",
background: "transparent",
display: "flex",
justifyContent: "center",
alignItems: "center",
...style,
},
icon,
...toastOptions,
}
);

return toastId;
};

const snackbar: typeof toast & {
show: (
message: string | ReactNode,

Check warning on line 172 in packages/ui/src/snackbar/index.tsx

View workflow job for this annotation

GitHub Actions / Release

'message' is defined but never used
options?: ToastOptions & Partial<SnackbarProps>

Check warning on line 173 in packages/ui/src/snackbar/index.tsx

View workflow job for this annotation

GitHub Actions / Release

'options' is defined but never used
) => void;
} = toast;
snackbar.show = showSnackbar;

export { snackbar };
39 changes: 39 additions & 0 deletions pnpm-lock.yaml

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

0 comments on commit 98b8077

Please sign in to comment.