Skip to content

Commit

Permalink
Fix unclickable inner text issue in IE11
Browse files Browse the repository at this point in the history
This is caused by “mousedown” being prevented on each result, which stops the results list from auto-closing as you blur away from the text input.

This breaks IE11, see issue:
#307

Workaround: Remove “mousedown” blocking. Replace with short delay on results list close state to allow click to reach each result first.
  • Loading branch information
colinrotherham committed Oct 31, 2018
1 parent b49b667 commit a404a44
Show file tree
Hide file tree
Showing 8 changed files with 43 additions and 38 deletions.
2 changes: 1 addition & 1 deletion dist/accessible-autocomplete.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/accessible-autocomplete.min.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/lib/accessible-autocomplete.preact.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/lib/accessible-autocomplete.preact.min.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/lib/accessible-autocomplete.react.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/lib/accessible-autocomplete.react.min.js.map

Large diffs are not rendered by default.

30 changes: 12 additions & 18 deletions src/autocomplete.js
Expand Up @@ -67,6 +67,7 @@ export default class Autocomplete extends Component {
this.state = {
focused: null,
hovered: null,
clicked: null,
menuOpen: false,
options: props.defaultValue ? [props.defaultValue] : [],
query: props.defaultValue,
Expand All @@ -85,7 +86,6 @@ export default class Autocomplete extends Component {
this.handleOptionBlur = this.handleOptionBlur.bind(this)
this.handleOptionClick = this.handleOptionClick.bind(this)
this.handleOptionFocus = this.handleOptionFocus.bind(this)
this.handleOptionMouseDown = this.handleOptionMouseDown.bind(this)
this.handleOptionMouseEnter = this.handleOptionMouseEnter.bind(this)

this.handleInputBlur = this.handleInputBlur.bind(this)
Expand All @@ -102,6 +102,7 @@ export default class Autocomplete extends Component {

componentWillUnmount () {
clearTimeout(this.$pollInput)
clearTimeout(this.$blurInput)
}

// Applications like Dragon NaturallySpeaking will modify the
Expand All @@ -125,10 +126,10 @@ export default class Autocomplete extends Component {
}

componentDidUpdate (prevProps, prevState) {
const { focused } = this.state
const { focused, clicked } = this.state
const componentLostFocus = focused === null
const focusedChanged = prevState.focused !== focused
const focusDifferentElement = focusedChanged && !componentLostFocus
const focusDifferentElement = (focusedChanged && !componentLostFocus) || clicked !== null
if (focusDifferentElement) {
this.elementReferences[focused].focus()
}
Expand Down Expand Up @@ -168,6 +169,7 @@ export default class Autocomplete extends Component {
}
this.setState({
focused: null,
clicked: null,
menuOpen: newState.menuOpen || false,
query: newQuery,
selected: null
Expand All @@ -181,8 +183,8 @@ export default class Autocomplete extends Component {
}

handleOptionBlur (event, index) {
const { focused, menuOpen, options, selected } = this.state
const focusingOutsideComponent = event.relatedTarget === null
const { focused, clicked, menuOpen, options, selected } = this.state
const focusingOutsideComponent = event.relatedTarget === null && clicked === null
const focusingInput = event.relatedTarget === this.elementReferences[-1]
const focusingAnotherOption = focused !== index && focused !== -1
const blurComponent = (!focusingAnotherOption && focusingOutsideComponent) || !(focusingAnotherOption || focusingInput)
Expand All @@ -198,13 +200,14 @@ export default class Autocomplete extends Component {
handleInputBlur (event) {
const { focused, menuOpen, options, query, selected } = this.state
const focusingAnOption = focused !== -1
clearTimeout(this.$blurInput)
if (!focusingAnOption) {
const keepMenuOpen = menuOpen && isIosDevice()
const newQuery = isIosDevice() ? query : this.templateInputValue(options[selected])
this.handleComponentBlur({
this.$blurInput = setTimeout(() => this.handleComponentBlur({
menuOpen: keepMenuOpen,
query: newQuery
})
}), 200)
}
}

Expand Down Expand Up @@ -267,9 +270,11 @@ export default class Autocomplete extends Component {
handleOptionClick (event, index) {
const selectedOption = this.state.options[index]
const newQuery = this.templateInputValue(selectedOption)
clearTimeout(this.$blurInput)
this.props.onConfirm(selectedOption)
this.setState({
focused: -1,
clicked: index,
hovered: null,
menuOpen: false,
query: newQuery,
Expand All @@ -278,16 +283,6 @@ export default class Autocomplete extends Component {
this.forceUpdate()
}

handleOptionMouseDown (event) {
// Safari triggers focusOut before click, but if you
// preventDefault on mouseDown, you can stop that from happening.
// If this is removed, clicking on an option in Safari will trigger
// `handleOptionBlur`, which closes the menu, and the click will
// trigger on the element underneath instead.
// See: http://stackoverflow.com/questions/7621711/how-to-prevent-blur-running-when-clicking-a-link-in-jquery
event.preventDefault()
}

handleUpArrow (event) {
event.preventDefault()
const { menuOpen, selected } = this.state
Expand Down Expand Up @@ -511,7 +506,6 @@ export default class Autocomplete extends Component {
key={index}
onFocusOut={(event) => this.handleOptionBlur(event, index)}
onClick={(event) => this.handleOptionClick(event, index)}
onMouseDown={this.handleOptionMouseDown}
onMouseEnter={(event) => this.handleOptionMouseEnter(event, index)}
ref={(optionEl) => { this.elementReferences[index] = optionEl }}
role='option'
Expand Down
39 changes: 25 additions & 14 deletions test/functional/index.js
Expand Up @@ -227,33 +227,44 @@ describe('Autocomplete', () => {
})

describe('blurring input', () => {
it('unfocuses component', () => {
it('unfocuses component', (done) => {
autocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: -1, selected: -1 })
autocomplete.handleInputBlur({ relatedTarget: null })
expect(autocomplete.state.focused).to.equal(null)
expect(autocomplete.state.menuOpen).to.equal(false)
expect(autocomplete.state.query).to.equal('fr')
// Using setTimeouts here since changes in values take a while to reflect in lists
setTimeout(() => {
expect(autocomplete.state.focused).to.equal(null)
expect(autocomplete.state.menuOpen).to.equal(false)
expect(autocomplete.state.query).to.equal('fr')
done()
}, 250)
})

describe('with autoselect and onConfirm', () => {
it('unfocuses component, updates query, triggers onConfirm', () => {
it('unfocuses component, updates query, triggers onConfirm', (done) => {
autoselectOnSelectAutocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: -1, selected: 0 })
autoselectOnSelectAutocomplete.handleInputBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
expect(autoselectOnSelectAutocomplete.state.focused).to.equal(null)
expect(autoselectOnSelectAutocomplete.state.menuOpen).to.equal(false)
expect(autoselectOnSelectAutocomplete.state.query).to.equal('France')
expect(onConfirmTriggered).to.equal(true)
// Using setTimeouts here since changes in values take a while to reflect in lists
setTimeout(() => {
expect(autoselectOnSelectAutocomplete.state.focused).to.equal(null)
expect(autoselectOnSelectAutocomplete.state.menuOpen).to.equal(false)
expect(autoselectOnSelectAutocomplete.state.query).to.equal('France')
expect(onConfirmTriggered).to.equal(true)
done()
}, 250)
})
})

describe('with confirmOnBlur false', () => {
it('unfocuses component, does not touch query, does not trigger onConfirm', () => {
it('unfocuses component, does not touch query, does not trigger onConfirm', (done) => {
confirmOnBlurAutocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: -1, selected: 0 })
confirmOnBlurAutocomplete.handleInputBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
expect(confirmOnBlurAutocomplete.state.focused).to.equal(null)
expect(confirmOnBlurAutocomplete.state.menuOpen).to.equal(false)
expect(confirmOnBlurAutocomplete.state.query).to.equal('fr')
expect(onConfirmTriggered).to.equal(false)
setTimeout(() => {
expect(confirmOnBlurAutocomplete.state.focused).to.equal(null)
expect(confirmOnBlurAutocomplete.state.menuOpen).to.equal(false)
expect(confirmOnBlurAutocomplete.state.query).to.equal('fr')
expect(onConfirmTriggered).to.equal(false)
done()
}, 250)
})
})
})
Expand Down

0 comments on commit a404a44

Please sign in to comment.