-
Notifications
You must be signed in to change notification settings - Fork 841
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix EuiComboBox focus trap #866
Changes from 5 commits
7f6953e
e0c81c9
a5e37c8
3f799e4
425945e
f5e2aca
e34a195
d622d46
f4fe254
26065e8
087374c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -137,26 +137,42 @@ export class EuiComboBox extends Component { | |
}; | ||
|
||
tabAway = amount => { | ||
if (![-1, 1].includes(amount)) { | ||
throw new Error(`tabAway expects amount to be -1 or 1, but received ${amount}`); | ||
} | ||
|
||
const tabbableItems = tabbable(document); | ||
const comboBoxIndex = tabbableItems.indexOf(this.searchInput); | ||
|
||
// Wrap to last tabbable if tabbing backwards. | ||
if (amount < 0) { | ||
if (comboBoxIndex === 0) { | ||
tabbableItems[tabbableItems.length - 1].focus(); | ||
return; | ||
if (document.activeElement === this.searchInput) { | ||
const searchInputIndex = tabbableItems.indexOf(this.searchInput); | ||
|
||
// Wrap to last tabbable if tabbing backwards. | ||
if (amount === -1) { | ||
if (searchInputIndex === 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
tabbableItems[tabbableItems.length - 1].focus(); | ||
return; | ||
} | ||
} | ||
|
||
// Otherwise tab to the next adjacent item. | ||
tabbableItems[searchInputIndex + amount].focus(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. test if this exceeds |
||
return; | ||
} | ||
|
||
// Wrap to first tabbable if tabbing forwards. | ||
if (amount > 0) { | ||
if (comboBoxIndex === tabbableItems.length - 1) { | ||
tabbableItems[0].focus(); | ||
return; | ||
if (document.activeElement === this.clearButton) { | ||
const clearButtonIndex = tabbableItems.indexOf(this.clearButton); | ||
|
||
// Wrap to first tabbable if tabbing forwards. | ||
if (amount === 1) { | ||
if (clearButtonIndex === tabbableItems.length - 1) { | ||
tabbableItems[0].focus(); | ||
return; | ||
} | ||
} | ||
} | ||
|
||
tabbableItems[comboBoxIndex + amount].focus(); | ||
// Otherwise tab to the next adjacent item. | ||
tabbableItems[clearButtonIndex + amount].focus(); | ||
} | ||
}; | ||
|
||
incrementActiveOptionIndex = throttle(amount => { | ||
|
@@ -290,14 +306,10 @@ export class EuiComboBox extends Component { | |
}; | ||
|
||
onFocus = () => { | ||
document.addEventListener('click', this.onDocumentFocusChange); | ||
document.addEventListener('focusin', this.onDocumentFocusChange); | ||
this.openList(); | ||
} | ||
|
||
onBlur = () => { | ||
document.removeEventListener('click', this.onDocumentFocusChange); | ||
document.removeEventListener('focusin', this.onDocumentFocusChange); | ||
this.closeList(); | ||
} | ||
|
||
|
@@ -309,6 +321,7 @@ export class EuiComboBox extends Component { | |
|| this.optionsList === event.target | ||
|| this.optionsList && this.optionsList.contains(event.target) | ||
) { | ||
this.onFocus(); | ||
return; | ||
} | ||
|
||
|
@@ -417,6 +430,10 @@ export class EuiComboBox extends Component { | |
} | ||
}; | ||
|
||
onOpenListClick = () => { | ||
this.searchInput.focus(); | ||
}; | ||
|
||
onSearchChange = (searchValue) => { | ||
if (this.props.onSearchChange) { | ||
this.props.onSearchChange(searchValue); | ||
|
@@ -450,8 +467,14 @@ export class EuiComboBox extends Component { | |
this.options[index] = node; | ||
}; | ||
|
||
clearButtonRef = node => { | ||
this.clearButton = node; | ||
}; | ||
|
||
componentDidMount() { | ||
this._isMounted = true; | ||
document.addEventListener('click', this.onDocumentFocusChange); | ||
document.addEventListener('focusin', this.onDocumentFocusChange); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By leaving these event listeners active while the component is mounted, we can open the list when the user tabs backwards to give the clear button focus. |
||
|
||
// TODO: This will need to be called once the actual stylesheet loads. | ||
setTimeout(() => { | ||
|
@@ -586,10 +609,10 @@ export class EuiComboBox extends Component { | |
onClear={isClearable && !isDisabled ? this.clearSelectedOptions : undefined} | ||
hasSelectedOptions={selectedOptions.length > 0} | ||
isListOpen={isListOpen} | ||
onOpen={this.openList} | ||
onClose={this.closeList} | ||
onOpenListClick={this.onOpenListClick} | ||
singleSelection={singleSelection} | ||
isDisabled={isDisabled} | ||
clearButtonRef={this.clearButtonRef} | ||
/> | ||
|
||
{optionsList} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; | |
import classNames from 'classnames'; | ||
|
||
import { | ||
ICON_SIDES, | ||
EuiFormControlLayout, | ||
} from '../form_control_layout'; | ||
|
||
|
@@ -81,7 +82,14 @@ EuiFieldNumber.propTypes = { | |
max: PropTypes.number, | ||
step: PropTypes.number, | ||
value: numberOrEmptyString, | ||
icon: PropTypes.string, | ||
icon: PropTypes.oneOfType([ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A lot of these components purposefully limited prop declarations. Is there a reason to add iconSide and onClick to the number field? I kind of like giving people less options here which is why I only exposed the props in some of the compoents. Generally, when we've given people the power to use things, they start using them. I'm just a little worried about seeing random action icons on the right side of inputs that lack context. Down arrow, clear? Makes sense. Random logstash icon with some hidden meaning and an onClick action has me worried 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh great catch! This was an oversight on my part. |
||
PropTypes.string, | ||
PropTypes.shape({ | ||
type: PropTypes.string, | ||
side: PropTypes.oneOf(ICON_SIDES), | ||
onClick: PropTypes.func, | ||
}), | ||
]), | ||
isInvalid: PropTypes.bool, | ||
fullWidth: PropTypes.bool, | ||
isLoading: PropTypes.bool, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IE doesn't support
includes
. What do you think ofif (Math.abs(amount) !== 1)
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh interesting! We're using
includes
in other places in EUI. Maybe we should just add documentation stating that we expect consumers to polyfill ES2015 features, e.g. with babel-polyfill. I think that's fair, since EUI is intended for usage within Elastic and I'd be surprised if we couldn't meet that requirements.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds like the right thing since the methods are already being used