diff --git a/packages/mui-base/src/FormControl/index.ts b/packages/mui-base/src/FormControl/index.ts index 8af0c5ba618fe1..36bffd4e36536c 100644 --- a/packages/mui-base/src/FormControl/index.ts +++ b/packages/mui-base/src/FormControl/index.ts @@ -8,6 +8,7 @@ export type { FormControlRootSlotPropsOverrides, FormControlState, UseFormControlContextReturnValue, + FormControlOwnProps, } from './FormControl.types'; export * from './formControlClasses'; diff --git a/packages/mui-material-next/migration.md b/packages/mui-material-next/migration.md index 8c0392f6968044..7ede7be08fc344 100644 --- a/packages/mui-material-next/migration.md +++ b/packages/mui-material-next/migration.md @@ -132,19 +132,23 @@ 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`. -## InputBase +## FormControl -### Removed the `inputComponent` prop +### Renamed `FormControlState` -The `inputComponent` is deprecated in favor of `slots.input`: +The `FormControlState` interface was renamed to `FormControlContextValue`: ```diff - +-import { FormControlState } from '@mui/material'; ++import { FormControlContextValue } from '@mui/material-next'; ``` +### Removed the `standard` variant + +The standard variant is no longer supported in Material You, use the `filled` or `outlined` variants instead. + +## InputBase + ### Removed `inputProps` `inputProps` are deprecated in favor of `slotProps.input`: diff --git a/packages/mui-material-next/src/FormControl/FormControl.d.ts b/packages/mui-material-next/src/FormControl/FormControl.d.ts deleted file mode 100644 index 6c23df2da656ba..00000000000000 --- a/packages/mui-material-next/src/FormControl/FormControl.d.ts +++ /dev/null @@ -1,133 +0,0 @@ -import * as React from 'react'; -import { SxProps } from '@mui/system'; -import { OverridableStringUnion, OverridableComponent, OverrideProps } from '@mui/types'; -import { Theme } from '../styles'; -import { FormControlClasses } from './formControlClasses'; - -export interface FormControlPropsSizeOverrides {} -export interface FormControlPropsColorOverrides {} - -export interface FormControlOwnProps { - /** - * The content of the component. - */ - children?: React.ReactNode; - /** - * Override or extend the styles applied to the component. - */ - classes?: Partial; - /** - * 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/#adding-new-colors). - * @default 'primary' - */ - color?: OverridableStringUnion< - 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning', - FormControlPropsColorOverrides - >; - /** - * If `true`, the label, input and helper text should be displayed in a disabled state. - * @default false - */ - disabled?: boolean; - /** - * If `true`, the label is displayed in an error state. - * @default false - */ - error?: boolean; - /** - * If `true`, the component will take up the full width of its container. - * @default false - */ - fullWidth?: boolean; - /** - * If `true`, the component is displayed in focused state. - */ - focused?: boolean; - /** - * 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 `dense` or `normal`, will adjust vertical spacing of this and contained components. - * @default 'none' - */ - margin?: 'dense' | 'normal' | 'none'; - /** - * If `true`, the label will indicate that the `input` is required. - * @default false - */ - required?: boolean; - /** - * The size of the component. - * @default 'medium' - */ - size?: OverridableStringUnion<'small' | 'medium', FormControlPropsSizeOverrides>; - /** - * The system prop that allows defining system overrides as well as additional CSS styles. - */ - sx?: SxProps; - /** - * The variant to use. - * @default 'outlined' - */ - variant?: 'standard' | 'outlined' | 'filled'; -} - -export interface FormControlTypeMap< - AdditionalProps = {}, - RootComponent extends React.ElementType = 'div', -> { - props: AdditionalProps & FormControlOwnProps; - defaultComponent: RootComponent; -} - -/** - * Provides context such as filled/focused/error/required for form inputs. - * Relying on the context provides high flexibility and ensures that the state always stays - * consistent across the children of the `FormControl`. - * This context is used by the following components: - * - * * FormLabel - * * FormHelperText - * * Input - * * InputLabel - * - * You can find one composition example below and more going to [the demos](https://mui.com/material-ui/react-text-field/#components). - * - * ```jsx - * - * Email address - * - * We'll never share your email. - * - * ``` - * - * ⚠️ Only one `InputBase` can be used within a FormControl because it creates visual inconsistencies. - * For instance, only one input can be focused at the same time, the state shouldn't be shared. - * - * Demos: - * - * - [Checkbox](https://mui.com/material-ui/react-checkbox/) - * - [Radio Group](https://mui.com/material-ui/react-radio-button/) - * - [Switch](https://mui.com/material-ui/react-switch/) - * - [Text Field](https://mui.com/material-ui/react-text-field/) - * - * API: - * - * - [FormControl API](https://mui.com/material-ui/api/form-control/) - */ -declare const FormControl: OverridableComponent; - -export type FormControlProps< - RootComponent extends React.ElementType = FormControlTypeMap['defaultComponent'], - AdditionalProps = {}, -> = OverrideProps, RootComponent> & { - component?: React.ElementType; -}; - -export default FormControl; diff --git a/packages/mui-material-next/src/FormControl/FormControl.test.js b/packages/mui-material-next/src/FormControl/FormControl.test.js index 0027766b0f3a8e..ed22b74a058d50 100644 --- a/packages/mui-material-next/src/FormControl/FormControl.test.js +++ b/packages/mui-material-next/src/FormControl/FormControl.test.js @@ -1,12 +1,12 @@ -/* eslint-disable mocha/no-skipped-tests */ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; -import { describeConformance, act, createRenderer } from '@mui-internal/test-utils'; +import { describeConformance, act, createRenderer, fireEvent } from '@mui-internal/test-utils'; import FormControl, { formControlClasses as classes } from '@mui/material-next/FormControl'; -// TODO: replace with material-next/OutlinedInput +// TODO v6: replace with material-next/FilledInput import InputBase from '@mui/material-next/InputBase'; -// TODO: replace with material-next/Select +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'; @@ -24,10 +24,19 @@ describe('', () => { describeConformance(, () => ({ classes, inheritComponent: 'div', + ThemeProvider: CssVarsProvider, + createTheme: extendTheme, render, refInstanceof: window.HTMLDivElement, - testComponentPropWith: 'fieldset', muiName: 'MuiFormControl', + slots: { + root: { + expectedClassName: classes.root, + testWithElement: 'fieldset', + }, + }, + testRootOverrides: { slotName: 'root', slotClassName: classes.root }, + testComponentPropWith: 'fieldset', testVariantProps: { margin: 'dense' }, skip: ['componentsProp'], })); @@ -85,8 +94,7 @@ describe('', () => { }); }); - // TODO: needs InputBase + FormControl integrated - describe.skip('prop: disabled', () => { + describe('prop: disabled', () => { it('will be unfocused if it gets disabled', () => { const readContext = spy(); const { container, setProps } = render( @@ -134,12 +142,73 @@ describe('', () => { }); }); - describe('input', () => { - // TODO: needs InputBase + FormControl integrated - it.skip('should be filled when a value is set', () => { + describe('registering input', () => { + it("should warn if more than one input is rendered regardless how it's nested", () => { + expect(() => { + render( + + +
+ {/* should work regardless how it's nested */} + +
+
, + ); + }).toErrorDev([ + 'MUI: There are multiple `InputBase` components inside a FormControl.\nThis creates visual inconsistencies, only use one `InputBase`.', + // React 18 Strict Effects run mount effects twice + React.version.startsWith('18') && + 'MUI: There are multiple `InputBase` components inside a FormControl.\nThis creates visual inconsistencies, only use one `InputBase`.', + ]); + }); + + it('should not warn if only one input is rendered', () => { + expect(() => { + render( + + + , + ); + }).not.toErrorDev(); + }); + + it('should not warn when toggling between inputs', () => { + // this will ensure that deregistering was called during unmount + function ToggleFormInputs() { + const [flag, setFlag] = React.useState(true); + + return ( + + {flag ? ( + + ) : ( + // TODO v6: use material-next/Select + + )} + + + ); + } + + const { getByText } = render(); + expect(() => { + fireEvent.click(getByText('toggle')); + }).not.toErrorDev(); + }); + }); + + // TODO v6: needs FilledInput + FormControl integrated + // eslint-disable-next-line mocha/no-skipped-tests + describe.skip('input', () => { + it('should be filled when a value is set', () => { const readContext = spy(); render( + {/* TODO v6: use material-next/FilledInput */} , @@ -147,11 +216,11 @@ describe('', () => { expect(readContext.args[0][0]).to.have.property('filled', true); }); - // TODO: needs InputBase + FormControl integrated - it.skip('should be filled when a value is set through inputProps', () => { + it('should be filled when a value is set through inputProps', () => { const readContext = spy(); render( + {/* TODO v6: use material-next/FilledInput */} , @@ -159,11 +228,11 @@ describe('', () => { expect(readContext.args[0][0]).to.have.property('filled', true); }); - // TODO: needs InputBase + FormControl integrated - it.skip('should be filled when a defaultValue is set', () => { + it('should be filled when a defaultValue is set', () => { const readContext = spy(); render( + {/* TODO v6: use material-next/FilledInput */} , @@ -175,6 +244,7 @@ describe('', () => { const readContext = spy(); render( + {/* TODO v6: use material-next/FilledInput */} } /> , @@ -182,11 +252,11 @@ describe('', () => { expect(readContext.args[0][0]).to.have.property('adornedStart', false); }); - // TODO: needs InputBase + FormControl integrated - it.skip('should be adornedStart with a startAdornment', () => { + it('should be adornedStart with a startAdornment', () => { const readContext = spy(); render( + {/* TODO v6: use material-next/FilledInput */} } /> , @@ -195,7 +265,8 @@ describe('', () => { }); }); - // TODO: unskip and refactor when integrating material-next/Select + // TODO v6: needs material-next/Select + FormControl integrated + // eslint-disable-next-line mocha/no-skipped-tests describe.skip('select', () => { it('should not be adorned without a startAdornment', () => { const readContext = spy(); diff --git a/packages/mui-material-next/src/FormControl/FormControl.js b/packages/mui-material-next/src/FormControl/FormControl.tsx similarity index 79% rename from packages/mui-material-next/src/FormControl/FormControl.js rename to packages/mui-material-next/src/FormControl/FormControl.tsx index b7e291351e38fb..01968d28bc303f 100644 --- a/packages/mui-material-next/src/FormControl/FormControl.js +++ b/packages/mui-material-next/src/FormControl/FormControl.tsx @@ -1,8 +1,9 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import clsx from 'clsx'; import { unstable_composeClasses as composeClasses } from '@mui/base/composeClasses'; +import { useSlotProps } from '@mui/base'; +import { OverridableComponent } from '@mui/types'; import { unstable_capitalize as capitalize, unstable_isMuiElement as isMuiElement, @@ -11,9 +12,10 @@ import useThemeProps from '../styles/useThemeProps'; import styled from '../styles/styled'; import { isFilled, isAdornedStart } from '../InputBase/utils'; import FormControlContext from './FormControlContext'; +import { FormControlTypeMap, FormControlOwnerState, FormControlProps } from './FormControl.types'; import { getFormControlUtilityClasses } from './formControlClasses'; -const useUtilityClasses = (ownerState) => { +const useUtilityClasses = (ownerState: FormControlOwnerState) => { const { classes, margin, fullWidth } = ownerState; const slots = { root: ['root', margin !== 'none' && `margin${capitalize(margin)}`, fullWidth && 'fullWidth'], @@ -26,13 +28,13 @@ const FormControlRoot = styled('div', { name: 'MuiFormControl', slot: 'Root', overridesResolver: ({ ownerState }, styles) => { - return { - ...styles.root, - ...styles[`margin${capitalize(ownerState.margin)}`], - ...(ownerState.fullWidth && styles.fullWidth), - }; + return [ + styles.root, + styles[`margin${capitalize(ownerState.margin)}`], + ownerState.fullWidth && styles.fullWidth, + ]; }, -})(({ ownerState }) => ({ +})<{ ownerState: FormControlOwnerState }>(({ ownerState }) => ({ display: 'inline-flex', flexDirection: 'column', position: 'relative', @@ -79,13 +81,15 @@ const FormControlRoot = styled('div', { * ⚠️ Only one `InputBase` can be used within a FormControl because it creates visual inconsistencies. * For instance, only one input can be focused at the same time, the state shouldn't be shared. */ -const FormControl = React.forwardRef(function FormControl(inProps, ref) { +const FormControl = React.forwardRef(function FormControl< + RootComponentType extends React.ElementType = FormControlTypeMap['defaultComponent'], +>(inProps: FormControlProps, forwardedRef: React.ForwardedRef) { const props = useThemeProps({ props: inProps, name: 'MuiFormControl' }); const { children, - className, + classes: classesProp = {}, color = 'primary', - component = 'div', + component: componentProp, disabled = false, error = false, focused: visuallyFocused, @@ -94,26 +98,12 @@ const FormControl = React.forwardRef(function FormControl(inProps, ref) { margin = 'none', required = false, size = 'medium', + slotProps = {}, + slots = {}, variant = 'outlined', ...other } = props; - const ownerState = { - ...props, - color, - component, - disabled, - error, - fullWidth, - hiddenLabel, - margin, - required, - size, - variant, - }; - - const classes = useUtilityClasses(ownerState); - const [adornedStart, setAdornedStart] = React.useState(() => { // We need to iterate through the children and find the Input in order // to fully support server-side rendering. @@ -125,7 +115,10 @@ const FormControl = React.forwardRef(function FormControl(inProps, ref) { return; } - const input = isMuiElement(child, ['Select']) ? child.props.input : child; + const input = + React.isValidElement(child) && isMuiElement(child, ['Select']) + ? child.props.input + : child; if (input && isAdornedStart(input.props)) { initialAdornedStart = true; @@ -146,7 +139,10 @@ const FormControl = React.forwardRef(function FormControl(inProps, ref) { return; } - if (isFilled(child.props, true) || isFilled(child.props.inputProps, true)) { + if ( + React.isValidElement(child) && + (isFilled(child.props, true) || isFilled(child.props.inputProps, true)) + ) { initialFilled = true; } }); @@ -162,7 +158,7 @@ const FormControl = React.forwardRef(function FormControl(inProps, ref) { const focused = visuallyFocused !== undefined && !disabled ? visuallyFocused : focusedState; - let registerEffect; + let registerEffect: undefined | (() => () => void); if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line react-hooks/rules-of-hooks const registeredInput = React.useRef(false); @@ -183,6 +179,25 @@ const FormControl = React.forwardRef(function FormControl(inProps, ref) { }; } + const ownerState = { + ...props, + classes: classesProp, + color, + component: componentProp, + disabled, + error, + filled, + focused, + fullWidth, + hiddenLabel, + margin, + required, + size, + variant, + }; + + const classes = useUtilityClasses(ownerState); + const childContext = React.useMemo(() => { return { adornedStart, @@ -226,25 +241,30 @@ const FormControl = React.forwardRef(function FormControl(inProps, ref) { variant, ]); + const Root = slots.root ?? FormControlRoot; + const rootProps = useSlotProps({ + elementType: Root, + externalSlotProps: slotProps.root, + externalForwardedProps: other, + additionalProps: { + ref: forwardedRef, + as: componentProp, + }, + ownerState, + className: classes.root, + }); + return ( - - {children} - + {children} ); -}); +}) as OverridableComponent; FormControl.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" | // ---------------------------------------------------------------------- /** * The content of the component. @@ -254,10 +274,6 @@ FormControl.propTypes /* remove-proptypes */ = { * 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 @@ -317,6 +333,21 @@ FormControl.propTypes /* remove-proptypes */ = { PropTypes.oneOf(['medium', 'small']), PropTypes.string, ]), + /** + * The props used for each slot inside the FormControl. + * @default {} + */ + slotProps: PropTypes.shape({ + root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + }), + /** + * The components used for each slot inside the FormControl. + * Either a string to use a HTML element or a component. + * @default {} + */ + slots: PropTypes.shape({ + root: PropTypes.elementType, + }), /** * The system prop that allows defining system overrides as well as additional CSS styles. */ @@ -329,7 +360,7 @@ FormControl.propTypes /* remove-proptypes */ = { * The variant to use. * @default 'outlined' */ - variant: PropTypes.oneOf(['filled', 'outlined', 'standard']), -}; + variant: PropTypes.oneOf(['filled', 'outlined']), +} as any; export default FormControl; diff --git a/packages/mui-material-next/src/FormControl/FormControl.types.ts b/packages/mui-material-next/src/FormControl/FormControl.types.ts new file mode 100644 index 00000000000000..9c7b27a5df8b03 --- /dev/null +++ b/packages/mui-material-next/src/FormControl/FormControl.types.ts @@ -0,0 +1,93 @@ +import * as React from 'react'; +import { SxProps } from '@mui/system'; +import { FormControlOwnProps as BaseFormControlOwnProps } from '@mui/base/FormControl'; +import { OverridableStringUnion, OverrideProps, Simplify } from '@mui/types'; +import { Theme } from '../styles'; +import { FormControlClasses } from './formControlClasses'; + +export interface FormControlPropsSizeOverrides {} +export interface FormControlPropsColorOverrides {} + +export interface FormControlOwnProps extends BaseFormControlOwnProps { + /** + * The content of the component. + */ + children?: React.ReactNode; + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + /** + * 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/#adding-new-colors). + * @default 'primary' + */ + color?: OverridableStringUnion< + 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning', + FormControlPropsColorOverrides + >; + /** + * If `true`, the component will take up the full width of its container. + * @default false + */ + fullWidth?: boolean; + /** + * If `true`, the component is displayed in focused state. + */ + focused?: boolean; + /** + * 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 `dense` or `normal`, will adjust vertical spacing of this and contained components. + * @default 'none' + */ + margin?: 'dense' | 'normal' | 'none'; + /** + * The size of the component. + * @default 'medium' + */ + size?: OverridableStringUnion<'small' | 'medium', FormControlPropsSizeOverrides>; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; + /** + * The variant to use. + * @default 'outlined' + */ + variant?: 'outlined' | 'filled'; +} + +export interface FormControlTypeMap< + AdditionalProps = {}, + RootComponentType extends React.ElementType = 'div', +> { + props: FormControlOwnProps & AdditionalProps; + defaultComponent: RootComponentType; +} + +export type FormControlProps< + RootComponentType extends React.ElementType = FormControlTypeMap['defaultComponent'], + AdditionalProps = {}, +> = OverrideProps, RootComponentType> & { + component?: React.ElementType; +}; + +type MaterialDesignOwnerStateKeys = + | 'classes' + | 'color' + | 'margin' + | 'size' + | 'fullWidth' + | 'hiddenLabel' + | 'variant'; + +export type FormControlOwnerState = Simplify< + Required> & FormControlProps +>; diff --git a/packages/mui-material-next/src/FormControl/FormControlContext.ts b/packages/mui-material-next/src/FormControl/FormControlContext.ts index 606057104b1457..16cb4e9c83ca55 100644 --- a/packages/mui-material-next/src/FormControl/FormControlContext.ts +++ b/packages/mui-material-next/src/FormControl/FormControlContext.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import type { FormControlProps } from './FormControl'; +import { FormControlProps } from './FormControl.types'; type ContextFromPropsKey = | 'color' @@ -8,26 +8,26 @@ type ContextFromPropsKey = | 'fullWidth' | 'hiddenLabel' | 'margin' - | 'onBlur' - | 'onFocus' | 'required' | 'size' | 'variant'; -export interface FormControlState extends Pick { +export interface FormControlContextValue extends Pick { adornedStart: boolean; filled: boolean; focused: boolean; + onBlur: (event?: React.FocusEvent) => void; + onFocus: (event: React.FocusEvent) => void; onEmpty: () => void; onFilled: () => void; - registerEffect: () => void; + registerEffect: undefined | (() => () => void); setAdornedStart: React.Dispatch>; } /** - * @ignore - internal component. + * @internal */ -const FormControlContext = React.createContext(undefined); +const FormControlContext = React.createContext(undefined); if (process.env.NODE_ENV !== 'production') { FormControlContext.displayName = 'FormControlContext'; diff --git a/packages/mui-material-next/src/FormControl/formControlState.js b/packages/mui-material-next/src/FormControl/formControlState.js index eb46904ff6cc0a..86c999f26e24c6 100644 --- a/packages/mui-material-next/src/FormControl/formControlState.js +++ b/packages/mui-material-next/src/FormControl/formControlState.js @@ -1,4 +1,6 @@ +// TODO v6: decide whether to update/refactor this, keep as-is, or drop it export default function formControlState({ props, states, muiFormControl }) { + // for every prop in `states` that is undefined, set it with the value from formControlContext return states.reduce((acc, state) => { acc[state] = props[state]; diff --git a/packages/mui-material-next/src/FormControl/index.d.ts b/packages/mui-material-next/src/FormControl/index.d.ts deleted file mode 100644 index 7eaec9a8312087..00000000000000 --- a/packages/mui-material-next/src/FormControl/index.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { default } from './FormControl'; -export * from './FormControl'; - -export { default as useFormControl } from './useFormControl'; - -export { FormControlState } from './FormControlContext'; - -export { default as formControlClasses } from './formControlClasses'; -export * from './formControlClasses'; diff --git a/packages/mui-material-next/src/FormControl/index.js b/packages/mui-material-next/src/FormControl/index.ts similarity index 100% rename from packages/mui-material-next/src/FormControl/index.js rename to packages/mui-material-next/src/FormControl/index.ts diff --git a/packages/mui-material-next/src/FormControl/useFormControl.ts b/packages/mui-material-next/src/FormControl/useFormControl.ts index cf24c6cad1ffe8..8cf6ac69fbce7f 100644 --- a/packages/mui-material-next/src/FormControl/useFormControl.ts +++ b/packages/mui-material-next/src/FormControl/useFormControl.ts @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; -import FormControlContext, { FormControlState } from './FormControlContext'; +import FormControlContext, { FormControlContextValue } from './FormControlContext'; -export default function useFormControl(): FormControlState | undefined { +export default function useFormControl(): FormControlContextValue | undefined { return React.useContext(FormControlContext); } diff --git a/packages/mui-material-next/src/InputBase/InputBase.test.js b/packages/mui-material-next/src/InputBase/InputBase.test.js index 3774b529c76751..96eb55612ed2ca 100644 --- a/packages/mui-material-next/src/InputBase/InputBase.test.js +++ b/packages/mui-material-next/src/InputBase/InputBase.test.js @@ -1,4 +1,3 @@ -/* eslint-disable mocha/no-skipped-tests */ import * as React from 'react'; import PropTypes from 'prop-types'; import { expect } from 'chai'; @@ -10,11 +9,12 @@ import { fireEvent, screen, } from '@mui-internal/test-utils'; -import { ThemeProvider } from '@emotion/react'; -import FormControl, { useFormControl } from '@mui/material/FormControl'; +import FormControl, { useFormControl } from '@mui/material-next/FormControl'; +// TODO v6: replace with material-next/InputAdornment import InputAdornment from '@mui/material/InputAdornment'; +// TODO v6: replace with material-next/TextField import TextField from '@mui/material/TextField'; -import { createTheme } from '@mui/material/styles'; +// TODO v6: replace with material-next/Select import Select from '@mui/material/Select'; import InputBase, { inputBaseClasses as classes } from '@mui/material-next/InputBase'; import { CssVarsProvider, extendTheme } from '@mui/material-next/styles'; @@ -112,7 +112,7 @@ describe('', () => { setProps({ disabled: true }); expect(handleBlur.callCount).to.equal(1); - // // check if focus not initiated again + // check if focus not initiated again expect(handleFocus.callCount).to.equal(1); }); }); @@ -144,8 +144,8 @@ describe('', () => { ); const input = getByRole('textbox'); + // TODO v6: refactor this test with @testing-library/user-event // simulating user input: gain focus, key input (keydown, (input), change, keyup), blur - act(() => { input.focus(); }); @@ -221,11 +221,15 @@ describe('', () => { }); }); - describe('prop: slots', () => { - // TODO: delete, covered by describeConformance - xit('should accept any html component', () => { + // for InputBase, the `component` prop is called `inputComponent` so it's skipped + // in describeConformance and manually tested here + describe('prop: inputComponent', () => { + it('should accept any html component', () => { const { getByTestId } = render( - , + , ); expect(getByTestId('input-component')).to.have.property('nodeName', 'SPAN'); }); @@ -238,14 +242,13 @@ describe('', () => { return ; }); - render(); + render(); expect(typeof injectedProps.onBlur).to.equal('function'); expect(typeof injectedProps.onFocus).to.equal('function'); }); - // TODO: requires material-next/FormControl - describe.skip('target mock implementations', () => { + describe('target mock implementations', () => { it('can just mock the value', () => { const MockedValue = React.forwardRef(function MockedValue(props, ref) { const { onChange } = props; @@ -277,7 +280,8 @@ describe('', () => { it("can expose the input component's ref through the inputComponent prop", () => { const FullTarget = React.forwardRef(function FullTarget(props, ref) { - return ; + const { ownerState, ...otherProps } = props; + return ; }); function FilledState(props) { @@ -299,9 +303,7 @@ describe('', () => { }); }); - // TODO: unskip and refactor when integrating material-next/FormControl - - describe.skip('with FormControl', () => { + describe('with FormControl', () => { it('should have the formControl class', () => { const { getByTestId } = render( @@ -636,7 +638,7 @@ describe('', () => { expect(getByTestId('adornment')).not.to.equal(null); }); - // TODO: use material-next/Select + // TODO v6: use material-next/Select it('should allow a Select as an adornment', () => { render( ', () => { }); describe('prop: focused', () => { - it.skip('should render correct border color with `ThemeProvider` imported from `@emotion/react`', function test() { + // TODO v6: requires material-next/OutlinedInput + // 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)) { this.skip(); } - const theme = createTheme({ - palette: { - mode: 'light', - primary: { - main: 'rgb(0, 191, 165)', + const theme = extendTheme({ + colorSchemes: { + light: { + palette: { + primary: { + main: 'rgb(0, 191, 165)', + }, + }, }, }, }); const { getByRole } = render( - + + {/* TODO v6: use material-next/TextField or OutlinedInput */} - , + , ); + + // this `fieldset` is the (internal) NotchedOutline component const fieldset = getByRole('textbox').nextSibling; expect(fieldset).toHaveComputedStyle({ borderTopColor: 'rgb(0, 191, 165)', diff --git a/packages/mui-material-next/src/InputBase/InputBase.tsx b/packages/mui-material-next/src/InputBase/InputBase.tsx index 3b162a71ebe9f6..02d5b103cfa521 100644 --- a/packages/mui-material-next/src/InputBase/InputBase.tsx +++ b/packages/mui-material-next/src/InputBase/InputBase.tsx @@ -16,10 +16,9 @@ import { unstable_capitalize as capitalize, unstable_useEnhancedEffect as useEnhancedEffect, } from '@mui/utils'; -import { OverridableComponent } from '@mui/types'; -// import formControlState from '@mui/material/FormControl/formControlState'; -import FormControlContext from '@mui/material/FormControl/FormControlContext'; -import useFormControl from '@mui/material/FormControl/useFormControl'; +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'; import useThemeProps from '../styles/useThemeProps'; import { isFilled } from './utils'; @@ -262,9 +261,10 @@ const InputBase = React.forwardRef(function InputBase< disabled: disabledProp, disableInjectingGlobalStyles, endAdornment, - error, + error: errorProp, fullWidth = false, id, + inputComponent: inputComponentProp = 'input', inputRef: inputRefProp, margin, maxRows, @@ -280,7 +280,7 @@ const InputBase = React.forwardRef(function InputBase< placeholder, readOnly, renderSuffix, - required, + required: requiredProp, rows, size: sizeProp, slotProps = {}, @@ -293,31 +293,22 @@ const InputBase = React.forwardRef(function InputBase< const { current: isControlled } = React.useRef(value != null); - // TODO: integrate material-next/FormControl const muiFormControl = useFormControl(); - /* if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line react-hooks/rules-of-hooks React.useEffect(() => { - if (muiFormControl) { + if (muiFormControl && muiFormControl.registerEffect) { return muiFormControl.registerEffect(); } - return undefined; + return undefined; }, [muiFormControl]); } - const fcs = formControlState({ - props, - muiFormControl, - states: ['color', 'disabled', 'error', 'hiddenLabel', 'size', 'required', 'filled'], - }); - fcs.focused = muiFormControl ? muiFormControl.focused : focused; - */ const onFilled = muiFormControl && muiFormControl.onFilled; const onEmpty = muiFormControl && muiFormControl.onEmpty; - // TODO: needs material-next/FormControl & material-next/Outlined|FilledInput + // TODO: needs material-next/Outlined|FilledInput const checkDirty = React.useCallback( (obj: any) => { if (isFilled(obj)) { @@ -342,9 +333,9 @@ const InputBase = React.forwardRef(function InputBase< onFocus(event); } - // if (muiFormControl && muiFormControl.onFocus) { - // muiFormControl.onFocus(event); - // } + if (muiFormControl && muiFormControl.onFocus) { + muiFormControl.onFocus(event); + } }; const handleBlur = (event?: React.FocusEvent) => { @@ -352,9 +343,9 @@ const InputBase = React.forwardRef(function InputBase< onBlur(event); } - // if (muiFormControl && muiFormControl.onBlur) { - // muiFormControl.onBlur(event); - // } + if (muiFormControl && muiFormControl.onBlur) { + muiFormControl.onBlur(event); + } }; const handleChange = ( @@ -375,7 +366,7 @@ const InputBase = React.forwardRef(function InputBase< }; const handleClick = (event: React.PointerEvent) => { - if (onClick /* && !fcs.disabled */) { + if (onClick) { onClick(event); } }; @@ -386,19 +377,20 @@ const InputBase = React.forwardRef(function InputBase< } }, [muiFormControl, startAdornment]); + const required = requiredProp ?? muiFormControl?.required; + const { getRootProps, getInputProps, focused: focusedState, - // TODO: what if this `formControlContext` is completely ignored and the component uses a completely separate one similar to Joy - // formControlContext, error: errorState, disabled: disabledState, inputRef, + // ignore Base UI's formControlContext } = useInput({ - disabled: disabledProp, + disabled: disabledProp ?? muiFormControl?.disabled, defaultValue, - error, + error: errorProp ?? muiFormControl?.error, onBlur: handleBlur, onClick: handleClick, onChange: handleChange, @@ -408,19 +400,19 @@ const InputBase = React.forwardRef(function InputBase< inputRef: inputRefProp, }); - // TODO: integrate ownerState properties with material-next/FormControl: color, disabled, error, focused, hiddenLabel, size const ownerState = { ...props, - color: colorProp || 'primary', + color: colorProp ?? muiFormControl?.color ?? 'primary', disabled: disabledState, endAdornment, error: errorState, - focused: focusedState, + focused: muiFormControl?.focused ?? focusedState, formControl: muiFormControl, fullWidth, - hiddenLabel: false, // TODO: material-next/FormControl integration + hiddenLabel: muiFormControl?.hiddenLabel ?? false, multiline, - size: sizeProp, + required, + size: sizeProp ?? muiFormControl?.size, startAdornment, type, }; @@ -442,6 +434,11 @@ const InputBase = React.forwardRef(function InputBase< type, }; + let InputComponent = inputComponentProp; + if (multiline && InputComponent === 'input') { + InputComponent = TextareaAutosize; + } + const Root = slots.root || InputBaseRoot; const rootProps: WithOptionalOwnerState = useSlotProps({ elementType: Root, @@ -455,15 +452,13 @@ const InputBase = React.forwardRef(function InputBase< className: [classes.root, className], }); - const InputComponent = multiline - ? slots.textarea ?? TextareaAutosize - : slots.input ?? InputBaseInput; + const Input = multiline ? slots.textarea ?? TextareaAutosize : slots.input ?? InputBaseInput; const inputProps: WithOptionalOwnerState = useSlotProps({ // TextareaAutosize doesn't support ownerState, we manually change the // elementType so ownerState is excluded from the return value (this doesn't // affect other returned props) - elementType: InputComponent === TextareaAutosize ? 'textarea' : InputComponent, + elementType: Input === TextareaAutosize ? 'textarea' : Input, getSlotProps: (otherHandlers: EventHandlers) => { return getInputProps({ ...propsToForwardToInputSlot, @@ -472,9 +467,10 @@ const InputBase = React.forwardRef(function InputBase< }, externalSlotProps: slotProps.input, additionalProps: { + as: InputComponent, rows: multiline ? rows : undefined, ...(multiline && - !isHostComponent(InputComponent) && { + !isHostComponent(Input) && { minRows: rows || minRows, maxRows: rows || maxRows, }), @@ -513,19 +509,38 @@ const InputBase = React.forwardRef(function InputBase< {startAdornment} - + {endAdornment} {renderSuffix ? renderSuffix({ - // ...fcs, + // TODO: requires integrating with OutlinedInput + // ...formControlState({ + // props, + // muiFormControl, + // states: ['color', 'disabled', 'error', 'hiddenLabel', 'size', 'required', 'filled'] + // }), + ...muiFormControl, startAdornment, }) : null} ); -}) as OverridableComponent; +}) as InputBaseComponent; + +interface InputBaseComponent { + ( + props: { + /** + * The component used for the input node. + * Either a string to use a HTML element or a component. + */ + inputComponent?: C; + } & OverrideProps, + ): JSX.Element | null; + propTypes?: any; +} InputBase.propTypes /* remove-proptypes */ = { // ----------------------------- Warning -------------------------------- @@ -536,16 +551,6 @@ 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. @@ -556,18 +561,10 @@ 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 @@ -608,6 +605,11 @@ InputBase.propTypes /* remove-proptypes */ = { * The id of the `input` element. */ id: PropTypes.string, + /** + * The component used for the input node. + * Either a string to use a HTML element or a component. + */ + inputComponent: PropTypes.elementType, /** * Pass a ref to the `input` element. */ @@ -648,10 +650,6 @@ 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 */ diff --git a/packages/mui-material-next/src/InputBase/InputBase.types.ts b/packages/mui-material-next/src/InputBase/InputBase.types.ts index 50880175c1e740..086ee8a62846f0 100644 --- a/packages/mui-material-next/src/InputBase/InputBase.types.ts +++ b/packages/mui-material-next/src/InputBase/InputBase.types.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import { SlotComponentProps } from '@mui/base'; -import { FormControlState } from '@mui/material/FormControl'; +import { FormControlContextValue } from '@mui/material-next/FormControl/FormControlContext'; import { UseInputRootSlotProps } from '@mui/base/useInput'; import { SxProps } from '@mui/system'; import { OverridableStringUnion, OverrideProps, Simplify } from '@mui/types'; @@ -261,11 +261,13 @@ export interface InputBaseTypeMap< export type InputBaseProps< RootComponentType extends React.ElementType = InputBaseTypeMap['defaultComponent'], AdditionalProps = {}, -> = OverrideProps, RootComponentType>; +> = OverrideProps, RootComponentType> & { + inputComponent?: React.ElementType; +}; export type InputBaseOwnerState = Simplify< InputBaseOwnProps & { - formControl: FormControlState | undefined; + formControl: FormControlContextValue | undefined; hiddenLabel?: boolean; focused: boolean; type: React.InputHTMLAttributes['type'] | undefined;