Commit 10e0217
authored
feat: add Autocomplete component to next/ (#4808)
* feat: add Autocomplete component to next/
Closes #4433
* fix: narrow value and defaultValue types to string in AutocompleteProps
* refactor: use Popover API and CSS Anchor Positioning for Autocomplete dropdown
* fix: resolve lint errors and fix dropdown anchor positioning in Autocomplete
* feat(next): complete WAI-ARIA, keyboard nav and UX for Autocomplete
- Full WAI-ARIA combobox pattern: aria-activedescendant, aria-expanded,
aria-controls, aria-autocomplete, aria-haspopup, live region (role=status)
- Keyboard navigation: ArrowDown/Up (anchors to selected option on reopen),
Enter to confirm, Escape to close
- UX: show all options on open/reopen, filter only while actively typing
(isFiltering), scroll selected option into view on open
- Fix Popover API light-dismiss race via isMouseInteraction ref
- Fix: consume flag in handleFocus to prevent stale state on Tab cycles
- Fix: onBlur closes listbox when tabbing away from the field
- Fix: remove role="option" from the no-options placeholder item
- Fix: index-based keys handle duplicate option strings safely
- CSS: CUBE CSS with typography tokens, remove BEM-style class names
- Tests: 35 passing — keyboard nav, a11y (jest-axe), states, Tab-away
* fix(next): hide error icon in Autocomplete to avoid duplicate with search icon
* feat(next): extract Menu and MenuItem as shared visual components
Menu and MenuItem are thin visual wrappers (no ARIA hardcoded) that accept
any role via spread props — supporting role="listbox"/role="option" for
Autocomplete today and role="menu"/role="menuitem" for future navigation menus.
Autocomplete is refactored to use Menu/MenuItem. The dropdown styles move to
menu.css; autocomplete.css retains only Autocomplete-specific overrides.
The anchor CSS custom property is renamed from --autocomplete-anchor to
--menu-anchor for reuse by Select and Combobox.
* feat(next): add allowCustomValue and clear button to Autocomplete
allowCustomValue shows an "Add" option in the dropdown when the user types a
value not present in the options list. Selecting it calls onOptionSelect with
the typed value.
The clear button (×) appears in the input endAdornment whenever the field has
a value. It is always rendered (visibility:hidden when empty) to prevent layout
shift. onClear and clearLabel props follow the same pattern as Search.
* feat(next): redesign allowCustomValue in Autocomplete
- Add sticky "add option" row at top of listbox (always visible when allowCustomValue)
- Show typed value inline; disable row when value matches existing option
- Persist user-added custom values across listbox open/close
- Fix Enter key confirming custom value without arrow-key navigation
- Fix focus-after-select reopening the listbox (isMouseInteraction guard)
- Move add-option to index 0; shift regular options by +1 for keyboard nav
- Add AllowCustomValue story and update Figma Code Connect
- Update tests for new add-option design
* fix(next): align Menu and Autocomplete visuals with Figma
- MenuItem.tsx: merge className prop with eds-menu-item base class
- menu.css: use border instead of outline/shadow, overlap input with
margin-block-start: -1px, correct spacing tokens, font-weight 400,
letter-spacing, font-feature-settings, antialiased smoothing
- autocomplete.css: use border-medium in active state, suppress focus
ring when dropdown is open, square connecting corners, fix add-option
hover/active background for disabled state
- Autocomplete.tsx: show focus ring on add-option regardless of
customValueTyped state
- Autocomplete.stories.tsx: move to Inputs/ folder
* fix(next): remove unused addOptionIndex variable in Autocomplete
* feat(next): add loading state to Autocomplete
- Add loading and loadingText props
- Show Progress.Circular spinner in endAdornment when loading
- Show loadingText in dropdown while loading
- Remove dead iconTone/data-color-appearance from search icon (CSS handles state colors)
- Fix lint errors in test file (container.querySelector, .closest, spurious async)
* fix(next): add aria-posinset and aria-setsize to Autocomplete options
* fix(next): improve screen reader label for disabled add-option in Autocomplete
* fix(next): auto-highlight single result and improve add-option UX in Autocomplete
- Enable add-option only when there are no filtered results (not just no exact match)
- Auto-highlight the single remaining option when filtering narrows to one result
- Auto-highlight add-option when no results match and allowCustomValue is set
- Allows Enter to confirm without requiring arrow key navigation
* fix(next): fix failing Autocomplete tests after aria and UX changes
* fix(next): address review feedback on Autocomplete
- Replace aria-hidden+tabIndex on clear button with inert attribute (no layout shift, cleaner a11y)
- Sync internalValue when selectedOption prop changes externally
- Add onClick to options for AT that synthesises click without mousedown
- Remove redundant aria-live=polite (role=status implies it)
- Fix WCAG 2.5.3: show 'Add: {value}' as visible text to match accessible name
- Replace deprecated clip:rect with clip-path:inset(50%) in sr-only
- Restore HTMLElement.prototype.matches in afterAll to prevent test leak
* feat(next): align Autocomplete API with industry conventions
- Rename selectedOption → value (the selected option)
- Rename onOptionSelect → onValueChange (selection callback)
- Rename value → inputValue (controlled search text)
- Rename defaultValue → defaultInputValue (uncontrolled search text default)
Follows the value/onValueChange/inputValue pattern used by Base UI,
Radix UI, and designsystemet's Combobox component.
* fix(next): correct inert attribute type from string to boolean
* fix(next): address remaining review feedback on Autocomplete
- Use aria-labelledby={inputId} on listbox (works for all label types)
- Remove unused AutocompleteOption type export
- Memoize allOptions and filteredOptions with useMemo
- Add comment to Menu.tsx clarifying it is a generic container (caller sets role)
- Add comment to menu.css explaining --menu-anchor custom property inheritance
* feat(next): add generic T support and extended API to Autocomplete
- Make AutocompleteProps generic (T = string) — fixes TS conditional
type error that prevented teams from wrapping the component generically
- Add getOptionLabel, getOptionValue, optionDisabled, optionsFilter,
renderOption, onInputChange, onCustomValueConfirm
- Replace onOptionsChange with onValueChange (returns T directly, not
{selectedItems: T[]}) for cleaner TypeScript and AI-assisted coding
- Split custom value handling: onValueChange for list options,
onCustomValueConfirm for user-typed values — keeps types unambiguous
when T = string
- Rewrite stories with organised argTypes (8 categories), accurate
descriptions, and new Controlled, ObjectOptions, CustomOptionRendering,
DisabledOptions, AllowCustomValue stories
* test(next): suppress aria-valid-attr-value axe false positive in Autocomplete
aria-controls referencing a display:none popover is flagged by axe-core
but is valid per WAI-ARIA 1.2 (axe-core#3141).
* fix(next): add teal scrollbar to Autocomplete dropdown menu
Uses --eds-color-bg-accent-fill-emphasis-default, the static accent token
that resolves to the EDS interactive teal without needing data-color-appearance.
* fix(next): fix Menu density support and hover/disabled patterns
- Remove hardcoded selectable-space override, add data-selectable-space=md
so menu items respond correctly to density modes
- Wrap hover in @media (hover: hover) and (pointer: fine)
- Use :is(:disabled, [aria-disabled='true']) for disabled state
* feat(next): add density story and fix Menu squished proportions
Add DensityModes story to Autocomplete showing spacious vs comfortable
side by side. Add data-space-proportions="squished" to Menu so vertical
padding matches the compact proportions used by all other components.
* fix(next): address round-2 review feedback on Autocomplete
- Fix useEffect value-sync firing on every render when getOptionLabel is
inline — drop getLabelFn from deps, add eslint-disable with explanation
- Clear internalValue when value transitions to undefined
- Use getOptionValue as React key when provided, fall back to label
- Align add-option onClick guard to click-time (matches regular options)
- Add role="presentation" to loading/no-options items inside listbox
- Clamp activeIndex when filteredItems shrinks (async search)
- Rename isMouseInteraction → suppressNextFocusOpen for clarity
- Fix disabled menu item color: subtle → disabled (matches all other components)
- Expand handleFocus/handleClick comment explaining mouse event chain
- Add eslint-disable explanation on scroll-on-open effect
- Add hidden:true note to test file explaining jsdom popover limitation
* fix(next): fix lint error and defaultInputValue bug in Autocomplete
- Replace role="presentation" aria-disabled on loading/no-options rows
with role="presentation" data-status (aria-disabled is invalid on
presentation role per jsx-a11y/role-supports-aria-props)
- Separate cursor styles: not-allowed for disabled, default for data-status
- Fix defaultInputValue being overwritten on mount when value is undefined
* refactor(next): extract shared select option utils from Autocomplete
- Add next/utils/SelectOption.types.ts with SelectOptionProps<T> shared
by Autocomplete, NativeSelect, and Combobox
- Add next/utils/selectOptions.ts with pure utility functions:
resolveOptionLabel, resolveOptionKey, isOptionDisabled, defaultOptionsFilter
- Update AutocompleteProps to extend SelectOptionProps<T>
- Replace inline logic in Autocomplete.tsx with the shared utils
- Add unit tests for all four utility functions
* fix(next): address round-3 review feedback on Autocomplete
- move prevValueRef declaration above the useEffect that uses it
- rename disabled → optionIsDisabled in filteredItems.map to avoid shadowing outer disabled prop
- remove useCallback from getLabelFn (not in any dependency arrays)
- add comment to isControlled clarifying it governs inputValue, not value
- add JSDoc to onClear describing controlled-mode behaviour
- add regression tests: inline getOptionLabel re-render stability and duplicate-label key collision
* fix(next): address round-4 review feedback on Autocomplete
- omit internal combobox handlers (onMouseDown, onClick, onFocus, onBlur,
onKeyDown, onKeyUp) from AutocompleteProps so consumer spreads cannot
silently override them
- broaden onValueChange to (value: T | undefined) => void and call it with
undefined from handleClear so controlled-mode parents are notified on clear
- update value-sync effect comment to reference inputValue control specifically
- add comment on inline newFiltered in handleChange explaining why filteredItems
cannot be reused there
* fix(next): show error icon in Autocomplete invalid state
Removes hardcoded hideErrorIcon — the Figma design shows the error icon
in the invalid state alongside the clear button.
* fix(next): remove invalid-state background hack from Autocomplete dropdown
Menu items are transparent by default and inherit the floating background
from the menu panel — no override needed in invalid state. Removes the
direct background-color override and the brightness(0.97) hover filter hack.
* feat(next): add Async and WithReactHookForm stories to Autocomplete
Async story demonstrates the full server-side search pattern:
onInputChange + debounce + optionsFilter={() => true} + loading + renderOption.
WithReactHookForm story shows Controller integration with value/onValueChange,
onClear resetting the field, and invalid/helperMessage wiring for validation.
* fix(next): prevent defaultInputValue being overwritten on mount
Adds mountedRef to skip the value-sync effect on initial render.
The useState initializer already seeds internalValue correctly, so
running the effect on mount overwrote defaultInputValue whenever
value was also provided.
Adds two regression tests for the defaultInputValue describe block.
* chore(next): reorder Autocomplete stories — DensityModes moved to end
Places Async directly after Loading (natural pairing) and DensityModes
last as a theming/visual concern rather than a feature story.
* fix(next): address review feedback on Autocomplete
- ArrowUp from first option now wraps to last instead of closing listbox
- Live region announces loadingText when loading=true instead of "No results"
- Fix argTypes onValueChange summary to include | undefined
- Remove omitted event handlers from argTypes (onFocus/onBlur/onKeyDown etc)
- Move Introduction story default options to story args
* fix(next): stop ArrowUp at first Autocomplete option instead of wrapping1 parent fbe6874 commit 10e0217
18 files changed
Lines changed: 2524 additions & 0 deletions
File tree
- packages/eds-core-react/src/components/next
- Autocomplete
- Menu
- utils
Lines changed: 41 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
0 commit comments