diff --git a/docs/pages/material-ui/api/native-select.json b/docs/pages/material-ui/api/native-select.json index 98b7363fbb70b1..d90e75525a6913 100644 --- a/docs/pages/material-ui/api/native-select.json +++ b/docs/pages/material-ui/api/native-select.json @@ -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, diff --git a/docs/pages/material-ui/api/select.json b/docs/pages/material-ui/api/select.json index dfc27fce6b09ee..75511f6656161f 100644 --- a/docs/pages/material-ui/api/select.json +++ b/docs/pages/material-ui/api/select.json @@ -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, diff --git a/docs/translations/api-docs/native-select/native-select.json b/docs/translations/api-docs/native-select/native-select.json index 52a6b0e4a5e45c..7e7314bfa92aa0 100644 --- a/docs/translations/api-docs/native-select/native-select.json +++ b/docs/translations/api-docs/native-select/native-select.json @@ -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" } } } diff --git a/docs/translations/api-docs/select/select.json b/docs/translations/api-docs/select/select.json index 9375b66b2b851a..d821115dcd5e19 100644 --- a/docs/translations/api-docs/select/select.json +++ b/docs/translations/api-docs/select/select.json @@ -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": "error={true}" } } } diff --git a/packages/mui-material/src/NativeSelect/NativeSelectInput.d.ts b/packages/mui-material/src/NativeSelect/NativeSelectInput.d.ts index 4dd9d0fc05d670..bca7299cdce3cd 100644 --- a/packages/mui-material/src/NativeSelect/NativeSelectInput.d.ts +++ b/packages/mui-material/src/NativeSelect/NativeSelectInput.d.ts @@ -7,6 +7,7 @@ export interface NativeSelectInputProps extends React.SelectHTMLAttributes; variant?: 'standard' | 'outlined' | 'filled'; + error?: boolean; sx?: SxProps; } diff --git a/packages/mui-material/src/NativeSelect/NativeSelectInput.js b/packages/mui-material/src/NativeSelect/NativeSelectInput.js index 95bdcdbae34b77..b9ff1c48cbc182 100644 --- a/packages/mui-material/src/NativeSelect/NativeSelectInput.js +++ b/packages/mui-material/src/NativeSelect/NativeSelectInput.js @@ -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'], }; @@ -80,6 +80,7 @@ const NativeSelectSelect = styled('select', { return [ styles.select, styles[ownerState.variant], + ownerState.error && styles.error, { [`&.${nativeSelectClasses.multiple}`]: styles.multiple }, ]; }, @@ -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); @@ -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. */ diff --git a/packages/mui-material/src/NativeSelect/NativeSelectInput.test.js b/packages/mui-material/src/NativeSelect/NativeSelectInput.test.js index fe3d3b2e3497c6..28e4a4867f5a0c 100644 --- a/packages/mui-material/src/NativeSelect/NativeSelectInput.test.js +++ b/packages/mui-material/src/NativeSelect/NativeSelectInput.test.js @@ -111,4 +111,45 @@ describe('', () => { ).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( + + + + + + , + ); + expect(container.querySelector(`.${nativeSelectClasses.select}`)).toHaveComputedStyle( + selectStyle, + ); + expect(container.querySelector(`.${nativeSelectClasses.icon}`)).toHaveComputedStyle( + iconStyle, + ); + }); + }); }); diff --git a/packages/mui-material/src/NativeSelect/nativeSelectClasses.ts b/packages/mui-material/src/NativeSelect/nativeSelectClasses.ts index 7dae35c59ad08b..4d387b0cb9826e 100644 --- a/packages/mui-material/src/NativeSelect/nativeSelectClasses.ts +++ b/packages/mui-material/src/NativeSelect/nativeSelectClasses.ts @@ -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; @@ -50,6 +52,7 @@ const nativeSelectClasses: NativeSelectClasses = generateUtilityClasses('MuiNati 'iconOutlined', 'iconStandard', 'nativeInput', + 'error', ]); export default nativeSelectClasses; diff --git a/packages/mui-material/src/Select/Select.js b/packages/mui-material/src/Select/Select.js index 75cd04eac98857..deff5dda5dd855 100644 --- a/packages/mui-material/src/Select/Select.js +++ b/packages/mui-material/src/Select/Select.js @@ -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: , - outlined: , - filled: , + standard: , + outlined: , + filled: , }[variant]; - const ownerState = { ...props, variant, classes: classesProp }; - const classes = useUtilityClasses(ownerState); - const inputComponentRef = useForkRef(ref, InputComponent.ref); return ( @@ -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`. diff --git a/packages/mui-material/src/Select/Select.test.js b/packages/mui-material/src/Select/Select.test.js index 1f9dd561466348..5f27e00467ffde 100644 --- a/packages/mui-material/src/Select/Select.test.js +++ b/packages/mui-material/src/Select/Select.test.js @@ -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('', () => { 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( + + + , + ); + + 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( + +