MultiSelect
Several options at once, counted in the trigger and named as removable chips.
A multi-select asks for a set rather than a value, so it reports in two places: the trigger counts what is held, and the chips below it name each choice and offer to remove it. It shares the dropdown's menu, search, keyboard navigation, disabled options, and differs by accepting a list, never closing on a choice, and keeping the selection visible after the menu is gone.
Usage
import { MultiSelect } from "@asheeui/web";
const topics = [
{ label: "Invoices", value: "invoices" },
{ label: "Payments", value: "payments" },
];
<MultiSelect
label="Topics you follow"
InputLabel="Choose topics..."
options={topics}
value={values}
onChange={setValues}
/>
Two props say what the field is called. label is the field's label, above the control, and
InputLabel is the trigger's empty text, shown while nothing is selected, it defaults to
"Select options", so state it. A value array controls the selection; without one the
component keeps the accumulated selection itself and still reports every change through
onChange.
Choosing an option toggles it, and the menu stays open, so a set is built in one visit rather than one visit per option.
Selection and chips
The chips are derived from value, so by default they cannot disagree with the selection.
chipLabel puts a caption above them, and disableChipDisplay hides the panel entirely
while the trigger keeps counting, which suits a field whose chips would crowd the form. Chip
appearance comes from the chip bag: color, size, radius and a className override.
Its size is registered as sm, its colour falls back to the trigger's resolved colour, and
its radius to the trigger's radius and then sm.
When the selection is owned elsewhere, a table's filter store, a wizard, chipOptions
replaces the derived chips and handleAddChip and handleRemoveChip replace the toggle, so
the component draws what it is given instead of deciding.
Search is on by default here, unlike the dropdown: isSearch adds the field at the top of
the menu, searchPlaceholder and searchInputName name it, and belowList renders under
the options.
Props
The trigger follows the field family's web defaults, size is md and variant is
bordered, while radius follows the application's defaultRadius and color its
defaultColor. A status of error outranks that colour with the semantic danger role
and sets aria-invalid, which is the family's rule rather than this component's.
The chip bag resolves separately: chip.size is registered sm, chip.color falls back to
the resolved trigger colour, and chip.radius to the trigger's radius and then sm. Write
them under components.multiSelect.chip to change them once, or in the chip prop to
change them for one instance.
Accessibility
- The trigger is a real
<button type="button">witharia-haspopup="listbox"andaria-expanded, named by the field's label. The description and message are referenced witharia-describedby, and anerrorstatus setsaria-invalid. - The list is a
role="listbox"carryingaria-multiselectable, and each option is arole="option"witharia-selected. Choosing one toggles it without closing the menu, so a keyboard user can build the whole set before Escape closes it and returns focus to the trigger. - Keyboard navigation is the menu's: ↑ and ↓ move the current option and wrap, Home and End jump to the edges, typed characters jump to the first option whose label starts with them, and the search field filters the list as you type.
- Each chip's remove control is a real button whose accessible name is "Remove" followed by the option's label, so a chip can be removed without a pointer; while the field is disabled those controls leave the tab order with it.
The label, description and message behave as the field anatomy describes.