diff --git a/CHANGELOG.md b/CHANGELOG.md index da18cc8d4c8..82209d78213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [`master`](https://github.com/elastic/eui/tree/master) - Added some opacity options to `EuiLineSeries` and `EuiAreaSeries` ([#1198](https://github.com/elastic/eui/pull/1198)) +- Added `initialFocus` prop for focus trapping to `EuiPopover` and `EuiModal` ([#1099](https://github.com/elastic/eui/pull/1099)) ## [`4.1.0`](https://github.com/elastic/eui/tree/v4.1.0) diff --git a/src-docs/src/views/modal/modal.js b/src-docs/src/views/modal/modal.js index 0633d2d0303..43c774ccd86 100644 --- a/src-docs/src/views/modal/modal.js +++ b/src-docs/src/views/modal/modal.js @@ -87,6 +87,7 @@ export class Modal extends Component { diff --git a/src-docs/src/views/popover/trap_focus.js b/src-docs/src/views/popover/trap_focus.js index bb8846c650b..1c4f3bbbda9 100644 --- a/src-docs/src/views/popover/trap_focus.js +++ b/src-docs/src/views/popover/trap_focus.js @@ -48,23 +48,24 @@ export default class extends Component { button={button} isOpen={this.state.isPopoverOpen} closePopover={this.closePopover.bind(this)} + initialFocus="[id=asdf2]" > diff --git a/src/components/form/switch/__snapshots__/switch.test.js.snap b/src/components/form/switch/__snapshots__/switch.test.js.snap index c4f178c0c99..1e7b46f8300 100644 --- a/src/components/form/switch/__snapshots__/switch.test.js.snap +++ b/src/components/form/switch/__snapshots__/switch.test.js.snap @@ -8,6 +8,7 @@ exports[`EuiSwitch is rendered 1`] = ` aria-label="aria-label" class="euiSwitch__input" data-test-subj="test subject string" + id="test" type="checkbox" /> this.modal, + initialFocus, }} > { @@ -89,6 +91,12 @@ EuiModal.propTypes = { PropTypes.number, PropTypes.string, ]), + /** specifies what element should initially have focus; Can be a DOM node, or a selector string (which will be passed to document.querySelector() to find the DOM node), or a function that returns a DOM node. */ + initialFocus: PropTypes.oneOfType([ + PropTypes.instanceOf(HTMLElement), + PropTypes.func, + PropTypes.string, + ]), }; EuiModal.defaultProps = { diff --git a/src/components/overlay_mask/overlay_mask.js b/src/components/overlay_mask/overlay_mask.js index cc60eb2a4f3..4032f639dce 100644 --- a/src/components/overlay_mask/overlay_mask.js +++ b/src/components/overlay_mask/overlay_mask.js @@ -33,11 +33,12 @@ export class EuiOverlayMask extends Component { } this.overlayMaskNode.setAttribute(key, rest[key]); }); + + document.body.appendChild(this.overlayMaskNode); } componentDidMount() { document.body.classList.add('euiBody-hasOverlayMask'); - document.body.appendChild(this.overlayMaskNode); } componentWillUnmount() { diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js index 12dd244dc43..4b91bd030e0 100644 --- a/src/components/popover/popover.js +++ b/src/components/popover/popover.js @@ -76,6 +76,13 @@ const DEFAULT_POPOVER_STYLES = { const GROUP_NUMERIC = /^([\d.]+)/; +function getElementFromInitialFocus(initialFocus) { + const initialFocusType = typeof initialFocus; + if (initialFocusType === 'string') return document.querySelector(initialFocus); + if (initialFocusType === 'function') return initialFocus(); + return initialFocus; +} + export class EuiPopover extends Component { static getDerivedStateFromProps(nextProps, prevState) { if (prevState.prevProps.isOpen && !nextProps.isOpen) { @@ -139,10 +146,25 @@ export class EuiPopover extends Component { } // Otherwise let's focus the first tabbable item and expedite input from the user. - const tabbableItems = tabbable(this.panel); - if (tabbableItems.length) { - tabbableItems[0].focus(); + let focusTarget; + + if (this.props.initialFocus != null) { + focusTarget = getElementFromInitialFocus(this.props.initialFocus); + // there's a race condition between the popover content becoming visible and this function call + // if the element isn't visible yet (due to css styling) then it can't accept focus + // so wait for another render and try again + const visibility = window.getComputedStyle(focusTarget).visibility; + if (visibility === 'hidden') { + this.updateFocus(); + } + } else { + const tabbableItems = tabbable(this.panel); + if (tabbableItems.length) { + focusTarget = tabbableItems[0]; + } } + + if (focusTarget != null) focusTarget.focus(); }); } @@ -311,6 +333,7 @@ export class EuiPopover extends Component { hasArrow, repositionOnScroll, // eslint-disable-line no-unused-vars zIndex, // eslint-disable-line no-unused-vars + initialFocus, // eslint-disable-line no-unused-vars ...rest } = this.props; @@ -444,6 +467,12 @@ EuiPopover.propTypes = { repositionOnScroll: PropTypes.bool, /** By default, popover content inherits the z-index of the anchor component; pass zIndex to override */ zIndex: PropTypes.number, + /** specifies what element should initially have focus; Can be a DOM node, or a selector string (which will be passed to document.querySelector() to find the DOM node), or a function that returns a DOM node. */ + initialFocus: PropTypes.oneOfType([ + PropTypes.instanceOf(HTMLElement), + PropTypes.func, + PropTypes.string, + ]), }; EuiPopover.defaultProps = {