Skip to content
Draft
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
95 changes: 95 additions & 0 deletions packages/react/src/combobox/value/ComboboxValue.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,101 @@ describe('<Combobox.Value />', () => {
});
});

describe('multiple selection', () => {
it('displays comma-separated labels from items array', async () => {
const items = [
{ value: 'sans', label: 'Sans-serif' },
{ value: 'serif', label: 'Serif' },
{ value: 'mono', label: 'Monospace' },
];

await render(
<Combobox.Root defaultValue={[items[0], items[1]]} items={items} multiple>
<Combobox.Trigger>
<span data-testid="value">
<Combobox.Value />
</span>
</Combobox.Trigger>
<Combobox.Portal>
<Combobox.Positioner>
<Combobox.Popup>
<Combobox.Input />
<Combobox.List>
{(item: any) => (
<Combobox.Item key={item.value} value={item}>
{item.label}
</Combobox.Item>
)}
</Combobox.List>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>,
);

expect(screen.getByTestId('value')).to.have.text('Sans-serif, Serif');
});

it('supports ReactNode labels for multiple selections', async () => {
const items = [
{ value: 'bold', label: <strong>Bold Text</strong> },
{ value: 'italic', label: <em>Italic Text</em> },
];

await render(
<Combobox.Root defaultValue={items} items={items} multiple>
<Combobox.Trigger>
<span data-testid="value">
<Combobox.Value />
</span>
</Combobox.Trigger>
<Combobox.Portal>
<Combobox.Positioner>
<Combobox.Popup>
<Combobox.List>
{(item: any) => (
<Combobox.Item key={item.value} value={item}>
{item.label}
</Combobox.Item>
)}
</Combobox.List>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>,
);

const value = screen.getByTestId('value');
expect(value.querySelector('strong')).to.have.text('Bold Text');
expect(value.querySelector('em')).to.have.text('Italic Text');
expect(value).to.have.text('Bold Text, Italic Text');
});

it('falls back to raw values when items are not provided', async () => {
await render(
<Combobox.Root defaultValue={['serif', 'mono']} multiple>
<Combobox.Trigger>
<span data-testid="value">
<Combobox.Value />
</span>
</Combobox.Trigger>
<Combobox.Portal>
<Combobox.Positioner>
<Combobox.Popup>
<Combobox.List>
<Combobox.Item value="serif">Serif</Combobox.Item>
<Combobox.Item value="mono">Monospace</Combobox.Item>
</Combobox.List>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>,
);

expect(screen.getByTestId('value')).to.have.text('serif, mono');
});
});

describe('primitive values', () => {
it('handles string values correctly', async () => {
await render(
Expand Down
5 changes: 4 additions & 1 deletion packages/react/src/combobox/value/ComboboxValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as React from 'react';
import { useStore } from '@base-ui-components/utils/store';
import { useComboboxRootContext } from '../root/ComboboxRootContext';
import { resolveSelectedLabel } from '../../utils/resolveValueLabel';
import { resolveMultipleLabels, resolveSelectedLabel } from '../../utils/resolveValueLabel';
import { selectors } from '../store';

/**
Expand All @@ -19,12 +19,15 @@ export function ComboboxValue(props: ComboboxValue.Props): React.ReactElement {
const itemToStringLabel = useStore(store, selectors.itemToStringLabel);
const selectedValue = useStore(store, selectors.selectedValue);
const items = useStore(store, selectors.items);
const multiple = useStore(store, selectors.selectionMode) === 'multiple';

let returnValue = null;
if (typeof childrenProp === 'function') {
returnValue = childrenProp(selectedValue);
} else if (childrenProp != null) {
returnValue = childrenProp;
} else if (multiple && Array.isArray(selectedValue)) {
returnValue = resolveMultipleLabels(selectedValue, items, itemToStringLabel);
} else {
returnValue = resolveSelectedLabel(selectedValue, items, itemToStringLabel);
}
Expand Down
57 changes: 53 additions & 4 deletions packages/react/src/select/value/SelectValue.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -475,17 +475,66 @@ describe('<Select.Value />', () => {

await render(
<Select.Root value={['sans', 'serif']} items={items} multiple>
<Select.Value data-testid="value" />
<Select.Trigger>
<span data-testid="value">
<Select.Value />
</span>
</Select.Trigger>
</Select.Root>,
);

expect(screen.getByTestId('value')).to.have.text('Sans-serif, Serif');
});

it('displays comma-separated labels for multiple values with items array', async () => {
const items = [
{ value: 'serif', label: 'Serif' },
{ value: 'mono', label: 'Monospace' },
];

await render(
<Select.Root value={['serif', 'mono']} items={items} multiple>
<Select.Trigger>
<span data-testid="value">
<Select.Value />
</span>
</Select.Trigger>
</Select.Root>,
);

expect(screen.getByTestId('value')).to.have.text('sans, serif');
expect(screen.getByTestId('value')).to.have.text('Serif, Monospace');
});

it('displays comma-separated values for multiple values with items array', async () => {
it('supports ReactNode labels for multiple selections', async () => {
const items = [
{ value: 'bold', label: <strong>Bold Text</strong> },
{ value: 'italic', label: <em>Italic Text</em> },
];

await render(
<Select.Root value={['bold', 'italic']} items={items} multiple>
<Select.Trigger>
<span data-testid="value">
<Select.Value />
</span>
</Select.Trigger>
</Select.Root>,
);

const value = screen.getByTestId('value');
expect(value.querySelector('strong')).to.have.text('Bold Text');
expect(value.querySelector('em')).to.have.text('Italic Text');
expect(value).to.have.text('Bold Text, Italic Text');
});

it('falls back to raw values when no items are provided', async () => {
await render(
<Select.Root value={['serif', 'mono']} multiple>
<Select.Value data-testid="value" />
<Select.Trigger>
<span data-testid="value">
<Select.Value />
</span>
</Select.Trigger>
</Select.Root>,
);

Expand Down
20 changes: 12 additions & 8 deletions packages/react/src/select/value/SelectValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useStore } from '@base-ui-components/utils/store';
import type { BaseUIComponentProps } from '../../utils/types';
import { useRenderElement } from '../../utils/useRenderElement';
import { useSelectRootContext } from '../root/SelectRootContext';
import { resolveSelectedLabel, resolveMultipleLabels } from '../../utils/resolveValueLabel';
import { resolveMultipleLabels, resolveSelectedLabel } from '../../utils/resolveValueLabel';
import { selectors } from '../store';
import { StateAttributesMapping } from '../../utils/getStateAttributesProps';

Expand All @@ -30,6 +30,7 @@ export const SelectValue = React.forwardRef(function SelectValue(
const items = useStore(store, selectors.items);
const itemToStringLabel = useStore(store, selectors.itemToStringLabel);
const serializedValue = useStore(store, selectors.serializedValue);
const multiple = useStore(store, selectors.multiple);

const state: SelectValue.State = React.useMemo(
() => ({
Expand All @@ -39,13 +40,16 @@ export const SelectValue = React.forwardRef(function SelectValue(
[value, serializedValue],
);

const children =
typeof childrenProp === 'function'
? childrenProp(value)
: (childrenProp ??
(Array.isArray(value)
? resolveMultipleLabels(value, itemToStringLabel)
: resolveSelectedLabel(value, items, itemToStringLabel)));
let children = null;
if (typeof childrenProp === 'function') {
children = childrenProp(value);
} else if (childrenProp != null) {
children = childrenProp;
} else if (multiple && Array.isArray(value)) {
children = resolveMultipleLabels(value, items, itemToStringLabel);
} else {
children = resolveSelectedLabel(value, items, itemToStringLabel);
}

const element = useRenderElement('span', componentProps, {
state,
Expand Down
17 changes: 15 additions & 2 deletions packages/react/src/utils/resolveValueLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,23 @@ export function resolveSelectedLabel(

export function resolveMultipleLabels(
values: any[] | undefined,
items: ItemsInput,
itemToStringLabel?: (item: any) => string,
): string {
): React.ReactNode {
if (!Array.isArray(values) || values.length === 0) {
return '';
}
return values.map((v) => stringifyAsLabel(v, itemToStringLabel)).join(', ');

const labels = values.map((v) => resolveSelectedLabel(v, items, itemToStringLabel));

const nodes: React.ReactNode[] = [];

labels.forEach((label, index) => {
if (index > 0) {
nodes.push(', ');
}
nodes.push(<React.Fragment key={index}>{label}</React.Fragment>);
});

return nodes;
}
Loading