Skip to content

RFC: Enabling Overrides of the ComboboxField's Option Resetting Logic #1

@ITenthusiasm

Description

@ITenthusiasm

RFC: Enabling Overrides of the ComboboxField's Option Resetting Logic

NOTE: Most developers will not need this feature. It is only useful for people who have extremely niche performance needs. Read the Filtering Performance Enhancements guide before reading this RFC. This post will not make sense unless you are familiar with how the ComboboxField's performance tuning works.


For those seeking to enhance the Combobox component's filtering performance, there is one notable limitation: Option Resetting. Option Resetting is the process where all ComboboxOptions are marked as filtered in (ComboboxOption.filteredOut = false) and are inserted into the internal matchingOptions list in proper order. This occurs when the user collpases the combobox, or when the developer swaps out some of the ComboboboxOptions in the DOM while the combobox is collapsed. Option Resetting was implemented by the ComboboxField for two reasons:

  1. Easier Option Selection:

    When a user selects an option, that option's label becomes the combobox's text content. If the user re-expands the combobox, it is highly likely that the only option which will match the combobox's current filter (i.e., the combobox's current text content) will be the currently-selected option. But if the user is expanding the combobox because they want to see/select a different option, then they'll have to change the filter with their keyboard to see other options.

    This is very inconvenient, especially if the user is working with a clearable ComboboxField. (This is because the user would have to empty the filter to see all of the options again, but emptying the filter clears the value of a clearable ComboboxField.) A better UX is to display all of the options whenever the combobox is re-expanded, and this is accomplished by performing Option Resetting when the combobox is collapsed. If the user wants to see a smaller list of options, they can simply update the filter after expanding the combobox.

  2. Keeping matchingOptions Up-to-Date:

    When the developer adds, removes, or otherwise swaps out some of the ComboboxOptions belonging to a given Combobox component, the ComboboxField's internal list of matchingOptions becomes obsolete (because it may point to options which no longer exist, or lack options which were newly added). At this point, the most reliable way to bring the matchingOptions up-to-date is to perform Option Resetting (if the ComboboxField is currently collapsed).

    This guarantees that no newly-added options were accidentally/incorrectly marked as filteredOut = true, and it also guarantees that the matchingOptions list will be correct when the combobox is expanded. (Note that Option Resetting does not occur when the ComboboxOptions are swapped if the combobox is expanded, because the user might be in the process of filtering the options. In this case, the options are filtered instead of being reset.)

Because Option Resetting requires the ComboboxField to visit each of the ComboboxOptions that it owns, its time complexity is O(n). This time cost is only incurred during option swapping (which is highly unlikely to happen with Trie-based implementations), or when the user collapses the combobox (which will hopefully make the time cost less noticeable); so in practice, it shouldn't be too much of a problem. However, the time cost might still be inconvenient in some circumstances. And since the implementation for Option Resetting is not currently overridable, there is no way to circumvent that inconvenience (yet).

Note: None of this is a concern if you aren't filtering through an egregiously large list of options. If you aren't working with several thousands of options or more, then you won't encounter any performance problems.

The whole point of making the option filtering logic overridable was to meet each developer's performance needs. For the most part, we're meeting those needs! But we might be missing one small piece with the Option Resetting dilemma.

To be fair, it's possible that the current implementation of the ComboboxField is actually sufficient for all people (given how infrequently options will be swapped and/or the combobox will be collapsed). But we dont't know that for certain... And that's the point of this RFC: To figure out what developers need, and to discuss potential solutions.

Potential Solutions

Until developers explicitly communicate that there's a need for this feature, it probably won't be worked on. But we can at least put some initial ideas down on what a future implementation could look like to make the process smoother. Below are some ideas:

Exposing an Overridable getResetOptions() Method

Today, the ComboboxField has a publicly overridable getFilteredOptions() method which is strictly used by the internal ComboboxField.#filterOptions() method to filter the options. ComboboxField.#resetOptions() is the internal method which is responsible for the Option Resetting logic. We could refactor this method to call a publicly overridable getResetOptions() method, similar to what is done for ComboboxField.#filterOptions().

This approach would enable developers to do things like the following:

  • Return 0 matching options when Option Resetting occurs.
    • This indicates that the user must supply a filter to see any options. This reduces the time complexity for Option Resetting to O(1) for Trie-based implementations that are already seeking to get as many performance gains as possible.
  • Return the first N options when Option Resetting occurs.
    • This indicates that a user can see a subset of all possible options. However, to see other options, they must still supply more specific filters.
  • Return the already-existing matchingOptions.
    • This indicates that no new options will be found by performing Option Resetting. For example, options which are not filtered client-side but are instead loaded asynchronously never need to perform real Option Resetting on collapse.

The one thing that this approach doesn't do is help developers who want to use Trie-based filtering, but who also want to show users every option when the combobox is expanded (or has its filter emptied). For that, we'd need to go one level deeper...

Exposing Overridable option Navigation Methods

To understand why overriding option navigation could improve the performance of Option Resetting, it's first important to understand how all ComboboxOptions can be presented to the user without iterating over any of them.

If you have a Trie-based implementation for filtering, how do you show all available ComboboxOptions to the user when their filter is empty (or after Option Resetting occurs) without iterating over all of the options? Well, since the options are hidden/revealed with CSS rather than JS, the answer is quite simple: Just show all of the options when the combobox is :empty (i.e., when the filter is empty).

[role="combobox"]:empty + [role="listbox"] > [role="option"] {
  display: block;
  visibility: visible;
}

Note that the CSS above only takes care of scenarios where the filter is emptied, not where the options are reset. However, we could easily add a :--options-reset custom CSS state to the ComboboxField whenever Option Resetting occurs. (This custom state would be removed when the filter changes.) Then, using both [role="combobox"]:empty and [role="combobox"]:state(--options-reset), we could successfully toggle the visibility of all options with pure CSS — no O(n) looping in JS needed.

Note: We could also leverage a data-* attribute in addition to the custom CSS state for those wanting better backwards compatibility as well.

However, this only handles the visibility of the options. All users (including Screen Reader Users) will be able to see all options with this approach. But the CSS alone does not enable users to navigate the options with a Keyboard.

Currently, the ComboboxField handles Keyboard-based option navigation in this way: In Filter Mode, navigate only the matchingOptions; otherwise, navigate all of the options. This navigation implementation requires us to place every single option within the matchingOptions list whenever Option Resetting happens. However, looping over all the options is exactly what we want to avoid in performance-critical situations.

If option navigation became overridable (via method overrides) such that developers could switch between navigating only the matchingOptions or all options based on whether the user's filter was empty (or based on whether Option Resetting had just recently occurred), then developers could enable users to navigate all options without ever iterating over any of them. The process would look like this:

  • Return an empty array for matchingOptions whenever the user empties their filter (in getFilteredOptions()) and whenever Option Resetting occurs (in getResetOptions()). This saves the ComboboxField from having to do any unnecessary iterations at all.
    • Note: Developers will still have to clean up any previously-matching options in getFilteredOptions() and getResetOptions() as needed (e.g., what the Trie-based implementation does). But when these methods are optimized, the time complexity for this should be approximately O(1). (See the guide on performance enhancements mentioned at the beginning of this RFC for additional details.)
  • Override the option navigation to navigate the literal ComboboxOption elements (e.g., with firstElementChild, nextElementSibling, lastElementChild and previousElementSibling) whenever the combobox is :empty or is :state(--options-reset). [data-options-reset] may be used instead of :state() for better backwards compatibility.
    • If the combobox is not :empty or :state(--options-reset), then the matchingOptions should be navigated instead, as usual.
  • Ensure that the CSS displays/hides options under the right circumstances. The options should be hidden by default. An option should only be displayed if it is [data-filtered-in] or if its owning combobox is :empty or :state(--options-reset).

If this seems a bit complex, this is the cost of extremely-high performance. But the positive side is that all of this would be documented by the library. And the code which developers would have to write to gain these extra performance benefits is actually very minimal.

The only question is whether developers need this kind of power for resetting options performantly (and perhaps if any better solutions exist). Again, that's the point of this RFC.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions