diff --git a/docs/pages/api-docs/filled-input.json b/docs/pages/api-docs/filled-input.json index 75ba88cd2fcea2..9b31ef3631c3dd 100644 --- a/docs/pages/api-docs/filled-input.json +++ b/docs/pages/api-docs/filled-input.json @@ -25,6 +25,7 @@ "required": { "type": { "name": "bool" } }, "rows": { "type": { "name": "union", "description": "number
| string" } }, "startAdornment": { "type": { "name": "node" } }, + "sx": { "type": { "name": "object" } }, "type": { "type": { "name": "string" }, "default": "'text'" }, "value": { "type": { "name": "any" } } }, @@ -57,6 +58,6 @@ "filename": "/packages/material-ui/src/FilledInput/FilledInput.js", "inheritance": { "component": "InputBase", "pathname": "/api/input-base/" }, "demos": "", - "styledComponent": false, + "styledComponent": true, "cssComponent": false } diff --git a/docs/pages/api-docs/input-base.json b/docs/pages/api-docs/input-base.json index 33a2c33b479849..e5b675c6abd771 100644 --- a/docs/pages/api-docs/input-base.json +++ b/docs/pages/api-docs/input-base.json @@ -4,6 +4,11 @@ "autoFocus": { "type": { "name": "bool" } }, "classes": { "type": { "name": "object" } }, "color": { "type": { "name": "enum", "description": "'primary'
| 'secondary'" } }, + "components": { + "type": { "name": "shape", "description": "{ Input?: elementType, Root?: elementType }" }, + "default": "{}" + }, + "componentsProps": { "type": { "name": "object" }, "default": "{}" }, "defaultValue": { "type": { "name": "any" } }, "disabled": { "type": { "name": "bool" } }, "endAdornment": { "type": { "name": "node" } }, diff --git a/docs/translations/api-docs/filled-input/filled-input.json b/docs/translations/api-docs/filled-input/filled-input.json index 8eb7f3062d7a1c..ab148a1af6045f 100644 --- a/docs/translations/api-docs/filled-input/filled-input.json +++ b/docs/translations/api-docs/filled-input/filled-input.json @@ -26,6 +26,7 @@ "required": "If true, the input element is required. The prop defaults to the value (false) inherited from the parent FormControl component.", "rows": "Number of rows to display when multiline option is set to true.", "startAdornment": "Start InputAdornment for this component.", + "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", "type": "Type of the input element. It should be a valid HTML5 input type.", "value": "The value of the input element, required for a controlled component." }, diff --git a/docs/translations/api-docs/input-base/input-base.json b/docs/translations/api-docs/input-base/input-base.json index 09b6c70c33d863..eb9722b2594774 100644 --- a/docs/translations/api-docs/input-base/input-base.json +++ b/docs/translations/api-docs/input-base/input-base.json @@ -5,6 +5,8 @@ "autoFocus": "If true, the input element is focused during the first mount.", "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", "color": "The color of the component. It supports those theme colors that make sense for this component. The prop defaults to the value ('primary') inherited from the parent FormControl component.", + "components": "The components used for each slot inside the InputBase. Either a string to use a HTML element or a component.", + "componentsProps": "The props used for each slot inside the Input.", "defaultValue": "The default value. Use when the component is not controlled.", "disabled": "If true, the component is disabled. The prop defaults to the value (false) inherited from the parent FormControl component.", "endAdornment": "End InputAdornment for this component.", diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.js b/packages/material-ui/src/Autocomplete/Autocomplete.js index d12e426b23f599..918afdaacd9620 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.js +++ b/packages/material-ui/src/Autocomplete/Autocomplete.js @@ -102,7 +102,7 @@ export const styles = (theme) => ({ padding: '2.5px 4px', }, }, - '&[class*="MuiFilledInput-root"]': { + '&.MuiFilledInput-root': { paddingTop: 19, paddingLeft: 8, '$hasPopupIcon &, $hasClearIcon &': { @@ -111,16 +111,16 @@ export const styles = (theme) => ({ '$hasPopupIcon$hasClearIcon &': { paddingRight: 52 + 4 + 9, }, - '& $input': { + '& .MuiFilledInput-input': { padding: '7px 4px', }, '& $endAdornment': { right: 9, }, }, - '&[class*="MuiFilledInput-root"][class*="MuiFilledInput-sizeSmall"]': { + '&.MuiFilledInput-root.MuiInputBase-sizeSmall': { paddingBottom: 1, - '& $input': { + '& .MuiFilledInput-input': { padding: '2.5px 4px', }, }, diff --git a/packages/material-ui/src/FilledInput/FilledInput.d.ts b/packages/material-ui/src/FilledInput/FilledInput.d.ts index 16e5bf64fdc76a..16cb96ce162570 100644 --- a/packages/material-ui/src/FilledInput/FilledInput.d.ts +++ b/packages/material-ui/src/FilledInput/FilledInput.d.ts @@ -1,4 +1,5 @@ -import { InternalStandardProps as StandardProps } from '..'; +import { SxProps } from '@material-ui/system'; +import { InternalStandardProps as StandardProps, Theme } from '..'; import { InputBaseProps } from '../InputBase'; export interface FilledInputProps extends StandardProps { @@ -45,6 +46,10 @@ export interface FilledInputProps extends StandardProps { * If `true`, the input will not have an underline. */ disableUnderline?: boolean; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; } export type FilledInputClassKey = keyof NonNullable; diff --git a/packages/material-ui/src/FilledInput/FilledInput.js b/packages/material-ui/src/FilledInput/FilledInput.js index 733d542aa3f0d9..8d52f0914049d6 100644 --- a/packages/material-ui/src/FilledInput/FilledInput.js +++ b/packages/material-ui/src/FilledInput/FilledInput.js @@ -1,48 +1,67 @@ import * as React from 'react'; +import { deepmerge, refType } from '@material-ui/utils'; import PropTypes from 'prop-types'; -import clsx from 'clsx'; -import { refType } from '@material-ui/utils'; +import { unstable_composeClasses as composeClasses } from '@material-ui/unstyled'; import InputBase from '../InputBase'; -import withStyles from '../styles/withStyles'; +import experimentalStyled, { shouldForwardProp } from '../styles/experimentalStyled'; +import useThemeProps from '../styles/useThemeProps'; +import { getFilledInputUtilityClass } from './filledInputClasses'; +import { + overridesResolver as inputBaseOverridesResolver, + InputBaseRoot, + InputBaseComponent as InputBaseInput, +} from '../InputBase/InputBase'; -export const styles = (theme) => { +const overridesResolver = (props, styles) => { + const { styleProps } = props; + return deepmerge(inputBaseOverridesResolver(props, styles), { + ...(!styleProps.disableUnderline && styles.underline), + }); +}; + +const useUtilityClasses = (styleProps) => { + const { classes, disableUnderline } = styleProps; + + const slots = { + root: ['root', !disableUnderline && 'underline'], + input: ['input'], + }; + + return composeClasses(slots, getFilledInputUtilityClass, classes); +}; + +const FilledInputRoot = experimentalStyled( + InputBaseRoot, + { shouldForwardProp: (prop) => shouldForwardProp(prop) || prop === 'classes' }, + { name: 'MuiFilledInput', slot: 'Root', overridesResolver }, +)(({ theme, styleProps }) => { const light = theme.palette.mode === 'light'; const bottomLineColor = light ? 'rgba(0, 0, 0, 0.42)' : 'rgba(255, 255, 255, 0.7)'; const backgroundColor = light ? 'rgba(0, 0, 0, 0.09)' : 'rgba(255, 255, 255, 0.09)'; - return { /* Styles applied to the root element. */ - root: { - position: 'relative', - backgroundColor, - borderTopLeftRadius: theme.shape.borderRadius, - borderTopRightRadius: theme.shape.borderRadius, - transition: theme.transitions.create('background-color', { - duration: theme.transitions.duration.shorter, - easing: theme.transitions.easing.easeOut, - }), - '&:hover': { - backgroundColor: light ? 'rgba(0, 0, 0, 0.13)' : 'rgba(255, 255, 255, 0.13)', - // Reset on touch devices, it doesn't add specificity - '@media (hover: none)': { - backgroundColor, - }, - }, - '&$focused': { - backgroundColor: light ? 'rgba(0, 0, 0, 0.09)' : 'rgba(255, 255, 255, 0.09)', - }, - '&$disabled': { - backgroundColor: light ? 'rgba(0, 0, 0, 0.12)' : 'rgba(255, 255, 255, 0.12)', + position: 'relative', + backgroundColor, + borderTopLeftRadius: theme.shape.borderRadius, + borderTopRightRadius: theme.shape.borderRadius, + transition: theme.transitions.create('background-color', { + duration: theme.transitions.duration.shorter, + easing: theme.transitions.easing.easeOut, + }), + '&:hover': { + backgroundColor: light ? 'rgba(0, 0, 0, 0.13)' : 'rgba(255, 255, 255, 0.13)', + // Reset on touch devices, it doesn't add specificity + '@media (hover: none)': { + backgroundColor, }, }, - /* Styles applied to the root element if color secondary. */ - colorSecondary: { - '&$underline:after': { - borderBottomColor: theme.palette.secondary.main, - }, + '&.Mui-focused': { + backgroundColor: light ? 'rgba(0, 0, 0, 0.09)' : 'rgba(255, 255, 255, 0.09)', + }, + '&.Mui-disabled': { + backgroundColor: light ? 'rgba(0, 0, 0, 0.12)' : 'rgba(255, 255, 255, 0.12)', }, - /* Styles applied to the root element unless `disableUnderline={true}`. */ - underline: { + ...(!styleProps.disableUnderline && { '&:after': { borderBottom: `2px solid ${theme.palette.primary.main}`, left: 0, @@ -57,11 +76,14 @@ export const styles = (theme) => { easing: theme.transitions.easing.easeOut, }), pointerEvents: 'none', // Transparent to the hover style. + ...(styleProps.color === 'secondary' && { + borderBottomColor: theme.palette.secondary.main, + }), }, - '&$focused:after': { + '&.Mui-focused:after': { transform: 'scaleX(1)', }, - '&$error:after': { + '&.Mui-error:after': { borderBottomColor: theme.palette.error.main, transform: 'scaleX(1)', // error is always underlined in red }, @@ -78,87 +100,84 @@ export const styles = (theme) => { }), pointerEvents: 'none', // Transparent to the hover style. }, - '&:hover:not($disabled):before': { + '&:hover:not(.Mui-disabled):before': { borderBottom: `1px solid ${theme.palette.text.primary}`, }, - '&$disabled:before': { + '&.Mui-disabled:before': { borderBottomStyle: 'dotted', }, - }, - /* Pseudo-class applied to the root element if the component is focused. */ - focused: {}, - /* Pseudo-class applied to the root element if `disabled={true}`. */ - disabled: {}, - /* Styles applied to the root element if `startAdornment` is provided. */ - adornedStart: { + }), + ...(styleProps.startAdornment && { paddingLeft: 12, - }, - /* Styles applied to the root element if `endAdornment` is provided. */ - adornedEnd: { + }), + ...(styleProps.endAdornment && { paddingRight: 12, - }, - /* Pseudo-class applied to the root element if `error={true}`. */ - error: {}, - /* Styles applied to the input element if `size="small"`. */ - sizeSmall: {}, - /* Styles applied to the root element if `multiline={true}`. */ - multiline: { + }), + ...(styleProps.multiline && { padding: '25px 12px 8px', - '&$sizeSmall': { + ...(styleProps.size === 'small' && { paddingTop: 21, paddingBottom: 4, - }, - '&$hiddenLabel': { + }), + ...(styleProps.hiddenLabel && { paddingTop: 16, paddingBottom: 17, - }, - }, - /* Styles applied to the root element if `hiddenLabel={true}`. */ - hiddenLabel: {}, - /* Styles applied to the input element. */ - input: { - padding: '25px 12px 8px', - '&:-webkit-autofill': { - WebkitBoxShadow: theme.palette.mode === 'light' ? null : '0 0 0 100px #266798 inset', - WebkitTextFillColor: theme.palette.mode === 'light' ? null : '#fff', - caretColor: theme.palette.mode === 'light' ? null : '#fff', - borderTopLeftRadius: 'inherit', - borderTopRightRadius: 'inherit', - }, - }, - /* Styles applied to the input element if `size="small"`. */ - inputSizeSmall: { - paddingTop: 21, - paddingBottom: 4, - }, - /* Styles applied to the `input` if in ``. */ - inputHiddenLabel: { - paddingTop: 16, - paddingBottom: 17, - '&$inputSizeSmall': { - paddingTop: 8, - paddingBottom: 9, - }, - }, - /* Styles applied to the input element if `multiline={true}`. */ - inputMultiline: { - padding: 0, - }, - /* Styles applied to the input element if `startAdornment` is provided. */ - inputAdornedStart: { - paddingLeft: 0, - }, - /* Styles applied to the input element if `endAdornment` is provided. */ - inputAdornedEnd: { - paddingRight: 0, - }, + }), + }), }; -}; +}); + +const FilledInputInput = experimentalStyled( + InputBaseInput, + { shouldForwardProp: (prop) => shouldForwardProp(prop) || prop === 'classes' }, + { name: 'MuiFilledInput', slot: 'Input' }, +)(({ theme, styleProps }) => ({ + paddingTop: 25, + paddingRight: 12, + paddingBottom: 8, + paddingLeft: 12, + '&:-webkit-autofill': { + WebkitBoxShadow: theme.palette.mode === 'light' ? null : '0 0 0 100px #266798 inset', + WebkitTextFillColor: theme.palette.mode === 'light' ? null : '#fff', + caretColor: theme.palette.mode === 'light' ? null : '#fff', + borderTopLeftRadius: 'inherit', + borderTopRightRadius: 'inherit', + }, + ...(styleProps.size === 'small' && { + paddingTop: 21, + paddingBottom: 4, + }), + ...(styleProps.hiddenLabel && { + paddingTop: 16, + paddingBottom: 17, + }), + /* Styles applied to the input element if `multiline={true}`. */ + ...(styleProps.multiline && { + paddingTop: 0, + paddingBottom: 0, + paddingLeft: 0, + paddingRight: 0, + }), + /* Styles applied to the input element if `startAdornment` is provided. */ + ...(styleProps.startAdornment && { + paddingLeft: 0, + }), + /* Styles applied to the input element if `endAdornment` is provided. */ + ...(styleProps.endAdornment && { + paddingRight: 0, + }), + ...(styleProps.hiddenLabel && + styleProps.size === 'small' && { + paddingTop: 8, + paddingBottom: 9, + }), +})); + +const FilledInput = React.forwardRef(function FilledInput(inProps, ref) { + const props = useThemeProps({ props: inProps, name: 'MuiFilledInput' }); -const FilledInput = React.forwardRef(function FilledInput(props, ref) { const { disableUnderline, - classes, fullWidth = false, inputComponent = 'input', multiline = false, @@ -166,21 +185,27 @@ const FilledInput = React.forwardRef(function FilledInput(props, ref) { ...other } = props; + const styleProps = { + ...props, + fullWidth, + inputComponent, + multiline, + type, + }; + + const classes = useUtilityClasses(props); + return ( ); }); @@ -307,6 +332,10 @@ FilledInput.propTypes = { * Start `InputAdornment` for this component. */ startAdornment: PropTypes.node, + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx: PropTypes.object, /** * Type of the `input` element. It should be [a valid HTML5 input type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types). * @default 'text' @@ -320,4 +349,4 @@ FilledInput.propTypes = { FilledInput.muiName = 'Input'; -export default withStyles(styles, { name: 'MuiFilledInput' })(FilledInput); +export default FilledInput; diff --git a/packages/material-ui/src/FilledInput/FilledInput.test.js b/packages/material-ui/src/FilledInput/FilledInput.test.js index 02003c8e7e578c..92282c4573ca86 100644 --- a/packages/material-ui/src/FilledInput/FilledInput.test.js +++ b/packages/material-ui/src/FilledInput/FilledInput.test.js @@ -1,24 +1,24 @@ import * as React from 'react'; import { expect } from 'chai'; -import { getClasses, createMount, createClientRender, describeConformance } from 'test/utils'; +import { createMount, createClientRender, describeConformanceV5 } from 'test/utils'; import FilledInput from './FilledInput'; import InputBase from '../InputBase'; +import classes from './filledInputClasses'; describe('', () => { - let classes; const mount = createMount(); const render = createClientRender(); - before(() => { - classes = getClasses(); - }); - - describeConformance(, () => ({ + describeConformanceV5(, () => ({ classes, inheritComponent: InputBase, mount, refInstanceof: window.HTMLDivElement, - skip: ['componentProp'], + muiName: 'MuiFilledInput', + testDeepOverrides: { slotName: 'input', slotClassName: classes.input }, + testVariantProps: { variant: 'contained', fullWidth: true }, + testStateOverrides: { prop: 'size', value: 'small', styleKey: 'sizeSmall' }, + skip: ['componentProp', 'componentsProp'], })); it('should have the underline class', () => { diff --git a/packages/material-ui/src/FilledInput/filledInputClasses.d.ts b/packages/material-ui/src/FilledInput/filledInputClasses.d.ts new file mode 100644 index 00000000000000..61979e72aa0d33 --- /dev/null +++ b/packages/material-ui/src/FilledInput/filledInputClasses.d.ts @@ -0,0 +1,11 @@ +export interface FilledInputClasses { + root: string; + underline: string; + input: string; +} + +declare const filledInputClasses: FilledInputClasses; + +export function getFilledInputUtilityClass(slot: string): string; + +export default filledInputClasses; diff --git a/packages/material-ui/src/FilledInput/filledInputClasses.js b/packages/material-ui/src/FilledInput/filledInputClasses.js new file mode 100644 index 00000000000000..20b29a507dc290 --- /dev/null +++ b/packages/material-ui/src/FilledInput/filledInputClasses.js @@ -0,0 +1,9 @@ +import { generateUtilityClasses, generateUtilityClass } from '@material-ui/unstyled'; + +export function getFilledInputUtilityClass(slot) { + return generateUtilityClass('MuiFilledInput', slot); +} + +const filledInputClasses = generateUtilityClasses('MuiFilledInput', ['root', 'underline', 'input']); + +export default filledInputClasses; diff --git a/packages/material-ui/src/FilledInput/index.d.ts b/packages/material-ui/src/FilledInput/index.d.ts index fb808aeea4e9cd..f3576964636f51 100644 --- a/packages/material-ui/src/FilledInput/index.d.ts +++ b/packages/material-ui/src/FilledInput/index.d.ts @@ -1,2 +1,5 @@ export { default } from './FilledInput'; export * from './FilledInput'; + +export { default as filledInputClasses } from './filledInputClasses'; +export * from './filledInputClasses'; diff --git a/packages/material-ui/src/FilledInput/index.js b/packages/material-ui/src/FilledInput/index.js index 717577f9ee2552..91b5a314f0050b 100644 --- a/packages/material-ui/src/FilledInput/index.js +++ b/packages/material-ui/src/FilledInput/index.js @@ -1 +1,4 @@ export { default } from './FilledInput'; + +export { default as filledInputClasses } from './filledInputClasses'; +export * from './filledInputClasses'; diff --git a/packages/material-ui/src/InputBase/InputBase.d.ts b/packages/material-ui/src/InputBase/InputBase.d.ts index 1d0aa05ce59399..b4e8db3eac3483 100644 --- a/packages/material-ui/src/InputBase/InputBase.d.ts +++ b/packages/material-ui/src/InputBase/InputBase.d.ts @@ -72,6 +72,36 @@ export interface InputBaseProps * The prop defaults to the value (`'primary'`) inherited from the parent FormControl component. */ color?: 'primary' | 'secondary'; + /** + * The components used for each slot inside the InputBase. + * Either a string to use a HTML element or a component. + * @default {} + */ + components?: { + Root?: React.ElementType; + Input?: React.ElementType; + }; + + /** + * The props used for each slot inside the Input. + * @default {} + */ + componentsProps?: { + root?: { + as: React.ElementType; + styleProps?: Omit & { + hiddenLabel?: boolean; + focused?: boolean; + }; + }; + input?: { + as?: React.ElementType; + styleProps?: Omit & { + hiddenLabel?: boolean; + focused?: boolean; + }; + }; + }; /** * The default value. Use when the component is not controlled. */ diff --git a/packages/material-ui/src/InputBase/InputBase.js b/packages/material-ui/src/InputBase/InputBase.js index 31caf600980e1a..fdfb67001be8a0 100644 --- a/packages/material-ui/src/InputBase/InputBase.js +++ b/packages/material-ui/src/InputBase/InputBase.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import { refType, elementTypeAcceptingRef, deepmerge } from '@material-ui/utils'; import MuiError from '@material-ui/utils/macros/MuiError.macro'; -import { unstable_composeClasses as composeClasses } from '@material-ui/unstyled'; +import { unstable_composeClasses as composeClasses, isHostComponent } from '@material-ui/unstyled'; import formControlState from '../FormControl/formControlState'; import FormControlContext, { useFormControl } from '../FormControl/FormControlContext'; import experimentalStyled, { shouldForwardProp } from '../styles/experimentalStyled'; @@ -16,7 +16,7 @@ import GlobalStyles from '../GlobalStyles'; import { isFilled } from './utils'; import inputBaseClasses, { getInputBaseUtilityClass } from './inputBaseClasses'; -const overridesResolver = (props, styles) => { +export const overridesResolver = (props, styles) => { const { styleProps } = props; return deepmerge(styles.root || {}, { @@ -87,7 +87,7 @@ const useUtilityClasses = (styleProps) => { return composeClasses(slots, getInputBaseUtilityClass, classes); }; -const InputBaseRoot = experimentalStyled( +export const InputBaseRoot = experimentalStyled( 'div', {}, { @@ -119,7 +119,7 @@ const InputBaseRoot = experimentalStyled( }), })); -const InputBaseComponent = experimentalStyled( +export const InputBaseComponent = experimentalStyled( 'input', { shouldForwardProp: (prop) => shouldForwardProp(prop) || prop === 'classes' }, { @@ -225,6 +225,8 @@ const InputBase = React.forwardRef(function InputBase(inProps, ref) { autoFocus, className, color, + components = {}, + componentsProps = {}, defaultValue, disabled, endAdornment, @@ -253,6 +255,10 @@ const InputBase = React.forwardRef(function InputBase(inProps, ref) { startAdornment, type = 'text', value: valueProp, + /* eslint-disable-next-line react/prop-types */ + isRtl, + /* eslint-disable-next-line react/prop-types */ + theme, ...other } = props; @@ -471,6 +477,12 @@ const InputBase = React.forwardRef(function InputBase(inProps, ref) { const classes = useUtilityClasses(styleProps); + const Root = components.Root || InputBaseRoot; + const rootProps = componentsProps.root || {}; + + const Input = components.Input || InputBaseComponent; + inputProps = { ...inputProps, ...componentsProps.input }; + return ( - {startAdornment} - + ); }); @@ -560,6 +580,20 @@ InputBase.propTypes = { * The prop defaults to the value (`'primary'`) inherited from the parent FormControl component. */ color: PropTypes.oneOf(['primary', 'secondary']), + /** + * The components used for each slot inside the InputBase. + * Either a string to use a HTML element or a component. + * @default {} + */ + components: PropTypes.shape({ + Input: PropTypes.elementType, + Root: PropTypes.elementType, + }), + /** + * The props used for each slot inside the Input. + * @default {} + */ + componentsProps: PropTypes.object, /** * The default value. Use when the component is not controlled. */ diff --git a/packages/material-ui/src/InputBase/InputBase.test.js b/packages/material-ui/src/InputBase/InputBase.test.js index 8d9e69d7e99ea8..e0dc33f39eba36 100644 --- a/packages/material-ui/src/InputBase/InputBase.test.js +++ b/packages/material-ui/src/InputBase/InputBase.test.js @@ -21,7 +21,7 @@ describe('', () => { refInstanceof: window.HTMLDivElement, muiName: 'MuiInputBase', testVariantProps: { size: 'small' }, - skip: ['componentProp', 'componentsProp'], + skip: ['componentProp'], })); it('should render an inside the div', () => {