Skip to content

Commit

Permalink
[base-ui][Select] Fix Select button layout shift, add placeholder prop (
Browse files Browse the repository at this point in the history
  • Loading branch information
mj12albert authored Sep 14, 2023
1 parent 97a1e7a commit ad905f5
Show file tree
Hide file tree
Showing 16 changed files with 62 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { styled } from '@mui/system';

export default function UnstyledSelectCustomRenderValue() {
return (
<CustomSelect renderValue={renderValue}>
<CustomSelect renderValue={renderValue} placeholder="Select an option…">
<StyledOption value={10}>Ten</StyledOption>
<StyledOption value={20}>Twenty</StyledOption>
<StyledOption value={30}>Thirty</StyledOption>
Expand Down Expand Up @@ -42,7 +42,7 @@ CustomSelect.propTypes = {

function renderValue(option) {
if (option == null) {
return <span>Select an option...</span>;
return null;
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { styled } from '@mui/system';

export default function UnstyledSelectCustomRenderValue() {
return (
<CustomSelect renderValue={renderValue}>
<CustomSelect renderValue={renderValue} placeholder="Select an option…">
<StyledOption value={10}>Ten</StyledOption>
<StyledOption value={20}>Twenty</StyledOption>
<StyledOption value={30}>Thirty</StyledOption>
Expand All @@ -28,7 +28,7 @@ function CustomSelect(props: SelectProps<number, false>) {

function renderValue(option: SelectOption<number> | null) {
if (option == null) {
return <span>Select an option...</span>;
return null;
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<CustomSelect renderValue={renderValue}>
<CustomSelect renderValue={renderValue} placeholder="Select an option…">
<StyledOption value={10}>Ten</StyledOption>
<StyledOption value={20}>Twenty</StyledOption>
<StyledOption value={30}>Thirty</StyledOption>
Expand Down
2 changes: 1 addition & 1 deletion docs/data/base/components/select/UnstyledSelectGrouping.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { styled } from '@mui/system';

export default function UnstyledSelectGrouping() {
return (
<CustomSelect>
<CustomSelect placeholder="Choose a character…">
<CustomOptionGroup label="Hobbits">
<StyledOption value="Frodo">Frodo</StyledOption>
<StyledOption value="Sam">Sam</StyledOption>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { styled } from '@mui/system';

export default function UnstyledSelectGrouping() {
return (
<CustomSelect>
<CustomSelect placeholder="Choose a character…">
<CustomOptionGroup label="Hobbits">
<StyledOption value="Frodo">Frodo</StyledOption>
<StyledOption value="Sam">Sam</StyledOption>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<CustomSelect>
<CustomSelect placeholder="Choose a character…">
<CustomOptionGroup label="Hobbits">
<StyledOption value="Frodo">Frodo</StyledOption>
<StyledOption value="Sam">Sam</StyledOption>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default function UnstyledSelectObjectValuesForm() {
name="character"
id="object-value-default-button"
aria-labelledby="object-value-default-label object-value-default-button"
placeholder="Choose a character…"
>
{characters.map((character) => (
<StyledOption key={character.name} value={character}>
Expand Down Expand Up @@ -64,6 +65,7 @@ export default function UnstyledSelectObjectValuesForm() {
name="character"
id="object-value-serialize-button"
aria-labelledby="object-value-serialize-label object-value-serialize-button"
placeholder="Choose a character…"
>
{characters.map((character) => (
<StyledOption key={character.name} value={character}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default function UnstyledSelectObjectValuesForm() {
name="character"
id="object-value-default-button"
aria-labelledby="object-value-default-label object-value-default-button"
placeholder="Choose a character…"
>
{characters.map((character) => (
<StyledOption key={character.name} value={character}>
Expand Down Expand Up @@ -63,6 +64,7 @@ export default function UnstyledSelectObjectValuesForm() {
name="character"
id="object-value-serialize-button"
aria-labelledby="object-value-serialize-label object-value-serialize-button"
placeholder="Choose a character…"
>
{characters.map((character) => (
<StyledOption key={character.name} value={character}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Popper } from '@mui/base';

export default function UnstyledSelectRichOptions() {
return (
<CustomSelect>
<CustomSelect placeholder="Select country…">
{countries.map((c) => (
<StyledOption key={c.code} value={c.code} label={c.label}>
<img
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Popper } from '@mui/base';

export default function UnstyledSelectRichOptions() {
return (
<CustomSelect>
<CustomSelect placeholder="Select country…">
{countries.map((c) => (
<StyledOption key={c.code} value={c.code} label={c.label}>
<img
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<CustomSelect>
<CustomSelect placeholder="Select country…">
{countries.map((c) => (
<StyledOption key={c.code} value={c.code} label={c.label}>
<img
Expand Down
1 change: 1 addition & 0 deletions docs/pages/base-ui/api/select.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"name": { "type": { "name": "string" } },
"onChange": { "type": { "name": "func" } },
"onListboxOpenChange": { "type": { "name": "func" } },
"placeholder": { "type": { "name": "node" } },
"renderValue": { "type": { "name": "func" } },
"required": { "type": { "name": "bool" }, "default": "false" },
"slotProps": {
Expand Down
1 change: 1 addition & 0 deletions docs/translations/api-docs-base/select/select.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"onListboxOpenChange": {
"description": "Callback fired when the component requests to be opened. Use in controlled mode (see listboxOpen)."
},
"placeholder": { "description": "Text to show when there is no selected value." },
"renderValue": {
"description": "Function that customizes the rendering of the selected value."
},
Expand Down
29 changes: 28 additions & 1 deletion packages/mui-base/src/Select/Select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('<Select />', () => {
const { render } = createRenderer();

const componentToTest = (
<Select defaultListboxOpen slotProps={{ popper: { disablePortal: true } }}>
<Select defaultListboxOpen defaultValue={1} slotProps={{ popper: { disablePortal: true } }}>
<OptionGroup label="Group">
<Option value={1}>1</Option>
</OptionGroup>
Expand Down Expand Up @@ -801,6 +801,19 @@ describe('<Select />', () => {
});
});

describe('prop: placeholder', () => {
it('renders when no value is selected ', () => {
const { getByRole } = render(
<Select placeholder="Placeholder text">
<Option value={1}>One</Option>
<Option value={2}>Two</Option>
</Select>,
);

expect(getByRole('combobox')).to.have.text('Placeholder text');
});
});

describe('prop: renderValue', () => {
it('renders the selected value using the renderValue prop', () => {
const { getByRole } = render(
Expand All @@ -824,6 +837,20 @@ describe('<Select />', () => {
expect(getByRole('combobox')).to.have.text('One');
});

it('renders a zero-width space when there is no selected value nor placeholder and renderValue is not provided', () => {
const { getByRole } = render(
<Select>
<Option value={1}>One</Option>
<Option value={2}>Two</Option>
</Select>,
);

const select = getByRole('combobox');
const zws = select.querySelector('.notranslate');

expect(zws).not.to.equal(null);
});

it('renders the selected values (multiple) using the renderValue prop', () => {
const { getByRole } = render(
<Select
Expand Down
15 changes: 13 additions & 2 deletions packages/mui-base/src/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function defaultRenderValue<OptionValue>(
return <React.Fragment>{selectedOptions.map((o) => o.label).join(', ')}</React.Fragment>;
}

return selectedOptions?.label ?? '';
return selectedOptions?.label ?? null;
}

function useUtilityClasses<OptionValue extends {}, Multiple extends boolean>(
Expand Down Expand Up @@ -86,6 +86,7 @@ const Select = React.forwardRef(function Select<
onListboxOpenChange,
getOptionAsString = defaultOptionStringifier,
renderValue: renderValueProp,
placeholder,
slotProps = {},
slots = {},
value: valueProp,
Expand Down Expand Up @@ -209,7 +210,13 @@ const Select = React.forwardRef(function Select<

return (
<React.Fragment>
<Button {...buttonProps}>{renderValue(selectedOptionsMetadata)}</Button>
<Button {...buttonProps}>
{renderValue(selectedOptionsMetadata) ?? placeholder ?? (
// fall back to a zero-width space to prevent layout shift
// from https://github.com/mui/material-ui/pull/24563
<span className="notranslate">&#8203;</span>
)}
</Button>
{buttonDefined && (
<PopperComponent {...popperProps}>
<ListboxRoot {...listboxProps}>
Expand Down Expand Up @@ -307,6 +314,10 @@ Select.propTypes /* remove-proptypes */ = {
* Use in controlled mode (see listboxOpen).
*/
onListboxOpenChange: PropTypes.func,
/**
* Text to show when there is no selected value.
*/
placeholder: PropTypes.node,
/**
* Function that customizes the rendering of the selected value.
*/
Expand Down
4 changes: 4 additions & 0 deletions packages/mui-base/src/Select/Select.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ export interface SelectOwnProps<OptionValue extends {}, Multiple extends boolean
* Function that customizes the rendering of the selected value.
*/
renderValue?: (option: SelectValue<SelectOption<OptionValue>, Multiple>) => React.ReactNode;
/**
* Text to show when there is no selected value.
*/
placeholder?: React.ReactNode;
/**
* The props used for each slot inside the Input.
* @default {}
Expand Down

0 comments on commit ad905f5

Please sign in to comment.