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:
-
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.
-
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.
RFC: Enabling Overrides of the
ComboboxField's Option Resetting LogicNOTE: 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
Comboboxcomponent's filtering performance, there is one notable limitation: Option Resetting. Option Resetting is the process where allComboboxOptions are marked as filtered in (ComboboxOption.filteredOut = false) and are inserted into the internalmatchingOptionslist in proper order. This occurs when the user collpases thecombobox, or when the developer swaps out some of theComboboboxOptions in the DOM while thecomboboxis collapsed. Option Resetting was implemented by theComboboxFieldfor two reasons:Easier Option Selection:
When a user selects an option, that option's label becomes the
combobox's text content. If the user re-expands thecombobox, it is highly likely that the only option which will match thecombobox's current filter (i.e., thecombobox's current text content) will be the currently-selected option. But if the user is expanding thecomboboxbecause 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
clearableComboboxField. (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 aclearableComboboxField.) A better UX is to display all of the options whenever thecomboboxis re-expanded, and this is accomplished by performing Option Resetting when thecomboboxis collapsed. If the user wants to see a smaller list of options, they can simply update the filter after expanding thecombobox.Keeping
matchingOptionsUp-to-Date:When the developer adds, removes, or otherwise swaps out some of the
ComboboxOptions belonging to a givenComboboxcomponent, theComboboxField's internal list ofmatchingOptionsbecomes 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 thematchingOptionsup-to-date is to perform Option Resetting (if theComboboxFieldis currently collapsed).This guarantees that no newly-added options were accidentally/incorrectly marked as
filteredOut = true, and it also guarantees that thematchingOptionslist will be correct when thecomboboxis expanded. (Note that Option Resetting does not occur when theComboboxOptions are swapped if thecomboboxis 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
ComboboxFieldto visit each of theComboboxOptions that it owns, its time complexity isO(n). This time cost is only incurred during option swapping (which is highly unlikely to happen withTrie-based implementations), or when the user collapses thecombobox(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).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
ComboboxFieldis actually sufficient for all people (given how infrequently options will be swapped and/or thecomboboxwill 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()MethodToday, the
ComboboxFieldhas a publicly overridablegetFilteredOptions()method which is strictly used by the internalComboboxField.#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 overridablegetResetOptions()method, similar to what is done forComboboxField.#filterOptions().This approach would enable developers to do things like the following:
O(1)forTrie-based implementations that are already seeking to get as many performance gains as possible.Noptions when Option Resetting occurs.matchingOptions.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 thecomboboxis expanded (or has its filter emptied). For that, we'd need to go one level deeper...Exposing Overridable
optionNavigation MethodsTo understand why overriding
optionnavigation could improve the performance of Option Resetting, it's first important to understand how allComboboxOptions 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 availableComboboxOptions 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 thecomboboxis:empty(i.e., when the filter is empty).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-resetcustom CSS state to theComboboxFieldwhenever Option Resetting occurs. (This custom state would be removed when the filter changes.) Then, using both[role="combobox"]:emptyand[role="combobox"]:state(--options-reset), we could successfully toggle the visibility of all options with pure CSS — noO(n)looping in JS needed.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
ComboboxFieldhandles Keyboard-based option navigation in this way: In Filter Mode, navigate only thematchingOptions; otherwise, navigate all of the options. This navigation implementation requires us to place every single option within thematchingOptionslist 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
matchingOptionsor 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:matchingOptionswhenever the user empties their filter (ingetFilteredOptions()) and whenever Option Resetting occurs (ingetResetOptions()). This saves theComboboxFieldfrom having to do any unnecessary iterations at all.getFilteredOptions()andgetResetOptions()as needed (e.g., what theTrie-based implementation does). But when these methods are optimized, the time complexity for this should be approximatelyO(1). (See the guide on performance enhancements mentioned at the beginning of this RFC for additional details.)ComboboxOptionelements (e.g., withfirstElementChild,nextElementSibling,lastElementChildandpreviousElementSibling) whenever thecomboboxis:emptyor is:state(--options-reset).[data-options-reset]may be used instead of:state()for better backwards compatibility.comboboxis not:emptyor:state(--options-reset), then thematchingOptionsshould be navigated instead, as usual.[data-filtered-in]or if its owningcomboboxis:emptyor: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.