From 3ea0ebf028ce685cab7e9a6f9a9afbe7ada9dd76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Tue, 21 Jun 2022 11:14:32 +0200 Subject: [PATCH 1/2] [FormControlUnstyled] Accept callbacks in componentsProps (#33180) --- .../pages/base/api/form-control-unstyled.json | 1 - .../form-control-unstyled.json | 1 - .../FormControlUnstyled.spec.tsx | 8 +++ .../FormControlUnstyled.test.tsx | 1 - .../FormControlUnstyled.tsx | 61 +++++++++++-------- .../FormControlUnstyled.types.ts | 14 ++++- .../formControlUnstyledClasses.ts | 2 +- 7 files changed, 57 insertions(+), 31 deletions(-) diff --git a/docs/pages/base/api/form-control-unstyled.json b/docs/pages/base/api/form-control-unstyled.json index 5d77d6b2aa9ac7..dfa47ef716b696 100644 --- a/docs/pages/base/api/form-control-unstyled.json +++ b/docs/pages/base/api/form-control-unstyled.json @@ -1,7 +1,6 @@ { "props": { "children": { "type": { "name": "union", "description": "node
| func" } }, - "className": { "type": { "name": "string" } }, "component": { "type": { "name": "elementType" } }, "components": { "type": { "name": "shape", "description": "{ Root?: elementType }" }, diff --git a/docs/translations/api-docs/form-control-unstyled/form-control-unstyled.json b/docs/translations/api-docs/form-control-unstyled/form-control-unstyled.json index a46d7219137358..d8fa4c79da1fd0 100644 --- a/docs/translations/api-docs/form-control-unstyled/form-control-unstyled.json +++ b/docs/translations/api-docs/form-control-unstyled/form-control-unstyled.json @@ -2,7 +2,6 @@ "componentDescription": "Provides context such as filled/focused/error/required for form inputs.\nRelying on the context provides high flexibility and ensures that the state always stays\nconsistent across the children of the `FormControl`.\nThis context is used by the following components:\n\n* FormLabel\n* FormHelperText\n* Input\n* InputLabel\n\nYou can find one composition example below and more going to [the demos](https://mui.com/material-ui/react-text-field/#components).\n\n```jsx\n\n Email address\n \n We'll never share your email.\n\n```\n\n⚠️ Only one `Input` can be used within a FormControl because it create visual inconsistencies.\nFor instance, only one input can be focused at the same time, the state shouldn't be shared.", "propDescriptions": { "children": "The content of the component.", - "className": "Class name applied to the root element.", "component": "The component used for the root node. Either a string to use a HTML element or a component.", "components": "The components used for each slot inside the FormControl. Either a string to use a HTML element or a component.", "disabled": "If true, the label, input and helper text should be displayed in a disabled state.", diff --git a/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.spec.tsx b/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.spec.tsx index 870f372275f245..6c46361dae0147 100644 --- a/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.spec.tsx +++ b/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.spec.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import FormControlUnstyled from '@mui/base/FormControlUnstyled'; import { expectType } from '@mui/types'; +import { FormControlUnstyledRootSlotProps } from './FormControlUnstyled.types'; const CustomComponent: React.FC<{ stringProp: string; numberProp: number }> = () =>
; @@ -33,3 +34,10 @@ const FormControlUnstyledTest = () => ( />
); + +function Root(props: FormControlUnstyledRootSlotProps) { + const { ownerState, ...other } = props; + return
; +} + +const StyledFormControl = ; diff --git a/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.test.tsx b/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.test.tsx index 08f4a9f0163ab1..82cb07b03690c2 100644 --- a/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.test.tsx +++ b/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.test.tsx @@ -23,7 +23,6 @@ describe('', () => { expectedClassName: formControlUnstyledClasses.root, }, }, - skip: ['componentsPropsCallbacks'], // not implemented yet })); describe('initial state', () => { diff --git a/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.tsx b/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.tsx index 0641ad0bbd439f..d8a19bd1e356e2 100644 --- a/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.tsx +++ b/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.tsx @@ -1,23 +1,41 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import clsx from 'clsx'; import { OverridableComponent } from '@mui/types'; import { unstable_useControlled as useControlled } from '@mui/utils'; import FormControlUnstyledContext from './FormControlUnstyledContext'; -import appendOwnerState from '../utils/appendOwnerState'; -import classes from './formControlUnstyledClasses'; +import { getFormControlUnstyledUtilityClass } from './formControlUnstyledClasses'; import { FormControlUnstyledProps, NativeFormControlElement, FormControlUnstyledTypeMap, FormControlUnstyledOwnerState, FormControlUnstyledState, + FormControlUnstyledRootSlotProps, } from './FormControlUnstyled.types'; +import { useSlotProps, WithOptionalOwnerState } from '../utils'; +import composeClasses from '../composeClasses'; function hasValue(value: unknown) { return value != null && !(Array.isArray(value) && value.length === 0) && value !== ''; } +function useUtilityClasses(ownerState: FormControlUnstyledOwnerState) { + const { disabled, error, filled, focused, required } = ownerState; + + const slots = { + root: [ + 'root', + disabled && 'disabled', + focused && 'focused', + error && 'error', + filled && 'filled', + required && 'required', + ], + }; + + return composeClasses(slots, getFormControlUnstyledUtilityClass, {}); +} + /** * 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 @@ -56,7 +74,6 @@ const FormControlUnstyled = React.forwardRef(function FormControlUnstyled< const { defaultValue, children, - className, component, components = {}, componentsProps = {}, @@ -112,8 +129,19 @@ const FormControlUnstyled = React.forwardRef(function FormControlUnstyled< value: value ?? '', }; + const classes = useUtilityClasses(ownerState); + const Root = component ?? components.Root ?? 'div'; - const rootProps = appendOwnerState(Root, { ...other, ...componentsProps.root }, ownerState); + const rootProps: WithOptionalOwnerState = useSlotProps({ + elementType: Root, + externalSlotProps: componentsProps.root, + externalForwardedProps: other, + additionalProps: { + ref, + }, + ownerState, + className: classes.root, + }); const renderChildren = () => { if (typeof children === 'function') { @@ -125,22 +153,7 @@ const FormControlUnstyled = React.forwardRef(function FormControlUnstyled< return ( - - {renderChildren()} - + {renderChildren()} ); }) as OverridableComponent; @@ -157,10 +170,6 @@ FormControlUnstyled.propTypes /* remove-proptypes */ = { PropTypes.node, PropTypes.func, ]), - /** - * Class name applied to the root element. - */ - className: PropTypes.string, /** * The component used for the root node. * Either a string to use a HTML element or a component. @@ -178,7 +187,7 @@ FormControlUnstyled.propTypes /* remove-proptypes */ = { * @ignore */ componentsProps: PropTypes.shape({ - root: PropTypes.object, + root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), }), /** * @ignore diff --git a/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.types.ts b/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.types.ts index 3ebf6d3633ddeb..88490b464745a6 100644 --- a/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.types.ts +++ b/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.types.ts @@ -1,5 +1,6 @@ import * as React from 'react'; import { OverrideProps, Simplify } from '@mui/types'; +import { SlotComponentProps } from '../utils'; export type NativeFormControlElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement; @@ -23,7 +24,11 @@ export interface FormControlUnstyledOwnProps { Root?: React.ElementType; }; componentsProps?: { - root?: React.HTMLAttributes & FormControlUnstyledComponentsPropsOverrides; + root?: SlotComponentProps< + 'div', + FormControlUnstyledComponentsPropsOverrides, + FormControlUnstyledOwnerState + >; }; defaultValue?: unknown; /** @@ -68,6 +73,7 @@ export type FormControlUnstyledOwnerState = Simplify< Omit & Required> & { filled: boolean; + focused: boolean; } >; @@ -81,3 +87,9 @@ export type FormControlUnstyledState = Simplify< onFocus: () => void; } >; + +export type FormControlUnstyledRootSlotProps = { + children: React.ReactNode | ((state: FormControlUnstyledState) => React.ReactNode); + className?: string; + ownerState: FormControlUnstyledOwnerState; +}; diff --git a/packages/mui-base/src/FormControlUnstyled/formControlUnstyledClasses.ts b/packages/mui-base/src/FormControlUnstyled/formControlUnstyledClasses.ts index 5854f8a7f390ea..74353b4655e1a1 100644 --- a/packages/mui-base/src/FormControlUnstyled/formControlUnstyledClasses.ts +++ b/packages/mui-base/src/FormControlUnstyled/formControlUnstyledClasses.ts @@ -18,7 +18,7 @@ export interface FormControlUnstyledClasses { export type FormControlUnstyledClassKey = keyof FormControlUnstyledClasses; -export function getFormControlUnstyledUtilityClasses(slot: string): string { +export function getFormControlUnstyledUtilityClass(slot: string): string { return generateUtilityClass('BaseFormControl', slot); } From dba967d2e24475711b5fe870bc9d8083a1485252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Tue, 21 Jun 2022 11:16:55 +0200 Subject: [PATCH 2/2] [SwitchUnstyled] Use useSlotProps (#33174) --- docs/pages/base/api/switch-unstyled.json | 1 - .../switch-unstyled/switch-unstyled.json | 1 - .../src/ButtonUnstyled/ButtonUnstyled.tsx | 7 +- .../src/MenuItemUnstyled/MenuItemUnstyled.tsx | 7 +- .../src/MenuUnstyled/MenuUnstyled.tsx | 8 +- .../src/SwitchUnstyled/SwitchUnstyled.tsx | 94 ++++++++++--------- 6 files changed, 52 insertions(+), 66 deletions(-) diff --git a/docs/pages/base/api/switch-unstyled.json b/docs/pages/base/api/switch-unstyled.json index 5257f4e064640c..9718539a58fd6e 100644 --- a/docs/pages/base/api/switch-unstyled.json +++ b/docs/pages/base/api/switch-unstyled.json @@ -1,7 +1,6 @@ { "props": { "checked": { "type": { "name": "bool" } }, - "className": { "type": { "name": "string" } }, "component": { "type": { "name": "elementType" } }, "components": { "type": { diff --git a/docs/translations/api-docs/switch-unstyled/switch-unstyled.json b/docs/translations/api-docs/switch-unstyled/switch-unstyled.json index d575b536683c4d..0d5b850920c7a1 100644 --- a/docs/translations/api-docs/switch-unstyled/switch-unstyled.json +++ b/docs/translations/api-docs/switch-unstyled/switch-unstyled.json @@ -2,7 +2,6 @@ "componentDescription": "The foundation for building custom-styled switches.", "propDescriptions": { "checked": "If true, the component is checked.", - "className": "Class name applied to the root element.", "component": "The component used for the Root slot. Either a string to use a HTML element or a component. This is equivalent to components.Root. If both are provided, the component is used.", "components": "The components used for each slot inside the Switch. Either a string to use a HTML element or a component.", "componentsProps": "The props used for each slot inside the Switch.", diff --git a/packages/mui-base/src/ButtonUnstyled/ButtonUnstyled.tsx b/packages/mui-base/src/ButtonUnstyled/ButtonUnstyled.tsx index 3c40c2aefcd0a6..195fa80881c5bc 100644 --- a/packages/mui-base/src/ButtonUnstyled/ButtonUnstyled.tsx +++ b/packages/mui-base/src/ButtonUnstyled/ButtonUnstyled.tsx @@ -43,7 +43,6 @@ const ButtonUnstyled = React.forwardRef(function ButtonUnstyled< const { action, children, - className, component, components = {}, componentsProps = {}, @@ -96,7 +95,7 @@ const ButtonUnstyled = React.forwardRef(function ButtonUnstyled< ref: forwardedRef, }, ownerState, - className: [classes.root, className], + className: classes.root, }); return {children}; @@ -122,10 +121,6 @@ ButtonUnstyled.propTypes /* remove-proptypes */ = { * @ignore */ children: PropTypes.node, - /** - * @ignore - */ - className: PropTypes.string, /** * The component used for the Root slot. * Either a string to use a HTML element or a component. diff --git a/packages/mui-base/src/MenuItemUnstyled/MenuItemUnstyled.tsx b/packages/mui-base/src/MenuItemUnstyled/MenuItemUnstyled.tsx index 86ff0e350bd6b1..9490ab9d104c45 100644 --- a/packages/mui-base/src/MenuItemUnstyled/MenuItemUnstyled.tsx +++ b/packages/mui-base/src/MenuItemUnstyled/MenuItemUnstyled.tsx @@ -32,7 +32,6 @@ const MenuItemUnstyled = React.forwardRef(function MenuItemUnstyled( ) { const { children, - className, disabled: disabledProp = false, component, components = {}, @@ -57,7 +56,7 @@ const MenuItemUnstyled = React.forwardRef(function MenuItemUnstyled( getSlotProps: getRootProps, externalSlotProps: componentsProps.root, externalForwardedProps: other, - className: [classes.root, className], + className: classes.root, ownerState, }); @@ -73,10 +72,6 @@ MenuItemUnstyled.propTypes /* remove-proptypes */ = { * @ignore */ children: PropTypes.node, - /** - * @ignore - */ - className: PropTypes.string, /** * @ignore */ diff --git a/packages/mui-base/src/MenuUnstyled/MenuUnstyled.tsx b/packages/mui-base/src/MenuUnstyled/MenuUnstyled.tsx index fa76f470892809..c52b1ff8bcbc93 100644 --- a/packages/mui-base/src/MenuUnstyled/MenuUnstyled.tsx +++ b/packages/mui-base/src/MenuUnstyled/MenuUnstyled.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import clsx from 'clsx'; import { HTMLElementType, refType } from '@mui/utils'; import MenuUnstyledContext, { MenuUnstyledContextType } from './MenuUnstyledContext'; import { @@ -41,7 +40,6 @@ const MenuUnstyled = React.forwardRef(function MenuUnstyled( actions, anchorEl, children, - className, component, components = {}, componentsProps = {}, @@ -94,7 +92,7 @@ const MenuUnstyled = React.forwardRef(function MenuUnstyled( role: undefined, ref: forwardedRef, }, - className: clsx(classes.root, className), + className: classes.root, ownerState, }) as MenuUnstyledRootSlotProps; @@ -148,10 +146,6 @@ MenuUnstyled.propTypes /* remove-proptypes */ = { * @ignore */ children: PropTypes.node, - /** - * @ignore - */ - className: PropTypes.string, /** * @ignore */ diff --git a/packages/mui-base/src/SwitchUnstyled/SwitchUnstyled.tsx b/packages/mui-base/src/SwitchUnstyled/SwitchUnstyled.tsx index 0cab4c7a321ebc..86a62daafd8e78 100644 --- a/packages/mui-base/src/SwitchUnstyled/SwitchUnstyled.tsx +++ b/packages/mui-base/src/SwitchUnstyled/SwitchUnstyled.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import clsx from 'clsx'; +import composeClasses from '../composeClasses'; import useSwitch from './useSwitch'; -import classes from './switchUnstyledClasses'; -import appendOwnerState from '../utils/appendOwnerState'; +import { getSwitchUnstyledUtilityClass } from './switchUnstyledClasses'; import { SwitchUnstyledProps, SwitchUnstyledOwnerState, @@ -12,8 +11,26 @@ import { SwitchUnstyledThumbSlotProps, SwitchUnstyledTrackSlotProps, } from './SwitchUnstyled.types'; -import { WithOptionalOwnerState } from '../utils'; -import resolveComponentProps from '../utils/resolveComponentProps'; +import { useSlotProps, WithOptionalOwnerState } from '../utils'; + +const useUtilityClasses = (ownerState: SwitchUnstyledOwnerState) => { + const { checked, disabled, focusVisible, readOnly } = ownerState; + + const slots = { + root: [ + 'root', + checked && 'checked', + disabled && 'disabled', + focusVisible && 'focusVisible', + readOnly && 'readOnly', + ], + thumb: ['thumb'], + input: ['input'], + track: ['track'], + }; + + return composeClasses(slots, getSwitchUnstyledUtilityClass, {}); +}; /** * The foundation for building custom-styled switches. @@ -32,7 +49,6 @@ const SwitchUnstyled = React.forwardRef(function SwitchUnstyled( ) { const { checked: checkedProp, - className, component, components = {}, componentsProps = {}, @@ -44,7 +60,7 @@ const SwitchUnstyled = React.forwardRef(function SwitchUnstyled( onFocusVisible, readOnly: readOnlyProp, required, - ...otherProps + ...other } = props; const useSwitchProps = { @@ -68,56 +84,48 @@ const SwitchUnstyled = React.forwardRef(function SwitchUnstyled( readOnly, }; - const stateClasses = { - [classes.checked]: checked, - [classes.disabled]: disabled, - [classes.focusVisible]: focusVisible, - [classes.readOnly]: readOnly, - }; + const classes = useUtilityClasses(ownerState); const Root: React.ElementType = component ?? components.Root ?? 'span'; - const rootComponentProps = resolveComponentProps(componentsProps.root, ownerState); - const rootProps: WithOptionalOwnerState = appendOwnerState( - Root, - { - ...otherProps, - ...rootComponentProps, - className: clsx(classes.root, stateClasses, className, rootComponentProps?.className), + const rootProps: WithOptionalOwnerState = useSlotProps({ + elementType: Root, + externalSlotProps: componentsProps.root, + externalForwardedProps: other, + additionalProps: { + ref, }, ownerState, - ); + className: classes.root, + }); const Thumb: React.ElementType = components.Thumb ?? 'span'; - const thumbComponentProps = resolveComponentProps(componentsProps.thumb, ownerState); - const thumbProps: WithOptionalOwnerState = appendOwnerState( - Thumb, - { ...thumbComponentProps, className: clsx(classes.thumb, thumbComponentProps?.className) }, + const thumbProps: WithOptionalOwnerState = useSlotProps({ + elementType: Thumb, + externalSlotProps: componentsProps.thumb, ownerState, - ); + className: classes.thumb, + }); const Input: React.ElementType = components.Input ?? 'input'; - const inputComponentProps = resolveComponentProps(componentsProps.input, ownerState); - const inputProps: WithOptionalOwnerState = appendOwnerState( - Input, - { - ...getInputProps(), - ...inputComponentProps, - className: clsx(classes.input, inputComponentProps?.className), - }, + const inputProps: WithOptionalOwnerState = useSlotProps({ + elementType: Input, + getSlotProps: getInputProps, + externalSlotProps: componentsProps.input, ownerState, - ); + className: classes.input, + }); const Track: React.ElementType = components.Track === null ? () => null : components.Track ?? 'span'; - const trackComponentProps = resolveComponentProps(componentsProps.track, ownerState); - const trackProps: WithOptionalOwnerState = appendOwnerState( - Track, - { ...trackComponentProps, className: clsx(classes.track, trackComponentProps?.className) }, + const trackProps: WithOptionalOwnerState = useSlotProps({ + elementType: Track, + externalSlotProps: componentsProps.track, ownerState, - ); + className: classes.track, + }); return ( - + @@ -134,10 +142,6 @@ SwitchUnstyled.propTypes /* remove-proptypes */ = { * If `true`, the component is checked. */ checked: PropTypes.bool, - /** - * Class name applied to the root element. - */ - className: PropTypes.string, /** * The component used for the Root slot. * Either a string to use a HTML element or a component.