Skip to content

Commit 1da22b2

Browse files
committed
feat: Add optional width and zIndex props to SideSheet component
1 parent 8058032 commit 1da22b2

5 files changed

Lines changed: 343 additions & 6 deletions

File tree

src/organisms/SideSheet/SideSheet.stories.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,68 @@ export const StatefulModal: Story = {
335335
},
336336
};
337337

338+
export const CustomWidth: Story = {
339+
args: {
340+
type: 'standard',
341+
width: 800,
342+
},
343+
play: async ({ canvasElement, context }) => {
344+
const canvas = within(canvasElement);
345+
const { title } = context.args;
346+
347+
const heading = await canvas.findByRole('heading', {
348+
name: title as string,
349+
level: 2,
350+
});
351+
const sheet = heading.parentElement?.parentElement;
352+
353+
if (!(sheet instanceof HTMLElement)) {
354+
throw new Error('Could not locate the side sheet element');
355+
}
356+
357+
await waitFor(() => {
358+
expect(sheet.offsetWidth).toBe(800);
359+
});
360+
},
361+
};
362+
363+
export const CustomZIndex: Story = {
364+
args: {
365+
type: 'modal',
366+
zIndex: 1234,
367+
},
368+
parameters: {
369+
layout: 'fullscreen',
370+
},
371+
decorators: (Story) => (
372+
<FloatingStoryWrapper>
373+
<Story />
374+
</FloatingStoryWrapper>
375+
),
376+
play: async ({ canvasElement, context }) => {
377+
const canvas = within(canvasElement);
378+
const { title } = context.args;
379+
380+
const heading = await canvas.findByRole('heading', {
381+
name: title as string,
382+
level: 2,
383+
});
384+
385+
let element: HTMLElement | null = heading;
386+
let foundZIndex: string | null = null;
387+
while (element) {
388+
const computed = getComputedStyle(element).zIndex;
389+
if (computed && computed !== 'auto') {
390+
foundZIndex = computed;
391+
break;
392+
}
393+
element = element.parentElement;
394+
}
395+
396+
await expect(foundZIndex).toBe('1234');
397+
},
398+
};
399+
338400
export const StatefulModalWithScrim: Story = {
339401
args: {
340402
type: 'modal',

src/organisms/SideSheet/SideSheet.styles.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { CSSProperties } from 'react';
2+
13
import { colors, elevation, shape, spacings } from 'src/atoms/style';
24
import type { SideSheetProps } from 'src/organisms';
35

@@ -7,11 +9,12 @@ import { css, styled } from 'styled-components';
79
interface WrapperProps {
810
$type: NonNullable<SideSheetProps['type']>;
911
$withShadow?: boolean;
12+
$zIndex?: CSSProperties['zIndex'];
1013
}
1114

1215
export const Wrapper = styled(motion.div)<WrapperProps>`
1316
overflow: hidden;
14-
${({ $type, $withShadow }) => {
17+
${({ $type, $withShadow, $zIndex }) => {
1518
if ($type === 'standard') return '';
1619
1720
if ($type === 'floating') {
@@ -21,6 +24,10 @@ export const Wrapper = styled(motion.div)<WrapperProps>`
2124
right: ${spacings.medium};
2225
box-shadow: ${elevation.sticky};
2326
border-radius: ${shape.corners.borderRadius};
27+
${$zIndex !== undefined &&
28+
css`
29+
z-index: ${$zIndex};
30+
`}
2431
`;
2532
}
2633
@@ -29,18 +36,27 @@ export const Wrapper = styled(motion.div)<WrapperProps>`
2936
top: 64px;
3037
right: 0;
3138
box-shadow: ${$withShadow ? elevation.above_scrim : 'none'};
39+
${$zIndex !== undefined &&
40+
css`
41+
z-index: ${$zIndex};
42+
`}
3243
`;
3344
}}
3445
`;
3546

3647
interface SheetProps {
3748
$type: NonNullable<SideSheetProps['type']>;
49+
$width?: CSSProperties['width'];
3850
}
3951

4052
export const Sheet = styled.div<SheetProps>`
4153
display: flex;
4254
flex-direction: column;
43-
width: 500px;
55+
width: ${({ $width }) => {
56+
if ($width === undefined) return '500px';
57+
if (typeof $width === 'number') return `${$width}px`;
58+
return $width;
59+
}};
4460
background: ${colors.ui.background__default.rgba};
4561
${({ $type }) => {
4662
if ($type === 'standard') {
@@ -64,12 +80,16 @@ export const Header = styled.div`
6480
padding: ${spacings.medium} ${spacings.large};
6581
`;
6682

67-
export const ScrimWrapper = styled(motion.div)`
83+
interface ScrimWrapperProps {
84+
$zIndex?: CSSProperties['zIndex'];
85+
}
86+
87+
export const ScrimWrapper = styled(motion.div)<ScrimWrapperProps>`
6888
position: absolute;
6989
top: 0;
7090
left: 0;
7191
width: 100%;
7292
overflow: hidden;
7393
height: calc(100vh - 64px);
74-
z-index: 10000;
94+
z-index: ${({ $zIndex }) => $zIndex ?? 10000};
7595
`;
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import { faker } from '@faker-js/faker';
2+
3+
import { colors, elevation, shape, spacings } from 'src/atoms/style';
4+
import { render, screen, userEvent } from 'src/tests/jsdomtest-utils';
5+
import { SideSheet } from './SideSheet';
6+
7+
function getRenderedElements(content: string) {
8+
const sheet = screen.getByText(content).parentElement!;
9+
const wrapper = sheet.parentElement!;
10+
11+
return { sheet, wrapper };
12+
}
13+
14+
test('renders standard content and header elements', () => {
15+
const title = faker.animal.dog();
16+
const content = faker.lorem.sentence();
17+
const headerAction = faker.company.buzzVerb();
18+
19+
render(
20+
<SideSheet
21+
open
22+
onClose={vi.fn()}
23+
title={title}
24+
headerElements={<button type="button">{headerAction}</button>}
25+
>
26+
<div>{content}</div>
27+
</SideSheet>
28+
);
29+
30+
expect(screen.getByText(title)).toBeInTheDocument();
31+
expect(screen.getByText(content)).toBeInTheDocument();
32+
expect(
33+
screen.getByRole('button', {
34+
name: headerAction,
35+
})
36+
).toBeInTheDocument();
37+
});
38+
39+
test('does not render sheet content when closed', () => {
40+
const title = faker.animal.cat();
41+
const content = faker.lorem.sentence();
42+
43+
render(
44+
<SideSheet open={false} onClose={vi.fn()} title={title}>
45+
<div>{content}</div>
46+
</SideSheet>
47+
);
48+
49+
expect(screen.queryByText(title)).not.toBeInTheDocument();
50+
expect(screen.queryByText(content)).not.toBeInTheDocument();
51+
expect(
52+
screen.queryByRole('button', {
53+
name: /close side sheet/i,
54+
})
55+
).not.toBeInTheDocument();
56+
});
57+
58+
test('calls onClose when clicking the close button', async () => {
59+
const title = faker.animal.crocodilia();
60+
const content = faker.lorem.sentence();
61+
const handleOnClose = vi.fn();
62+
const user = userEvent.setup();
63+
64+
render(
65+
<SideSheet open onClose={handleOnClose} title={title}>
66+
<div>{content}</div>
67+
</SideSheet>
68+
);
69+
70+
await user.click(
71+
screen.getByRole('button', {
72+
name: /close side sheet/i,
73+
})
74+
);
75+
76+
expect(handleOnClose).toHaveBeenCalledTimes(1);
77+
});
78+
79+
test('calls onClose when clicking the scrim', async () => {
80+
const title = faker.animal.fish();
81+
const content = faker.lorem.sentence();
82+
const handleOnClose = vi.fn();
83+
const user = userEvent.setup();
84+
85+
render(
86+
<SideSheet
87+
open
88+
onClose={handleOnClose}
89+
title={title}
90+
type="modal"
91+
withScrim
92+
>
93+
<div>{content}</div>
94+
</SideSheet>
95+
);
96+
97+
await user.click(screen.getByTestId('side-sheet-scrim'));
98+
99+
expect(handleOnClose).toHaveBeenCalledTimes(1);
100+
});
101+
102+
test('renders standard styles with the default width', () => {
103+
const title = faker.animal.horse();
104+
const content = faker.lorem.sentence();
105+
106+
render(
107+
<SideSheet open onClose={vi.fn()} title={title}>
108+
<div>{content}</div>
109+
</SideSheet>
110+
);
111+
112+
const { sheet } = getRenderedElements(content);
113+
114+
expect(sheet).toHaveStyleRule('width', '500px');
115+
expect(sheet).toHaveStyleRule('background', 'none');
116+
expect(sheet).toHaveStyleRule(
117+
'border-left',
118+
`2px solid ${colors.ui.background__heavy.rgba}`
119+
);
120+
});
121+
122+
test('renders modal styles with a numeric width and z-index', () => {
123+
const title = faker.animal.lion();
124+
const content = faker.lorem.sentence();
125+
126+
render(
127+
<SideSheet
128+
open
129+
onClose={vi.fn()}
130+
title={title}
131+
type="modal"
132+
width={640}
133+
zIndex={1234}
134+
>
135+
<div>{content}</div>
136+
</SideSheet>
137+
);
138+
139+
const { sheet, wrapper } = getRenderedElements(content);
140+
141+
expect(wrapper).toHaveStyleRule('position', 'fixed');
142+
expect(wrapper).toHaveStyleRule('top', '64px');
143+
expect(wrapper).toHaveStyleRule('right', '0');
144+
expect(wrapper).toHaveStyleRule('box-shadow', elevation.above_scrim);
145+
expect(wrapper).toHaveStyleRule('z-index', '1234');
146+
147+
expect(sheet).toHaveStyleRule('width', '640px');
148+
expect(sheet).toHaveStyleRule(
149+
'background',
150+
colors.ui.background__default.rgba
151+
);
152+
expect(sheet).toHaveStyleRule('height', 'calc(100vh - 64px)');
153+
});
154+
155+
test('renders floating styles with a custom z-index', () => {
156+
const title = faker.animal.rabbit();
157+
const content = faker.lorem.sentence();
158+
159+
render(
160+
<SideSheet
161+
open
162+
onClose={vi.fn()}
163+
title={title}
164+
type="floating"
165+
zIndex={4321}
166+
>
167+
<div>{content}</div>
168+
</SideSheet>
169+
);
170+
171+
const { wrapper } = getRenderedElements(content);
172+
173+
expect(wrapper).toHaveStyleRule('position', 'fixed');
174+
expect(wrapper).toHaveStyleRule('top', `calc(64px + ${spacings.medium})`);
175+
expect(wrapper).toHaveStyleRule('right', spacings.medium);
176+
expect(wrapper).toHaveStyleRule('box-shadow', elevation.sticky);
177+
expect(wrapper).toHaveStyleRule('border-radius', shape.corners.borderRadius);
178+
expect(wrapper).toHaveStyleRule('z-index', '4321');
179+
});
180+
181+
test('renders scrim styles, disables shadow and supports string widths', () => {
182+
const title = faker.animal.bird();
183+
const content = faker.lorem.sentence();
184+
const width = '40rem';
185+
const { rerender } = render(
186+
<SideSheet
187+
open
188+
onClose={vi.fn()}
189+
title={title}
190+
type="modal"
191+
withScrim
192+
width={width}
193+
>
194+
<div>{content}</div>
195+
</SideSheet>
196+
);
197+
198+
let elements = getRenderedElements(content);
199+
200+
expect(screen.getByTestId('side-sheet-scrim')).toHaveStyleRule(
201+
'z-index',
202+
'10000'
203+
);
204+
expect(elements.wrapper).toHaveStyleRule('box-shadow', 'none');
205+
expect(elements.sheet).toHaveStyleRule('width', width);
206+
207+
rerender(
208+
<SideSheet
209+
open
210+
onClose={vi.fn()}
211+
title={title}
212+
type="modal"
213+
withScrim
214+
width={width}
215+
zIndex={9876}
216+
>
217+
<div>{content}</div>
218+
</SideSheet>
219+
);
220+
221+
elements = getRenderedElements(content);
222+
223+
expect(screen.getByTestId('side-sheet-scrim')).toHaveStyleRule(
224+
'z-index',
225+
'9876'
226+
);
227+
expect(elements.wrapper).toHaveStyleRule('z-index', '9876');
228+
});

0 commit comments

Comments
 (0)