Skip to content

Commit

Permalink
Modal fix: disable scroll when modal is open, prevent click-through (#…
Browse files Browse the repository at this point in the history
…435)

* disable scroll when modal is open, prevent click-through

* always use add/removeEventListener

* add tests
  • Loading branch information
lipp authored and Fa-So committed Mar 5, 2018
1 parent 9023949 commit 8443922
Show file tree
Hide file tree
Showing 2 changed files with 242 additions and 33 deletions.
107 changes: 84 additions & 23 deletions src/js/modal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,96 @@
import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import keycode from 'keycode'

const scrollKeys = [
keycode('left'),
keycode('right'),
keycode('up'),
keycode('down'),
keycode('space'),
keycode('pgup'),
keycode('pgdn'),
keycode('end'),
keycode('home')
].reduce((scrollKeys, key) => {
scrollKeys[key] = true
return scrollKeys
}, {})

export const preventDefault = (e) => {
e.preventDefault()
e.returnValue = false
}

/**
* Modal component
*/
var Modal = ({header, body, footer, visible, toggle}) => (
<div
className={classNames('mdc-Modal-overlay', {'is-visible': visible})}
onClick={() => toggle(false)}
onTouchEnd={() => toggle(false)}
>
<div
className={classNames('mdc-Modal', {'is-visible': visible})}
onClick={(e) => e.stopPropagation()}
onTouchEnd={(e) => e.stopPropagation()}
>
<div className='mdc-Modal-content'>
<div className='mdc-Modal-header'>
{header}
</div>
<div className='mdc-Modal-body'>
{body}
class Modal extends React.Component {
handleScrollAndEscKeys = (e) => {
if (scrollKeys[e.keyCode]) {
preventDefault(e)
return false
} else if (e.keyCode === keycode('esc')) {
this.props.toggle(false)
}
}

disableScroll () {
window.addEventListener('DOMMouseScroll', preventDefault, false)
window.addEventListener('wheel', preventDefault, false)
window.addEventListener('touchmove', preventDefault, false)
window.addEventListener('keydown', this.handleScrollAndEscKeys, false)
}

enableScroll () {
window.removeEventListener('DOMMouseScroll', preventDefault, false)
window.removeEventListener('wheel', preventDefault, false)
window.removeEventListener('touchmove', preventDefault, false)
window.removeEventListener('keydown', this.handleScrollAndEscKeys, false)
}

componentWillReceiveProps (props) {
if (props.visible && !this.props.visible) {
document.activeElement.blur()
this.disableScroll()
} else if (!props.visible && this.props.visible) {
this.enableScroll()
}
}

render () {
const {header, body, footer, visible, toggle} = this.props
return (
<div
className={classNames('mdc-Modal-overlay', {'is-visible': visible})}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
toggle(false)
}}
>
<div
className={classNames('mdc-Modal', {'is-visible': visible})}
onClick={(e) => e.stopPropagation()}
onTouchEnd={(e) => e.stopPropagation()}
>
<div className='mdc-Modal-content'>
<div className='mdc-Modal-header'>
{header}
</div>
<div className='mdc-Modal-body'>
{body}
</div>
</div>
<div className='mdc-Modal-footer'>
{footer}
</div>
</div>
</div>
<div className='mdc-Modal-footer'>
{footer}
</div>
</div>
</div>
)
)
}
}

Modal.propTypes = {
header: PropTypes.element,
Expand Down
168 changes: 158 additions & 10 deletions src/js/modal/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import assert from 'assert'
import React from 'react'
import Modal from '../'
import {default as Modal, preventDefault} from '../'
import {mount} from 'enzyme'

describe('Modal', () => {
Expand All @@ -26,15 +26,6 @@ describe('Modal', () => {
wrapper.find('.mdc-Modal-overlay').simulate('click')
})

it('should callback when touching dark background', (done) => {
const callback = (visible) => {
assert.equal(visible, false)
done()
}
const wrapper = mount(<Modal visible toggle={callback} />)
wrapper.find('.mdc-Modal-overlay').simulate('touchend')
})

it('should have a custom header', () => {
const header = <p>my custom header</p>
const wrapper = mount(<Modal visible header={header} />)
Expand All @@ -52,4 +43,161 @@ describe('Modal', () => {
const wrapper = mount(<Modal visible footer={footer} />)
assert.equal(wrapper.find('.mdc-Modal-footer').text(), 'my custom footer')
})

it('.disableScroll() should replace window event listeners with preventDefault', () => {
const wrapper = mount(<Modal />)
const calls = []
const addEventListenerBak = window.addEventListener
window.addEventListener = (type, dispatch, flag) => {
calls.push({
type,
dispatch,
flag
})
}
wrapper.instance().disableScroll()
assert.deepEqual(calls[0], {
type: 'DOMMouseScroll',
dispatch: preventDefault,
flag: false
})
assert.deepEqual(calls[1], {
type: 'wheel',
dispatch: preventDefault,
flag: false
})
assert.deepEqual(calls[2], {
type: 'touchmove',
dispatch: preventDefault,
flag: false
})
assert.deepEqual(calls[3], {
type: 'keydown',
dispatch: wrapper.instance().handleScrollAndEscKeys,
flag: false
})
window.addEventListener = addEventListenerBak
})

it('.enableScroll() should restore event listeners', () => {
const wrapper = mount(<Modal />)
const addEventListenerBak = window.addEventListener
const removeEventListenerBak = window.removeEventListener
const calls = []
window.addEventListener = () => {}
window.removeEventListener = (type, dispatch, flag) => {
calls.push({
type,
dispatch,
flag
})
}
wrapper.instance().disableScroll()
wrapper.instance().enableScroll()
assert.deepEqual(calls[0], {
type: 'DOMMouseScroll',
dispatch: preventDefault,
flag: false
})
assert.deepEqual(calls[1], {
type: 'wheel',
dispatch: preventDefault,
flag: false
})
assert.deepEqual(calls[2], {
type: 'touchmove',
dispatch: preventDefault,
flag: false
})
assert.deepEqual(calls[3], {
type: 'keydown',
dispatch: wrapper.instance().handleScrollAndEscKeys,
flag: false
})
window.addEventListener = addEventListenerBak
window.removeEventListener = removeEventListenerBak
})

it('helper preventDefault should preventDefault and set returnValue=false', () => {
let wasCalled
const event = {
preventDefault: () => {
wasCalled = true
}
}
preventDefault(event)
assert.equal(event.returnValue, false)
assert(wasCalled)
})

it('should disable scroll and blur activeElement when getting visible', () => {
const wrapper = mount(<Modal />)
let disableWasCalled
wrapper.instance().disableScroll = () => {
disableWasCalled = true
}
let blurWasCalled
document.activeElement.blur = () => {
blurWasCalled = true
}
wrapper.setProps({visible: true})
assert(disableWasCalled)
assert(blurWasCalled)
})

it('should enable scroll when getting invisible', () => {
const wrapper = mount(<Modal visible />)
let enableWasCalled
wrapper.instance().enableScroll = () => {
enableWasCalled = true
}
wrapper.setProps({visible: false})
assert(enableWasCalled)
})

it('click on modal should not propagate', () => {
const wrapper = mount(<Modal visible />)
let wasCalled
wrapper.find('.mdc-Modal').simulate('click', {
stopPropagation: () => {
wasCalled = true
}
})
assert(wasCalled)
})

it('touchend on modal should not propagate', () => {
const wrapper = mount(<Modal visible />)
let wasCalled
wrapper.find('.mdc-Modal').simulate('touchend', {
stopPropagation: () => {
wasCalled = true
}
})
assert(wasCalled)
})

it('handleScrollAndEscKeys should prevent default for scroll keys', () => {
const wrapper = mount(<Modal visible />)
let wasCalled
let result = wrapper.instance().handleScrollAndEscKeys({
keyCode: 32,
preventDefault: () => {
wasCalled = true
}
})
assert(wasCalled)
assert.equal(result, false)
})

it('handleScrollAndEscKeys should prevent default for scroll keys', () => {
let toggleArg
const wrapper = mount(<Modal visible toggle={(arg) => {
toggleArg = arg
}} />)
wrapper.instance().handleScrollAndEscKeys({
keyCode: 27
})
assert.equal(toggleArg, false)
})
})

0 comments on commit 8443922

Please sign in to comment.