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

[Select] Make error part of the ownerState to enable overriding styles with it in theme #36422

Merged
merged 17 commits into from Apr 3, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/pages/material-ui/api/native-select.json
Expand Up @@ -35,9 +35,10 @@
"iconFilled",
"iconOutlined",
"iconStandard",
"nativeInput"
"nativeInput",
"error"
],
"globalClasses": { "disabled": "Mui-disabled" },
"globalClasses": { "disabled": "Mui-disabled", "error": "Mui-error" },
"name": "MuiNativeSelect"
},
"spread": true,
Expand Down
5 changes: 3 additions & 2 deletions docs/pages/material-ui/api/select.json
Expand Up @@ -50,9 +50,10 @@
"iconFilled",
"iconOutlined",
"iconStandard",
"nativeInput"
"nativeInput",
"error"
],
"globalClasses": { "disabled": "Mui-disabled" },
"globalClasses": { "disabled": "Mui-disabled", "error": "Mui-error" },
"name": "MuiSelect"
},
"spread": true,
Expand Down
4 changes: 4 additions & 0 deletions docs/translations/api-docs/native-select/native-select.json
Expand Up @@ -65,6 +65,10 @@
"nativeInput": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the underlying native input component"
},
"error": {
"description": "State class applied to {{nodeName}}.",
"nodeName": "the select component `error` class"
}
}
}
5 changes: 5 additions & 0 deletions docs/translations/api-docs/select/select.json
Expand Up @@ -79,6 +79,11 @@
"nativeInput": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the underlying native input component"
},
"error": {
"description": "State class applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the root element",
"conditions": "<code>error={true}</code>"
}
}
}
Expand Up @@ -7,6 +7,7 @@ export interface NativeSelectInputProps extends React.SelectHTMLAttributes<HTMLS
IconComponent: React.ElementType;
inputRef?: React.Ref<HTMLSelectElement>;
variant?: 'standard' | 'outlined' | 'filled';
error?: boolean;
sx?: SxProps<Theme>;
}

Expand Down
20 changes: 17 additions & 3 deletions packages/mui-material/src/NativeSelect/NativeSelectInput.js
Expand Up @@ -8,10 +8,10 @@ import nativeSelectClasses, { getNativeSelectUtilityClasses } from './nativeSele
import styled, { rootShouldForwardProp } from '../styles/styled';

const useUtilityClasses = (ownerState) => {
const { classes, variant, disabled, multiple, open } = ownerState;
const { classes, variant, disabled, multiple, open, error } = ownerState;

const slots = {
select: ['select', variant, disabled && 'disabled', multiple && 'multiple'],
select: ['select', variant, disabled && 'disabled', multiple && 'multiple', error && 'error'],
icon: ['icon', `icon${capitalize(variant)}`, open && 'iconOpen', disabled && 'disabled'],
};

Expand Down Expand Up @@ -80,6 +80,7 @@ const NativeSelectSelect = styled('select', {
return [
styles.select,
styles[ownerState.variant],
ownerState.error && styles.error,
{ [`&.${nativeSelectClasses.multiple}`]: styles.multiple },
];
},
Expand Down Expand Up @@ -124,12 +125,21 @@ const NativeSelectIcon = styled('svg', {
* @ignore - internal component.
*/
const NativeSelectInput = React.forwardRef(function NativeSelectInput(props, ref) {
const { className, disabled, IconComponent, inputRef, variant = 'standard', ...other } = props;
const {
className,
disabled,
error,
IconComponent,
inputRef,
variant = 'standard',
...other
} = props;

const ownerState = {
...props,
disabled,
variant,
error,
};

const classes = useUtilityClasses(ownerState);
Expand Down Expand Up @@ -168,6 +178,10 @@ NativeSelectInput.propTypes = {
* If `true`, the select is disabled.
*/
disabled: PropTypes.bool,
/**
* If `true`, the `select input` will indicate an error.
*/
error: PropTypes.bool,
/**
* The icon that displays the arrow.
*/
Expand Down
41 changes: 41 additions & 0 deletions packages/mui-material/src/NativeSelect/NativeSelectInput.test.js
Expand Up @@ -111,4 +111,45 @@ describe('<NativeSelectInput />', () => {
).to.toHaveComputedStyle(combinedStyle);
});
});

describe('theme styleOverrides:', () => {
it('should override with error style when `select` has `error` state', function test() {
if (/jsdom/.test(window.navigator.userAgent)) {
this.skip();
}

const iconStyle = { color: 'rgb(255, 0, 0)' };
const selectStyle = { color: 'rgb(255, 192, 203)' };

const theme = createTheme({
components: {
MuiNativeSelect: {
styleOverrides: {
icon: (props) => ({
...(props.ownerState.error && iconStyle),
}),
select: (props) => ({
...(props.ownerState.error && selectStyle),
}),
},
},
},
});

const { container } = render(
<ThemeProvider theme={theme}>
<NativeSelectInput error IconComponent="div">
<option value={'first'}>First</option>
<option value={'second'}>Second</option>
</NativeSelectInput>
</ThemeProvider>,
);
expect(container.querySelector(`.${nativeSelectClasses.select}`)).toHaveComputedStyle(
selectStyle,
);
expect(container.querySelector(`.${nativeSelectClasses.icon}`)).toHaveComputedStyle(
iconStyle,
);
});
});
});
3 changes: 3 additions & 0 deletions packages/mui-material/src/NativeSelect/nativeSelectClasses.ts
Expand Up @@ -28,6 +28,8 @@ export interface NativeSelectClasses {
iconStandard: string;
/** Styles applied to the underlying native input component. */
nativeInput: string;
/** State class applied to the select component `error` class. */
error: string;
}

export type NativeSelectClassKey = keyof NativeSelectClasses;
Expand All @@ -50,6 +52,7 @@ const nativeSelectClasses: NativeSelectClasses = generateUtilityClasses('MuiNati
'iconOutlined',
'iconStandard',
'nativeInput',
'error',
]);

export default nativeSelectClasses;
15 changes: 8 additions & 7 deletions packages/mui-material/src/Select/Select.js
Expand Up @@ -65,22 +65,22 @@ const Select = React.forwardRef(function Select(inProps, ref) {
const fcs = formControlState({
props,
muiFormControl,
states: ['variant'],
states: ['variant', 'error'],
});

const variant = fcs.variant || variantProp;

const ownerState = { ...props, variant, classes: classesProp };
const classes = useUtilityClasses(ownerState);

const InputComponent =
input ||
{
standard: <StyledInput />,
outlined: <StyledOutlinedInput label={label} />,
filled: <StyledFilledInput />,
standard: <StyledInput ownerState={ownerState} />,
outlined: <StyledOutlinedInput label={label} ownerState={ownerState} />,
filled: <StyledFilledInput ownerState={ownerState} />,
}[variant];

const ownerState = { ...props, variant, classes: classesProp };
const classes = useUtilityClasses(ownerState);

const inputComponentRef = useForkRef(ref, InputComponent.ref);

return (
Expand All @@ -91,6 +91,7 @@ const Select = React.forwardRef(function Select(inProps, ref) {
inputComponent,
inputProps: {
children,
error: fcs.error,
IconComponent,
variant,
type: undefined, // We render a select. We can ignore the type provided by the `Input`.
Expand Down
67 changes: 67 additions & 0 deletions packages/mui-material/src/Select/Select.test.js
Expand Up @@ -18,6 +18,7 @@ import InputLabel from '@mui/material/InputLabel';
import Select from '@mui/material/Select';
import Divider from '@mui/material/Divider';
import classes from './selectClasses';
import { nativeSelectClasses } from '../NativeSelect';

describe('<Select />', () => {
const { clock, render } = createRenderer({ clock: 'fake' });
Expand Down Expand Up @@ -1439,6 +1440,72 @@ describe('<Select />', () => {
expect(container.getElementsByClassName(classes.select)[0]).to.toHaveComputedStyle(selectStyle);
});

describe('theme styleOverrides:', () => {
it('should override with error style when `native select` has `error` state', function test() {
if (/jsdom/.test(window.navigator.userAgent)) {
this.skip();
}

const iconStyle = { color: 'rgb(255, 0, 0)' };

const theme = createTheme({
components: {
MuiNativeSelect: {
styleOverrides: {
icon: (props) => ({
...(props.ownerState.error && iconStyle),
}),
},
},
},
});

const { container } = render(
<ThemeProvider theme={theme}>
<Select value="first" error IconComponent="div" native>
<option value="first">first</option>
</Select>
</ThemeProvider>,
);

expect(container.querySelector(`.${nativeSelectClasses.icon}`)).toHaveComputedStyle(
iconStyle,
);
});

it('should override with error style when `select` has `error` state', function test() {
if (/jsdom/.test(window.navigator.userAgent)) {
this.skip();
}

const iconStyle = { color: 'rgb(255, 0, 0)' };
const selectStyle = { color: 'rgb(255, 192, 203)' };

const theme = createTheme({
components: {
MuiSelect: {
styleOverrides: {
icon: (props) => ({
...(props.ownerState.error && iconStyle),
}),
select: (props) => ({
...(props.ownerState.error && selectStyle),
}),
},
},
},
});

const { container } = render(
<ThemeProvider theme={theme}>
<Select value="" error IconComponent="div" />
</ThemeProvider>,
);
expect(container.querySelector(`.${classes.select}`)).toHaveComputedStyle(selectStyle);
expect(container.querySelector(`.${classes.icon}`)).toHaveComputedStyle(iconStyle);
});
});

['standard', 'outlined', 'filled'].forEach((variant) => {
it(`variant overrides should work for "${variant}" variant`, function test() {
const theme = createTheme({
Expand Down
1 change: 1 addition & 0 deletions packages/mui-material/src/Select/SelectInput.d.ts
Expand Up @@ -17,6 +17,7 @@ export interface SelectInputProps<T = unknown> {
autoWidth: boolean;
defaultOpen?: boolean;
disabled?: boolean;
error?: boolean;
IconComponent?: React.ElementType;
inputRef?: (
ref: HTMLSelectElement | { node: HTMLInputElement; value: SelectInputProps<T>['value'] },
Expand Down
12 changes: 10 additions & 2 deletions packages/mui-material/src/Select/SelectInput.js
Expand Up @@ -27,6 +27,7 @@ const SelectSelect = styled('div', {
// Win specificity over the input base
{ [`&.${selectClasses.select}`]: styles.select },
{ [`&.${selectClasses.select}`]: styles[ownerState.variant] },
{ [`&.${selectClasses.error}`]: styles.error },
{ [`&.${selectClasses.multiple}`]: styles.multiple },
];
},
Expand Down Expand Up @@ -83,10 +84,10 @@ function isEmpty(display) {
}

const useUtilityClasses = (ownerState) => {
const { classes, variant, disabled, multiple, open } = ownerState;
const { classes, variant, disabled, multiple, open, error } = ownerState;

const slots = {
select: ['select', variant, disabled && 'disabled', multiple && 'multiple'],
select: ['select', variant, disabled && 'disabled', multiple && 'multiple', error && 'error'],
icon: ['icon', `icon${capitalize(variant)}`, open && 'iconOpen', disabled && 'disabled'],
nativeInput: ['nativeInput'],
};
Expand All @@ -109,6 +110,7 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) {
defaultValue,
disabled,
displayEmpty,
error = false,
IconComponent,
inputRef: inputRefProp,
labelId,
Expand Down Expand Up @@ -475,6 +477,7 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) {
variant,
value,
open,
error,
};

const classes = useUtilityClasses(ownerState);
Expand Down Expand Up @@ -510,6 +513,7 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) {
)}
</SelectSelect>
<SelectNativeInput
aria-invalid={error}
value={Array.isArray(value) ? value.join(',') : value}
name={name}
ref={inputRef}
Expand Down Expand Up @@ -606,6 +610,10 @@ SelectInput.propTypes = {
* If `true`, the selected item is displayed even if its value is empty.
*/
displayEmpty: PropTypes.bool,
/**
* If `true`, the `select input` will indicate an error.
*/
error: PropTypes.bool,
/**
* The icon that displays the arrow.
*/
Expand Down
3 changes: 3 additions & 0 deletions packages/mui-material/src/Select/selectClasses.ts
Expand Up @@ -26,6 +26,8 @@ export interface SelectClasses {
iconStandard: string;
/** Styles applied to the underlying native input component. */
nativeInput: string;
/** State class applied to the root element if `error={true}`. */
error: string;
}

export type SelectClassKey = keyof SelectClasses;
Expand All @@ -48,6 +50,7 @@ const selectClasses: SelectClasses = generateUtilityClasses('MuiSelect', [
'iconOutlined',
'iconStandard',
'nativeInput',
'error',
]);

export default selectClasses;