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

use react-virtualized to virtualize EuiComboBox options list #670

Merged
merged 16 commits into from
Apr 18, 2018

Conversation

nreese
Copy link
Contributor

@nreese nreese commented Apr 13, 2018

fixes #651

paired with @cjcenizal to convert EuiComboBox rendering to use react-virtualized.

Rendering long lists was really slow because each list item was added to the DOM. With virtualization, only the list items that are visible in the EuiComboBoxOptionsList panel are in the DOM and there is great performance for large lists.

@nreese
Copy link
Contributor Author

nreese commented Apr 13, 2018

@cjcenizal I think I have gone about as far as I can with this. Remaining questions/actions

  • rowHeight is getting set to a hard coded value. This needs to be determined dynamically. Update: Add rowHeight property to support renderOption that renders custom height values.
  • Truncate labels that wrap when virtualization is used.
  • Should virtualization be configurable via a prop or should it always be used? By using virtualization, you loose the ability to have rows with different heights. Maybe this is more important for some users because then no labels get truncated. Update: No, visualization should not be configurable via a prop until there is a good reason to do so.
  • Does a prop need to be added for rendering group title? If renderOption creates rows of height 40, then users will need a function to create group title rows of the same height. Update: Not needed yet.
  • Fix keyboard navigation, i.e. using the up and down arrows to go through the list of options

Copy link
Contributor

@cjcenizal cjcenizal left a comment

Choose a reason for hiding this comment

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

This is looking so awesome! I created nreese#1 to address the text truncation and do some minor cleanup. It should address some of the comments I left below.

I think virtualization should be always-on by default. This will keep the interface simpler so the consumer doesn't need to worry about this. And if it turns out we should allow the consumer to control this then it's easier to add a prop than to remove one.

Does a prop need to be added for rendering a group title?

I don't have the cycles to think this through, so for now I'm comfortable punting on this. My gut tells me we should see what kinds of use cases people have for rendering options and virtualization and then make a decision once we have more info. Will you be using custom rendering in your use case?

We also need to add another checkbox to the list of to-do's:

  • Fix keyboard navigation, i.e. using the up and down arrows to go through the list of options

It works surprisingly well when just using the down arrow key, but it doesn't wrap to the end when you use the up arrow key to go past the first option.

Also, somehow I got this error by using the arrow keys to navigating and then clicking on the input:

image

@@ -485,6 +490,7 @@ export class EuiComboBox extends Component {
let optionsList;

if (!noSuggestions && isListOpen) {
console.log("width", width);
Copy link
Contributor

Choose a reason for hiding this comment

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

Leftover console.log.

overflow: hidden;
}

.ReactVirtualized__List {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we put this inside of the .euiComboBoxOptionsList class so these styles are scoped to this component?

.euiComboBoxOptionsList {
  @include euiFormControlSize;
  box-sizing: content-box; /* 1 */
  margin-left: -1px; /* 1 */
  z-index: $euiZComboBox;
  position: absolute; /* 2 */
  top: 0; /* 2 */

  .ReactVirtualized__List {
    @include euiScrollBar;
  }
}

.euiComboBoxTitle {
font-size: $euiFontSizeXS;
padding: $euiSizeXS $euiSizeS $euiSizeXS 0;
padding: ($euiSizeXS + $euiSizeS - 1px) $euiSizeS $euiSizeXS 0; /* 1 */
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we tweak this to be:

padding: ($euiSizeXS + $euiSizeS - 1px) $euiSizeS $euiSizeXS;

And then can we edit .euiComboBoxOption so that its padding (line 3 of _combo_box_option.scss) is this:

padding: $euiSizeXS $euiSizeS $euiSizeXS #{$euiSizeM + $euiSizeXS};

optionsList.push(renderedOption);
});
const optionHeight = 27; // TODO dynamically figure this out
const numVisiableOptions = matchingOptions.length < 7 ? matchingOptions.length : 7;
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor typo, this should be numVisibleOptions

});
if (matchingOptionsForGroup.length > 0) {
// Add option for group label
matchingOptions.push({ label: option.label, isGroupLabelOption: true });
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice improvement!

@nreese
Copy link
Contributor Author

nreese commented Apr 16, 2018

we should see what kinds of use cases people have for rendering options and virtualization and then make a decision once we have more info. Will you be using custom rendering in your use case?

No, my use case will not use custom rendering.

Copy link
Contributor

@cjcenizal cjcenizal left a comment

Choose a reason for hiding this comment

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

Nice! Focus tracking looks great. I found a couple more things and submitted nreese#2 to address them. Once we merge it I think we're good to go.

};

clearActiveOption = () => {
this.activeOptionIndex = undefined;
this.state.activeOptionIndex = undefined;
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 we want to call setState here, right?

Copy link
Contributor

@cjcenizal cjcenizal left a comment

Choose a reason for hiding this comment

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

🌊 Rad!! I only had a few comments, all minor.

code: virtualizedHtml,
}],
text: (
<p>
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we tweak this to fix some spacing and a typo:

      <p>
        <EuiCode>EuiComboBoxList</EuiCode> uses <Link to="https://github.com/bvaughn/react-virtualized">react-virtualized</Link>{' '}
        to only render visible options to be super fast no matter how many options there are.
      </p>

<EuiCode>option</EuiCode> object to store metadata about the option for use in this callback.
</p>
<Fragment>
<p>
Copy link
Contributor

Choose a reason for hiding this comment

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

This will fix some more spacing issues:

        <p>
          You can provide a <EuiCode>renderOption</EuiCode> prop which will accept <EuiCode>option</EuiCode>{' '}
          and <EuiCode>searchValue</EuiCode> arguments. Use the <EuiCode>value</EuiCode> prop of the{' '}
          <EuiCode>option</EuiCode> object to store metadata about the option for use in this callback.
        </p>

@@ -149,7 +151,7 @@ export class EuiComboBox extends Component {
tabbableItems[comboBoxIndex + amount].focus();
};

incrementActiveOptionIndex = amount => {
incrementActiveOptionIndex = _.throttle(amount => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we import the throttle module?

import { throttle } from 'lodash';

this.setState({
activeOptionIndex: nextActiveOptionIndex,
});
}, 200);
Copy link
Contributor

Choose a reason for hiding this comment

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

200 feels a bit sluggish to me -- 100 feels snappier, can we use that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

with 100 I was still getting the keydown to be faster than the rendering and I could hold the key down and the focus option would not keep up

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.

Only stuff I caught in here was that disabled items shouldn't be focusable (this is a regression from master)

image

And that there's a minor hitch in the truncation in the custom example. Likely though that would be adjusted on the custom element itself.

image

@cjcenizal
Copy link
Contributor

@snide I made the options focusable so that screen readers can pick them up and read them out as "disabled". Otherwise someone using a screen reader won't know that the option is in the list. This can be a little weird for sighted users but I think it's worth it for the accessibility gain. What do you think?

You're right about the custom rendering example; it would have to be handled by the consumer. I thought about updating the example to truncate the text, but it would mean introducing a custom class for a single example and I wasn't sure it was worth it. We can update the example text to provide guidance on this though.

@snide
Copy link
Contributor

snide commented Apr 18, 2018

I think it shouldn't be focusable, for similar reasons that @cchaos had the last time this came up in #653 (comment). I think its safest to follow what the browser provides for disabled states on button tags.

I don't feel strongly enough on it to prevent this PR from being merged though since it's a great improvement.

@cjcenizal
Copy link
Contributor

I think it's OK to keep it in this case because it replicates the behavior of a select element in terms of keyboard accessibility. Here's a capture of using the example on w3schools:

select_accessibility

The only aspect which is odd is that you can focus the disabled item. Sighted users will have the "not-allowed" cursor and dimmed visual state to signal that the item can't be selected, so I think it's a worthwhile tradeoff.

Let's keep it in for now and get @aphelionz's input on this at some point. Maybe he knows of a better solution.

@nreese nreese merged commit cd12e5b into elastic:master Apr 18, 2018
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.

EuiComboBox: Improve search performance for long lists of options
3 participants