Skip to content

Commit 90177d0

Browse files
authored
Disable drawer background scrolling if drawer has background overlay (#1156)
1 parent 03869fa commit 90177d0

File tree

3 files changed

+299
-1
lines changed

3 files changed

+299
-1
lines changed

src/Drawer/Drawer.jsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, {
2-
createContext, useEffect, useState, useCallback,
2+
createContext, useEffect, useState, useCallback, useRef,
33
} from 'react';
44
import * as propTypes from 'prop-types';
55
import classNames from 'classnames';
@@ -33,6 +33,7 @@ const Drawer = ({
3333
size,
3434
onRequestClose,
3535
}) => {
36+
const isCurrentlyOpen = useRef(false);
3637
const [expanded, setExpanded] = useState(defaultExpanded);
3738

3839
const handleExpand = () => setExpanded(!expanded);
@@ -58,6 +59,28 @@ const Drawer = ({
5859
};
5960
}, [handleEscKeyPress, visible]);
6061

62+
useEffect(() => {
63+
// isCurrentlyOpen ref accounts for a case where you could have multiple drawers
64+
// on one page and you try to access one of them via their url. Without using ref, the
65+
// Drawer--open would be potentially removed via other
66+
// closed drawer because of a race condition
67+
function disableBackgroundScrolling() {
68+
if (visible && !isCurrentlyOpen.current) {
69+
document.body.classList.add('Drawer--open');
70+
isCurrentlyOpen.current = true;
71+
}
72+
73+
if (!visible && isCurrentlyOpen.current) {
74+
document.body.classList.remove('Drawer--open');
75+
isCurrentlyOpen.current = false;
76+
}
77+
}
78+
79+
if (hasBackgroundOverlay) {
80+
disableBackgroundScrolling();
81+
}
82+
}, [hasBackgroundOverlay, visible]);
83+
6184
return (
6285
<>
6386
{

src/Drawer/Drawer.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@
5050
display: flex;
5151
flex-direction: column;
5252

53+
&--open {
54+
overflow: hidden;
55+
}
56+
5357
&--behind-nav {
5458
padding-top: 48px;
5559
}

src/Drawer/Drawer.test.jsx

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import React, { useState } from 'react';
2+
import propTypes from 'prop-types';
3+
4+
import { render, screen } from '@testing-library/react';
5+
import userEvent from '@testing-library/user-event';
6+
7+
import Drawer from './Drawer';
8+
9+
const elements = {
10+
drawerHeader: {
11+
get: () => screen.getByRole('heading', { level: 1, name: /drawer header/ }),
12+
},
13+
drawerChildren: {
14+
get: () => screen.getByText('children'),
15+
},
16+
drawerOverlay: {
17+
get: () => screen.getByRole('presentation'),
18+
query: () => screen.queryByRole('presentation'),
19+
},
20+
drawerOneToggleVisibilityButton: {
21+
get: () => screen.getByRole('button', { name: /toggle visibility drawerOne/ }),
22+
},
23+
drawerTwoToggleVisibilityButton: {
24+
get: () => screen.getByRole('button', { name: /toggle visibility drawerTwo/ }),
25+
},
26+
drawerThreeToggleVisibilityButton: {
27+
get: () => screen.getByRole('button', { name: /toggle visibility drawerThree/ }),
28+
},
29+
};
30+
31+
function SetupDrawerWithChildren(props) {
32+
const defaultProps = {
33+
onRequestClose: () => {},
34+
};
35+
36+
return (
37+
<Drawer {...defaultProps} {...props}>
38+
<div>children</div>
39+
</Drawer>
40+
);
41+
}
42+
43+
function SetupMultipleDrawers({
44+
drawerOneVisibleDefault,
45+
drawerTwoVisibleDefault,
46+
drawerThreeVisibleDefault,
47+
}) {
48+
const [isDrawerOneVisible, setIsDrawerOneVisible] = useState(drawerOneVisibleDefault);
49+
const [isDrawerTwoVisible, setIsDrawerTwoVisible] = useState(drawerTwoVisibleDefault);
50+
const [isDrawerThreeVisible, setIsDrawerThreeVisible] = useState(drawerThreeVisibleDefault);
51+
52+
return (
53+
<div>
54+
<button
55+
type="button"
56+
onClick={() => setIsDrawerOneVisible((prevState) => !prevState)}
57+
>
58+
toggle visibility drawerOne
59+
</button>
60+
61+
<button
62+
type="button"
63+
onClick={() => setIsDrawerTwoVisible((prevState) => !prevState)}
64+
>
65+
toggle visibility drawerTwo
66+
</button>
67+
68+
<button
69+
type="button"
70+
onClick={() => setIsDrawerThreeVisible((prevState) => !prevState)}
71+
>
72+
toggle visibility drawerThree
73+
</button>
74+
75+
<Drawer
76+
visible={isDrawerOneVisible}
77+
onRequestClose={() => setIsDrawerOneVisible(false)}
78+
>
79+
<div>childrenDrawerOne</div>
80+
</Drawer>
81+
<Drawer
82+
visible={isDrawerTwoVisible}
83+
onRequestClose={() => setIsDrawerTwoVisible(false)}
84+
>
85+
<div>childrenDrawerTwo</div>
86+
</Drawer>
87+
<Drawer
88+
visible={isDrawerThreeVisible}
89+
onRequestClose={() => setIsDrawerThreeVisible(false)}
90+
>
91+
<div>childrenDrawerThree</div>
92+
</Drawer>
93+
</div>
94+
);
95+
}
96+
97+
SetupMultipleDrawers.propTypes = {
98+
drawerOneVisibleDefault: propTypes.bool,
99+
drawerThreeVisibleDefault: propTypes.bool,
100+
drawerTwoVisibleDefault: propTypes.bool,
101+
};
102+
103+
SetupMultipleDrawers.defaultProps = {
104+
drawerOneVisibleDefault: false,
105+
drawerTwoVisibleDefault: false,
106+
drawerThreeVisibleDefault: false,
107+
};
108+
109+
describe('Drawer', () => {
110+
beforeEach(() => {
111+
// Need to manually clean classList on body since jsdom instance can stay
112+
// the same across specs https://github.com/jestjs/jest/issues/1224
113+
window.document.body.classList.remove(...window.document.body.classList);
114+
});
115+
116+
describe('When component renders single drawer', () => {
117+
describe('when visible is false', () => {
118+
it('renders its children', () => {
119+
render(<SetupDrawerWithChildren visible={false} />);
120+
121+
expect(elements.drawerChildren.get()).toBeInTheDocument();
122+
});
123+
124+
it('has drawer overlay', () => {
125+
render(<SetupDrawerWithChildren visible={false} />);
126+
127+
expect(elements.drawerOverlay.get()).toBeInTheDocument();
128+
});
129+
130+
it('does not have visible drawer overlay', () => {
131+
render(<SetupDrawerWithChildren visible={false} />);
132+
133+
expect(elements.drawerOverlay.get()).toBeInTheDocument();
134+
expect(elements.drawerOverlay.get().classList).not.toContain('DrawerBackgroundOverlay--active');
135+
});
136+
137+
it('does not call onRequestClose when pressing ESC key', () => {
138+
const onRequestClose = jest.fn();
139+
140+
render(<SetupDrawerWithChildren visible={false} onRequestClose={onRequestClose} />);
141+
142+
userEvent.keyboard('{Escape}');
143+
144+
expect(onRequestClose).not.toHaveBeenCalled();
145+
});
146+
147+
it('body tag does not have Drawer--open', () => {
148+
const { container } = render(<SetupDrawerWithChildren visible={false} />);
149+
const body = container.closest('body');
150+
151+
expect(body.classList).not.toContain('Drawer--open');
152+
});
153+
154+
describe('when hasBackgroundOverlay is false', () => {
155+
it('does not have drawer overlay', () => {
156+
render(<SetupDrawerWithChildren hasBackgroundOverlay={false} visible={false} />);
157+
158+
expect(elements.drawerOverlay.query()).not.toBeInTheDocument();
159+
});
160+
161+
it('body tag does not have Drawer--open', () => {
162+
// eslint-disable-next-line max-len
163+
const { container } = render(<SetupDrawerWithChildren hasBackgroundOverlay={false} visible={false} />);
164+
const body = container.closest('body');
165+
166+
expect(body.classList).not.toContain('Drawer--open');
167+
});
168+
});
169+
});
170+
171+
describe('when visible is true', () => {
172+
it('renders its children', () => {
173+
render(<SetupDrawerWithChildren visible />);
174+
175+
expect(elements.drawerChildren.get()).toBeInTheDocument();
176+
});
177+
178+
it('has drawer overlay', () => {
179+
render(<SetupDrawerWithChildren visible />);
180+
181+
expect(elements.drawerOverlay.get()).toBeInTheDocument();
182+
});
183+
184+
it('has visible drawer overlay', () => {
185+
render(<SetupDrawerWithChildren visible />);
186+
187+
expect(elements.drawerOverlay.get()).toBeInTheDocument();
188+
expect(elements.drawerOverlay.get().classList).toContain('DrawerBackgroundOverlay--active');
189+
});
190+
191+
it('calls onRequestClose when pressing ESC key', () => {
192+
const onRequestClose = jest.fn();
193+
194+
render(<SetupDrawerWithChildren visible onRequestClose={onRequestClose} />);
195+
196+
userEvent.keyboard('{Escape}');
197+
198+
expect(onRequestClose).toHaveBeenCalled();
199+
});
200+
201+
it('body tag has Drawer--open', () => {
202+
const { container } = render(<SetupDrawerWithChildren visible />);
203+
const body = container.closest('body');
204+
205+
expect(body.classList).toContain('Drawer--open');
206+
});
207+
208+
describe('when hasBackgroundOverlay is false', () => {
209+
it('does not have drawer overlay', () => {
210+
render(<SetupDrawerWithChildren hasBackgroundOverlay={false} visible />);
211+
212+
expect(elements.drawerOverlay.query()).not.toBeInTheDocument();
213+
});
214+
215+
it('body tag does not have Drawer--open', () => {
216+
// eslint-disable-next-line max-len
217+
const { container } = render(<SetupDrawerWithChildren hasBackgroundOverlay={false} visible />);
218+
const body = container.closest('body');
219+
220+
expect(body.classList).not.toContain('Drawer--open');
221+
});
222+
});
223+
});
224+
});
225+
226+
describe('When component renders multiple Drawers', () => {
227+
describe('with drawerOne visible by default', () => {
228+
it('body tag has Drawer--open', () => {
229+
const { container } = render(<SetupMultipleDrawers drawerOneVisibleDefault />);
230+
const body = container.closest('body');
231+
232+
expect(body.classList).toContain('Drawer--open');
233+
});
234+
235+
describe('when user clicks on drawerOne toggle visibility button', () => {
236+
it('body tag does not have Drawer--open after click', () => {
237+
const { container } = render(<SetupMultipleDrawers drawerOneVisibleDefault />);
238+
const body = container.closest('body');
239+
240+
expect(body.classList).toContain('Drawer--open');
241+
242+
userEvent.click(elements.drawerOneToggleVisibilityButton.get());
243+
244+
expect(body.classList).not.toContain('Drawer--open');
245+
});
246+
});
247+
});
248+
249+
describe('with no drawers visible by default', () => {
250+
it('body tag does not have Drawer--open', () => {
251+
const { container } = render(<SetupMultipleDrawers />);
252+
const body = container.closest('body');
253+
254+
expect(body.classList).not.toContain('Drawer--open');
255+
});
256+
257+
describe('when user clicks on drawerOne toggle visibility button', () => {
258+
it('body tag has Drawer--open after click', () => {
259+
const { container } = render(<SetupMultipleDrawers />);
260+
const body = container.closest('body');
261+
262+
expect(body.classList).not.toContain('Drawer--open');
263+
264+
userEvent.click(elements.drawerOneToggleVisibilityButton.get());
265+
266+
expect(body.classList).toContain('Drawer--open');
267+
});
268+
});
269+
});
270+
});
271+
});

0 commit comments

Comments
 (0)