diff --git a/apps/www/src/content/docs/components/select/index.mdx b/apps/www/src/content/docs/components/select/index.mdx index 2c2925bff..3d2a7d972 100644 --- a/apps/www/src/content/docs/components/select/index.mdx +++ b/apps/www/src/content/docs/components/select/index.mdx @@ -117,6 +117,8 @@ The container that holds the Select items. +Set `alignItemWithTrigger` to open the list over the trigger, with the selected item lined up on the trigger's value. Base UI turns it off for touch input and when there is not enough space, and then places the list the normal way. `side` and `align` only apply when the list is placed the normal way. + ### Item Individual clickable options within the Select. diff --git a/apps/www/src/content/docs/components/select/props.ts b/apps/www/src/content/docs/components/select/props.ts index 80ddce754..14924698a 100644 --- a/apps/www/src/content/docs/components/select/props.ts +++ b/apps/www/src/content/docs/components/select/props.ts @@ -79,6 +79,12 @@ export interface SelectContentProps { */ sideOffset?: number; + /** + * Overlaps the trigger so the selected item's text lines up with the trigger's value. Has no effect in autocomplete mode. + * @default false + */ + alignItemWithTrigger?: boolean; + /** Additional CSS class names. */ className?: string; diff --git a/packages/react/components/select/__tests__/select.test.tsx b/packages/react/components/select/__tests__/select.test.tsx index ebf572071..8b196a985 100644 --- a/packages/react/components/select/__tests__/select.test.tsx +++ b/packages/react/components/select/__tests__/select.test.tsx @@ -188,6 +188,18 @@ describe('Select', () => { ).toHaveAttribute('aria-checked', 'true'); }); + it('keeps the item checkbox out of the tab order', async () => { + const user = userEvent.setup(); + render(); + await openSelect(user); + + const option = screen.getAllByRole('option')[0]; + expect(within(option).getByRole('checkbox')).toHaveAttribute( + 'tabindex', + '-1' + ); + }); + it('fires onValueChange exactly once per click on an option', async () => { const handleValueChange = vi.fn(); render(); diff --git a/packages/react/components/select/select-content.tsx b/packages/react/components/select/select-content.tsx index 1b6e20a52..123d10e99 100644 --- a/packages/react/components/select/select-content.tsx +++ b/packages/react/components/select/select-content.tsx @@ -28,6 +28,7 @@ export function SelectContent({ sideOffset = 4, side = 'bottom', align = 'start', + alignItemWithTrigger = false, radius, ...props }: SelectContentProps) { @@ -80,7 +81,7 @@ export function SelectContent({ side={side} align={align} className={styles.positioner} - alignItemWithTrigger={false} + alignItemWithTrigger={alignItemWithTrigger} data-slot='select-positioner' > )} - {children} + {autocomplete ? ( + + {children} + + ) : ( + } + className={styles.itemText} + data-slot='select-item-text' + > + {children} + + )} ) : ( children @@ -81,7 +93,7 @@ export function SelectItem({ {...props} render={(renderProps, state) => (
- {multiple && } + {multiple && } {element}
)} diff --git a/packages/react/components/select/select.module.css b/packages/react/components/select/select.module.css index b5f5ae2fb..049dfe411 100644 --- a/packages/react/components/select/select.module.css +++ b/packages/react/components/select/select.module.css @@ -22,6 +22,11 @@ overflow: auto; } +/* With alignItemWithTrigger, Base UI sizes the positioner and the popup must fill it. */ +.content[data-side="none"] { + max-height: none; +} + .content[data-starting-style], .content[data-ending-style] { opacity: 0; @@ -286,3 +291,8 @@ .hidden { display: none; } + +.itemText { + flex: 1; + min-width: 0; +}