diff --git a/docs/pages/material-ui/api/native-select.json b/docs/pages/material-ui/api/native-select.json index 6caa6f0..fa37463 100644 --- a/docs/pages/material-ui/api/native-select.json +++ b/docs/pages/material-ui/api/native-select.json @@ -70,7 +70,7 @@ { "key": "iconOpen", "className": "MuiNativeSelect-iconOpen", - "description": "Styles applied to the icon component if the popup is open.", + "description": "This class is never applied, a native select doesn't expose whether its popup is open.", "isGlobal": false }, { diff --git a/docs/translations/api-docs/native-select/native-select.json b/docs/translations/api-docs/native-select/native-select.json index 99344b7..2d6ff6f 100644 --- a/docs/translations/api-docs/native-select/native-select.json +++ b/docs/translations/api-docs/native-select/native-select.json @@ -48,9 +48,7 @@ "conditions": "variant=\"filled\"" }, "iconOpen": { - "description": "Styles applied to {{nodeName}} if {{conditions}}.", - "nodeName": "the icon component", - "conditions": "the popup is open" + "description": "This class is never applied, a native select doesn't expose whether its popup is open." }, "iconOutlined": { "description": "Styles applied to {{nodeName}} if {{conditions}}.", diff --git a/packages/mui-material/src/NativeSelect/NativeSelectInput.js b/packages/mui-material/src/NativeSelect/NativeSelectInput.js index c7b420d..d08e235 100644 --- a/packages/mui-material/src/NativeSelect/NativeSelectInput.js +++ b/packages/mui-material/src/NativeSelect/NativeSelectInput.js @@ -12,17 +12,17 @@ import rootShouldForwardProp from '../styles/rootShouldForwardProp'; import inputAdornmentClasses from '../InputAdornment/inputAdornmentClasses'; const useUtilityClasses = (ownerState) => { - const { classes, variant, disabled, multiple, open, error } = ownerState; + const { classes, variant, disabled, multiple, error } = ownerState; const slots = { select: ['select', variant, disabled && 'disabled', multiple && 'multiple', error && 'error'], - icon: ['icon', `icon${capitalize(variant)}`, open && 'iconOpen', disabled && 'disabled'], + icon: ['icon', `icon${capitalize(variant)}`, disabled && 'disabled'], }; return composeClasses(slots, getNativeSelectUtilityClasses, classes); }; -export const StyledSelectSelect = styled('select', { +const StyledSelectSelect = styled('select', { name: 'MuiNativeSelect', })(({ theme }) => ({ // Reset @@ -130,7 +130,7 @@ const NativeSelectSelect = styled(StyledSelectSelect, { }, })({}); -export const StyledSelectIcon = styled('svg', { +const StyledSelectIcon = styled('svg', { name: 'MuiNativeSelect', })(({ theme }) => ({ // We use a position absolute over a flexbox in order to forward the pointer events @@ -146,12 +146,6 @@ export const StyledSelectIcon = styled('svg', { color: (theme.vars || theme).palette.action.disabled, }, variants: [ - { - props: ({ ownerState }) => ownerState.open, - style: { - transform: 'rotate(180deg)', - }, - }, { props: { variant: 'filled', @@ -176,11 +170,7 @@ const NativeSelectIcon = styled(StyledSelectIcon, { slot: 'Icon', overridesResolver: (props, styles) => { const { ownerState } = props; - return [ - styles.icon, - ownerState.variant && styles[`icon${capitalize(ownerState.variant)}`], - ownerState.open && styles.iconOpen, - ]; + return [styles.icon, ownerState.variant && styles[`icon${capitalize(ownerState.variant)}`]]; }, })({}); diff --git a/packages/mui-material/src/NativeSelect/nativeSelectClasses.ts b/packages/mui-material/src/NativeSelect/nativeSelectClasses.ts index 8301846..5b7d3ed 100644 --- a/packages/mui-material/src/NativeSelect/nativeSelectClasses.ts +++ b/packages/mui-material/src/NativeSelect/nativeSelectClasses.ts @@ -18,7 +18,7 @@ export interface NativeSelectClasses { disabled: string; /** Styles applied to the icon component. */ icon: string; - /** Styles applied to the icon component if the popup is open. */ + /** This class is never applied, a native select doesn't expose whether its popup is open. */ iconOpen: string; /** Styles applied to the icon component if `variant="filled"`. */ iconFilled: string; diff --git a/packages/mui-material/src/Select/Select.test.js b/packages/mui-material/src/Select/Select.test.js index de50eb1..1459a6d 100644 --- a/packages/mui-material/src/Select/Select.test.js +++ b/packages/mui-material/src/Select/Select.test.js @@ -2774,6 +2774,41 @@ describe(' + + + + , + ); + + expect(container.querySelector(`.${classes.select}`)).not.toHaveComputedStyle({ + textTransform: 'uppercase', + }); + expect(container.querySelector(`.${classes.icon}`)).not.toHaveComputedStyle({ + textTransform: 'uppercase', + }); + }); + describe('prop: input', () => { it('merges `ref` of `Select` and `input`', () => { const Input = React.forwardRef(function Input(props, ref) { diff --git a/packages/mui-material/src/Select/SelectInput.js b/packages/mui-material/src/Select/SelectInput.js index 806baf3..30be750 100644 --- a/packages/mui-material/src/Select/SelectInput.js +++ b/packages/mui-material/src/Select/SelectInput.js @@ -9,8 +9,9 @@ import refType from '@mui/utils/refType'; import useTimeout from '@mui/utils/useTimeout'; import ownerDocument from '../utils/ownerDocument'; import Menu from '../Menu/Menu'; -import { StyledSelectSelect, StyledSelectIcon } from '../NativeSelect/NativeSelectInput'; import { isFilled } from '../InputBase/utils'; +import inputBaseClasses from '../InputBase/inputBaseClasses'; +import inputAdornmentClasses from '../InputAdornment/inputAdornmentClasses'; import { styled } from '../zero-styled'; import slotShouldForwardProp from '../styles/slotShouldForwardProp'; import useEnhancedEffect from '../utils/useEnhancedEffect'; @@ -72,7 +73,7 @@ function isMouseEventInsideElement(event, element) { ); } -const SelectSelect = styled(StyledSelectSelect, { +const SelectSelect = styled('div', { name: 'MuiSelect', slot: 'Select', overridesResolver: (props, styles) => { @@ -85,7 +86,29 @@ const SelectSelect = styled(StyledSelectSelect, { { [`&.${selectClasses.multiple}`]: styles.multiple }, ]; }, -})({ +})(({ theme }) => ({ + // When interacting quickly, the text can end up selected. + // Native select can't be selected either. + userSelect: 'none', + // Reset + borderRadius: 0, + cursor: 'pointer', + '&:focus': { + // Reset Chrome style + borderRadius: 0, + }, + [`&.${selectClasses.disabled}`]: { + cursor: 'default', + }, + [`& ~ .${inputAdornmentClasses.root}`]: { + position: 'absolute', + top: '50%', + transform: 'translateY(-50%)', + right: 'calc(var(--_caret, 24px) + (var(--_endAdornment, 28px) - 1.5rem)/2)', // 1.5rem is the default icon size + }, + [`.${inputBaseClasses.root}:has(> &)`]: { + '--_endAdornment': '0px', + }, // Win specificity over the input base [`&.${selectClasses.select}`]: { height: 'auto', // Resets for multiple select with chips @@ -94,16 +117,108 @@ const SelectSelect = styled(StyledSelectSelect, { whiteSpace: 'nowrap', overflow: 'hidden', }, -}); + variants: [ + { + props: ({ ownerState }) => + ownerState.variant !== 'filled' && ownerState.variant !== 'outlined', + style: { + [`.${inputBaseClasses.root}:has(> &)`]: { + '--_caret': '24px', + }, + [`.${inputBaseClasses.root}:has(> & ~ .${inputAdornmentClasses.root})`]: { + '--_endAdornment': '28px', + }, + // Bump specificity to allow extending custom inputs + '&&&': { + paddingRight: 'calc(var(--_caret, 24px) + var(--_endAdornment, 0px))', + minWidth: 16, // So it doesn't collapse. + }, + }, + }, + { + props: { + variant: 'filled', + }, + style: { + [`.${inputBaseClasses.root}:has(> &)`]: { + '--_caret': '32px', + }, + [`.${inputBaseClasses.root}:has(> & ~ .${inputAdornmentClasses.root})`]: { + '--_endAdornment': '28px', + }, + '&&&': { + paddingRight: 'calc(var(--_caret, 32px) + var(--_endAdornment, 0px))', + }, + }, + }, + { + props: { + variant: 'outlined', + }, + style: { + [`.${inputBaseClasses.root}:has(> &)`]: { + '--_caret': '32px', + }, + [`.${inputBaseClasses.root}:has(> & ~ .${inputAdornmentClasses.root})`]: { + '--_endAdornment': '28px', + }, + borderRadius: (theme.vars || theme).shape.borderRadius, + '&:focus': { + borderRadius: (theme.vars || theme).shape.borderRadius, // Reset the reset for Chrome style + }, + '&&&': { + paddingRight: 'calc(var(--_caret, 32px) + var(--_endAdornment, 0px))', + }, + }, + }, + ], +})); -const SelectIcon = styled(StyledSelectIcon, { +const SelectIcon = styled('svg', { name: 'MuiSelect', slot: 'Icon', overridesResolver: (props, styles) => { const { ownerState } = props; return [styles.icon, ownerState.open && styles.iconOpen]; }, -})({}); +})(({ theme }) => ({ + // We use a position absolute over a flexbox in order to forward the pointer events + // to the input and to support wrapping tags.. + position: 'absolute', + right: 0, + // Center vertically, height is 1em + top: 'calc(50% - .5em)', + // Don't block pointer events on the select under the icon. + pointerEvents: 'none', + color: (theme.vars || theme).palette.action.active, + [`&.${selectClasses.disabled}`]: { + color: (theme.vars || theme).palette.action.disabled, + }, + variants: [ + { + props: ({ ownerState }) => ownerState.open, + style: { + transform: 'rotate(180deg)', + }, + }, + { + props: { + variant: 'filled', + }, + style: { + right: 7, + }, + }, + { + props: { + variant: 'outlined', + }, + style: { + right: 7, + }, + }, + ], +})); const SelectNativeInput = styled('input', { shouldForwardProp: (prop) => slotShouldForwardProp(prop) && prop !== 'classes', @@ -838,7 +953,6 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) { return (