Skip to content

Commit

Permalink
fix(Drawer): fix the focus cannot be moved to elements outside the Dr…
Browse files Browse the repository at this point in the history
…awer when `backdrop=false` (#3716)
  • Loading branch information
simonguo committed Apr 3, 2024
1 parent 99ef0b3 commit f044445
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 79 deletions.
5 changes: 5 additions & 0 deletions src/Drawer/styles/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
left: 0;
width: 100%;
height: 100%;

&.rs-drawer-no-backdrop {
pointer-events: none;
}
}

// Container that the drawer scrolls within
Expand All @@ -18,6 +22,7 @@
position: fixed;
z-index: @zindex-drawer;
box-shadow: var(--rs-drawer-shadow);
pointer-events: auto;
// Prevent Chrome on Windows from adding a focus outline. For details, see
// https://github.com/twbs/bootstrap/pull/10951.
outline: 0;
Expand Down
111 changes: 102 additions & 9 deletions src/Drawer/test/DrawerSpec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,22 +71,115 @@ describe('Drawer', () => {
});

it('Should close the drawer when the backdrop is clicked', () => {
const onCloseSpy = sinon.spy();
const onClose = sinon.spy();

render(<Drawer data-testid="wrapper" open onClose={onCloseSpy} />);
render(<Drawer open onClose={onClose} />);

userEvent.click(screen.getByTestId('wrapper'));
userEvent.click(screen.getByTestId('drawer-wrapper'));

expect(onCloseSpy).to.have.been.calledOnce;
expect(onClose).to.have.been.calledOnce;
});

it('Should not close the drawer when the "static" drawer is clicked', () => {
const onCloseSpy = sinon.spy();
render(<Drawer data-testid="wrapper" open onClose={onCloseSpy} backdrop="static" />);
const onClose = sinon.spy();
render(<Drawer open onClose={onClose} backdrop="static" />);

userEvent.click(screen.getByTestId('wrapper'));
userEvent.click(screen.getByTestId('drawer-wrapper'));

expect(onCloseSpy).to.not.have.been.calledOnce;
expect(onClose).to.not.have.been.calledOnce;
});

it('Should render backdrop', () => {
render(<Drawer open backdrop />);

expect(screen.queryByTestId('backdrop')).to.exist;
expect(screen.getByTestId('drawer-wrapper')).to.not.have.class('rs-drawer-no-backdrop');
});

it('Should not render backdrop', () => {
render(<Drawer open backdrop={false} />);

expect(screen.queryByTestId('backdrop')).to.not.exist;
expect(screen.getByTestId('drawer-wrapper')).to.have.class('rs-drawer-no-backdrop');
});

describe('Focused state', () => {
let focusableContainer: HTMLElement | null = null;

beforeEach(() => {
focusableContainer = document.createElement('div');
focusableContainer.tabIndex = 0;
document.body.appendChild(focusableContainer);
focusableContainer.focus();
});

afterEach(() => {
document.body.removeChild(focusableContainer as HTMLElement);
});

it('Should focus on the Drawer when it is opened', () => {
const onOpen = sinon.spy();

const { rerender } = render(
<Drawer onOpen={onOpen} open={false}>
<Drawer.Header />
</Drawer>
);

expect(focusableContainer).to.have.focus;

rerender(
<Drawer onOpen={onOpen} open={true}>
<Drawer.Header />
</Drawer>
);

expect(onOpen).to.have.been.calledOnce;
expect(screen.getByTestId('drawer-wrapper')).to.have.focus;
});

it('Should be forced to focus on Drawer', () => {
render(
<Drawer open backdrop={false} enforceFocus>
test
</Drawer>
);
(focusableContainer as HTMLElement).focus();
(focusableContainer as HTMLElement).dispatchEvent(new FocusEvent('focus'));

expect(screen.getByTestId('drawer-wrapper')).to.have.focus;
});

it('Should be focused on container outside of Drawer', () => {
render(
<Drawer open backdrop={false} enforceFocus={false}>
test
</Drawer>
);
(focusableContainer as HTMLElement).focus();
(focusableContainer as HTMLElement).dispatchEvent(new FocusEvent('focus'));

expect(focusableContainer).to.have.focus;
});

it('Should be focused on container outside of Drawer when backdrop is not displayed', () => {
render(
<Drawer open backdrop={false}>
test
</Drawer>
);
(focusableContainer as HTMLElement).focus();
(focusableContainer as HTMLElement).dispatchEvent(new FocusEvent('focus'));

expect(focusableContainer).to.have.focus;
});

it('Should only call onOpen once', () => {
const onOpen = sinon.spy();
render(<Drawer open onOpen={onOpen}></Drawer>);

expect(onOpen).to.be.calledOnce;
});
});

describe('Size variants', () => {
Expand All @@ -110,7 +203,7 @@ describe('Drawer', () => {
expect(screen.getByRole('dialog')).to.have.class('rs-drawer-full');

expect(console.warn).to.have.been.calledWith(
'"full" property of "Modal" has been deprecated.\nUse size="full" instead.'
'"full" property of "Drawer" has been deprecated.\nUse size="full" instead.'
);
});

Expand Down
47 changes: 20 additions & 27 deletions src/Drawer/test/DrawerStylesSpec.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,37 @@
import React from 'react';
import Drawer from '../index';
import { render } from '@testing-library/react';
import { getStyle } from '@test/utils';
import { render, screen } from '@testing-library/react';

import '../styles/index.less';

describe('Drawer styles', () => {
it('Should render the correct styles', () => {
const instanceRef = React.createRef();
render(<Drawer open />);

// FIXME Add missing `ref` delcaration for Drawer
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
render(<Drawer ref={instanceRef} open />);

const drawer = (instanceRef.current as HTMLDivElement).querySelector(
'.rs-drawer'
) as HTMLElement;

assert.equal(getStyle(drawer, 'position'), 'fixed');
assert.equal(getStyle(drawer, 'zIndex'), '1050');
assert.equal(getStyle(drawer, 'overflow'), 'visible');
expect(screen.getByRole('dialog')).to.have.style('z-index', '1050');
expect(screen.getByRole('dialog')).to.have.style('position', 'fixed');
expect(screen.getByRole('dialog')).to.have.style('overflow', 'visible');
});

it('Should have a wrapper that fills the window', () => {
const instanceRef = React.createRef();
// FIXME Add missing `ref` delcaration for Drawer
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
render(<Drawer ref={instanceRef} open />);
render(<Drawer open />);

const wrapper = instanceRef.current as HTMLDivElement;
const wrapper = screen.getByTestId('drawer-wrapper');
const windowHeight = window.innerHeight;
const windowWidth = window.innerWidth;

assert.equal(getStyle(wrapper, 'position'), 'fixed');
assert.equal(getStyle(wrapper, 'zIndex'), '1050');
assert.equal(getStyle(wrapper, 'width'), `${windowWidth}px`);
assert.equal(getStyle(wrapper, 'height'), `${windowHeight}px`);
assert.equal(getStyle(wrapper, 'left'), `0px`);
assert.equal(getStyle(wrapper, 'top'), `0px`);
expect(wrapper).to.have.style('position', 'fixed');
expect(wrapper).to.have.style('z-index', '1050');
expect(wrapper).to.have.style('width', `${windowWidth}px`);
expect(wrapper).to.have.style('height', `${windowHeight}px`);
expect(wrapper).to.have.style('left', `0px`);
expect(wrapper).to.have.style('top', `0px`);
});

it('Should not render backdrop', () => {
render(<Drawer open backdrop={false} />);

expect(screen.getByTestId('drawer-wrapper')).to.have.style('pointer-events', 'none');
expect(screen.getByRole('dialog')).to.have.style('pointer-events', 'auto');
});
});
40 changes: 29 additions & 11 deletions src/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,29 +67,30 @@ interface ModalComponent extends RsRefForwardingComponent<'div', ModalProps> {
*/
const Modal: ModalComponent = React.forwardRef((props: ModalProps, ref) => {
const {
animation = Bounce,
animationProps,
animationTimeout = 300,
'aria-labelledby': ariaLabelledby,
'aria-describedby': ariaDescribedby,
backdropClassName,
backdrop = true,
className,
children,
classPrefix = 'modal',
dialogClassName,
backdropClassName,
backdrop = true,
dialogStyle,
animation = Bounce,
open,
size = 'sm',
full,
dialogAs: Dialog = ModalDialog,
animationProps,
animationTimeout = 300,
enforceFocus: enforceFocusProp,
full,
overflow = true,
open,
onClose,
onEntered,
onEntering,
onExited,
role = 'dialog',
size = 'sm',
id: idProp,
'aria-labelledby': ariaLabelledby,
'aria-describedby': ariaDescribedby,
...rest
} = props;

Expand Down Expand Up @@ -199,15 +200,32 @@ const Modal: ModalComponent = React.forwardRef((props: ModalProps, ref) => {
sizeKey = placement === 'top' || placement === 'bottom' ? 'height' : 'width';
}

const enforceFocus = useMemo(() => {
if (typeof enforceFocusProp === 'boolean') {
return enforceFocusProp;
}

// When the Drawer is displayed and the backdrop is not displayed, the focus is not restricted.
if (isDrawer && backdrop === false) {
return false;
}
}, [backdrop, enforceFocusProp, isDrawer]);

const wrapperClassName = merge(prefix`wrapper`, {
[prefix`no-backdrop`]: backdrop === false
});

return (
<ModalContext.Provider value={modalContextValue}>
<BaseModal
data-testid={isDrawer ? 'drawer-wrapper' : 'modal-wrapper'}
{...rest}
ref={ref}
backdrop={backdrop}
enforceFocus={enforceFocus}
open={open}
onClose={onClose}
className={prefix`wrapper`}
className={wrapperClassName}
onEntered={handleEntered}
onEntering={handleEntering}
onExited={handleExited}
Expand Down

0 comments on commit f044445

Please sign in to comment.