Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[joy-ui][Select] Support selection of multiple options #39454

Merged
merged 29 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from 24 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
31 changes: 31 additions & 0 deletions docs/data/joy/components/select/SelectMultiple.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react';
import Select from '@mui/joy/Select';
import Option from '@mui/joy/Option';

export default function SelectMultiple() {
const handleChange = (event, newValue) => {
console.log(`You have choosen "${newValue}"`);
};
return (
<Select
defaultValue={['dog']}
multiple
onChange={handleChange}
sx={{
minWidth: '13rem',
}}
slotProps={{
listbox: {
sx: {
width: '100%',
},
},
}}
>
<Option value="dog">Dog</Option>
<Option value="cat">Cat</Option>
<Option value="fish">Fish</Option>
<Option value="bird">Bird</Option>
</Select>
);
}
34 changes: 34 additions & 0 deletions docs/data/joy/components/select/SelectMultiple.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as React from 'react';
import Select from '@mui/joy/Select';
import Option from '@mui/joy/Option';

export default function SelectMultiple() {
const handleChange = (
event: React.SyntheticEvent | null,
newValue: Array<string> | null,
) => {
console.log(`You have choosen "${newValue}"`);
};
return (
<Select
defaultValue={['dog']}
multiple
onChange={handleChange}
sx={{
minWidth: '13rem',
}}
slotProps={{
listbox: {
sx: {
width: '100%',
},
},
}}
>
<Option value="dog">Dog</Option>
<Option value="cat">Cat</Option>
<Option value="fish">Fish</Option>
<Option value="bird">Bird</Option>
</Select>
);
}
37 changes: 37 additions & 0 deletions docs/data/joy/components/select/SelectMultipleAppearance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as React from 'react';
import Select from '@mui/joy/Select';
import Option from '@mui/joy/Option';
import { Box, Chip } from '@mui/joy';

export default function SelectMultipleAppearance() {
return (
<Select
multiple
defaultValue={['dog', 'cat']}
renderValue={(selected) => (
<Box sx={{ display: 'flex', gap: '0.25rem' }}>
{selected.map((selectedOption) => (
<Chip variant="outlined" color="success">
{selectedOption.label}
</Chip>
))}
</Box>
)}
sx={{
minWidth: '15rem',
}}
slotProps={{
listbox: {
sx: {
width: '100%',
},
},
}}
>
<Option value="dog">Dog</Option>
<Option value="cat">Cat</Option>
<Option value="fish">Fish</Option>
<Option value="bird">Bird</Option>
</Select>
);
}
37 changes: 37 additions & 0 deletions docs/data/joy/components/select/SelectMultipleAppearance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as React from 'react';
import Select from '@mui/joy/Select';
import Option from '@mui/joy/Option';
import { Box, Chip } from '@mui/joy';

export default function SelectMultipleAppearance() {
return (
<Select
multiple
defaultValue={['dog', 'cat']}
renderValue={(selected) => (
<Box sx={{ display: 'flex', gap: '0.25rem' }}>
{selected.map((selectedOption) => (
<Chip variant="outlined" color="success">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<Chip variant="outlined" color="success">
<Chip variant="soft" color="primary">

I think it looks better than outlined and success.

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I'll update

{selectedOption.label}
</Chip>
))}
</Box>
)}
sx={{
minWidth: '15rem',
}}
slotProps={{
listbox: {
sx: {
width: '100%',
},
},
}}
>
<Option value="dog">Dog</Option>
<Option value="cat">Cat</Option>
<Option value="fish">Fish</Option>
<Option value="bird">Bird</Option>
</Select>
);
}
19 changes: 19 additions & 0 deletions docs/data/joy/components/select/select.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,25 @@ const App = () => (
);
```

### Multiple selections

Set the `multiple` prop to let your users select multiple options from the list.
In contrast with single-selection mode, the options popup doesn't close after an item is selected, which enables users to continue choosing more options.

Note that in multiple selection mode, the `value` prop (and `defaultValue`) is an array.

{{"demo": "SelectMultiple.js"}}

#### Selected value appearance

Use the `renderValue` prop to customize the display of the selected options.

{{"demo": "SelectMultipleAppearance.js"}}

#### Form submission

Show what the value looks like after submitted a form.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to add a couple of more demos:

#### Selected value appearance

Display avatars of selected users end with "x friends".

#### Form submission

Show what the value looks like after submitted a form.

### Listbox

#### Maximum height
Expand Down
2 changes: 2 additions & 0 deletions docs/pages/joy-ui/api/select.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"indicator": { "type": { "name": "node" } },
"listboxId": { "type": { "name": "string" } },
"listboxOpen": { "type": { "name": "bool" } },
"multiple": { "type": { "name": "bool" } },
"name": { "type": { "name": "string" } },
"onChange": { "type": { "name": "func" } },
"onClose": { "type": { "name": "func" } },
Expand Down Expand Up @@ -113,6 +114,7 @@
"disabled",
"expanded",
"focusVisible",
"multiple",
"popper",
"sizeLg",
"sizeMd",
Expand Down
6 changes: 6 additions & 0 deletions docs/translations/api-docs-joy/select/select.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"description": "<code>id</code> attribute of the listbox element. Also used to derive the <code>id</code> attributes of options."
},
"listboxOpen": { "description": "Controls the open state of the select&#39;s listbox." },
"multiple": {
"description": "If <code>true</code>, selecting multiple values is allowed. This affects the type of the <code>value</code>, <code>defaultValue</code>, and <code>onChange</code> props."
},
"name": {
"description": "Name of the element. For example used by the server to identify the fields in form submits. If the name is provided, the component will render a hidden input element that can be submitted to a server."
},
Expand Down Expand Up @@ -85,6 +88,9 @@
"description": "Class name applied to {{nodeName}}.",
"nodeName": "the listbox slot"
},
"multiple": {
"description": "Class name applied to the root slot if <code>multiple=true</code>"
},
"colorPrimary": {
"description": "Class name applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the root slot",
Expand Down
75 changes: 69 additions & 6 deletions packages/mui-joy/src/Select/Select.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,40 +105,103 @@ interface Value {
<Select
slotProps={{
root: (ownerState) => {
expectType<SelectOwnerState<any>, typeof ownerState>(ownerState);
expectType<SelectOwnerState<any, false>, typeof ownerState>(ownerState);
return {
'data-testid': 'test',
};
},
button: (ownerState) => {
expectType<SelectOwnerState<any>, typeof ownerState>(ownerState);
expectType<SelectOwnerState<any, false>, typeof ownerState>(ownerState);
return {
'data-testid': 'test',
};
},
startDecorator: (ownerState) => {
expectType<SelectOwnerState<any>, typeof ownerState>(ownerState);
expectType<SelectOwnerState<any, false>, typeof ownerState>(ownerState);
return {
'data-testid': 'test',
};
},
endDecorator: (ownerState) => {
expectType<SelectOwnerState<any>, typeof ownerState>(ownerState);
expectType<SelectOwnerState<any, false>, typeof ownerState>(ownerState);
return {
'data-testid': 'test',
};
},
indicator: (ownerState) => {
expectType<SelectOwnerState<any>, typeof ownerState>(ownerState);
expectType<SelectOwnerState<any, false>, typeof ownerState>(ownerState);
return {
'data-testid': 'test',
};
},
listbox: (ownerState) => {
expectType<SelectOwnerState<any>, typeof ownerState>(ownerState);
expectType<SelectOwnerState<any, false>, typeof ownerState>(ownerState);
return {
'data-testid': 'test',
};
},
}}
/>;

const handleChange = (
e: React.MouseEvent | React.KeyboardEvent | React.FocusEvent | null,
val: number | null,
) => {};

const handleMultiChange = (
e: React.MouseEvent | React.KeyboardEvent | React.FocusEvent | null,
val: number[] | null,
) => {};

<Select value={10} onChange={handleChange} />;
<Select<number, true> value={[10]} onChange={handleMultiChange} />;
<Select<number, 'a', true> value={[10]} component="a" />;
<Select multiple value={[10]} component="button" />;
<Select multiple defaultValue={[10]} />;
<Select multiple value={[10]} />;

<Select
value={10}
// @ts-expect-error
onChange={handleMultiChange}
/>;

<Select<number, true>
value={[10]}
// @ts-expect-error
onChange={handleChange}
/>;

<Select
defaultValue={10}
// @ts-expect-error
onChange={handleMultiChange}
/>;

<Select<number, true>
defaultValue={[10]}
// @ts-expect-error
onChange={handleChange}
/>;
<Select value={10} onChange={handleChange} />;

<Select<number, true> onChange={handleMultiChange} value={[10]} />;

<Select defaultValue={10} onChange={handleChange} />;

<Select<number, true> defaultValue={[10]} onChange={handleMultiChange} />;

// @ts-expect-error
<Select<number, false> value={[10]} />;
// @ts-expect-error
<Select<number, false> defaultValue={[10]} />;
// @ts-expect-error
<Select multiple defaultValue={10} />;
// @ts-expect-error
<Select multiple value={10} />;
// @ts-expect-error
<Select multiple value={10} component="button" />;
// @ts-expect-error
<Select value={[10]} component="button" />;
// @ts-expect-error
<Select value={[10]} />;
28 changes: 28 additions & 0 deletions packages/mui-joy/src/Select/Select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -646,4 +646,32 @@ describe('Joy <Select />', () => {
});
expect(getByRole('combobox', { hidden: true })).to.have.attribute('aria-expanded', 'false');
});

describe('prop: multiple', () => {
it('renders the selected values (multiple) using the renderValue prop', () => {
const { getByRole } = render(
<Select
multiple
defaultValue={[1, 2]}
renderValue={(values) => values.map((v) => `${v.label} (${v.value})`).join(', ')}
>
<Option value={1}>One</Option>
<Option value={2}>Two</Option>
</Select>,
);

expect(getByRole('combobox')).to.have.text('One (1), Two (2)');
});

it('renders the selected values (multiple) as comma-separated list of labels if renderValue is not provided', () => {
const { getByRole } = render(
<Select multiple defaultValue={[1, 2]}>
<Option value={1}>One</Option>
<Option value={2}>Two</Option>
</Select>,
);

expect(getByRole('combobox')).to.have.text('One, Two');
});
});
});
Loading