diff --git a/docs/pages/experiments/md3/inputs.tsx b/docs/pages/experiments/md3/inputs.tsx index 5a80eb6e60d349..eb9b76b7d7b349 100644 --- a/docs/pages/experiments/md3/inputs.tsx +++ b/docs/pages/experiments/md3/inputs.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import Stack from '@mui/material/Stack'; +import Divider from '@mui/material/Divider'; import Md2FilledInput from '@mui/material/FilledInput'; import { ThemeProvider, createTheme } from '@mui/material/styles'; import FilledInput from '@mui/material-next/FilledInput'; @@ -14,16 +15,22 @@ export default function MaterialYouInputs() { return ( +
MD2
- - + + +
- + +
MD3
+ +
- - + + +
diff --git a/packages/mui-material-next/migration.md b/packages/mui-material-next/migration.md index 724be901347fe4..4b1620a9ebd768 100644 --- a/packages/mui-material-next/migration.md +++ b/packages/mui-material-next/migration.md @@ -140,6 +140,19 @@ If you need to prevent default on a `key-up` and/or `key-down` event, then besid This is to ensure that default is prevented when the `ButtonBase` root is not a native button, for example, when the root element used is a `span`. +## FilledInput + +### Removed `inputProps` + +`inputProps` are removed in favor of `slotProps.input`: + +```diff + +``` + ## FormControl ### Renamed `FormControlState` diff --git a/packages/mui-material-next/src/FilledInput/FilledInput.d.ts b/packages/mui-material-next/src/FilledInput/FilledInput.d.ts deleted file mode 100644 index 74e6dd93fccf9a..00000000000000 --- a/packages/mui-material-next/src/FilledInput/FilledInput.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { SxProps } from '@mui/system'; -import { InternalStandardProps as StandardProps, Theme } from '@mui/material'; -import { InputBaseProps } from '@mui/material/InputBase'; -import { FilledInputClasses } from './filledInputClasses'; - -export interface FilledInputProps extends StandardProps { - /** - * Override or extend the styles applied to the component. - */ - classes?: Partial; - /** - * If `true`, the label is hidden. - * This is used to increase density for a `FilledInput`. - * Be sure to add `aria-label` to the `input` element. - * @default false - */ - hiddenLabel?: boolean; - /** - * 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; -} - -/** - * - * Demos: - * - * - [Text Field](https://mui.com/material-ui/react-text-field/) - * - * API: - * - * - [FilledInput API](https://mui.com/material-ui/api/filled-input/) - * - inherits [InputBase API](https://mui.com/material-ui/api/input-base/) - */ -declare const FilledInput: ((props: FilledInputProps) => JSX.Element) & { muiName: string }; - -export default FilledInput; diff --git a/packages/mui-material-next/src/FilledInput/FilledInput.test.js b/packages/mui-material-next/src/FilledInput/FilledInput.test.tsx similarity index 65% rename from packages/mui-material-next/src/FilledInput/FilledInput.test.js rename to packages/mui-material-next/src/FilledInput/FilledInput.test.tsx index e7ce5fde0068d7..b989695090cdb3 100644 --- a/packages/mui-material-next/src/FilledInput/FilledInput.test.js +++ b/packages/mui-material-next/src/FilledInput/FilledInput.test.tsx @@ -1,13 +1,16 @@ import * as React from 'react'; import { expect } from 'chai'; import { createRenderer, describeConformance } from '@mui-internal/test-utils'; -import FilledInput, { filledInputClasses as classes } from '@mui/material/FilledInput'; -import InputBase from '@mui/material/InputBase'; +import { CssVarsProvider, extendTheme } from '@mui/material-next/styles'; +import FilledInput, { filledInputClasses as classes } from '@mui/material-next/FilledInput'; +import InputBase from '@mui/material-next/InputBase'; describe('', () => { const { render } = createRenderer(); describeConformance(, () => ({ + ThemeProvider: CssVarsProvider, + createTheme: extendTheme, classes, inheritComponent: InputBase, render, @@ -16,11 +19,9 @@ describe('', () => { testDeepOverrides: { slotName: 'input', slotClassName: classes.input }, testVariantProps: { variant: 'contained', fullWidth: true }, testStateOverrides: { prop: 'size', value: 'small', styleKey: 'sizeSmall' }, - testLegacyComponentsProp: true, slots: { - // can't test with DOM element as Input places an ownerState prop on it unconditionally. - root: { expectedClassName: classes.root, testWithElement: null }, - input: { expectedClassName: classes.input, testWithElement: null }, + root: { expectedClassName: classes.root }, + input: { expectedClassName: classes.input, testWithElement: 'input' }, }, skip: [ 'componentProp', @@ -30,9 +31,10 @@ describe('', () => { })); it('should have the underline class', () => { - const { container } = render(); - const root = container.firstChild; + const { getByTestId } = render(); + const root = getByTestId('test-input'); expect(root).not.to.equal(null); + expect(root).to.have.class(classes.underline); }); it('color={undefined} should not result in crash', () => { @@ -42,8 +44,8 @@ describe('', () => { }); it('can disable the underline', () => { - const { container } = render(); - const root = container.firstChild; + const { getByTestId } = render(); + const root = getByTestId('test-input'); expect(root).not.to.have.class(classes.underline); }); @@ -52,19 +54,15 @@ describe('', () => { expect(document.querySelector('.error')).not.to.equal(null); }); - it('should respects the componentsProps if passed', () => { - render(); - expect(document.querySelector('[data-test=test]')).not.to.equal(null); - }); - it('should respect the classes coming from InputBase', () => { - render( + const { getByTestId } = render( , ); - expect(document.querySelector('[data-test=test]')).toHaveComputedStyle({ marginTop: '10px' }); + const root = getByTestId('test-input'); + expect(root).toHaveComputedStyle({ marginTop: '10px' }); }); }); diff --git a/packages/mui-material-next/src/FilledInput/FilledInput.js b/packages/mui-material-next/src/FilledInput/FilledInput.tsx similarity index 54% rename from packages/mui-material-next/src/FilledInput/FilledInput.js rename to packages/mui-material-next/src/FilledInput/FilledInput.tsx index 1c4c18c2fbaa59..8c89dc2bc7c9e4 100644 --- a/packages/mui-material-next/src/FilledInput/FilledInput.js +++ b/packages/mui-material-next/src/FilledInput/FilledInput.tsx @@ -1,20 +1,23 @@ 'use client'; import * as React from 'react'; -import { refType, deepmerge } from '@mui/utils'; +import { refType } from '@mui/utils'; import PropTypes from 'prop-types'; -import { unstable_composeClasses as composeClasses } from '@mui/base/composeClasses'; -// TODO v6: use material-next/InputBase +import { unstable_composeClasses as composeClasses, useSlotProps } from '@mui/base'; +import { DefaultComponentProps, OverrideProps } from '@mui/types'; +import { useThemeProps, styled } from '../styles'; +import { rootShouldForwardProp } from '../styles/styled'; import { + InputBaseRoot, + InputBaseInput, rootOverridesResolver as inputBaseRootOverridesResolver, inputOverridesResolver as inputBaseInputOverridesResolver, -} from '@mui/material/InputBase/InputBase'; +} from '../InputBase/InputBase'; import InputBase from '../InputBase'; -import styled, { rootShouldForwardProp } from '../styles/styled'; -import useThemeProps from '../styles/useThemeProps'; +import { InputBaseOwnerState } from '../InputBase/InputBase.types'; import filledInputClasses, { getFilledInputUtilityClass } from './filledInputClasses'; -import { InputBaseRoot, InputBaseInput } from '../InputBase/InputBase'; +import { FilledInputOwnerState, FilledInputProps, FilledInputTypeMap } from './FilledInput.types'; -const useUtilityClasses = (ownerState) => { +const useUtilityClasses = (ownerState: FilledInputOwnerState) => { const { classes, disableUnderline } = ownerState; const slots = { @@ -36,44 +39,55 @@ const FilledInputRoot = styled(InputBaseRoot, { slot: 'Root', overridesResolver: (props, styles) => { const { ownerState } = props; + return [ ...inputBaseRootOverridesResolver(props, styles), !ownerState.disableUnderline && styles.underline, ]; }, -})(({ theme, ownerState }) => { - 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.06)' : 'rgba(255, 255, 255, 0.09)'; - const hoverBackground = light ? 'rgba(0, 0, 0, 0.09)' : 'rgba(255, 255, 255, 0.13)'; - const disabledBackground = light ? 'rgba(0, 0, 0, 0.12)' : 'rgba(255, 255, 255, 0.12)'; +})<{ ownerState: FilledInputOwnerState }>(({ theme, ownerState }) => { + const { vars: tokens } = theme; + return { + '--md-comp-filled-input-active-indicator-color': tokens.sys.color.onSurfaceVariant, + '--md-comp-filled-input-container-color': tokens.sys.color.surfaceContainerHighest, + '--md-comp-filled-input-disabled-container-color': tokens.sys.color.onSurface, + '--md-comp-filled-input-disabled-container-opacity': 0.04, + '--md-comp-filled-input-error-active-indicator-color': tokens.sys.color.error, + '--md-comp-filled-input-error-hover-active-indicator-color': tokens.sys.color.onErrorContainer, + '--md-comp-filled-input-focus-active-indicator-color': + tokens.sys.color[ownerState.color ?? 'primary'], + '--md-comp-filled-input-hover-active-indicator-color': tokens.sys.color.onSurface, + '--md-comp-filled-input-hover-state-layer-opacity': tokens.sys.state.hover.stateLayerOpacity, position: 'relative', - backgroundColor: theme.vars ? theme.vars.palette.FilledInput.bg : backgroundColor, - borderTopLeftRadius: (theme.vars || theme).shape.borderRadius, - borderTopRightRadius: (theme.vars || theme).shape.borderRadius, + backgroundColor: 'var(--md-comp-filled-input-container-color)', + borderTopLeftRadius: tokens.shape.borderRadius, + borderTopRightRadius: tokens.shape.borderRadius, transition: theme.transitions.create('background-color', { duration: theme.transitions.duration.shorter, easing: theme.transitions.easing.easeOut, }), '&:hover': { - backgroundColor: theme.vars ? theme.vars.palette.FilledInput.hoverBg : hoverBackground, + backgroundColor: + 'color-mix(in srgb, var(--md-comp-filled-input-hover-active-indicator-color) calc(var(--md-comp-filled-input-hover-state-layer-opacity) * 100%), var(--md-comp-filled-input-container-color))', // Reset on touch devices, it doesn't add specificity '@media (hover: none)': { - backgroundColor: theme.vars ? theme.vars.palette.FilledInput.bg : backgroundColor, + backgroundColor: 'var(--md-comp-filled-input-container-color)', }, }, [`&.${filledInputClasses.focused}`]: { - backgroundColor: theme.vars ? theme.vars.palette.FilledInput.bg : backgroundColor, + backgroundColor: 'var(--md-comp-filled-input-container-color)', + '&:after': { + borderColor: 'var(--md-comp-filled-input-focus-active-indicator-color)', + }, }, [`&.${filledInputClasses.disabled}`]: { - backgroundColor: theme.vars ? theme.vars.palette.FilledInput.disabledBg : disabledBackground, + backgroundColor: + 'color-mix(in srgb, var(--md-comp-filled-input-disabled-container-color) var(--md-comp-filled-input-disabled-container-opacity), var(--md-comp-filled-input-container-color))', }, ...(!ownerState.disableUnderline && { '&:after': { - borderBottom: `2px solid ${ - (theme.vars || theme).palette[ownerState.color || 'primary']?.main - }`, + borderBottom: '2px solid var(--md-comp-filled-input-active-indicator-color)', left: 0, bottom: 0, // Doing the other way around crash on IE11 "''" https://github.com/cssinjs/jss/issues/242 @@ -94,15 +108,11 @@ const FilledInputRoot = styled(InputBaseRoot, { }, [`&.${filledInputClasses.error}`]: { '&:before, &:after': { - borderBottomColor: (theme.vars || theme).palette.error.main, + borderBottomColor: 'var(--md-comp-filled-input-error-active-indicator-color)', }, }, '&:before': { - borderBottom: `1px solid ${ - theme.vars - ? `rgba(${theme.vars.palette.common.onBackgroundChannel} / ${theme.vars.opacity.inputUnderline})` - : bottomLineColor - }`, + borderBottom: '1px solid var(--md-comp-filled-input-active-indicator-color)', left: 0, bottom: 0, // Doing the other way around crash on IE11 "''" https://github.com/cssinjs/jss/issues/242 @@ -115,7 +125,7 @@ const FilledInputRoot = styled(InputBaseRoot, { pointerEvents: 'none', // Transparent to the hover style. }, [`&:hover:not(.${filledInputClasses.disabled}, .${filledInputClasses.error}):before`]: { - borderBottom: `1px solid ${(theme.vars || theme).palette.text.primary}`, + borderBottom: '1px solid var(--md-comp-filled-input-active-indicator-color)', }, [`&.${filledInputClasses.disabled}:before`]: { borderBottomStyle: 'dotted', @@ -145,115 +155,163 @@ const FilledInputInput = styled(InputBaseInput, { name: 'MuiFilledInput', slot: 'Input', overridesResolver: inputBaseInputOverridesResolver, -})(({ theme, ownerState }) => ({ - paddingTop: 25, - paddingRight: 12, - paddingBottom: 8, - paddingLeft: 12, - ...(!theme.vars && { - '&:-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', - }, - }), - ...(theme.vars && { - '&:-webkit-autofill': { - borderTopLeftRadius: 'inherit', - borderTopRightRadius: 'inherit', - }, - [theme.getColorSchemeSelector('dark')]: { - '&:-webkit-autofill': { - WebkitBoxShadow: '0 0 0 100px #266798 inset', - WebkitTextFillColor: '#fff', - caretColor: '#fff', - }, - }, - }), - ...(ownerState.size === 'small' && { - paddingTop: 21, - paddingBottom: 4, - }), - ...(ownerState.hiddenLabel && { - paddingTop: 16, - paddingBottom: 17, - }), - ...(ownerState.multiline && { - paddingTop: 0, - paddingBottom: 0, - paddingLeft: 0, - paddingRight: 0, - }), - ...(ownerState.startAdornment && { - paddingLeft: 0, - }), - ...(ownerState.endAdornment && { - paddingRight: 0, - }), - ...(ownerState.hiddenLabel && - ownerState.size === 'small' && { - paddingTop: 8, - paddingBottom: 9, +})<{ ownerState: FilledInputOwnerState }>(({ theme, ownerState }) => { + const { vars: tokens } = theme; + + return { + paddingTop: 25, + paddingRight: 12, + paddingBottom: 8, + paddingLeft: 12, + ...(!tokens + ? { + [theme.getColorSchemeSelector('light')]: { + '&:-webkit-autofill': { + WebkitBoxShadow: null, + WebkitTextFillColor: null, + caretColor: null, + borderTopLeftRadius: 'inherit', + borderTopRightRadius: 'inherit', + }, + }, + [theme.getColorSchemeSelector('dark')]: { + '&:-webkit-autofill': { + WebkitBoxShadow: '0 0 0 100px #266798 inset', + WebkitTextFillColor: '#fff', + caretColor: '#fff', + borderTopLeftRadius: 'inherit', + borderTopRightRadius: 'inherit', + }, + }, + } + : { + '&:-webkit-autofill': { + borderTopLeftRadius: 'inherit', + borderTopRightRadius: 'inherit', + }, + // this could be undefined in unit tests + ...(theme.getColorSchemeSelector && { + [theme.getColorSchemeSelector('dark')]: { + '&:-webkit-autofill': { + WebkitBoxShadow: '0 0 0 100px #266798 inset', + WebkitTextFillColor: '#fff', + caretColor: '#fff', + }, + }, + }), + }), + ...(ownerState.size === 'small' && { + paddingTop: 21, + paddingBottom: 4, + }), + ...(ownerState.hiddenLabel && { + paddingTop: 16, + paddingBottom: 17, + }), + ...(ownerState.multiline && { + paddingTop: 0, + paddingBottom: 0, + paddingLeft: 0, + paddingRight: 0, + }), + ...(ownerState.startAdornment && { + paddingLeft: 0, }), -})); + ...(ownerState.endAdornment && { + paddingRight: 0, + }), + ...(ownerState.hiddenLabel && + ownerState.size === 'small' && { + paddingTop: 8, + paddingBottom: 9, + }), + }; +}); -const FilledInput = React.forwardRef(function FilledInput(inProps, ref) { +const FilledInput = React.forwardRef(function FilledInput< + RootComponentType extends React.ElementType, +>(inProps: FilledInputProps, forwardedRef: React.ForwardedRef) { const props = useThemeProps({ props: inProps, name: 'MuiFilledInput' }); const { disableUnderline, - components = {}, - componentsProps: componentsPropsProp, fullWidth = false, hiddenLabel, // declare here to prevent spreading to DOM inputComponent = 'input', multiline = false, - slotProps, - slots = {}, type = 'text', + slotProps = {}, + slots = {}, ...other } = props; - const ownerState = { + const ownerState: FilledInputOwnerState = { ...props, + disableUnderline, fullWidth, inputComponent, multiline, type, }; - const classes = useUtilityClasses(props); - const filledInputComponentsProps = { root: { ownerState }, input: { ownerState } }; + const classes = useUtilityClasses(ownerState); + + const Root = slots.root ?? FilledInputRoot; + const Input = slots.input ?? FilledInputInput; - const componentsProps = - slotProps ?? componentsPropsProp - ? deepmerge(slotProps ?? componentsPropsProp, filledInputComponentsProps) - : filledInputComponentsProps; + const rootProps = useSlotProps({ + elementType: Root, + externalSlotProps: slotProps.root, + additionalProps: { + ref: forwardedRef, + fullWidth, + inputComponent, + multiline, + type, + }, + externalForwardedProps: other, + ownerState: ownerState as FilledInputOwnerState & InputBaseOwnerState, + className: [classes.root], + }); - const RootSlot = slots.root ?? components.Root ?? FilledInputRoot; - const InputSlot = slots.input ?? components.Input ?? FilledInputInput; + const inputProps = useSlotProps({ + elementType: Input, + externalSlotProps: slotProps.input, + ownerState: ownerState as FilledInputOwnerState & InputBaseOwnerState, + className: [classes.input], + }); return ( ); -}); +}) as FilledInputComponent; + +interface FilledInputComponent { + ( + props: { + /** + * The component used for the input node. + * Either a string to use a HTML element or a component. + * @default 'input' + */ + inputComponent?: C; + } & OverrideProps, + ): JSX.Element | null; + (props: DefaultComponentProps): JSX.Element | null; + propTypes?: any; + muiName?: string; +} FilledInput.propTypes /* remove-proptypes */ = { // ----------------------------- Warning -------------------------------- // | These PropTypes are generated from the TypeScript type definitions | - // | To update them edit the d.ts file and run "yarn proptypes" | + // | To update them edit TypeScript types and run "yarn proptypes" | // ---------------------------------------------------------------------- /** * This prop helps users to fill forms faster, especially on mobile devices. @@ -265,6 +323,10 @@ FilledInput.propTypes /* remove-proptypes */ = { * If `true`, the `input` element is focused during the first mount. */ autoFocus: PropTypes.bool, + /** + * @ignore + */ + children: PropTypes.node, /** * Override or extend the styles applied to the component. */ @@ -276,34 +338,9 @@ FilledInput.propTypes /* remove-proptypes */ = { * The prop defaults to the value (`'primary'`) inherited from the parent FormControl component. */ color: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ - PropTypes.oneOf(['primary', 'secondary']), + PropTypes.oneOf(['error', 'info', 'primary', 'secondary', 'success', 'tertiary', 'warning']), PropTypes.string, ]), - /** - * The components used for each slot inside. - * - * This prop is an alias for the `slots` prop. - * It's recommended to use the `slots` prop instead. - * - * @default {} - */ - components: PropTypes.shape({ - Input: PropTypes.elementType, - Root: PropTypes.elementType, - }), - /** - * The extra props for the slot components. - * You can override the existing props or add new ones. - * - * This prop is an alias for the `slotProps` prop. - * It's recommended to use the `slotProps` prop instead, as `componentsProps` will be deprecated in the future. - * - * @default {} - */ - componentsProps: PropTypes.shape({ - input: PropTypes.object, - root: PropTypes.object, - }), /** * The default value. Use when the component is not controlled. */ @@ -343,16 +380,11 @@ FilledInput.propTypes /* remove-proptypes */ = { */ id: PropTypes.string, /** - * The component used for the `input` element. + * The component used for the input node. * Either a string to use a HTML element or a component. * @default 'input' */ - inputComponent: PropTypes.elementType, - /** - * [Attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Attributes) applied to the `input` element. - * @default {} - */ - inputProps: PropTypes.object, + inputComponent: PropTypes /* @typescript-to-proptypes-ignore */.elementType, /** * Pass a ref to the `input` element. */ @@ -366,13 +398,13 @@ FilledInput.propTypes /* remove-proptypes */ = { /** * Maximum number of rows to display when multiline option is set to true. */ - maxRows: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + maxRows: PropTypes.number, /** * Minimum number of rows to display when multiline option is set to true. */ - minRows: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + minRows: PropTypes.number, /** - * If `true`, a [TextareaAutosize](/material-ui/react-textarea-autosize/) element is rendered. + * If `true`, a `textarea` element is rendered. * @default false */ multiline: PropTypes.bool, @@ -404,24 +436,18 @@ FilledInput.propTypes /* remove-proptypes */ = { /** * Number of rows to display when multiline option is set to true. */ - rows: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + rows: PropTypes.number, /** - * The extra props for the slot components. - * You can override the existing props or add new ones. - * - * This prop is an alias for the `componentsProps` prop, which will be deprecated in the future. - * + * The props used for each slot inside the Input. * @default {} */ slotProps: PropTypes.shape({ - input: PropTypes.object, - root: PropTypes.object, + input: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), }), /** - * The components used for each slot inside. - * - * This prop is an alias for the `components` prop, which will be deprecated in the future. - * + * The components used for each slot inside the InputBase. + * Either a string to use a HTML element or a component. * @default {} */ slots: PropTypes.shape({ @@ -444,12 +470,35 @@ FilledInput.propTypes /* remove-proptypes */ = { * 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' */ - type: PropTypes.string, + type: PropTypes /* @typescript-to-proptypes-ignore */.oneOf([ + 'button', + 'checkbox', + 'color', + 'date', + 'datetime-local', + 'email', + 'file', + 'hidden', + 'image', + 'month', + 'number', + 'password', + 'radio', + 'range', + 'reset', + 'search', + 'submit', + 'tel', + 'text', + 'time', + 'url', + 'week', + ]), /** * The value of the `input` element, required for a controlled component. */ value: PropTypes.any, -}; +} as any; FilledInput.muiName = 'Input'; diff --git a/packages/mui-material-next/src/FilledInput/FilledInput.types.ts b/packages/mui-material-next/src/FilledInput/FilledInput.types.ts new file mode 100644 index 00000000000000..fe229efca59b7f --- /dev/null +++ b/packages/mui-material-next/src/FilledInput/FilledInput.types.ts @@ -0,0 +1,75 @@ +import { SxProps } from '@mui/system'; +// TODO v6: port to material-next +// eslint-disable-next-line no-restricted-imports +import { InternalStandardProps as StandardProps } from '@mui/material'; +import { OverrideProps, Simplify } from '@mui/types'; +import { Theme } from '../styles/Theme.types'; +import { InputBaseProps } from '../InputBase/InputBase.types'; +import { FilledInputClasses } from './filledInputClasses'; + +export interface FilledInputOwnProps + extends StandardProps> { + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + /** + * If `true`, the label is hidden. + * This is used to increase density for a `FilledInput`. + * Be sure to add `aria-label` to the `input` element. + * @default false + */ + hiddenLabel?: boolean; + /** + * If `true`, the input will not have an underline. + */ + disableUnderline?: boolean; + /** + * The components used for each slot inside the InputBase. + * Either a string to use a HTML element or a component. + * @default {} + */ + slots?: FilledInputSlots; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; +} + +export interface FilledInputSlots { + /** + * The component that renders the root. + * @default 'div' + */ + root?: React.ElementType; + /** + * The component that renders the input. + * @default 'input' + */ + input?: React.ElementType; +} + +export interface FilledInputTypeMap< + AdditionalProps = {}, + RootComponentType extends React.ElementType = 'div', +> { + props: FilledInputOwnProps & AdditionalProps; + defaultComponent: RootComponentType; +} + +export type FilledInputProps< + RootComponentType extends React.ElementType = FilledInputTypeMap['defaultComponent'], + AdditionalProps = {}, +> = OverrideProps, RootComponentType> & { + inputComponent?: React.ElementType; +}; + +export type FilledInputOwnerState = Simplify< + FilledInputOwnProps & { + disableUnderline?: boolean; + fullWidth: boolean; + inputComponent: React.ElementType; + multiline: boolean; + type?: React.InputHTMLAttributes['type']; + } +>; diff --git a/packages/mui-material-next/src/FilledInput/index.d.ts b/packages/mui-material-next/src/FilledInput/index.d.ts deleted file mode 100644 index f3576964636f51..00000000000000 --- a/packages/mui-material-next/src/FilledInput/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { default } from './FilledInput'; -export * from './FilledInput'; - -export { default as filledInputClasses } from './filledInputClasses'; -export * from './filledInputClasses'; diff --git a/packages/mui-material-next/src/FilledInput/index.js b/packages/mui-material-next/src/FilledInput/index.ts similarity index 100% rename from packages/mui-material-next/src/FilledInput/index.js rename to packages/mui-material-next/src/FilledInput/index.ts diff --git a/packages/mui-material-next/src/FormControl/FormControl.test.js b/packages/mui-material-next/src/FormControl/FormControl.test.tsx similarity index 86% rename from packages/mui-material-next/src/FormControl/FormControl.test.js rename to packages/mui-material-next/src/FormControl/FormControl.test.tsx index ed22b74a058d50..1068d199c74b54 100644 --- a/packages/mui-material-next/src/FormControl/FormControl.test.js +++ b/packages/mui-material-next/src/FormControl/FormControl.test.tsx @@ -3,17 +3,28 @@ import { expect } from 'chai'; import { spy } from 'sinon'; import { describeConformance, act, createRenderer, fireEvent } from '@mui-internal/test-utils'; import FormControl, { formControlClasses as classes } from '@mui/material-next/FormControl'; -// TODO v6: replace with material-next/FilledInput +import FilledInput from '@mui/material-next/FilledInput'; import InputBase from '@mui/material-next/InputBase'; import { CssVarsProvider, extendTheme } from '@mui/material-next/styles'; // TODO v6: replace with material-next/Select import Select from '@mui/material/Select'; import useFormControl from './useFormControl'; +type TestFormControlledComponent = { + onFilled: () => {}; + onEmpty: () => {}; + onFocus: () => {}; + onBlur: () => {}; +}; + describe('', () => { const { render } = createRenderer(); - function TestComponent(props) { + interface TestComponentProps { + contextCallback: (context: ReturnType) => void; + } + + function TestComponent(props: TestComponentProps) { const context = useFormControl(); React.useEffect(() => { props.contextCallback(context); @@ -47,7 +58,6 @@ describe('', () => { const root = container.firstChild; expect(root).not.to.have.class(classes.marginNormal); - expect(root).not.to.have.class(classes.sizeSmall); }); it('can have the margin normal class', () => { @@ -55,7 +65,6 @@ describe('', () => { const root = container.firstChild; expect(root).to.have.class(classes.marginNormal); - expect(root).not.to.have.class(classes.sizeSmall); }); it('can have the margin dense class', () => { @@ -106,7 +115,7 @@ describe('', () => { expect(readContext.args[0][0]).to.have.property('focused', false); act(() => { - container.querySelector('input').focus(); + container.querySelector('input')?.focus(); }); expect(readContext.lastCall.args[0]).to.have.property('focused', true); @@ -126,7 +135,7 @@ describe('', () => { ); expect(readContext.args[0][0]).to.have.property('focused', true); - container.querySelector('input').blur(); + container.querySelector('input')?.blur(); expect(readContext.args[0][0]).to.have.property('focused', true); }); @@ -201,27 +210,23 @@ describe('', () => { }); }); - // TODO v6: needs FilledInput + FormControl integrated - // eslint-disable-next-line mocha/no-skipped-tests - describe.skip('input', () => { + describe('input', () => { it('should be filled when a value is set', () => { const readContext = spy(); render( - {/* TODO v6: use material-next/FilledInput */} - + , ); expect(readContext.args[0][0]).to.have.property('filled', true); }); - it('should be filled when a value is set through inputProps', () => { + it('should be filled when a value is set through slotProps.input', () => { const readContext = spy(); render( - {/* TODO v6: use material-next/FilledInput */} - + , ); @@ -232,8 +237,7 @@ describe('', () => { const readContext = spy(); render( - {/* TODO v6: use material-next/FilledInput */} - + , ); @@ -244,8 +248,7 @@ describe('', () => { const readContext = spy(); render( - {/* TODO v6: use material-next/FilledInput */} - } /> + } /> , ); @@ -256,8 +259,7 @@ describe('', () => { const readContext = spy(); render( - {/* TODO v6: use material-next/FilledInput */} - } /> + } /> , ); @@ -287,7 +289,7 @@ describe('', () => { , ); - expect(readContext.args[0][0].adornedStart, true); + expect(readContext.args[0][0].adornedStart, 'true'); }); }); @@ -308,7 +310,7 @@ describe('', () => { describe('from props', () => { it('should have the required prop from the instance', () => { - const formControlRef = React.createRef(); + const formControlRef = React.createRef(); const { setProps } = render(); expect(formControlRef.current).to.have.property('required', false); @@ -318,7 +320,7 @@ describe('', () => { }); it('should have the error prop from the instance', () => { - const formControlRef = React.createRef(); + const formControlRef = React.createRef(); const { setProps } = render(); expect(formControlRef.current).to.have.property('error', false); @@ -328,7 +330,7 @@ describe('', () => { }); it('should have the margin prop from the instance', () => { - const formControlRef = React.createRef(); + const formControlRef = React.createRef(); const { setProps } = render(); expect(formControlRef.current).to.have.property('size', 'medium'); @@ -338,7 +340,7 @@ describe('', () => { }); it('should have the fullWidth prop from the instance', () => { - const formControlRef = React.createRef(); + const formControlRef = React.createRef(); const { setProps } = render(); expect(formControlRef.current).to.have.property('fullWidth', false); @@ -351,19 +353,19 @@ describe('', () => { describe('callbacks', () => { describe('onFilled', () => { it('should set the filled state', () => { - const formControlRef = React.createRef(); + const formControlRef = React.createRef(); render(); expect(formControlRef.current).to.have.property('filled', false); act(() => { - formControlRef.current.onFilled(); + formControlRef.current?.onFilled(); }); expect(formControlRef.current).to.have.property('filled', true); act(() => { - formControlRef.current.onFilled(); + formControlRef.current?.onFilled(); }); expect(formControlRef.current).to.have.property('filled', true); @@ -372,23 +374,23 @@ describe('', () => { describe('onEmpty', () => { it('should clean the filled state', () => { - const formControlRef = React.createRef(); + const formControlRef = React.createRef(); render(); act(() => { - formControlRef.current.onFilled(); + formControlRef.current?.onFilled(); }); expect(formControlRef.current).to.have.property('filled', true); act(() => { - formControlRef.current.onEmpty(); + formControlRef.current?.onEmpty(); }); expect(formControlRef.current).to.have.property('filled', false); act(() => { - formControlRef.current.onEmpty(); + formControlRef.current?.onEmpty(); }); expect(formControlRef.current).to.have.property('filled', false); @@ -397,18 +399,18 @@ describe('', () => { describe('handleFocus', () => { it('should set the focused state', () => { - const formControlRef = React.createRef(); + const formControlRef = React.createRef(); render(); expect(formControlRef.current).to.have.property('focused', false); act(() => { - formControlRef.current.onFocus(); + formControlRef.current?.onFocus(); }); expect(formControlRef.current).to.have.property('focused', true); act(() => { - formControlRef.current.onFocus(); + formControlRef.current?.onFocus(); }); expect(formControlRef.current).to.have.property('focused', true); @@ -417,24 +419,24 @@ describe('', () => { describe('handleBlur', () => { it('should clear the focused state', () => { - const formControlRef = React.createRef(); + const formControlRef = React.createRef(); render(); expect(formControlRef.current).to.have.property('focused', false); act(() => { - formControlRef.current.onFocus(); + formControlRef.current?.onFocus(); }); expect(formControlRef.current).to.have.property('focused', true); act(() => { - formControlRef.current.onBlur(); + formControlRef.current?.onBlur(); }); expect(formControlRef.current).to.have.property('focused', false); act(() => { - formControlRef.current.onBlur(); + formControlRef.current?.onBlur(); }); expect(formControlRef.current).to.have.property('focused', false); diff --git a/packages/mui-material-next/src/FormControl/FormControl.tsx b/packages/mui-material-next/src/FormControl/FormControl.tsx index 4d0f15b1c1306f..478a61dc14ad1b 100644 --- a/packages/mui-material-next/src/FormControl/FormControl.tsx +++ b/packages/mui-material-next/src/FormControl/FormControl.tsx @@ -141,7 +141,7 @@ const FormControl = React.forwardRef(function FormControl< if ( React.isValidElement(child) && - (isFilled(child.props, true) || isFilled(child.props.inputProps, true)) + (isFilled(child.props, true) || isFilled(child.props.slotProps?.input, true)) ) { initialFilled = true; } diff --git a/packages/mui-material-next/src/InputBase/InputBase.test.js b/packages/mui-material-next/src/InputBase/InputBase.test.tsx similarity index 82% rename from packages/mui-material-next/src/InputBase/InputBase.test.js rename to packages/mui-material-next/src/InputBase/InputBase.test.tsx index 96eb55612ed2ca..b91ed92548e827 100644 --- a/packages/mui-material-next/src/InputBase/InputBase.test.js +++ b/packages/mui-material-next/src/InputBase/InputBase.test.tsx @@ -18,6 +18,11 @@ import TextField from '@mui/material/TextField'; import Select from '@mui/material/Select'; import InputBase, { inputBaseClasses as classes } from '@mui/material-next/InputBase'; import { CssVarsProvider, extendTheme } from '@mui/material-next/styles'; +import { + InputBaseInputSlotPropsOverrides, + InputBaseOwnerState, + InputBaseProps, +} from './InputBase.types'; describe('', () => { const { render } = createRenderer(); @@ -53,6 +58,21 @@ describe('', () => { }); describe('multiline', () => { + describe('warning if multiline related props are passed without specifying the multiline prop', () => { + ['rows', 'minRows', 'maxRows'].forEach((multilineProp) => { + it(`warns if ${multilineProp} is passed without specifying multiline`, () => { + const multilineErrorMessage = `MUI: You have set multiline props on an single-line input.\nSet the \`multiline\` prop if you want to render a multi-line input.\nOtherwise they will be ignored.\nIgnored props: ${multilineProp}`; + expect(() => { + render(); + }).toErrorDev([ + multilineErrorMessage, + // React 18 Strict Effects run mount effects twice + React.version.startsWith('18') && multilineErrorMessage, + ]); + }); + }); + }); + it('should render a `textbox` with `aria-multiline`', () => { render(); @@ -185,7 +205,12 @@ describe('', () => { * * A ref is exposed to trigger a change event instead of using fireEvent.change */ - const BadInputComponent = React.forwardRef(function BadInputComponent(props, ref) { + const BadInputComponent = React.forwardRef(function BadInputComponent( + props: { + onChange: (arg: Record) => void; + }, + ref, + ) { const { onChange } = props; // simulates const handleChange = () => onChange({}) and passing that @@ -199,7 +224,7 @@ describe('', () => { onChange: PropTypes.func.isRequired, }; - const triggerChangeRef = React.createRef(); + const triggerChangeRef = React.createRef(); expect(() => { render( @@ -228,15 +253,21 @@ describe('', () => { const { getByTestId } = render( , ); expect(getByTestId('input-component')).to.have.property('nodeName', 'SPAN'); }); it('should inject onBlur and onFocus', () => { - let injectedProps; - const MyInputBase = React.forwardRef(function MyInputBase(props, ref) { + let injectedProps: Record = {}; + + const MyInputBase = React.forwardRef(function MyInputBase( + props: { ownerState: InputBaseOwnerState } & Record, + ref: React.ForwardedRef, + ) { injectedProps = props; const { ownerState, ...other } = props; return ; @@ -250,20 +281,27 @@ describe('', () => { describe('target mock implementations', () => { it('can just mock the value', () => { - const MockedValue = React.forwardRef(function MockedValue(props, ref) { + const MockedValue = React.forwardRef(function MockedValue( + props: { + onChange: React.ChangeEventHandler; + }, + ref: React.ForwardedRef, + ) { const { onChange } = props; - const handleChange = (event) => { - onChange({ target: { value: event.target.value } }); + const handleChange = (event: React.ChangeEvent) => { + onChange({ + target: { value: event.target.value }, + } as React.ChangeEvent); }; return ; }); MockedValue.propTypes = { onChange: PropTypes.func.isRequired }; - function FilledState(props) { - const { filled } = useFormControl(); - return filled: {String(filled)}; + function FilledState(props: { 'data-testid': string }) { + const formControlContext = useFormControl(); + return filled: {String(formControlContext?.filled)}; } const { getByRole, getByTestId } = render( @@ -279,14 +317,17 @@ describe('', () => { }); it("can expose the input component's ref through the inputComponent prop", () => { - const FullTarget = React.forwardRef(function FullTarget(props, ref) { + const FullTarget = React.forwardRef(function FullTarget( + props: { ownerState: InputBaseOwnerState } & Record, + ref: React.ForwardedRef, + ) { const { ownerState, ...otherProps } = props; return ; }); - function FilledState(props) { - const { filled } = useFormControl(); - return filled: {String(filled)}; + function FilledState(props: { 'data-testid': string }) { + const formControlContext = useFormControl(); + return filled: {String(formControlContext?.filled)}; } const { getByRole, getByTestId } = render( @@ -331,7 +372,7 @@ describe('', () => { describe('error', () => { it('should be overridden by props', () => { - function InputBaseInErrorForm(props) { + function InputBaseInErrorForm(props: InputBaseProps) { return ( @@ -361,7 +402,7 @@ describe('', () => { }); it('should be overridden by props', () => { - function InputBaseInFormWithMargin(props) { + function InputBaseInFormWithMargin(props: InputBaseProps) { return ( @@ -398,16 +439,21 @@ describe('', () => { }); }); + type TestFormController = { + onFocus: () => {}; + onBlur: () => {}; + }; + describe('focused', () => { it('prioritizes context focus', () => { const FormController = React.forwardRef((props, ref) => { - const { onBlur, onFocus } = useFormControl(); + const { onBlur, onFocus } = useFormControl() ?? {}; React.useImperativeHandle(ref, () => ({ onBlur, onFocus }), [onBlur, onFocus]); return null; }); - const controlRef = React.createRef(); + const controlRef = React.createRef(); const { getByRole, getByTestId } = render( @@ -421,22 +467,22 @@ describe('', () => { expect(getByTestId('root')).to.have.class(classes.focused); act(() => { - controlRef.current.onBlur(); + controlRef.current?.onBlur(); }); expect(getByTestId('root')).not.to.have.class(classes.focused); act(() => { - controlRef.current.onFocus(); + controlRef.current?.onFocus(); }); expect(getByTestId('root')).to.have.class(classes.focused); }); it('propagates focused state', () => { - function FocusedStateLabel(props) { - const { focused } = useFormControl(); - return ; + function FocusedStateLabel(props: { 'data-testid': string; htmlFor: string }) { + const formControlContext = useFormControl(); + return ; } const { getByRole, getByTestId } = render( @@ -459,9 +505,9 @@ describe('', () => { }); it('propagates filled state when uncontrolled', () => { - function FilledStateLabel(props) { - const { filled } = useFormControl(); - return ; + function FilledStateLabel(props: { 'data-testid': string }) { + const formControlContext = useFormControl(); + return ; } const { getByRole, getByTestId } = render( @@ -483,11 +529,11 @@ describe('', () => { }); it('propagates filled state when controlled', () => { - function FilledStateLabel(props) { - const { filled } = useFormControl(); - return ; + function FilledStateLabel(props: { 'data-testid': string }) { + const formControlContext = useFormControl(); + return ; } - function ControlledInputBase(props) { + function ControlledInputBase(props: InputBaseProps) { return ( @@ -552,7 +598,7 @@ describe('', () => { }); it('should be able to get a ref', () => { - const inputRef = React.createRef(); + const inputRef = React.createRef(); const { container } = render(); expect(inputRef.current).to.equal(container.querySelector('input')); }); @@ -560,21 +606,29 @@ describe('', () => { it('should not repeat the same classname', () => { const { container } = render(); const input = container.querySelector('input'); - const matches = input.className.match(/foo/g); + const matches = input?.className.match(/foo/g); expect(input).to.have.class('foo'); expect(matches).to.have.length(1); }); }); describe('prop: slots and slotProps', () => { - it('should call onChange inputProp callback with all params sent from custom inputComponent', () => { + // e.g. integration of react-select with InputBase + // https://github.com/mui/material-ui/issues/18130 + // react-select has a custom onChange that is essentially "(string, string) => void" + it('should call slotProps.input.onChange callback with all params sent from custom inputComponent', () => { const INPUT_VALUE = 'material'; const OUTPUT_VALUE = 'test'; - const MyInputBase = React.forwardRef(function MyInputBase(props, ref) { - const { onChange, ownerState, ...other } = props; + const MyInputBase = React.forwardRef(function MyInputBase( + props: { + onChange: (...args: string[]) => void; + }, + ref: React.ForwardedRef, + ) { + const { onChange, ...other } = props; - const handleChange = (e) => { + const handleChange = (e: React.ChangeEvent) => { onChange(e.target.value, OUTPUT_VALUE); }; @@ -585,17 +639,17 @@ describe('', () => { onChange: PropTypes.func.isRequired, }; - let outputArguments; - function parentHandleChange(...args) { + let outputArguments: string[] = []; + function parentHandleChange(...args: string[]) { outputArguments = args; } const { getByRole } = render( , }, }} />, @@ -670,7 +724,7 @@ describe('', () => { }); describe('prop: focused', () => { - // TODO v6: requires material-next/OutlinedInput + // TODO v6: requires material-next/TextField // eslint-disable-next-line mocha/no-skipped-tests it.skip('should render correct border color with a customized primary color supplied to CssVarsProvider', function test() { if (/jsdom/.test(window.navigator.userAgent)) { @@ -689,7 +743,7 @@ describe('', () => { }); const { getByRole } = render( - {/* TODO v6: use material-next/TextField or OutlinedInput */} + {/* TODO v6: use material-next/TextField */} , ); diff --git a/packages/mui-material-next/src/InputBase/InputBase.tsx b/packages/mui-material-next/src/InputBase/InputBase.tsx index fa5000b80ada3a..fcbb3b667027a3 100644 --- a/packages/mui-material-next/src/InputBase/InputBase.tsx +++ b/packages/mui-material-next/src/InputBase/InputBase.tsx @@ -11,12 +11,13 @@ import { WithOptionalOwnerState, } from '@mui/base'; import { useInput } from '@mui/base/useInput'; +import { CSSInterpolation } from '@mui/system'; +import { DefaultComponentProps, OverrideProps } from '@mui/types'; import { refType, unstable_capitalize as capitalize, unstable_useEnhancedEffect as useEnhancedEffect, } from '@mui/utils'; -import { OverrideProps } from '@mui/types'; import FormControlContext from '@mui/material-next/FormControl/FormControlContext'; import useFormControl from '@mui/material-next/FormControl/useFormControl'; import styled from '../styles/styled'; @@ -80,25 +81,30 @@ const useUtilityClasses = (ownerState: InputBaseOwnerState) => { return composeClasses(slots, getInputBaseUtilityClass, classes); }; +export function rootOverridesResolver( + props: InputBaseRootSlotProps, + styles: Record, +) { + const { ownerState } = props; + + return [ + styles.root, + ownerState.formControl && styles.formControl, + ownerState.startAdornment && styles.adornedStart, + ownerState.endAdornment && styles.adornedEnd, + ownerState.error && styles.error, + ownerState.size === 'small' && styles.sizeSmall, + ownerState.multiline && styles.multiline, + ownerState.color && styles[`color${capitalize(ownerState.color)}`], + ownerState.fullWidth && styles.fullWidth, + ownerState.hiddenLabel && styles.hiddenLabel, + ]; +} + export const InputBaseRoot = styled('div', { name: 'MuiInputBase', slot: 'Root', - overridesResolver: (props, styles) => { - const { ownerState } = props; - - return [ - styles.root, - ownerState.formControl && styles.formControl, - ownerState.startAdornment && styles.adornedStart, - ownerState.endAdornment && styles.adornedEnd, - ownerState.error && styles.error, - ownerState.size === 'small' && styles.sizeSmall, - ownerState.multiline && styles.multiline, - ownerState.color && styles[`color${capitalize(ownerState.color)}`], - ownerState.fullWidth && styles.fullWidth, - ownerState.hiddenLabel && styles.hiddenLabel, - ]; - }, + overridesResolver: rootOverridesResolver, })<{ ownerState: InputBaseOwnerState }>(({ theme, ownerState }) => { const { vars: tokens } = theme; @@ -129,22 +135,27 @@ export const InputBaseRoot = styled('div', { }; }); +export function inputOverridesResolver( + props: InputBaseInputSlotProps, + styles: Record, +) { + const { ownerState } = props; + + return [ + styles.input, + ownerState.size === 'small' && styles.inputSizeSmall, + ownerState.multiline && styles.inputMultiline, + ownerState.type === 'search' && styles.inputTypeSearch, + ownerState.startAdornment && styles.inputAdornedStart, + ownerState.endAdornment && styles.inputAdornedEnd, + ownerState.hiddenLabel && styles.inputHiddenLabel, + ]; +} + export const InputBaseInput = styled('input', { name: 'MuiInputBase', slot: 'Input', - overridesResolver: (props, styles) => { - const { ownerState } = props; - - return [ - styles.input, - ownerState.size === 'small' && styles.inputSizeSmall, - ownerState.multiline && styles.inputMultiline, - ownerState.type === 'search' && styles.inputTypeSearch, - ownerState.startAdornment && styles.inputAdornedStart, - ownerState.endAdornment && styles.inputAdornedEnd, - ownerState.hiddenLabel && styles.inputHiddenLabel, - ]; - }, + overridesResolver: inputOverridesResolver, })<{ ownerState: InputBaseOwnerState }>(({ theme, ownerState }) => { const { vars: tokens } = theme; @@ -291,6 +302,23 @@ const InputBase = React.forwardRef(function InputBase< ...other } = props; + if (process.env.NODE_ENV !== 'production') { + const definedMultilineProps = (['rows', 'minRows', 'maxRows'] as const).filter( + (multilineProp) => props[multilineProp] !== undefined, + ); + + if (!multiline && definedMultilineProps.length > 0) { + console.error( + [ + 'MUI: You have set multiline props on an single-line input.', + 'Set the `multiline` prop if you want to render a multi-line input.', + 'Otherwise they will be ignored.', + `Ignored props: ${definedMultilineProps.join(', ')}`, + ].join('\n'), + ); + } + } + const { current: isControlled } = React.useRef(value != null); const muiFormControl = useFormControl(); @@ -308,7 +336,7 @@ const InputBase = React.forwardRef(function InputBase< const onFilled = muiFormControl && muiFormControl.onFilled; const onEmpty = muiFormControl && muiFormControl.onEmpty; - // TODO: needs material-next/Outlined|FilledInput + // TODO: needs material-next/OutlinedInput const checkDirty = React.useCallback( (obj: any) => { if (isFilled(obj)) { @@ -535,10 +563,12 @@ interface InputBaseComponent { /** * The component used for the input node. * Either a string to use a HTML element or a component. + * @default 'input' */ inputComponent?: C; } & OverrideProps, ): JSX.Element | null; + (props: DefaultComponentProps): JSX.Element | null; propTypes?: any; } @@ -551,6 +581,16 @@ InputBase.propTypes /* remove-proptypes */ = { * @ignore */ 'aria-describedby': PropTypes.string, + /** + * Defines a string value that labels the current element. + * @see aria-labelledby. + */ + 'aria-label': PropTypes.string, + /** + * Identifies the element (or elements) that labels the current element. + * @see aria-describedby. + */ + 'aria-labelledby': PropTypes.string, /** * This prop helps users to fill forms faster, especially on mobile devices. * The name can be confusing, as it's more like an autofill. @@ -561,17 +601,33 @@ InputBase.propTypes /* remove-proptypes */ = { * If `true`, the `input` element is focused during the first mount. */ autoFocus: PropTypes.bool, + /** + * @ignore + */ + children: PropTypes.node, /** * Override or extend the styles applied to the component. */ classes: PropTypes.object, + /** + * @ignore + */ + className: PropTypes.string, /** * The color of the component. * It supports both default and custom theme colors, which can be added as shown in the * [palette customization guide](https://mui.com/material-ui/customization/palette/#custom-colors). * The prop defaults to the value (`'primary'`) inherited from the parent FormControl component. */ - color: PropTypes.oneOf(['error', 'info', 'primary', 'secondary', 'success', 'warning']), + color: PropTypes.oneOf([ + 'error', + 'info', + 'primary', + 'secondary', + 'success', + 'tertiary', + 'warning', + ]), /** * The default value. Use when the component is not controlled. */ @@ -608,6 +664,7 @@ InputBase.propTypes /* remove-proptypes */ = { /** * The component used for the input node. * Either a string to use a HTML element or a component. + * @default 'input' */ inputComponent: PropTypes.elementType, /** @@ -650,6 +707,10 @@ InputBase.propTypes /* remove-proptypes */ = { * You can pull out the new value by accessing `event.target.value` (string). */ onChange: PropTypes.func, + /** + * @ignore + */ + onClick: PropTypes.func, /** * @ignore */ @@ -726,7 +787,30 @@ InputBase.propTypes /* remove-proptypes */ = { * 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' */ - type: PropTypes.string, + type: PropTypes /* @typescript-to-proptypes-ignore */.oneOf([ + 'button', + 'checkbox', + 'color', + 'date', + 'datetime-local', + 'email', + 'file', + 'hidden', + 'image', + 'month', + 'number', + 'password', + 'radio', + 'range', + 'reset', + 'search', + 'submit', + 'tel', + 'text', + 'time', + 'url', + 'week', + ]), /** * The value of the `input` element, required for a controlled component. */ diff --git a/packages/mui-material-next/src/InputBase/InputBase.types.ts b/packages/mui-material-next/src/InputBase/InputBase.types.ts index fd4f8df7bcc570..41767dc2dc2238 100644 --- a/packages/mui-material-next/src/InputBase/InputBase.types.ts +++ b/packages/mui-material-next/src/InputBase/InputBase.types.ts @@ -12,57 +12,7 @@ export interface InputBasePropsColorOverrides {} export interface InputBaseRootSlotPropsOverrides {} export interface InputBaseInputSlotPropsOverrides {} -export interface SingleLineInputProps { - /** - * Maximum number of rows to display when multiline option is set to true. - */ - maxRows?: undefined; - /** - * Minimum number of rows to display when multiline option is set to true. - */ - minRows?: undefined; - /** - * If `true`, a `textarea` element is rendered. - * @default false - */ - multiline?: false; - /** - * Number of rows to display when multiline option is set to true. - */ - rows?: undefined; - /** - * 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' - */ - type?: React.HTMLInputTypeAttribute; -} - -export interface MultiLineInputProps { - /** - * Maximum number of rows to display when multiline option is set to true. - */ - maxRows?: number; - /** - * Minimum number of rows to display when multiline option is set to true. - */ - minRows?: number; - /** - * If `true`, a `textarea` element is rendered. - * @default false - */ - multiline: true; - /** - * Number of rows to display when multiline option is set to true. - */ - rows?: number; - /** - * 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' - */ - type?: undefined; -} - -export type InputBaseOwnProps = (SingleLineInputProps | MultiLineInputProps) & { +export type InputBaseOwnProps = { 'aria-describedby'?: string; /** * This prop helps users to fill forms faster, especially on mobile devices. @@ -85,7 +35,7 @@ export type InputBaseOwnProps = (SingleLineInputProps | MultiLineInputProps) & { * The prop defaults to the value (`'primary'`) inherited from the parent FormControl component. */ color?: OverridableStringUnion< - 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning', + 'primary' | 'secondary' | 'tertiary' | 'error' | 'info' | 'success' | 'warning', InputBasePropsColorOverrides >; /** @@ -131,11 +81,6 @@ export type InputBaseOwnProps = (SingleLineInputProps | MultiLineInputProps) & { * The prop defaults to the value (`'none'`) inherited from the parent FormControl component. */ margin?: 'dense' | 'none'; - /** - * If `true`, a [TextareaAutosize](/material-ui/react-textarea-autosize/) element is rendered. - * @default false - */ - multiline?: boolean; /** * Name attribute of the `input` element. */ @@ -183,18 +128,6 @@ export type InputBaseOwnProps = (SingleLineInputProps | MultiLineInputProps) & { required?: boolean; startAdornment?: React.ReactNode; }) => React.ReactNode; - /** - * Number of rows to display when multiline option is set to true. - */ - rows?: string | number; - /** - * Maximum number of rows to display when multiline option is set to true. - */ - maxRows?: string | number; - /** - * Minimum number of rows to display when multiline option is set to true. - */ - minRows?: string | number; /** * The size of the component. */ @@ -221,15 +154,32 @@ export type InputBaseOwnProps = (SingleLineInputProps | MultiLineInputProps) & { * The system prop that allows defining system overrides as well as additional CSS styles. */ sx?: SxProps; - /** - * 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' - */ - type?: string; /** * The value of the `input` element, required for a controlled component. */ value?: unknown; + /** + * Maximum number of rows to display when multiline option is set to true. + */ + maxRows?: number; + /** + * Minimum number of rows to display when multiline option is set to true. + */ + minRows?: number; + /** + * If `true`, a `textarea` element is rendered. + * @default false + */ + multiline?: boolean; + /** + * Number of rows to display when multiline option is set to true. + */ + rows?: number; + /** + * 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' + */ + type?: React.HTMLInputTypeAttribute; }; export interface InputBaseSlots { diff --git a/packages/mui-material/src/InputLabel/InputLabel.test.js b/packages/mui-material/src/InputLabel/InputLabel.test.js index 76a4d338e2e352..f2b2678faa73d9 100644 --- a/packages/mui-material/src/InputLabel/InputLabel.test.js +++ b/packages/mui-material/src/InputLabel/InputLabel.test.js @@ -128,7 +128,7 @@ describe('', () => { root: (props) => { return { ...(props.ownerState.focused === true && { - fontWeight: '700', + mixBlendMode: 'darken', }), }; }, @@ -140,14 +140,16 @@ describe('', () => { const { getByText } = render( - Bold Test Label + Test Label , ); - const label = getByText('Bold Test Label'); + const label = getByText('Test Label'); - expect(getComputedStyle(label).fontWeight).to.equal('700'); + expect(label).to.toHaveComputedStyle({ + mixBlendMode: 'darken', + }); }); }); });