From 89295fee76ba0c6e6155313989d46fee07057d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Thu, 23 Jun 2022 15:34:58 +0200 Subject: [PATCH] [Base] Improve the return type of useSlotProps Make useSlotProps (and the underlying appendOwnerState) aware of the type of element on which the props should be applied. Based on this, ownerState will either be present or undefined in the resulting type. --- .../FormControlUnstyled.tsx | 19 +++---- .../src/InputUnstyled/InputUnstyled.types.ts | 2 +- .../SwitchUnstyled/SwitchUnstyled.types.ts | 8 +-- .../src/utils/appendOwnerState.spec.tsx | 28 ++++++++++ .../mui-base/src/utils/appendOwnerState.ts | 51 ++++++++++++++----- .../mui-base/src/utils/useSlotProps.test.tsx | 4 ++ packages/mui-base/src/utils/useSlotProps.ts | 21 +++++--- 7 files changed, 100 insertions(+), 33 deletions(-) create mode 100644 packages/mui-base/src/utils/appendOwnerState.spec.tsx diff --git a/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.tsx b/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.tsx index d8a19bd1e356e2..a42d0017e0453b 100644 --- a/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.tsx +++ b/packages/mui-base/src/FormControlUnstyled/FormControlUnstyled.tsx @@ -131,6 +131,14 @@ const FormControlUnstyled = React.forwardRef(function FormControlUnstyled< const classes = useUtilityClasses(ownerState); + const renderChildren = () => { + if (typeof children === 'function') { + return children(childContext); + } + + return children; + }; + const Root = component ?? components.Root ?? 'div'; const rootProps: WithOptionalOwnerState = useSlotProps({ elementType: Root, @@ -138,22 +146,15 @@ const FormControlUnstyled = React.forwardRef(function FormControlUnstyled< externalForwardedProps: other, additionalProps: { ref, + children: renderChildren(), }, ownerState, className: classes.root, }); - const renderChildren = () => { - if (typeof children === 'function') { - return children(childContext); - } - - return children; - }; - return ( - {renderChildren()} + ); }) as OverridableComponent; diff --git a/packages/mui-base/src/InputUnstyled/InputUnstyled.types.ts b/packages/mui-base/src/InputUnstyled/InputUnstyled.types.ts index 86169a5ee2347a..51a6d2f994458a 100644 --- a/packages/mui-base/src/InputUnstyled/InputUnstyled.types.ts +++ b/packages/mui-base/src/InputUnstyled/InputUnstyled.types.ts @@ -145,7 +145,7 @@ export type InputUnstyledInputSlotProps = Simplify< 'aria-labelledby': React.AriaAttributes['aria-labelledby']; autoComplete: string | undefined; autoFocus: boolean | undefined; - className: string; + className?: string; id: string | undefined; name: string | undefined; onKeyDown: React.KeyboardEventHandler | undefined; diff --git a/packages/mui-base/src/SwitchUnstyled/SwitchUnstyled.types.ts b/packages/mui-base/src/SwitchUnstyled/SwitchUnstyled.types.ts index f4ba43793e182f..fed039313455e9 100644 --- a/packages/mui-base/src/SwitchUnstyled/SwitchUnstyled.types.ts +++ b/packages/mui-base/src/SwitchUnstyled/SwitchUnstyled.types.ts @@ -66,26 +66,26 @@ export type SwitchUnstyledOwnerState = Simplify< export type SwitchUnstyledRootSlotProps = { ownerState: SwitchUnstyledOwnerState; - className: string; + className?: string; children?: React.ReactNode; }; export type SwitchUnstyledThumbSlotProps = { ownerState: SwitchUnstyledOwnerState; - className: string; + className?: string; children?: React.ReactNode; }; export type SwitchUnstyledTrackSlotProps = { ownerState: SwitchUnstyledOwnerState; - className: string; + className?: string; children?: React.ReactNode; }; export type SwitchUnstyledInputSlotProps = Simplify< UseSwitchInputSlotProps & { ownerState: SwitchUnstyledOwnerState; - className: string; + className?: string; children?: React.ReactNode; } >; diff --git a/packages/mui-base/src/utils/appendOwnerState.spec.tsx b/packages/mui-base/src/utils/appendOwnerState.spec.tsx new file mode 100644 index 00000000000000..2a6f571cc555b0 --- /dev/null +++ b/packages/mui-base/src/utils/appendOwnerState.spec.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import appendOwnerState from './appendOwnerState'; + +const divProps = appendOwnerState('div', { otherProp: true }, { ownerStateProps: true }); + +// ownerState is not available on a host component +// @ts-expect-error +const test1 = divProps.ownerState.ownerStateProps; +// @ts-expect-error +const test2 = divProps.ownerState?.ownerStateProps; + +const componentProps = appendOwnerState( + () =>
, + { otherProp: true }, + { ownerStateProps: true }, +); + +// ownerState is present on a custom component +const test3: boolean = componentProps.ownerState.ownerStateProps; + +function test(element: React.ElementType) { + const props = appendOwnerState(element, { otherProp: true }, { ownerStateProps: true }); + + // ownerState may be present on a provided element type (it depends on its exact type) + // @ts-expect-error + const test4 = props.ownerState.ownerStateProps; + const test5: boolean | undefined = props.ownerState?.ownerStateProps; +} diff --git a/packages/mui-base/src/utils/appendOwnerState.ts b/packages/mui-base/src/utils/appendOwnerState.ts index b3a5fd8ebe6726..b6c87597b0f23b 100644 --- a/packages/mui-base/src/utils/appendOwnerState.ts +++ b/packages/mui-base/src/utils/appendOwnerState.ts @@ -1,26 +1,53 @@ +import { Simplify } from '@mui/types'; +import React from 'react'; import isHostComponent from './isHostComponent'; +/** + * Type of the ownerState based on the type of an element it applies to. + * This resolves to the provided OwnerState for React components and `undefined` for host components. + * Falls back to `OwnerState | undefined` when the exact type can't be determined in development time. + */ +type OwnerStateWhenApplicable< + ElementType extends React.ElementType, + OwnerState, +> = ElementType extends React.ComponentType + ? OwnerState + : ElementType extends keyof JSX.IntrinsicElements + ? undefined + : OwnerState | undefined; + +export type AppendOwnerStateReturnType< + ElementType extends React.ElementType, + OtherProps, + OwnerState, +> = Simplify< + OtherProps & { + ownerState: OwnerStateWhenApplicable; + } +>; + /** * Appends the ownerState object to the props, merging with the existing one if necessary. * - * @param elementType Type of the element that owns the `existingProps`. If the element is a DOM node, `ownerState` are not applied. - * @param existingProps Props of the element. + * @param elementType Type of the element that owns the `existingProps`. If the element is a DOM node, `ownerState` is not applied. + * @param otherProps Props of the element. * @param ownerState */ export default function appendOwnerState< - TExistingProps extends Record, - TOwnerState extends {}, + ElementType extends React.ElementType, + OtherProps extends Record, + OwnerState, >( - elementType: React.ElementType, - existingProps: TExistingProps = {} as TExistingProps, - ownerState: TOwnerState, -): TExistingProps & { ownerState?: TOwnerState } { + elementType: ElementType, + otherProps: OtherProps = {} as OtherProps, + ownerState: OwnerState, +): AppendOwnerStateReturnType { if (isHostComponent(elementType)) { - return existingProps; + return otherProps as AppendOwnerStateReturnType; } return { - ...existingProps, - ownerState: { ...existingProps.ownerState, ...ownerState }, - }; + ...otherProps, + ownerState: { ...otherProps.ownerState, ...ownerState }, + } as AppendOwnerStateReturnType; } diff --git a/packages/mui-base/src/utils/useSlotProps.test.tsx b/packages/mui-base/src/utils/useSlotProps.test.tsx index b4487d5325f509..08bb3c12953ed0 100644 --- a/packages/mui-base/src/utils/useSlotProps.test.tsx +++ b/packages/mui-base/src/utils/useSlotProps.test.tsx @@ -8,6 +8,7 @@ import useSlotProps, { UseSlotPropsParameters, UseSlotPropsResult } from './useS const { render } = createRenderer(); function callUseSlotProps< + ElementType extends React.ElementType, SlotProps, ExternalForwardedProps, ExternalSlotProps, @@ -15,6 +16,7 @@ function callUseSlotProps< OwnerState, >( parameters: UseSlotPropsParameters< + ElementType, SlotProps, ExternalForwardedProps, ExternalSlotProps, @@ -27,6 +29,7 @@ function callUseSlotProps< _: unknown, ref: React.Ref< UseSlotPropsResult< + ElementType, SlotProps, ExternalForwardedProps, ExternalSlotProps, @@ -44,6 +47,7 @@ function callUseSlotProps< const ref = React.createRef< UseSlotPropsResult< + ElementType, SlotProps, ExternalForwardedProps, ExternalSlotProps, diff --git a/packages/mui-base/src/utils/useSlotProps.ts b/packages/mui-base/src/utils/useSlotProps.ts index 6c93acbc493126..e544b6ae089646 100644 --- a/packages/mui-base/src/utils/useSlotProps.ts +++ b/packages/mui-base/src/utils/useSlotProps.ts @@ -1,10 +1,11 @@ import * as React from 'react'; import { unstable_useForkRef as useForkRef } from '@mui/utils'; -import appendOwnerState from './appendOwnerState'; +import appendOwnerState, { AppendOwnerStateReturnType } from './appendOwnerState'; import mergeSlotProps, { MergeSlotPropsParameters, WithRef } from './mergeSlotProps'; import resolveComponentProps from './resolveComponentProps'; export type UseSlotPropsParameters< + ElementType extends React.ElementType, SlotProps, ExternalForwardedProps, ExternalSlotProps, @@ -17,7 +18,7 @@ export type UseSlotPropsParameters< /** * The type of the component used in the slot. */ - elementType: React.ElementType; + elementType: ElementType; /** * The `componentsProps.*` of the unstyled component. */ @@ -32,16 +33,20 @@ export type UseSlotPropsParameters< }; export type UseSlotPropsResult< + ElementType extends React.ElementType, SlotProps, ExternalForwardedProps, ExternalSlotProps, AdditionalProps, OwnerState, -> = Omit & { - className?: string | undefined; - ownerState?: OwnerState | undefined; - ref: (instance: any | null) => void; -}; +> = AppendOwnerStateReturnType< + ElementType, + Omit & { + className?: string | undefined; + ref: (instance: any | null) => void; + }, + OwnerState +>; /** * Builds the props to be passed into the slot of an unstyled component. @@ -51,6 +56,7 @@ export type UseSlotPropsResult< * @param parameters.getSlotProps - A function that returns the props to be passed to the slot component. */ export default function useSlotProps< + ElementType extends React.ElementType, SlotProps, ExternalForwardedProps, ExternalSlotProps, @@ -58,6 +64,7 @@ export default function useSlotProps< OwnerState, >( parameters: UseSlotPropsParameters< + ElementType, SlotProps, ExternalForwardedProps, WithRef,