Skip to content

Commit b67497a

Browse files
authored
[LG-5293] feat: ContextDrawer (#2918)
* feat(context-drawer): create context-drawer package and install deps * feat(context-drawer): implement ButtonCorner component used internally for styling ContextDrawerButton * feat(context-drawer): implement ContextDrawerButton component * docs(context-drawer): add comment about internal ButtonCorner component and fix lint * [LG-5293] feat: ContextDrawer part 2 (#2922) * chore(context-drawer): update dependencies * fix(context-drawer): ContextDrawerButton glyph rotates * feat(context-drawer): implement ContextDrawer component with stories and specs * docs(context-drawer): README * fix(ContextDrawerButton): styling * refactor(ContextDrawer): hardcode additional spacing for bottomInterceptRef * style(ContextDrawer): improve visibility transition and collapse reference border * docs(context-drawer): README format * refactor(context-drawer): tab navigation with specs * update spec * Reduce redundant css selector specificity * feat(lib): add formatCssSize util (#2930) * feat(lib): add formatCssSize util * refactor(ContextDrawer): formatCssSize on expandedHeight * chore(lib): changeset * refactor(lib): formatCssSize trims string values before format * refactor(lib): formatCssSize types
1 parent 7537bad commit b67497a

28 files changed

+1337
-60
lines changed

.changeset/lemon-adults-accept.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@leafygreen-ui/lib': minor
3+
---
4+
5+
Adds `formatCssSize` util for properly formatting a number or string into a valid css value

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import Button from '@leafygreen-ui/button';
8181
| [@leafygreen-ui/code](./packages/code) | [![version](https://img.shields.io/npm/v/@leafygreen-ui/code)](https://www.npmjs.com/package/@leafygreen-ui/code) | ![downloads](https://img.shields.io/npm/dm/@leafygreen-ui/code?color=white) | [Live Example](http://mongodb.design/component/code/live-example) |
8282
| [@leafygreen-ui/combobox](./packages/combobox) | [![version](https://img.shields.io/npm/v/@leafygreen-ui/combobox)](https://www.npmjs.com/package/@leafygreen-ui/combobox) | ![downloads](https://img.shields.io/npm/dm/@leafygreen-ui/combobox?color=white) | [Live Example](http://mongodb.design/component/combobox/live-example) |
8383
| [@leafygreen-ui/confirmation-modal](./packages/confirmation-modal) | [![version](https://img.shields.io/npm/v/@leafygreen-ui/confirmation-modal)](https://www.npmjs.com/package/@leafygreen-ui/confirmation-modal) | ![downloads](https://img.shields.io/npm/dm/@leafygreen-ui/confirmation-modal?color=white) | [Live Example](http://mongodb.design/component/confirmation-modal/live-example) |
84+
| [@leafygreen-ui/context-drawer](./packages/context-drawer) | [![version](https://img.shields.io/npm/v/@leafygreen-ui/context-drawer)](https://www.npmjs.com/package/@leafygreen-ui/context-drawer) | ![downloads](https://img.shields.io/npm/dm/@leafygreen-ui/context-drawer?color=white) | [Live Example](http://mongodb.design/component/context-drawer/live-example) |
8485
| [@leafygreen-ui/copyable](./packages/copyable) | [![version](https://img.shields.io/npm/v/@leafygreen-ui/copyable)](https://www.npmjs.com/package/@leafygreen-ui/copyable) | ![downloads](https://img.shields.io/npm/dm/@leafygreen-ui/copyable?color=white) | [Live Example](http://mongodb.design/component/copyable/live-example) |
8586
| [@leafygreen-ui/date-picker](./packages/date-picker) | [![version](https://img.shields.io/npm/v/@leafygreen-ui/date-picker)](https://www.npmjs.com/package/@leafygreen-ui/date-picker) | ![downloads](https://img.shields.io/npm/dm/@leafygreen-ui/date-picker?color=white) | [Live Example](http://mongodb.design/component/date-picker/live-example) |
8687
| [@leafygreen-ui/date-utils](./packages/date-utils) | [![version](https://img.shields.io/npm/v/@leafygreen-ui/date-utils)](https://www.npmjs.com/package/@leafygreen-ui/date-utils) | ![downloads](https://img.shields.io/npm/dm/@leafygreen-ui/date-utils?color=white) | [Live Example](http://mongodb.design/component/date-utils/live-example) |

packages/context-drawer/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Context Drawer
2+
3+
![npm (scoped)](https://img.shields.io/npm/v/@leafygreen-ui/context-drawer.svg)
4+
5+
#### [View on MongoDB.design](https://www.mongodb.design/component/context-drawer/live-example/)
6+
7+
## Installation
8+
9+
### PNPM
10+
11+
```shell
12+
pnpm add @leafygreen-ui/context-drawer
13+
```
14+
15+
### Yarn
16+
17+
```shell
18+
yarn add @leafygreen-ui/context-drawer
19+
```
20+
21+
### NPM
22+
23+
```shell
24+
npm install @leafygreen-ui/context-drawer
25+
```
26+
27+
## Description
28+
29+
The Context Drawer is a container component that can be used to vertically expand and collapse content. It is intended to be used in situations where there is a need to reveal additional information or functionality in-context, without navigating the user to a new page or view.
30+
31+
## Example
32+
33+
```tsx
34+
import {
35+
ContextDrawer,
36+
ContextDrawerButton,
37+
} from '@leafygreen-ui/context-drawer';
38+
39+
<ContextDrawer
40+
reference={
41+
<div
42+
className={css`
43+
display: flex;
44+
flex-direction: column;
45+
`}
46+
>
47+
<h2>Reference Element</h2>
48+
<p>This is the element that the drawer is positioned relative to.</p>
49+
</div>
50+
}
51+
trigger={({ isOpen }) => <ContextDrawerButton isOpen={isOpen} />}
52+
content={
53+
<div>
54+
<p>This is the content of the drawer.</p>
55+
</div>
56+
}
57+
/>;
58+
```
59+
60+
## Properties
61+
62+
### ContextDrawer
63+
64+
| Prop | Type | Description | Default |
65+
| ---------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------- |
66+
| `content` | `ReactElement` | The content to be displayed within the drawer. | |
67+
| `defaultOpen` | `boolean` | The default open state of the drawer when it is uncontrolled. | `false` |
68+
| `expandedHeight` | `number \| string` | The maximum height of the content area when expanded. Can be a number (in pixels) or a string (e.g., '50%'). | `160` |
69+
| `isOpen` | `boolean` | The open state of the drawer. Providing this prop will switch the component to controlled mode. | |
70+
| `onOpenChange` | `ChangeEventHandler<boolean>` | Event handler called when the open state of the drawer changes. | |
71+
| `reference` | `ReactElement` | The element that the drawer is positioned relative to. | |
72+
| `trigger` | `ReactElement` or `(props: { isOpen: boolean }) => ReactElement` | The element that triggers the opening and closing of the drawer. Can be a React element or a function that receives the `isOpen` state. | |

packages/context-drawer/package.json

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
2+
{
3+
"name": "@leafygreen-ui/context-drawer",
4+
"version": "0.1.0",
5+
"description": "LeafyGreen UI Kit Context Drawer",
6+
"main": "./dist/umd/index.js",
7+
"module": "./dist/esm/index.js",
8+
"types": "./dist/types/index.d.ts",
9+
"license": "Apache-2.0",
10+
"exports": {
11+
".": {
12+
"require": "./dist/umd/index.js",
13+
"import": "./dist/esm/index.js",
14+
"types": "./dist/types/index.d.ts"
15+
},
16+
"./testing": {
17+
"require": "./dist/umd/testing/index.js",
18+
"import": "./dist/esm/testing/index.js",
19+
"types": "./dist/types/testing/index.d.ts"
20+
}
21+
},
22+
"scripts": {
23+
"build": "lg-build bundle",
24+
"tsc": "lg-build tsc",
25+
"docs": "lg-build docs"
26+
},
27+
"publishConfig": {
28+
"access": "public"
29+
},
30+
"dependencies": {
31+
"@leafygreen-ui/button": "workspace:^",
32+
"@leafygreen-ui/emotion": "workspace:^",
33+
"@leafygreen-ui/hooks": "workspace:^",
34+
"@leafygreen-ui/icon": "workspace:^",
35+
"@leafygreen-ui/lib": "workspace:^",
36+
"@leafygreen-ui/palette": "workspace:^",
37+
"@leafygreen-ui/tokens": "workspace:^",
38+
"react-intersection-observer": "^8.25.1"
39+
},
40+
"devDependencies": {
41+
"@faker-js/faker": "^8.0.2",
42+
"@storybook/test": "8.5.3",
43+
"@leafygreen-ui/typography": "workspace:^",
44+
"@lg-tools/build": "workspace:^"
45+
},
46+
"peerDependencies": {
47+
"@leafygreen-ui/leafygreen-provider": "workspace:^"
48+
},
49+
"homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/packages/context-drawer",
50+
"repository": {
51+
"type": "git",
52+
"url": "https://github.com/mongodb/leafygreen-ui"
53+
},
54+
"bugs": {
55+
"url": "https://jira.mongodb.org/projects/LG/summary"
56+
}
57+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { css, cx } from '@leafygreen-ui/emotion';
2+
import { Theme } from '@leafygreen-ui/lib';
3+
import { color } from '@leafygreen-ui/tokens';
4+
5+
import { Side } from './ButtonCorner.types';
6+
7+
const CORNER_SIZE = 8;
8+
const CORNER_OFFSET = 15;
9+
10+
const baseWrapperStyles = css`
11+
display: flex;
12+
position: absolute;
13+
top: 0;
14+
width: ${CORNER_SIZE}px;
15+
height: ${CORNER_SIZE}px;
16+
`;
17+
18+
export const getWrapperStyles = (side: Side) =>
19+
cx(baseWrapperStyles, {
20+
[css`
21+
left: -${CORNER_OFFSET}px;
22+
`]: side === Side.Left,
23+
[css`
24+
right: -${CORNER_OFFSET}px;
25+
transform: scaleX(-1);
26+
`]: side === Side.Right,
27+
});
28+
29+
export const getFill = (theme: Theme) => color[theme].background.info.default;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
3+
import { useDarkMode } from '@leafygreen-ui/leafygreen-provider';
4+
5+
import { getFill, getWrapperStyles } from './ButtonCorner.styles';
6+
import { Side } from './ButtonCorner.types';
7+
8+
/**
9+
* @returns component that renders the visually rounded sides of the ContextDrawerButton.
10+
*
11+
* @internal
12+
*/
13+
export const ButtonCorner = ({ side }: { side: Side }) => {
14+
const { theme } = useDarkMode();
15+
16+
return (
17+
<div className={getWrapperStyles(side)}>
18+
<svg
19+
width="8"
20+
height="8"
21+
viewBox="0 0 8 8"
22+
fill="none"
23+
xmlns="http://www.w3.org/2000/svg"
24+
>
25+
<path
26+
fillRule="evenodd"
27+
clipRule="evenodd"
28+
d="M0 0.000523481C4.39583 0.0500261 7.94998 3.60417 7.99949 8H8V0H0V0.000523481Z"
29+
fill={getFill(theme)}
30+
/>
31+
</svg>
32+
</div>
33+
);
34+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const Side = {
2+
Left: 'left',
3+
Right: 'right',
4+
} as const;
5+
export type Side = (typeof Side)[keyof typeof Side];
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { ButtonCorner } from './ButtonCorner';
2+
export { Side } from './ButtonCorner.types';
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import React, { useMemo, useState } from 'react';
2+
import { faker } from '@faker-js/faker';
3+
import {
4+
storybookArgTypes,
5+
storybookExcludedControlParams,
6+
StoryMetaType,
7+
} from '@lg-tools/storybook-utils';
8+
import { StoryFn, StoryObj } from '@storybook/react';
9+
10+
import { css } from '@leafygreen-ui/emotion';
11+
import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider';
12+
import { DarkModeProps, getTheme } from '@leafygreen-ui/lib';
13+
import {
14+
borderRadius,
15+
color,
16+
InteractionState,
17+
spacing,
18+
Variant,
19+
} from '@leafygreen-ui/tokens';
20+
import { Body, Subtitle } from '@leafygreen-ui/typography';
21+
22+
import { ContextDrawerProps } from './ContextDrawer/ContextDrawer.types';
23+
import { ContextDrawer } from './ContextDrawer';
24+
import { ContextDrawerButton } from './ContextDrawerButton';
25+
26+
const SEED = 0;
27+
faker.seed(SEED);
28+
29+
const defaultExcludedControls = [
30+
...storybookExcludedControlParams,
31+
'content',
32+
'onOpenChange',
33+
'reference',
34+
'trigger',
35+
];
36+
37+
const meta: StoryMetaType<typeof ContextDrawer> = {
38+
title: 'Components/ContextDrawer',
39+
component: ContextDrawer,
40+
parameters: {
41+
default: 'LiveExample',
42+
controls: {
43+
exclude: [...defaultExcludedControls, 'isOpen'],
44+
},
45+
generate: {
46+
combineArgs: {
47+
darkMode: [false, true],
48+
isOpen: [false, true],
49+
},
50+
decorator: (Instance, context) => {
51+
return (
52+
<LeafyGreenProvider darkMode={context?.args.darkMode}>
53+
<Instance
54+
reference={<MockReference darkMode={context?.args.darkMode} />}
55+
content={<MockContent />}
56+
trigger={({ isOpen }: { isOpen: boolean }) => (
57+
<ContextDrawerButton isOpen={isOpen} />
58+
)}
59+
/>
60+
</LeafyGreenProvider>
61+
);
62+
},
63+
},
64+
},
65+
args: {
66+
darkMode: false,
67+
defaultOpen: false,
68+
expandedHeight: 160,
69+
},
70+
argTypes: {
71+
darkMode: storybookArgTypes.darkMode,
72+
},
73+
};
74+
export default meta;
75+
76+
const MockReference = ({ darkMode }: DarkModeProps) => {
77+
const fillerText = useMemo(() => faker.lorem.paragraphs(2), []);
78+
79+
return (
80+
<div
81+
className={css`
82+
overflow: hidden;
83+
padding: ${spacing[400]}px;
84+
height: 200px;
85+
border: 1px solid
86+
${color[getTheme(!!darkMode)].border[Variant.Secondary][
87+
InteractionState.Default
88+
]};
89+
border-radius: ${borderRadius[400]}px;
90+
display: flex;
91+
flex-direction: column;
92+
gap: ${spacing[200]}px;
93+
`}
94+
>
95+
<Subtitle>Reference Element</Subtitle>
96+
<Body>{fillerText}</Body>
97+
</div>
98+
);
99+
};
100+
101+
const MockContent = () => {
102+
const fillerText = useMemo(() => faker.lorem.paragraphs(10), []);
103+
104+
return (
105+
<div
106+
className={css`
107+
padding: ${spacing[400]}px;
108+
`}
109+
>
110+
<Subtitle>Content Element</Subtitle>
111+
<Body>{fillerText}</Body>
112+
</div>
113+
);
114+
};
115+
116+
const TemplateComponent: StoryFn<ContextDrawerProps> = (
117+
props: ContextDrawerProps,
118+
) => {
119+
const [isOpen, setIsOpen] = useState(props.defaultOpen);
120+
121+
const isOpenVal = props.isOpen ?? isOpen;
122+
123+
const toggle = () => {
124+
setIsOpen(!isOpen);
125+
};
126+
127+
return (
128+
<ContextDrawer
129+
{...props}
130+
className={css`
131+
width: 100%;
132+
`}
133+
reference={<MockReference darkMode={props.darkMode} />}
134+
content={<MockContent />}
135+
trigger={
136+
<ContextDrawerButton isOpen={isOpenVal} onClick={toggle}>
137+
{isOpenVal ? 'Hide' : 'View'}
138+
</ContextDrawerButton>
139+
}
140+
isOpen={isOpenVal}
141+
/>
142+
);
143+
};
144+
145+
export const LiveExample: StoryObj<ContextDrawerProps> = {
146+
render: TemplateComponent,
147+
parameters: {
148+
chromatic: {
149+
disableSnapshot: true,
150+
},
151+
},
152+
};
153+
154+
export const Controlled: StoryObj<ContextDrawerProps> = {
155+
render: TemplateComponent,
156+
parameters: {
157+
controls: {
158+
exclude: [...defaultExcludedControls, 'defaultOpen', 'expandedHeight'],
159+
},
160+
chromatic: {
161+
disableSnapshot: true,
162+
},
163+
},
164+
args: {
165+
isOpen: true,
166+
},
167+
};
168+
169+
export const Generated: StoryObj<ContextDrawerProps> = {
170+
render: () => <></>,
171+
};

0 commit comments

Comments
 (0)