Skip to content

Commit

Permalink
[Select] Make error part of the ownerState to enable overriding sty…
Browse files Browse the repository at this point in the history
…les with it in theme (#36422)

Signed-off-by: Zeeshan Tamboli <[email protected]>
Co-authored-by: seunexplicit <[email protected]>
Co-authored-by: Zeeshan Tamboli <[email protected]>
  • Loading branch information
3 people committed Apr 3, 2023
1 parent 1f23d4c commit 0b7beb9
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 16 deletions.
5 changes: 3 additions & 2 deletions docs/pages/material-ui/api/native-select.json
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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>"
}
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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;

0 comments on commit 0b7beb9

Please sign in to comment.