Skip to content

Commit

Permalink
Merge pull request #243 from edx/alasdair/modal-enable-click-outside-…
Browse files Browse the repository at this point in the history
…to-close

fix(modal): enable closing modal when click outside it
  • Loading branch information
AlasdairSwan authored May 9, 2018
2 parents 7ef554c + 7a1326c commit fc52464
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 24 deletions.
16 changes: 16 additions & 0 deletions src/Modal/Modal.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,23 +135,27 @@ describe('<Modal />', () => {
jest.spyOn(console, 'error');
global.console.error.mockImplementation(() => {});
});

afterEach(() => {
global.console.error.mockRestore();
});

it('component receives props', () => {
wrapper = mount(<Modal title={title} body={body} onClose={() => {}} />);

modalOpen(false, wrapper);
wrapper.setProps({ open: true });
modalOpen(true, wrapper);
});

it('component receives props and ignores prop change', () => {
wrapper = mount(<Modal {...defaultProps} />);

modalOpen(true, wrapper);
wrapper.setProps({ title: 'Changed modal title' });
modalOpen(true, wrapper);
});

it('throws an error when an invalid parentSelector prop is passed', () => {
wrapper = mount(<ErrorWrappedModal
{...defaultProps}
Expand Down Expand Up @@ -183,6 +187,13 @@ describe('<Modal />', () => {
wrapper.find('button').at(0).simulate('keyDown', { key: 'Escape' });
modalOpen(false, wrapper);
});

it('closes when a user clicks outside of the modal', () => {
modalOpen(true, wrapper);
wrapper.find('.modal-backdrop').at(0).simulate('click');
modalOpen(false, wrapper);
});

it('calls callback function on close', () => {
const spy = jest.fn();

Expand All @@ -197,6 +208,7 @@ describe('<Modal />', () => {
wrapper.find('button').at(0).simulate('click');
expect(spy).toHaveBeenCalledTimes(1);
});

it('reopens after closed', () => {
modalOpen(true, wrapper);
wrapper.find('button').at(0).simulate('click');
Expand All @@ -223,6 +235,7 @@ describe('<Modal />', () => {
expect(buttons.at(0).html()).toEqual(document.activeElement.outerHTML);
modalOpen(true, wrapper);
});

it('does nothing on invalid keystroke + ctrl', () => {
const buttons = wrapper.find('button');

Expand All @@ -245,6 +258,7 @@ describe('<Modal />', () => {
it('has correct initial focus', () => {
expect(buttons.at(0).html()).toEqual(document.activeElement.outerHTML);
});

it('has reset focus after close and reopen', () => {
expect(buttons.at(0).html()).toEqual(document.activeElement.outerHTML);
wrapper.setProps({ open: false });
Expand All @@ -253,11 +267,13 @@ describe('<Modal />', () => {
modalOpen(true, wrapper);
expect(buttons.at(0).html()).toEqual(document.activeElement.outerHTML);
});

it('traps focus forwards on tab keystroke', () => {
expect(buttons.at(0).html()).toEqual(document.activeElement.outerHTML);
buttons.last().simulate('keyDown', { key: 'Tab' });
expect(buttons.at(0).html()).toEqual(document.activeElement.outerHTML);
});

it('traps focus backwards on shift + tab keystroke', () => {
expect(buttons.at(0).html()).toEqual(document.activeElement.outerHTML);
buttons.at(0).simulate('keyDown', { key: 'Tab', shiftKey: true });
Expand Down
49 changes: 25 additions & 24 deletions src/Modal/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,14 @@ class Modal extends React.Component {
[styles.show]: open,
[styles.fade]: !open,
})}
role="presentation"
onClick={this.close}
/>
<div
className={classNames(
styles.modal,
{
[styles['modal-dialog']]: open,
[styles['d-block']]: open,
[styles.show]: open,
[styles.fade]: !open,
Expand All @@ -188,34 +191,32 @@ class Modal extends React.Component {
{...(!renderHeaderCloseButton ? { tabIndex: '-1' } : {})}
{...(!renderHeaderCloseButton ? { ref: this.setFirstFocusableElement } : {})}
>
<div className={styles['modal-dialog']}>
<div className={styles['modal-content']}>
<div className={styles['modal-header']}>
<h2 className={styles['modal-title']} id={this.headerId}>{this.props.title}</h2>
{ renderHeaderCloseButton &&
<Button
label={<Icon className={['fa', 'fa-times']} />}
className={['p-1']}
aria-label={this.props.closeText}
onClick={this.close}
inputRef={this.setFirstFocusableElement}
onKeyDown={this.handleKeyDown}
/>
}
</div>
<div className={styles['modal-body']}>
{this.renderBody()}
</div>
<div className={styles['modal-footer']}>
{this.renderButtons()}
<div className={styles['modal-content']}>
<div className={styles['modal-header']}>
<h2 className={styles['modal-title']} id={this.headerId}>{this.props.title}</h2>
{ renderHeaderCloseButton &&
<Button
label={this.props.closeText}
buttonType="secondary"
label={<Icon className={['fa', 'fa-times']} />}
className={['p-1']}
aria-label={this.props.closeText}
onClick={this.close}
inputRef={this.setCloseButton}
inputRef={this.setFirstFocusableElement}
onKeyDown={this.handleKeyDown}
/>
</div>
}
</div>
<div className={styles['modal-body']}>
{this.renderBody()}
</div>
<div className={styles['modal-footer']}>
{this.renderButtons()}
<Button
label={this.props.closeText}
buttonType="secondary"
onClick={this.close}
inputRef={this.setCloseButton}
onKeyDown={this.handleKeyDown}
/>
</div>
</div>
</div>
Expand Down

0 comments on commit fc52464

Please sign in to comment.