@sproutsocial/seeds-react-drawer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +6 -0
- package/.eslintrc.js +4 -0
- package/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +12 -0
- package/dist/esm/index.js +291 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/index.d.mts +69 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +328 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +9 -0
- package/package.json +50 -0
- package/src/Drawer.stories.tsx +378 -0
- package/src/Drawer.tsx +308 -0
- package/src/DrawerTypes.ts +80 -0
- package/src/__tests__/Drawer.test.tsx +188 -0
- package/src/__tests__/Drawer.typetest.tsx +39 -0
- package/src/index.ts +5 -0
- package/src/styles.ts +35 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +12 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type {
|
|
3
|
+
TypeSystemCommonProps,
|
|
4
|
+
TypeStyledComponentsCommonProps,
|
|
5
|
+
} from "@sproutsocial/seeds-react-system-props";
|
|
6
|
+
import type { TypeBoxProps } from "@sproutsocial/seeds-react-box";
|
|
7
|
+
import type { TypeButtonProps } from "@sproutsocial/seeds-react-button";
|
|
8
|
+
|
|
9
|
+
type DrawerAnimationDirection = "left" | "right";
|
|
10
|
+
|
|
11
|
+
export interface TypeDrawerContext {
|
|
12
|
+
/** Callback for close button */
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
onClose?: () => any;
|
|
15
|
+
|
|
16
|
+
/** aria-label for drawer close button */
|
|
17
|
+
closeButtonLabel?: string;
|
|
18
|
+
}
|
|
19
|
+
// TODO: Should the render prop be a React.FC?
|
|
20
|
+
export interface TypeDrawerCloseButtonProps
|
|
21
|
+
extends Omit<TypeButtonProps, "children"> {
|
|
22
|
+
/** An optional function that receives the context of the parent drawer as an argument. Can be used to customize the on-close behavior. */
|
|
23
|
+
render?: React.FC<TypeDrawerContext>;
|
|
24
|
+
children?: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TypeDrawerHeaderProps extends TypeBoxProps {
|
|
28
|
+
title?: string;
|
|
29
|
+
children?: React.ReactNode;
|
|
30
|
+
|
|
31
|
+
/** An optional function that receives the context of the parent drawer as an argument. Can be used to customize the appearance of the header. */
|
|
32
|
+
render?: React.FC<TypeDrawerContext>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TypeInnerDrawerProps
|
|
36
|
+
extends Omit<TypeDrawerProps, "closeButtonLabel"> {
|
|
37
|
+
width: number;
|
|
38
|
+
direction: DrawerAnimationDirection;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type useBodyClicksProps = Pick<
|
|
42
|
+
TypeDrawerProps,
|
|
43
|
+
"closeTargets" | "onClose" | "disableCloseOnClickOutside"
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
export interface TypeUseCloseOnBodyClickProps
|
|
47
|
+
extends Pick<
|
|
48
|
+
TypeDrawerProps,
|
|
49
|
+
"closeTargets" | "onClose" | "disableCloseOnClickOutside"
|
|
50
|
+
> {
|
|
51
|
+
ref?: React.RefObject<HTMLElement | null>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface TypeDrawerProps
|
|
55
|
+
extends TypeStyledComponentsCommonProps,
|
|
56
|
+
TypeSystemCommonProps,
|
|
57
|
+
Omit<React.ComponentPropsWithoutRef<"nav">, "color"> {
|
|
58
|
+
children: React.ReactNode;
|
|
59
|
+
|
|
60
|
+
/** Label for the close button. Usually this should be "Close" */
|
|
61
|
+
closeButtonLabel: string;
|
|
62
|
+
|
|
63
|
+
/** Whether the drawer slides in from the left or right side of the screen */
|
|
64
|
+
direction?: DrawerAnimationDirection;
|
|
65
|
+
|
|
66
|
+
/** In some cases, you may not want the user to be able to click outside of the drawer to close it. You can disable that with this prop. */
|
|
67
|
+
disableCloseOnClickOutside?: boolean;
|
|
68
|
+
id: string;
|
|
69
|
+
isOpen: boolean;
|
|
70
|
+
offset?: number;
|
|
71
|
+
onClose: () => void;
|
|
72
|
+
zIndex?: number;
|
|
73
|
+
closeTargets?: Array<Element>;
|
|
74
|
+
width?: number;
|
|
75
|
+
focusLockExemptCheck?: (element: HTMLElement) => boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface TypeDrawerContentProps extends TypeBoxProps {
|
|
79
|
+
children?: React.ReactNode;
|
|
80
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/* eslint-disable testing-library/prefer-screen-queries */
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
render as testRender,
|
|
6
|
+
fireEvent,
|
|
7
|
+
waitFor,
|
|
8
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
9
|
+
import type { TypeDrawerProps } from "../DrawerTypes";
|
|
10
|
+
import Drawer from "../Drawer";
|
|
11
|
+
|
|
12
|
+
const StatefulDrawer = ({
|
|
13
|
+
isOpen,
|
|
14
|
+
onClose,
|
|
15
|
+
children,
|
|
16
|
+
...rest
|
|
17
|
+
}: TypeDrawerProps) => {
|
|
18
|
+
const [isDrawerOpen, setIsDrawerOpen] = useState(isOpen || false);
|
|
19
|
+
|
|
20
|
+
const onDrawerClose = () => {
|
|
21
|
+
setIsDrawerOpen(false);
|
|
22
|
+
onClose();
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Drawer
|
|
27
|
+
{...rest}
|
|
28
|
+
isOpen={isDrawerOpen}
|
|
29
|
+
onClose={onDrawerClose}
|
|
30
|
+
id="1"
|
|
31
|
+
closeButtonLabel="close button"
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</Drawer>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const render = ({
|
|
39
|
+
isOpen = false,
|
|
40
|
+
direction,
|
|
41
|
+
disableCloseOnClickOutside,
|
|
42
|
+
children = (
|
|
43
|
+
<React.Fragment>
|
|
44
|
+
<Drawer.Header title="Drawer Header" id="drawer-1-header" />
|
|
45
|
+
<Drawer.Content>
|
|
46
|
+
<p>Drawer Content</p>
|
|
47
|
+
</Drawer.Content>
|
|
48
|
+
</React.Fragment>
|
|
49
|
+
),
|
|
50
|
+
offset,
|
|
51
|
+
onClose = jest.fn(),
|
|
52
|
+
id = "drawer",
|
|
53
|
+
closeButtonLabel = "close",
|
|
54
|
+
width = 600,
|
|
55
|
+
}: Partial<TypeDrawerProps>) => {
|
|
56
|
+
const { baseElement } = testRender(<div id="main-content" />);
|
|
57
|
+
const mainContentRef = baseElement.querySelector("#main-content");
|
|
58
|
+
return testRender(
|
|
59
|
+
<div>
|
|
60
|
+
{baseElement.innerHTML}
|
|
61
|
+
<StatefulDrawer
|
|
62
|
+
isOpen={isOpen}
|
|
63
|
+
direction={direction}
|
|
64
|
+
offset={offset}
|
|
65
|
+
onClose={onClose}
|
|
66
|
+
closeButtonLabel={closeButtonLabel}
|
|
67
|
+
id={id}
|
|
68
|
+
disableCloseOnClickOutside={disableCloseOnClickOutside}
|
|
69
|
+
// @ts-ignore - We'll come back to test updating this
|
|
70
|
+
closeTargets={[mainContentRef]}
|
|
71
|
+
width={width}
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
</StatefulDrawer>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
describe("Drawer", () => {
|
|
80
|
+
it("should not be in the document by default", () => {
|
|
81
|
+
const { queryByText } = render({});
|
|
82
|
+
expect(queryByText(/drawer content/i)).not.toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should render drawer", () => {
|
|
86
|
+
const { getByText } = render({
|
|
87
|
+
isOpen: true,
|
|
88
|
+
});
|
|
89
|
+
expect(getByText(/drawer content/i)).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should close drawer on header click", async () => {
|
|
93
|
+
const { queryByText, getByText, getByLabelText } = render({
|
|
94
|
+
isOpen: true,
|
|
95
|
+
});
|
|
96
|
+
expect(getByText(/drawer content/i)).toBeInTheDocument();
|
|
97
|
+
fireEvent.click(getByLabelText("close button"));
|
|
98
|
+
await waitFor(() => {
|
|
99
|
+
expect(queryByText(/drawer content/i)).not.toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should close drawer on header click when using custom header", async () => {
|
|
104
|
+
const { queryByText, getByText, getByLabelText } = render({
|
|
105
|
+
isOpen: true,
|
|
106
|
+
children: (
|
|
107
|
+
<React.Fragment>
|
|
108
|
+
<Drawer.Header>
|
|
109
|
+
<h1>Title</h1>
|
|
110
|
+
<Drawer.CloseButton />
|
|
111
|
+
</Drawer.Header>
|
|
112
|
+
<Drawer.Content>
|
|
113
|
+
<p>Drawer Content</p>
|
|
114
|
+
</Drawer.Content>
|
|
115
|
+
</React.Fragment>
|
|
116
|
+
),
|
|
117
|
+
});
|
|
118
|
+
expect(getByText(/drawer content/i)).toBeInTheDocument();
|
|
119
|
+
fireEvent.click(getByLabelText("close button"));
|
|
120
|
+
await waitFor(() => {
|
|
121
|
+
expect(queryByText(/drawer content/i)).not.toBeInTheDocument();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should close drawer on outside click", async () => {
|
|
126
|
+
const { baseElement, queryByText, getByText } = render({
|
|
127
|
+
isOpen: true,
|
|
128
|
+
});
|
|
129
|
+
expect(getByText(/drawer content/i)).toBeInTheDocument();
|
|
130
|
+
fireEvent.click(baseElement.querySelector("#main-content") as Element);
|
|
131
|
+
await waitFor(() => {
|
|
132
|
+
expect(queryByText(/drawer content/i)).not.toBeInTheDocument();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should not close drawer on outside click when disabled", async () => {
|
|
137
|
+
const { baseElement, queryByText, getByText } = render({
|
|
138
|
+
isOpen: true,
|
|
139
|
+
disableCloseOnClickOutside: true,
|
|
140
|
+
});
|
|
141
|
+
expect(getByText(/drawer content/i)).toBeInTheDocument();
|
|
142
|
+
fireEvent.click(baseElement.querySelector("#main-content") as Element);
|
|
143
|
+
await waitFor(() => {
|
|
144
|
+
// eslint-disable-next-line testing-library/prefer-presence-queries
|
|
145
|
+
expect(queryByText(/drawer content/i)).toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should close drawer on esc key", async () => {
|
|
150
|
+
const { baseElement, queryByText, getByText } = render({
|
|
151
|
+
isOpen: true,
|
|
152
|
+
disableCloseOnClickOutside: true,
|
|
153
|
+
});
|
|
154
|
+
expect(getByText(/drawer content/i)).toBeInTheDocument();
|
|
155
|
+
fireEvent.keyDown(baseElement, {
|
|
156
|
+
key: "Escape",
|
|
157
|
+
});
|
|
158
|
+
await waitFor(() => {
|
|
159
|
+
expect(queryByText(/drawer content/i)).not.toBeInTheDocument();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
it("should have dialog role", () => {
|
|
163
|
+
const { getByRole } = render({
|
|
164
|
+
isOpen: true,
|
|
165
|
+
});
|
|
166
|
+
expect(getByRole("dialog")).toBeInTheDocument();
|
|
167
|
+
});
|
|
168
|
+
it("should have focus on drawer when opened", () => {
|
|
169
|
+
const { getByLabelText } = render({
|
|
170
|
+
isOpen: true,
|
|
171
|
+
});
|
|
172
|
+
expect(getByLabelText("close button")).toHaveFocus();
|
|
173
|
+
});
|
|
174
|
+
it("should have an h2 with id", () => {
|
|
175
|
+
const { getByRole } = render({
|
|
176
|
+
isOpen: true,
|
|
177
|
+
});
|
|
178
|
+
expect(getByRole("heading")).toHaveAttribute("id", "drawer-1-header");
|
|
179
|
+
});
|
|
180
|
+
it("should be 600px wide by default", () => {
|
|
181
|
+
const { getByRole } = render({ isOpen: true });
|
|
182
|
+
expect(getByRole("dialog")).toHaveStyle("width: 600px");
|
|
183
|
+
});
|
|
184
|
+
it("should customize the drawer width when provided", () => {
|
|
185
|
+
const { getByRole } = render({ isOpen: true, width: 400 });
|
|
186
|
+
expect(getByRole("dialog")).toHaveStyle("width: 400px");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import Drawer from "../Drawer";
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5
|
+
function DrawerTypes() {
|
|
6
|
+
const componentProps = {
|
|
7
|
+
id: "drawer-1",
|
|
8
|
+
closeButtonLabel: "close",
|
|
9
|
+
isOpen: true,
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
11
|
+
onClose: jest.fn(),
|
|
12
|
+
children: "Drawer Content",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<Drawer {...componentProps} />
|
|
18
|
+
<Drawer {...componentProps} direction="left">
|
|
19
|
+
<Drawer.Header title="test1">
|
|
20
|
+
<h1>Drawer Header</h1>
|
|
21
|
+
</Drawer.Header>
|
|
22
|
+
<Drawer.Content>
|
|
23
|
+
<p>Drawer Content</p>
|
|
24
|
+
</Drawer.Content>
|
|
25
|
+
</Drawer>
|
|
26
|
+
{/* @ts-expect-error - test that invalid type is rejected */}
|
|
27
|
+
<Drawer />
|
|
28
|
+
{/* @ts-expect-error - test that invalid type is rejected */}
|
|
29
|
+
<Drawer direction={true}>
|
|
30
|
+
<Drawer.Header>
|
|
31
|
+
<h1>Drawer Header</h1>
|
|
32
|
+
</Drawer.Header>
|
|
33
|
+
<Drawer.Content>
|
|
34
|
+
<p>Drawer Content</p>
|
|
35
|
+
</Drawer.Content>
|
|
36
|
+
</Drawer>
|
|
37
|
+
</>
|
|
38
|
+
);
|
|
39
|
+
}
|
package/src/index.ts
ADDED
package/src/styles.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { TypeDrawerProps } from "./DrawerTypes";
|
|
2
|
+
import styled, { css } from "styled-components";
|
|
3
|
+
import { COMMON } from "@sproutsocial/seeds-react-system-props";
|
|
4
|
+
import type { TypeSystemCommonProps } from "@sproutsocial/seeds-react-system-props";
|
|
5
|
+
|
|
6
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
7
|
+
|
|
8
|
+
export const Content = styled(Box)`
|
|
9
|
+
overflow-y: auto;
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
interface ContainerType
|
|
13
|
+
extends Pick<TypeDrawerProps, "offset" | "direction">,
|
|
14
|
+
TypeSystemCommonProps {
|
|
15
|
+
width: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const Container = styled.div<ContainerType>`
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
position: fixed;
|
|
22
|
+
top: 0;
|
|
23
|
+
height: 100%;
|
|
24
|
+
width: ${(props) => props.width}px;
|
|
25
|
+
background-color: ${(props) => props.theme.colors.container.background.base};
|
|
26
|
+
box-shadow: ${(props) => props.theme.shadows.high};
|
|
27
|
+
filter: blur(0);
|
|
28
|
+
|
|
29
|
+
${(props) => css`
|
|
30
|
+
${props.direction}: ${props.offset}px;
|
|
31
|
+
`}
|
|
32
|
+
|
|
33
|
+
${COMMON}
|
|
34
|
+
`;
|
|
35
|
+
export default Container;
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
export default defineConfig((options) => ({
|
|
4
|
+
entry: ["src/index.ts"],
|
|
5
|
+
format: ["cjs", "esm"],
|
|
6
|
+
clean: true,
|
|
7
|
+
legacyOutput: true,
|
|
8
|
+
dts: options.dts,
|
|
9
|
+
external: ["react"],
|
|
10
|
+
sourcemap: true,
|
|
11
|
+
metafile: options.metafile,
|
|
12
|
+
}));
|