Skip to content

Commit 6056809

Browse files
committed
feat: add snackbar component
1 parent 9879648 commit 6056809

File tree

11 files changed

+216
-0
lines changed

11 files changed

+216
-0
lines changed

packages/components/_provisional/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ export * from "./scrollable-container";
1313
export * from "./position-engine";
1414

1515
export * from "./top-bar";
16+
17+
export * from "./snackbar";
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React, { useCallback, useMemo, useState } from "react";
2+
import { SnackbarContext, type SnackbarContextProps } from "./context";
3+
import { generateId } from "./utils";
4+
import { Layer } from "@react-ck/layers";
5+
import { ElementCreator, type Item } from "./types";
6+
import { SnackbarItem } from "./SnackbarItem";
7+
import styles from "./styles/index.module.scss";
8+
import classNames from "classnames";
9+
10+
export interface SnackbarProps extends React.HTMLAttributes<HTMLDivElement> {
11+
initialItems?: ElementCreator[];
12+
}
13+
14+
export const Snackbar = ({
15+
initialItems,
16+
className,
17+
children,
18+
...otherProps
19+
}: Readonly<SnackbarProps>): React.ReactElement => {
20+
const [items, setItems] = useState<Item[]>(
21+
initialItems?.map((elementCreator) => {
22+
const id = generateId();
23+
const element = elementCreator(id);
24+
25+
return {
26+
id,
27+
element: <SnackbarItem>{element}</SnackbarItem>,
28+
};
29+
}) ?? [],
30+
);
31+
32+
const add = useCallback<SnackbarContextProps["add"]>((elementCreator) => {
33+
const id = generateId();
34+
const element = elementCreator(id);
35+
36+
setItems((v) => [
37+
...v,
38+
{
39+
id,
40+
element: <SnackbarItem>{element}</SnackbarItem>,
41+
},
42+
]);
43+
44+
return id;
45+
}, []);
46+
47+
const remove = useCallback<SnackbarContextProps["remove"]>((id) => {
48+
setItems((v) => v.filter((i) => i.id !== id));
49+
}, []);
50+
51+
const contextValue = useMemo(
52+
() => ({
53+
add,
54+
remove,
55+
}),
56+
[add, remove],
57+
);
58+
59+
return (
60+
<Layer elevation="popup">
61+
{items.length > 0 && (
62+
<div className={classNames(className, styles.root)} {...otherProps}>
63+
{items.map((i) => i.element)}
64+
</div>
65+
)}
66+
67+
<SnackbarContext.Provider value={contextValue}>{children}</SnackbarContext.Provider>
68+
</Layer>
69+
);
70+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from "react";
2+
import styles from "./styles/item.module.scss";
3+
import classNames from "classnames";
4+
5+
export type SnackbarItemProps = React.HTMLAttributes<HTMLDivElement>;
6+
7+
export const SnackbarItem = ({
8+
className,
9+
...otherProps
10+
}: Readonly<SnackbarItemProps>): React.ReactElement => (
11+
<div className={classNames(styles.root, className)} {...otherProps} />
12+
);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from "react";
2+
import { ElementCreator, Item } from "./types";
3+
4+
export interface SnackbarContextProps {
5+
add: (elementCreator: ElementCreator) => Item["id"];
6+
remove: (id: Item["id"]) => void;
7+
}
8+
9+
export const SnackbarContext = React.createContext<SnackbarContextProps>({
10+
add: () => {
11+
throw new Error("Snackbar context not initialized");
12+
},
13+
remove: () => {
14+
throw new Error("Snackbar context not initialized");
15+
},
16+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { useContext } from "react";
2+
import { SnackbarContext, type SnackbarContextProps } from "./context";
3+
4+
export const useSnackbarContext = (): SnackbarContextProps => useContext(SnackbarContext);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "./hook";
2+
3+
export * from "./Snackbar";
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@import "@react-ck/theme";
2+
3+
.root {
4+
position: fixed;
5+
flex-direction: column;
6+
top: 0;
7+
right: 0;
8+
max-height: 100%;
9+
width: 100%;
10+
max-width: 480px;
11+
box-sizing: border-box;
12+
display: flex;
13+
gap: get-spacing(1);
14+
padding: get-spacing(2) get-spacing(1);
15+
overflow: auto;
16+
pointer-events: none;
17+
filter: drop-shadow(0 1px 5px rgba(0, 0, 0, 0.1));
18+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.root {
2+
pointer-events: all;
3+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface Item {
2+
id: string;
3+
element: React.ReactNode;
4+
}
5+
6+
export type ElementCreator = (id: Item["id"]) => Item["element"];
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
let n = 0;
2+
3+
export const generateId = (): string => {
4+
n += 1;
5+
6+
return `${n}.${new Date().getTime()}.${Math.random()}`;
7+
};

0 commit comments

Comments
 (0)