Skip to content

Commit 4c5be1b

Browse files
committed
feat: add modal component
1 parent 4835b5a commit 4c5be1b

16 files changed

+252
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
extends: "@react-ck/babel-config",
3+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module "*.module.scss" {
2+
const content: Record<string, string>;
3+
export default content;
4+
}

packages/components/modal/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Hack for module resolution of non built packages
2+
export * from "./src/index";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { config as default } from "@react-ck/jest-config";
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "@react-ck/modal",
3+
"private": false,
4+
"version": "1.0.0",
5+
"main": "dist/index.js",
6+
"types": "dist/index.d.ts",
7+
"files": [
8+
"/dist"
9+
],
10+
"homepage": "https://github.com/abelflopes/react-ck/tree/master/packages/components/modal#readme",
11+
"repository": {
12+
"type": "git",
13+
"url": "git+https://github.com/abelflopes/react-ck.git"
14+
},
15+
"scripts": {
16+
"build": "NODE_ENV=production webpack",
17+
"lint:typescript": "tsc --noEmit",
18+
"test": "npx -y npm-run-all -s test:*",
19+
"test:unit": "jest --testPathPattern=\".unit.*\"",
20+
"test:snapshot": "jest --testPathPattern=\".snapshot.*\"",
21+
"test:snapshot:update": "jest --testPathPattern=\".snapshot.*\" -u"
22+
},
23+
"devDependencies": {
24+
"@react-ck/babel-config": "^1.0.0",
25+
"@react-ck/jest-config": "^1.0.0",
26+
"@react-ck/typescript-config": "^1.0.0",
27+
"@react-ck/webpack-config": "^1.0.0",
28+
"@types/react": "^18.2.33"
29+
},
30+
"peerDependencies": {
31+
"react": "^18.2.0"
32+
},
33+
"dependencies": {
34+
"@react-ck/button": "^1.2.6",
35+
"@react-ck/card": "^1.2.3",
36+
"@react-ck/icon": "^1.4.0",
37+
"@react-ck/layers": "^1.0.0",
38+
"@react-ck/overlay": "^1.1.6",
39+
"@react-ck/react-utils": "^1.0.0",
40+
"@react-ck/text": "^1.2.6",
41+
"@react-ck/theme": "^1.4.1",
42+
"classnames": "^2.3.2"
43+
}
44+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import React from "react";
2+
import { Modal } from "../src/index";
3+
import renderer from "react-test-renderer";
4+
5+
describe("Snapshot Modal", () => {
6+
test("renders correctly", async () => {
7+
const tree = renderer.create(<Modal>Modal</Modal>).toJSON();
8+
expect(tree).toMatchSnapshot();
9+
});
10+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from "react";
2+
import { Modal } from "../src/index";
3+
import { render, screen } from "@testing-library/react";
4+
import "@testing-library/jest-dom";
5+
6+
describe("Unit Modal", () => {
7+
test("renders correctly", async () => {
8+
const content = "Modal";
9+
render(<Modal>{content}</Modal>);
10+
const find = await screen.findByText(content);
11+
expect(find).toBeInTheDocument();
12+
});
13+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import classNames from "classnames";
2+
import styles from "./styles/index.module.scss";
3+
import React, { useCallback, useMemo, useState } from "react";
4+
import { ModalContext, type ModalContextProps, type ModalContextValue } from "./context";
5+
import { Card } from "@react-ck/card";
6+
import { Overlay } from "@react-ck/overlay";
7+
import { Icon } from "@react-ck/icon";
8+
import { Button } from "@react-ck/button";
9+
import { Text } from "@react-ck/text";
10+
import { Layer } from "@react-ck/layers";
11+
12+
interface ModalProps extends React.HTMLAttributes<HTMLDivElement> {}
13+
14+
// TODO: add annotation
15+
// TODO: add a11y https://react.dev/reference/react-dom/createPortal#rendering-a-modal-dialog-with-a-portal
16+
17+
/**
18+
* @param props - {@link React.HTMLAttributes}
19+
* @returns a React element
20+
*/
21+
22+
export const Modal = ({
23+
children,
24+
className,
25+
...otherProps
26+
}: Readonly<ModalProps>): React.ReactElement => {
27+
const [props, setProps] = useState<ModalContextValue>({
28+
header: undefined,
29+
footer: undefined,
30+
});
31+
32+
const setContextValue = useCallback<ModalContextProps["setValue"]>((value) => {
33+
setProps((v) => ({ ...v, ...value }));
34+
}, []);
35+
36+
const contextProps = useMemo<ModalContextProps>(
37+
() => ({
38+
setValue: setContextValue,
39+
}),
40+
[setContextValue],
41+
);
42+
43+
return (
44+
<Layer elevation="overlay">
45+
<div {...otherProps} className={classNames(styles.root, className)}>
46+
<Overlay />
47+
<Card className={styles.card}>
48+
<ModalContext.Provider value={contextProps}>
49+
{props.header && (
50+
<header
51+
{...props.header}
52+
className={classNames(styles.header, props.header.className)}>
53+
<Text type="h3" as="p" margin={false}>
54+
{props.header.heading}
55+
</Text>
56+
57+
<Button skin="ghost" icon={<Icon name="close" />} />
58+
</header>
59+
)}
60+
<main className={styles.content}>{children}</main>
61+
{props.footer && (
62+
<footer
63+
{...props.footer}
64+
className={classNames(styles.footer, props.footer.className)}
65+
/>
66+
)}
67+
</ModalContext.Provider>
68+
</Card>
69+
</div>
70+
</Layer>
71+
);
72+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { useContext, useEffect } from "react";
2+
import { ModalContext, type ModalContextValue } from "./context";
3+
4+
export const ModalFooter = (props: NonNullable<ModalContextValue["footer"]>): undefined => {
5+
const context = useContext(ModalContext);
6+
7+
useEffect(() => {
8+
context.setValue({ footer: props });
9+
10+
return () => {
11+
context.setValue({ footer: undefined });
12+
};
13+
}, [context, props]);
14+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { useContext, useEffect } from "react";
2+
import { ModalContext, type ModalContextValue } from "./context";
3+
4+
export const ModalHeader = (props: NonNullable<ModalContextValue["header"]>): undefined => {
5+
const context = useContext(ModalContext);
6+
7+
useEffect(() => {
8+
context.setValue({ header: props });
9+
10+
return () => {
11+
context.setValue({ header: undefined });
12+
};
13+
}, [context, props]);
14+
};

0 commit comments

Comments
 (0)