Skip to content
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

Merged
merged 11 commits into from
May 25, 2018
Merged

Conversation

cjcenizal
Copy link
Contributor

@cjcenizal cjcenizal commented May 23, 2018

Alternative approach to #845. Fixes #788 and #787. Also fixes a bug in which clicking the caret doesn't set focus to the search input.

This also seems to fix a bug in master, in which opening the list, using the down arrow to navigate to an item, and then hitting tab ends up setting focus to limbo. In this PR, you can't escape the list by tabbing. You have to hit Escape to return focus to the search input, and then you can tab away.

@nreese @chandlerprall What do you think of this approach? If you think this has promise then I need to update the CHANGELOG with the many breaking changes, update other instances in which EuiFormControlLayout is consumed, and fix and flesh out the tests.

combo_box_focus_trap

icon: PropTypes.string,
icon: PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
Copy link
Contributor Author

@cjcenizal cjcenizal May 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on this pattern? I was inspired a bit by https://www.youtube.com/watch?v=Yy7gFgETp0o&t=1079s.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a smarter pattern. One thing I think we probably should do is improve how we'll handle this kind of layout in our autodocs though. While importing the array values was never great and had similar problems, at least you could see the prop names themselves (which is probably the most important part). Right now it will be super vague and blackboxy. Most of the time when we do this kind of abstraction we actually only expose subsets of the original proplist (for example, color is not coming over from icon) so it will be harder to say "consult EuiIcon for props...etc).

image

Ultimately up to y'all, so I'll stay out of the decision here, but it'd be nice to have some sort of plan should we go down this route (if we are planning to adopt / migrate this pattern in more places where we nest components, which is what I'm imagining you two are thinking). Obviously this is a mostly internal component so the breaking change is less damaging than most, but if we started changing things like EuiButton with this pattern, I think we'd need to really consider the impact to benefit we're gaining.

@chandlerprall
Copy link
Contributor

Do we know what browser/OS combinations do or don't allow tabbing out of a native select box? Chrome/Mac at least does not let you tab out of the expanded options list.

@cjcenizal
Copy link
Contributor Author

cjcenizal commented May 23, 2018

On Mac:

  • Safari blocks you from tabbing out of the expanded options list.
  • Firefox will select the option you've highlighted and close the list.

This might be another case where we get to make up the rules a bit.

@chandlerprall
Copy link
Contributor

This approach does look a lot cleaner to me than #845

@nreese
Copy link
Contributor

nreese commented May 23, 2018

This approach does look a lot cleaner to me than #845

agreed. I will close 845 in favor of this

Copy link
Contributor

@chandlerprall chandlerprall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs a changelog but otherwise LGTM

componentDidMount() {
this._isMounted = true;
document.addEventListener('click', this.onDocumentFocusChange);
document.addEventListener('focusin', this.onDocumentFocusChange);
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

@cjcenizal
Copy link
Contributor Author

@chandlerprall @nreese I updated the tests, made some minor tweaks, and updated the CHANGELOG. Could you take another look please?

}

// Otherwise tab to the next adjacent item.
tabbableItems[searchInputIndex + amount].focus();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test if this exceeds tabbableItems.length?


// Wrap to last tabbable if tabbing backwards.
if (amount < 0) {
if (searchInputIndex === 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

amount may be less that -1, test if searchInputIndex + amount < 0

snide and others added 4 commits May 23, 2018 13:39
- Set focus on searchInput when you click the caret to open the combo box.
- Once open, clicking it again is a no-op.
- Redesign EuiFormControlLayout props to use icon and clear configuration objects.
@cjcenizal
Copy link
Contributor Author

@chandlerprall Thanks for the review! I addressed your comments by adding a check that the amount can only be -1 or 1 (which seems reasonable since this should only accept valid tabbing increments). This also solves issues where we tab beyond tabbableItems.length, since the other conditions in this method will wrap focus to the beginning of the tab order. Could you take another look?

Copy link
Contributor

@chandlerprall chandlerprall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one last change

@@ -137,26 +137,42 @@ export class EuiComboBox extends Component {
};

tabAway = amount => {
if (![-1, 1].includes(amount)) {
Copy link
Contributor

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 of if (Math.abs(amount) !== 1) ?

Copy link
Contributor Author

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.

Copy link
Contributor

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

@nreese
Copy link
Contributor

nreese commented May 23, 2018

This breaks the functionality of being able to close the list by clicking the caret. I think that is a nice feature to have is used in kibana functional tests.

@cjcenizal
Copy link
Contributor Author

cjcenizal commented May 24, 2018

I had to refactor EuiFormControlLayout significantly because it wasn't placing the DOM elements in the correct order when the icon is on the right with the clear button. @snide @cchaos Could you take a look at these layouts and make sure they make sense from a design perspective?

image

@cjcenizal cjcenizal requested review from snide and cchaos May 24, 2018 22:51
Copy link
Contributor

@snide snide left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Padding stuff I'd consider a blocker, rest is just asking questions. Code quality is sharp as always.

// Loading spinner needs adjustment if clear also exists
~ .euiFormControlLayout__loading {
right: $euiFormControlPadding*3;
> * + * {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more readable to me, but you'll still need a way to figure out how to calculate padding on the input.

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this again, it's only a problem in the generic examples and not the real form controls themselves. So ignore the above. But we might want to provide a comment about the issue if someone wants to use this stuff.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^ Well, kinda. See the comment below about the number field.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I'll add a note to the docs.

icon: PropTypes.string,
icon: PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a smarter pattern. One thing I think we probably should do is improve how we'll handle this kind of layout in our autodocs though. While importing the array values was never great and had similar problems, at least you could see the prop names themselves (which is probably the most important part). Right now it will be super vague and blackboxy. Most of the time when we do this kind of abstraction we actually only expose subsets of the original proplist (for example, color is not coming over from icon) so it will be harder to say "consult EuiIcon for props...etc).

image

Ultimately up to y'all, so I'll stay out of the decision here, but it'd be nice to have some sort of plan should we go down this route (if we are planning to adopt / migrate this pattern in more places where we nest components, which is what I'm imagining you two are thinking). Obviously this is a mostly internal component so the breaking change is less damaging than most, but if we started changing things like EuiButton with this pattern, I think we'd need to really consider the impact to benefit we're gaining.

@@ -81,7 +82,14 @@ EuiFieldNumber.propTypes = {
max: PropTypes.number,
step: PropTypes.number,
value: numberOrEmptyString,
icon: PropTypes.string,
icon: PropTypes.oneOfType([
Copy link
Contributor

Choose a reason for hiding this comment

The 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 😄

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. Looks like using them will actually break the layouts because of the assumptions the previous api made.

I think if these just get reverted to the strings only you're prolly ok? Leave the mapping stuff to the control only?

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh great catch! This was an oversight on my part.

@cjcenizal
Copy link
Contributor Author

cjcenizal commented May 25, 2018

@snide Thanks for your feedback! I've addressed your points. About the pattern of an icon prop accepting an object, I think we'll have to consider it on a case-by-case basis. It's good in situations where the icon needs so much customization like inside of combo_box.js, but makes much less sense for something like a button's icon, where these extra props won't do much good. I think the real benefit here is that we have a new pattern to apply to props which require this extra level of customization, which we can reach for when needed.

@nreese
Copy link
Contributor

nreese commented May 25, 2018

works great

Copy link
Contributor

@nreese nreese left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Contributor

@snide snide left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. Thanks.

@chandlerprall
Copy link
Contributor

Thanks for pushing forward on this! LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Combo Box is a keyboard trap
4 participants