-
Notifications
You must be signed in to change notification settings - Fork 843
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
Feature/1067 focus trap initialfocus #1099
Feature/1067 focus trap initialfocus #1099
Conversation
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.
Tested this in browser and it works great! Code LGTM too except for a couple a comments/questions.
@@ -33,11 +33,12 @@ export class EuiOverlayMask extends Component { | |||
} | |||
this.overlayMaskNode.setAttribute(key, rest[key]); | |||
}); | |||
|
|||
document.body.appendChild(this.overlayMaskNode); |
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.
Just curious, why did you move this into the constructor? Does this merit a comment to explain why it's better than being in componentDidMount
?
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.
Given this React DOM tree:
<a>
<b>
<c>
React will mount the tree into the document and then call componentDidMount
in the order c
-> b
-> a
. FocusTrap activates the focus in componentDidMount
, but if the target element is not in the document
an error is thrown. To ensure the target element is part of the document before FocusTrap
's componentDidMount
is triggered, the overlayMaskNode
must be appended to the document in the constructor, as its componentDidMount
happens after FocusTrap
's.
Because of this, to maintain & allow natural assumptions we build about React's lifecycle, it's best practice to always best to add the custom DOM elements in the constructor.
src/components/popover/popover.js
Outdated
@@ -193,7 +208,9 @@ export class EuiPopover extends Component { | |||
}, 250); | |||
} | |||
|
|||
this.updateFocus(); | |||
this.updateFocus( |
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.
It looks like updateFocus
doesn't accept any arguments, so is this argument necessary?
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.
Good catch! It did something at one point during my refactoring :)
src/components/modal/modal.js
Outdated
@@ -32,6 +33,7 @@ export class EuiModal extends Component { | |||
<FocusTrap | |||
focusTrapOptions={{ | |||
fallbackFocus: () => this.modal, | |||
initialFocus: initialFocus, |
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.
Minor nit but I think you could just leave off the assignment:
focusTrapOptions={{
fallbackFocus: () => this.modal,
initialFocus,
}}
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.
Yeah, that should be an eslint rule...
src/components/modal/modal.js
Outdated
@@ -65,4 +67,10 @@ EuiModal.propTypes = { | |||
className: PropTypes.string, | |||
children: PropTypes.node, | |||
onClose: PropTypes.func.isRequired, | |||
/** 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([ |
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.
General question; do you always type check properties you don't actually use? You're not using that prop, so you don't really care what it is. You're just passing this into FocusTrap
, and I assume this check matches that one, so it seems redundant. At least that's what I've been doing, but I'm open to arguments.
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.
Couple of reasons:
- enforces the interface as the boundary; yes, the prop is directly passed to
FocusTrap
but that's a detail I'd like to obscure - if I'm building something with
EuiModal
and want to know what this prop can be, I want to open upmodal.js
and see the answer right away without digging through any more code - our doc's find the proptypes to list based on this, specifying this prop any other way interferes with the output
@@ -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); |
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.
document.querySelector
will look on the entire page for the element to focus. You want to constrain this to just the contents of the popover, otherwise it doesn't work. Tested locally and confirmed.
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.
Bah! This is how the upstream library works too, it just happened that the thing I tried in the modal happened to be the only item.
I still can't get things to focus correctly in the popover, but document.querySelector
isn't why...
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.
I can't figure out why, but popover focus does not work. The props look right, the node exists, it says it's calling .focus()
. I tried putting a setTimeout
on the focus call since that's what the FocusTrap component does, but that didn't help.
...but the node definitely does not get focused:
It should look like this when that input is focused:
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.
Bah! This is how the upstream library works too, it just happened that the thing I tried in the modal happened to be the only item.
Yep, that's why :) I agree with your thoughts on this, I wish the upstream library didn't search the entire DOM.
Is your element display: none
or visibility: hidden
at the time of focus, or otherwise hidden (browser can't focus non-intractable elements)? To debug these issues when working on the PR I did the following in dev console:
const origfocus = HTMLElement.prototype.focus;
HTMLElement.prototype.focus = function() {
const style = window.getComputedStyle(this);
console.log(this); // what element is being focused
console.log(style.display, style.visibility);
origfocus.call(this);
};
Adding a debugger
in that function also helps to inspect the rest of the DOM at time of focus, or do other tests/inspections on the target element.
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.
Is your element display: none or visibility: hidden at the time of focus, or otherwise hidden
Nope, it's just an input in the popover content. We wrap popover, but primarily only so we can set ownFocus
to true by default. We do use a render prop, so maybe that is causing some timing issue... I'll try agian without the wrapper, the issue may not be here.
@chandlerprall here's a simple demo component I made, and the input in the popover does not get focused: import { EuiFlexItem, EuiButton, EuiPopover } from '@elastic/eui';
class PopoverDemo extends React.PureComponent {
state = {
isOpen: false,
};
onClick = () => {
this.setState(state => ({ isOpen: !state.isOpen }));
};
onClose = () => {
this.setState(() => ({ isOpen: false }));
};
renderControl = () => {
return <EuiButton onClick={this.onClick}>Show popover</EuiButton>;
};
render() {
return (
<EuiFlexItem grow={false}>
<EuiPopover
id="popover"
button={this.renderControl()}
isOpen={this.state.isOpen}
closePopover={this.onClose}
initialFocus="#popover-demo-input-1234"
ownFocus
>
<div>Popover content</div>
<div>
Input: <input id="popover-demo-input-1234" />
</div>
<EuiButton onClick={this.onClick}>Close popover</EuiButton>
</EuiPopover>
</EuiFlexItem>
);
}
} No dice. Am I doing something wrong? |
@w33ble I copied & pasted your demo component into the EUI Popover example and it worked as expected, focusing the input box when the popover opened. Can you confirm you're using the updates from this PR? |
I was able to reproduce the still-problematic case of the popover not focusing correctly in Firefox. |
@w33ble updated to fix the popover initial focus race condition, please test again |
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.
I can't get this to link correctly now that canvas is in the Kibana repo, so I can't really test this out. Fingers crossed you got it right!
@w33ble don't forget to link within xpack as well (might have been the issue) |
@snide Yeah, I linked in both places, but the canvas plugin build step always fails because it can't resolve babel plugins... I have no idea why it doesn't work when I link EUI, but does work normally. I spent most of my morning trying to fix it to no avail. |
Can confirm that linking EUI breaks the canvas build, we'll need to look into that at some point. To test, I built EUI locally and manually copied |
Closes #1067
initialFocus
prop toEuiModal
andEuiPopver
EuiModal
forwards the prop through toFocusTrap
EuiPopover
already does things around focusing, so the initialFocus functionality is replicated in the popover component instead of passing it toFocusTrap
.EuiSwitch
to actually use itsid
prop