By Jan Miksovsky on July 11, 2018
We've released v2.2 of the Elix web components library, which includes some new components for menus:
- PopupSource for buttons that invoke any kind of popup
- MenuButton for the common case of a button that invokes a menu
- Menu which contains the collection of menu items
- DropdownList for a
MenuButtonvariation that's effectively a completely customizable<select>element.
We want all these menu components to feel as polished and natural as native OS menus. Native menus have a number of subtle details, and getting the UI details right turns out to be outrageously complex. Menus are a good example of the fractal nature of UI design.
Menu positioning
Just to get started, we need to be able to position a menu with respect to a source button.
- On desktop, a menu should open on mouse down, not mouse up. This feels faster. It also allows the user to possibly select a menu item in a single drag operation.
- Only primary mousedown events should be considered. The menu should not appear on a right-click, since we want the user to still have access to the browser's own context menu.
- The menu should appear in the desired direction if there's room in that direction. If there isn't and there's room in the opposite direction, it should appear in that opposite direction. But if there isn't room in that direction either, it's preferable to show the menu in the original direction and constrain its height. (This can force scrolling.) Note that OS menus can extend outside a source window's boundaries but web components, like all HTML elements, are not allowed to extend outside the document viewport.
- The calculation of whether the menu will fit can't happen until the menu has actually rendered (many factors can influence the layout of the menu), but we don't want to let the menu be visible until we know which direction we want to use. So we layout the menu once while its invisible, see if it fits, move it to the desired position, then render it again to make it visible.
- Calculations of whether the menu fits in a particular direction are affected by the scroll position of the document.
- To complicate things, we'll want to put the keyboard focus in the menu — but moving the keyboard focus can cause scrolling as a side effect. So we'll have to complete all our menu layout and rendering before we try to move the focus into the menu.
Because these positioning rules generally apply to all popups invoked from buttons — not just menus, but also things like combo boxes — we've enshrined responsibility for position popups relative to a source button in a general-purpose PopupSource class.
Two ways of selecting a menu command with a mouse
Most people have probably never noticed there are two different ways of using a mouse to select an item from an OS menu:
Nearly every web menu handles only the first method: selecting a menu item in two clicks. But both macOS and Windows support selecting menu items in a drag operation. The two-click method is trivial to implement, but if we want to achieve the same usability of an OS menu, we'll want to also support the drag method.
A few of the more interesting problems:
- If the user mouses down, then moves the mouse away from the menu button and its associated menu, and then releases the mouse over the page background, the menu should be dismissed.
- Once the user drags into the menu with the mouse down, we want to automatically select the menu item underneath the mouse. However, those items can't get mouse events yet — the user still has the mouse down.
- If the user mouses down and then moves the mouse completely off the page, our menu button component will never receive a
mouseupevent. So we'll need to listen tomouseupevents on thedocumenttoo. - If the user mouses down, then releases the mouse over interior menu padding or a disabled item like a menu separator, the menu should also be dismissed.
- However, if the user opens the menu with a click, a subsequent click on interior menu padding or a menu separator should be absorbed and not close the menu.
This is all hard, but still doable. If you're on a laptop, try opening our MenuButton demo and confirming that the menu component feels like an OS menu.
Menus on mobile
Our two-click approach for menus should generally work on mobile devices, with some minor changes. Generally speaking, mobile menus appear when a tap ends and force use of the two-click method described above. To ensure the menu responds instantaneously, we must enable fast-tap behavior by applying the CSS touch-action: manipulation to the relevant elements.
Keyboard support and accessibility
As with all Elix components, we strive for excellent keyboard support. This benefits all users that want to use a keyboard and improves universal accessibility.
We allow users to invoke a menu button by pressing Space. The user can navigate the items in the resulting Menu with the full set of keyboard navigation keys supported by KeyboardDirectionMixin, KeyboardPagedSelectionMixin, and KeyboardPrefixSelectionMixin.
While our Menu component generally behaves like our ListBox, the accessibility rules for menus are different than lists. The role attributes involved are different, for one thing. Another way in which menu accessibility is different than that for lists is that the browser expects a menu to put the keyboard focus on an individual menu item.
Happily, our mixin-based approach to components was hugely helpful in letting us create a Menu component that worked mostly like our ListBox component, but with some differences.
Customizability
For styling and general customizability, all these menus components have replaceable parts. So you can use a MenuButton, but swap out the elements it uses by default with our own custom elements. You could:
- Have your menus show a semi-opaque backdrop over the page that's themed with your brand color.
- Change the popup (menu) portion so that the menu animates in with a sliding effect.
- Replace the
Menuthat contains the menu items with an element that lays out items as pie slices instead of the usual vertical orientation.
Bonus: a customizable select element
With our MenuButton component in hand, it was easy to create a DropdownList variation that shows the selected value as the menu button's label. The native <select> can only cope with text choices, but DropdownList can handle arbitrary content.