feat: add dropdown customization slots to Select and Multiselect - #4856
feat: add dropdown customization slots to Select and Multiselect#4856gethinwebster wants to merge 4 commits into
Conversation
Add renderDropdownHeader, renderDropdownFooter, and renderFilteringActions render props, plus dropdownRole and dropdownAriaDescribedby, to Select and Multiselect for customizing dropdown content. All new props are optional and backward compatible.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4856 +/- ##
==========================================
- Coverage 97.63% 97.63% -0.01%
==========================================
Files 957 958 +1
Lines 31097 31134 +37
Branches 11435 11459 +24
==========================================
+ Hits 30361 30397 +36
- Misses 689 690 +1
Partials 47 47 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Remove the dedicated dropdown-customization.scss. filter-row/filter-input/filtering-actions now live in the shared select/parts/styles.scss and are rendered by the Filter component (via a new filteringActions prop); the header/footer styles are declared directly in each component stylesheet (footer needs no z-index). No public API or behavior change.
There was a problem hiding this comment.
Pull request overview
Adds new dropdown customization slots to Select/Multiselect (header, footer, and inline filtering actions) along with accessibility-oriented props to control dropdown semantics (dropdownRole) and descriptions (dropdownAriaDescribedby). The implementation factors shared dropdown header/footer composition into a helper and updates styles, test utils, unit/integration tests, snapshots, and demo pages.
Changes:
- Add
renderDropdownHeader/renderDropdownFooter/renderFilteringActionsrender-props anddropdownRole/dropdownAriaDescribedbyto shared select props, wiring them into Select/Multiselect dropdown rendering. - Introduce shared
composeDropdownContenthelper and new styling hooks for custom header/footer and filtering-actions layout. - Add/extend test-utils finders, unit + browser integration tests, snapshots, and dev-pages showcasing the new slots.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/test-utils/dom/select/index.ts | Adds DOM test-utils finders for dropdown header/footer and filtering actions. |
| src/test-utils/dom/multiselect/index.ts | Adds DOM test-utils finders for dropdown header/footer and filtering actions. |
| src/select/utils/use-select.ts | Adds dropdownRole handling and exposes a closeDropdown callback for customization slots. |
| src/select/utils/dropdown-customization.tsx | New shared helper to compose dropdown header/footer nodes. |
| src/select/styles.scss | Adds Select dropdown header/footer styling hooks. |
| src/select/parts/styles.scss | Adds layout styles for filter row + inline filtering actions. |
| src/select/parts/filter.tsx | Renders optional inline filtering actions next to the filter input. |
| src/select/internal.tsx | Wires new customization props into Select dropdown header/footer and ARIA describedby joining. |
| src/select/interfaces.ts | Extends public Select/BaseSelect props and types for dropdown customization + ARIA props. |
| src/select/tests/dropdown-customization.test.tsx | New unit tests validating composition, props, and focus/close behavior. |
| src/select/integ/dropdown-customization.test.ts | New browser tests for keyboard reachability of interactive header/footer. |
| src/multiselect/use-multiselect.tsx | Passes through dropdownRole/dropdownAriaDescribedby and exposes closeDropdown. |
| src/multiselect/styles.scss | Adds Multiselect dropdown header/footer styling hooks. |
| src/multiselect/internal.tsx | Wires new customization props into Multiselect dropdown header/footer and ARIA describedby joining. |
| src/multiselect/interfaces.ts | Re-exports dropdown customization types via Select types for Multiselect. |
| src/multiselect/tests/dropdown-customization.test.tsx | New unit tests mirroring Select customization behavior for Multiselect. |
| src/tests/snapshot-tests/snapshots/test-utils-selectors.test.tsx.snap | Snapshot updates for new selector classnames. |
| src/tests/snapshot-tests/snapshots/documenter.test.ts.snap | Documenter snapshot updates for new public props/types and test-utils APIs. |
| pages/select/dropdown-customization.page.tsx | New dev page demonstrating Select dropdown customization variants. |
| pages/multiselect/dropdown-customization.page.tsx | New dev page demonstrating Multiselect dropdown customization variants. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
closeDropdownAndRefocus focused the trigger (whose onFocus already closes the dropdown) and then called closeDropdown() again, firing the close side effects twice. Focus the trigger to close, and only call closeDropdown() explicitly when the trigger is already focused.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/select/styles.scss:15
- PR description mentions extracting the header/footer styling into a shared SCSS mixin to avoid duplication, but the
.dropdown-header/.dropdown-footerrules are still duplicated here and insrc/multiselect/styles.scss(and there are no other SCSS occurrences). Either implement the shared mixin (and consume it from both files) or adjust the PR description to match the actual approach.
// Custom dropdown header/footer regions (renderDropdownHeader / renderDropdownFooter). Duplicated in the
// Multiselect stylesheet. The header can sit directly above the options list, so it is lifted with z-index
// to keep focusable content's focus ring above the list; the footer sits below the list with natural
// clearance and needs no lift.
…pdown customization Remove the inline filtering-actions slot, which coupled consumer content to the built-in filter input's layout and was fragile to style. Custom controls near the filter can use renderDropdownHeader instead. Filter reverts to the bare input; renderDropdownHeader/renderDropdownFooter, dropdownRole and dropdownAriaDescribedby are unchanged.
Description
Adds render-prop slots to
SelectandMultiselectfor customizing the dropdown, plus two props for consumers to own the accessibility semantics of that content:renderDropdownHeader— content pinned at the top of the dropdown, above the filter input and options list.renderDropdownFooter— content pinned at the bottom, always sticky (never scrolls), additive to the built-in status region.dropdownRole?: 'auto' | 'dialog'— lets a consumer opt the dropdown intodialogsemantics for interactive custom content, independent of whether filtering is enabled.dropdownAriaDescribedby?: string— addsaria-describedbyto the dropdown's listbox/dialog content.Each render prop receives
DropdownContentProps { filterText, closeDropdown }. The props live on the sharedBaseSelectProps, soSelectandMultiselectinherit them; a smallcomposeDropdownContenthelper wraps the custom header/footer around the filter and the built-in status region. The two short header/footer wrapper styles are declared in each component's stylesheet (the header is lifted withz-indexsince it can sit directly above the options list; the footer needs no lift).Backward compatible: all new props are optional. When omitted, the composed output is unchanged from today's behavior (verified — additive-only documenter/selectors snapshots, no removals).
Related links, issue #, if available: n/a
How has this been tested?
dropdown-customizationsuites for Select and Multiselect (composition/order,closeDropdowncloses + returns focus to the trigger exactly once,filteringType='none',dropdownRole,dropdownAriaDescribedby,expandToViewport). Full Select/Multiselect/dropdown scope passes (503 tests).Taband an interactive header via a backwardShift+Tab, with the dropdown staying open, in bothdialog(filteringType='auto') and plainlistbox(filteringType='none') modes — so interactive header/footer content is keyboard-reachable without requiringdropdownRole='dialog'.tsc --noEmitclean;npm run buildsucceeds; documenter andtest-utils-selectorssnapshots regenerated (additive only).pages/selectandpages/multiselectexercise the header/footer slots, including the no-filter header case, forceddropdownRole="dialog"withdropdownAriaDescribedby, andexpandToViewport.Review checklist
The following items are to be evaluated by the author(s) and the reviewer(s).
Correctness
CONTRIBUTING.md.CONTRIBUTING.md.Security
checkSafeUrlfunction.Testing
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.