Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/www/src/content/docs/components/select/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ The container that holds the Select items.

<auto-type-table path="./props.ts" name="SelectContentProps" />

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.
Expand Down
6 changes: 6 additions & 0 deletions apps/www/src/content/docs/components/select/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
12 changes: 12 additions & 0 deletions packages/react/components/select/__tests__/select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<BasicSelect multiple />);
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(<BasicSelect multiple onValueChange={handleValueChange} />);
Expand Down
3 changes: 2 additions & 1 deletion packages/react/components/select/select-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function SelectContent({
sideOffset = 4,
side = 'bottom',
align = 'start',
alignItemWithTrigger = false,
radius,
...props
}: SelectContentProps) {
Expand Down Expand Up @@ -80,7 +81,7 @@ export function SelectContent({
side={side}
align={align}
className={styles.positioner}
alignItemWithTrigger={false}
alignItemWithTrigger={alignItemWithTrigger}
data-slot='select-positioner'
>
<SelectPrimitive.Popup
Expand Down
16 changes: 14 additions & 2 deletions packages/react/components/select/select-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,19 @@ export function SelectItem({
{leadingIcon}
</div>
)}
<Text data-slot='select-item-text'>{children}</Text>
{autocomplete ? (
<Text className={styles.itemText} data-slot='select-item-text'>
{children}
</Text>
) : (
<SelectPrimitive.ItemText
render={<Text />}
className={styles.itemText}
data-slot='select-item-text'
>
{children}
</SelectPrimitive.ItemText>
)}
</>
) : (
children
Expand Down Expand Up @@ -81,7 +93,7 @@ export function SelectItem({
{...props}
render={(renderProps, state) => (
<div {...renderProps}>
{multiple && <Checkbox checked={state.selected} />}
{multiple && <Checkbox checked={state.selected} tabIndex={-1} />}
{element}
</div>
)}
Expand Down
10 changes: 10 additions & 0 deletions packages/react/components/select/select.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -286,3 +291,8 @@
.hidden {
display: none;
}

.itemText {
flex: 1;
min-width: 0;
}
Loading