Enhancement

Dropdown

A menu that opens beside its button, from markup on the page or from a template filled in per button

Import
@parallelogram-js/core/components/Dropdown
Selector
[data-dropdown]
Stylesheet
@parallelogram-js/core/styles/dropdown.css

Playground

About

Dropdown is Toggle with a place to sit and a menu to keep: it opens the target beside the trigger on the side data-dropdown-placement asks for, flips to the opposite side when there is no room, stays inside the viewport, and follows the trigger while the page scrolls. Everything Toggle does it does too -- aria-expanded and aria-controls on the trigger, closing on a click or focus outside, on Escape and when a link inside is followed, one group closing each other -- with the defaults a menu wants: outside clicks close it, and every dropdown on the page is in one group unless given its own, so opening one closes the rest.

The menu is either on the page already, named by data-dropdown-target, or built the first time the trigger opens from a <template> named by data-dropdown-template, with every {name} in its text and attribute values filled from the trigger's data-dropdown-param-<name> attributes and put back to nothing when it closes. A page with a menu per row keeps one template and a few attributes per row rather than a menu per row in the DOM. Values are substituted as text, never parsed as markup.

A menu opened from the keyboard puts focus on its first item, as the menu button pattern asks; one opened with a pointer leaves focus on the trigger, so that nothing looks chosen before anything has been, and an arrow key goes into it from there. Which it was is the data-focus-source record the framework keeps on <html>, the same one the focus rings follow.

Inside the menu, the arrow keys move between items, Home and End go to the ends, a letter jumps to the next item starting with it, Escape closes and returns focus to the trigger, and choosing an item dispatches dropdown:select with the item and the trigger's params, then closes -- unless the item or the menu carries data-dropdown-stay. The menu gets role="menu" and its items role="menuitem" when the markup has not said otherwise.

Usage

Register the component and the framework loads it the first time a page contains [data-dropdown].

import { Parallelogram } from '@parallelogram-js/core';

const app = Parallelogram.create();
app.components.add('[data-dropdown]', () => import('@parallelogram-js/core/components/Dropdown'));
app.run();

The framework is optional. Dropdown.enhanceAll() mounts the component on every matching element by itself, for a page that wants one enhancement rather than a framework:

import Dropdown from '@parallelogram-js/core/components/Dropdown';

Dropdown.enhanceAll();

It takes a selector and the component's options, and returns the instance it mounted them with. What it leaves out is what the framework adds: loading a component only on the pages that use it, mounting again after a page swap, and ordering with dependsOn.

Its styles come from the package stylesheet:

@import '@parallelogram-js/core/styles/dropdown.css';

Attributes

NameTypeDefaultDescription
data-dropdown-targetstringnoneA selector for a menu already on the page
data-dropdown-templatestringnoneA selector for a <template> whose content becomes the menu the first time the trigger opens; its one root element is the menu
data-dropdown-param-<name>stringnoneFills every {name} in the template's text and attribute values, as text, so a value cannot inject markup; passed along with dropdown:select. In a boolean attribute such as disabled or hidden, an empty, false or unfilled value takes the attribute away and anything else keeps it
data-dropdown-paramsstringnoneAll the params in one attribute: JSON, or key:value pairs separated by commas as in {id: 12, name: Ada}, braces and quotes optional; a data-dropdown-param-<name> attribute wins for its name
data-dropdown-disabledstringnoneThe data-dropdown-item names of the items to disable for this trigger, separated by commas or spaces
data-dropdown-hiddenstringnoneThe data-dropdown-item names of the items to leave out for this trigger: left out of a menu built from a template, hidden in a menu on the page
data-dropdown-itemstringnoneThe name data-dropdown-disabled and data-dropdown-hidden refer to an item by On an item.
data-dropdown-keepbooleanfalseKeep a menu built from a template in the DOM after it closes, rather than removing it For every instance, Dropdown.defaults.keep.
data-dropdown-placementbottom-start | bottom-end | bottom | top-start | top-end | top | left | rightbottom-startWhich side of the trigger the menu opens on and which edge it lines up with; it flips when there is no room For every instance, Dropdown.defaults.placement.
data-dropdown-offsetnumber4Pixels between the trigger and the menu For every instance, Dropdown.defaults.offset.
data-dropdown-match-widthbooleanfalseMake the menu at least as wide as the trigger For every instance, Dropdown.defaults.matchWidth.
data-dropdown-portalbooleanfalseMove the menu to the end of the body while it is open, for a trigger inside an ancestor that clips or transforms For every instance, Dropdown.defaults.portal.
data-dropdown-groupstringdropdownDropdowns that share a group name close each other; every dropdown is in one group unless given its own, and an empty name makes one independent For every instance, Dropdown.defaults.group.
data-dropdown-capturebooleantrueClose on a click or focus outside the trigger and menu For every instance, Dropdown.defaults.capture.
data-dropdown-close-navigationbooleantrueClose when a page link inside the menu is followed For every instance, Dropdown.defaults.closeOnNavigation.
data-dropdown-close-escapebooleantrueClose with Escape, returning focus to the trigger For every instance, Dropdown.defaults.closeOnEscape.
data-dropdown-manualbooleanfalseClose only from the trigger or the group For every instance, Dropdown.defaults.manual.
data-dropdown-animatebooleantrueWait for the stylesheet's animations before changing state For every instance, Dropdown.defaults.animateToggle.
data-dropdown-stayflagnoneChoosing this item, or any item in this menu, leaves the menu open On an item, or the menu.
data-dropdown-stateset by the componentclosed | opening | open | closingnoneThe menu's state; the stylesheet hides a closed menu and animates opening and closing On target.
data-dropdown-sideset by the componenttop | bottom | left | rightnoneThe side the menu took, which the stylesheet animates it in from On target.
data-dropdown-enhancedset by the componentflagnonePresent on a trigger while Dropdown is mounted on it

Events

NameDetailDescription
dropdown:show{ target: HTMLElement; trigger: HTMLElement; timestamp: number }Dispatched on the trigger when its menu opens DOM event and event bus
dropdown:hide{ target: HTMLElement; trigger: HTMLElement; timestamp: number }Dispatched on the trigger when its menu closes DOM event and event bus
dropdown:select{ item: HTMLElement; params: Record<string, string>; target: HTMLElement; trigger: HTMLElement; timestamp: number }Dispatched on the trigger when an item is chosen, with the item and the trigger's params; the menu then closes unless the item or the menu carries data-dropdown-stay DOM event and event bus
dropdown:mount{ element: HTMLElement; target: HTMLElement | null; isOpen: boolean; timestamp: number }A trigger was set up; the target is null until a templated menu is first built Event bus

CSS custom properties

NameDefaultDescription
--dropdown-z-index1000Stacking of an open menu
--dropdown-min-width10remNarrowest a menu is drawn
--dropdown-max-height60vhTallest a menu grows before it scrolls
--dropdown-padding0.25remSpace between the menu edge and its items
--dropdown-item-padding0.45rem 0.6remPadding of an item
--dropdown-item-radius0.2emCorner radius of an item’s hover
--dropdown-transition-durationvar(--framework-transition-duration)How long opening and closing take; none under prefers-reduced-motion
--dropdown-shift4pxHow far a menu moves while it opens and closes