Jan Miksovsky’s BlogArchive2018 AboutRSSJSONContact

Building a great combo box component is so much trickier than you'd think

Combo boxes are hard to implement well. At the most abstract level, a combo box combines a text input with a button that can invoke a popup. The popup contains a list or other data-entry element:

But even this simple visual arrangement hides many complexities. A while ago we shared the challenges building a great menu component, so we thought we’d offer a similar look at the many issues faced as we added a new family of ComboBox components to the Elix web components library.

Keyboard focus challenges

Keyboard focus proves to be one of the most challenging aspects of writing good general-purpose web components:

Beyond these points, very similar UI patterns may call for focus to be handled quite differently, making it tricky to establish reusable baseline behavior.

Where should the focus go in a combo box?

Handling keyboard focus for a combo box is particularly hard, because there are multiple potentially focusable elements. For a combo box containing a list, we want to keep the keyboard focus on the text input element, but the user may also want keys to navigate selection in the list.

If the user presses the Left or Right arrow, they most likely want to move the insertion point to the left or right. However, if they press the Up or Down arrow, it’s reasonable to assume they want to move to the previous or next selection in the list.

Moreover, the user wants to seamlessly move back and forth between typing and navigating.

Given these constraints, it seems best to always keep the keyboard focus on the input element. That means we need to listen for keystrokes that seem meant for the list (like Up, Down, Page Up, and Page Down) and tell the the list what to do via method calls. If the user clicks on the popup, we’ll let that click do whatever it would normally do (like select the clicked item), then ensure the focus is still on the input.

Controlling the input and popup with the keyboard

Keeping the focus on the input makes managing the popup harder. We usually want a popup to implicitly close if it loses focus, but in this case we won’t give the popup the focus at all.

We also face some odd challenges on mobile:

Auto-complete

Auto-complete is a common feature in combo boxes with lists. That feature is useful in other contexts, too, so we’ve implemented auto-complete in an AutoCompleteInput that we can use elsewhere.

Auto-complete presents its own challenges:

If we do manage to match the user’s input against a known list of text choices, we auto-complete the input text, and then leave the auto-completed portion of the text selected. If the user wants to enter something else, they can just keep typing, and that’ll automatically overwrite the selected (i.e., auto-completed) text.

Accessibility

We try as hard as we can to do an exemplary job supporting universal access in the Elix web components, including ARIA recommendations for combo boxes. A combo box, unfortunately, appears to hit the limits of what’s currently possible in web component accessibility.

For the time being, we do the best we can, and hope that the W3C Accessibility Object Model will open up the possibility of better accessibility.

Making ComboBox as general-purpose as possible

Our goal with ComboBox is to provide a solid general-purpose combo box class that can be used for any combination of a text input with a popup. The element featured in the combo box’s popup could be a list, or a calendar, or anything else.

To accommodate this level of flexibility, we’ve defined roles for our ComboBox family. Each role is filled by another component. For example, the base ComboBox class defines an input role that you can fill with any input-like element that should be shown in the combo box. By default, this role is filled with a standard <input> element, but you use other input elements in that role. Our AutoCompleteComboBox uses the aforementioned AutoCompleteInput, letting a combo box easily pick up auto-complete support.

So a ComboBox is being defined as quite an abstract thing: it has an input-like element, a button to toggle the popup, and the popup itself. Everything else — auto-complete behavior, the use of a list in the popup, and so on — is defined in more specialized component classes.

That flexibility lets us quickly adapt the combo box pattern to new contexts. For example, we created a FilterListBox that filters its items to: a) show only those items that match a text query string, and b) highlights the matching portion of the item text. We can then drop that FilterListBox into the list role of a ListComboBox and, voilà, we get a combo box that can filter items as the user types:

Factoring UI this way lets each component do a great job at its appointed task. In this case, we can take address all the complexity of keyboard support in the generic ComboBox component. You can then use that foundation for great keyboard support in more specialized combo box components.

Recombining components to implement new patterns

One other benefit of factoring UI this way is that components can be readily recombined to create new variations. A UI pattern that’s become common in development tools is a “list with search”. This pattern lets you quickly open a file or select a command by typing the first few letters, then selecting the best match from a list. Here’s an example from Chrome dev tools:

As you type into the input field, the list of files below is filtered to show only those whose names contain the text string. Moreover, the matching portion of the file name is highlighted.

We can quickly reproduce this general UI pattern as a web component by taking the AutoCompleteInput and FilterListBox described above, and wiring them together to create ListWithSearch:

This is an in-place variation of the FilterComboBox we saw earlier. ListWithSearch is useful in cases where the user is completely focused on selecting an item from a large set. That task focus permits us to place the input and list in situ, instead of needing to save space by hiding the list behind the popup. The user can apprehend the purpose of the UI elements more quickly, and can complete their task in fewer steps.