From ae22ea85de476a155ddacad823334c1043b4a7b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Mon, 13 Jun 2022 11:49:54 +0200 Subject: [PATCH 1/9] [SliderUnstyled] Use useSlotProps --- .../src/SliderUnstyled/SliderUnstyled.js | 94 ++++++++++++------- .../SliderUnstyled/SliderUnstyled.types.ts | 1 + .../mui-base/src/utils/mergeSlotProps.test.ts | 43 +++++++++ packages/mui-base/src/utils/mergeSlotProps.ts | 76 +++++++++------ packages/mui-base/src/utils/useSlotProps.ts | 6 +- 5 files changed, 154 insertions(+), 66 deletions(-) diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js index 9a10794dc7df11..fab3d1fe49504d 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js @@ -2,12 +2,12 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import { chainPropTypes } from '@mui/utils'; -import appendOwnerState from '../utils/appendOwnerState'; import isHostComponent from '../utils/isHostComponent'; import composeClasses from '../composeClasses'; import { getSliderUtilityClass } from './sliderUnstyledClasses'; import SliderValueLabelUnstyled from './SliderValueLabelUnstyled'; import useSlider, { valueToPercent } from './useSlider'; +import useSlotProps from '../utils/useSlotProps'; const Identity = (x) => x; @@ -59,7 +59,6 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { name, onChange, onChangeCommitted, - onMouseDown, orientation = 'horizontal', scale = Identity, step = 1, @@ -111,50 +110,83 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { ownerState.marked = marks.length > 0 && marks.some((mark) => mark.label); ownerState.dragging = dragging; + ownerState.focusVisible = focusVisible; + + const classes = useUtilityClasses(ownerState); const Root = component ?? components.Root ?? 'span'; - const rootProps = appendOwnerState(Root, { ...other, ...componentsProps.root }, ownerState); + const rootProps = useSlotProps({ + elementType: Root, + getSlotProps: getRootProps, + externalSlotProps: componentsProps.root, + externalForwardedProps: other, + ownerState, + className: [classes.root, className], + }); const Rail = components.Rail ?? 'span'; - const railProps = appendOwnerState(Rail, componentsProps.rail, ownerState); + const railProps = useSlotProps({ + elementType: Rail, + externalSlotProps: componentsProps.rail, + ownerState, + className: classes.rail, + }); const Track = components.Track ?? 'span'; - const trackProps = appendOwnerState(Track, componentsProps.track, ownerState); - const trackStyle = { - ...axisProps[axis].offset(trackOffset), - ...axisProps[axis].leap(trackLeap), - }; + const trackProps = useSlotProps({ + elementType: Track, + externalSlotProps: componentsProps.track, + additionalProps: { + style: { + ...axisProps[axis].offset(trackOffset), + ...axisProps[axis].leap(trackLeap), + }, + }, + ownerState, + className: classes.track, + }); const Thumb = components.Thumb ?? 'span'; - const thumbProps = appendOwnerState(Thumb, componentsProps.thumb, ownerState); + const thumbProps = useSlotProps({ + elementType: Thumb, + getSlotProps: getThumbProps, + externalSlotProps: componentsProps.thumb, + ownerState, + }); const ValueLabel = components.ValueLabel ?? SliderValueLabelUnstyled; - const valueLabelProps = appendOwnerState(ValueLabel, componentsProps.valueLabel, ownerState); + const valueLabelProps = useSlotProps({ + elementType: ValueLabel, + externalSlotProps: componentsProps.valueLabel, + ownerState, + }); const Mark = components.Mark ?? 'span'; - const markProps = appendOwnerState(Mark, componentsProps.mark, ownerState); + const markProps = useSlotProps({ + elementType: Mark, + externalSlotProps: componentsProps.mark, + ownerState, + }); const MarkLabel = components.MarkLabel ?? 'span'; - const markLabelProps = appendOwnerState(MarkLabel, componentsProps.markLabel, ownerState); + const markLabelProps = useSlotProps({ + elementType: MarkLabel, + externalSlotProps: componentsProps.markLabel, + ownerState, + }); const Input = components.Input || 'input'; - const inputProps = appendOwnerState(Input, componentsProps.input, ownerState); - const hiddenInputProps = getHiddenInputProps(); - - const classes = useUtilityClasses(ownerState); + const inputProps = useSlotProps({ + elementType: Input, + getSlotProps: getHiddenInputProps, + externalSlotProps: componentsProps.input, + ownerState, + }); return ( - - - + + + {marks .filter((mark) => mark.value >= min && mark.value <= max) .map((mark, index) => { @@ -234,7 +266,6 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts index 355772f61783d4..35ebe3217780c1 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts @@ -11,6 +11,7 @@ import { export type SliderUnstyledOwnerState = SliderUnstyledProps & { disabled: boolean; + focusVisible: boolean; isRtl: boolean; mark: boolean | Mark[]; max: number; diff --git a/packages/mui-base/src/utils/mergeSlotProps.test.ts b/packages/mui-base/src/utils/mergeSlotProps.test.ts index 41f2af10344e7e..d2cb7270ba6338 100644 --- a/packages/mui-base/src/utils/mergeSlotProps.test.ts +++ b/packages/mui-base/src/utils/mergeSlotProps.test.ts @@ -75,6 +75,49 @@ describe('mergeSlotProps', () => { expect(merged.props.className).to.contain('externalSlot'); }); + it('merges the style props', () => { + const getSlotProps = () => ({ + style: { + fontSize: '12px', + textAlign: 'center' as const, + }, + }); + + const additionalProps = { + style: { + fontSize: '14px', + color: 'red', + }, + }; + + const externalForwardedProps = { + style: { + fontWeight: 500, + }, + }; + + const externalSlotProps = { + style: { + textDecoration: 'underline', + }, + }; + + const merged = mergeSlotProps({ + getSlotProps, + additionalProps, + externalForwardedProps, + externalSlotProps, + }); + + expect(merged.props.style).to.deep.equal({ + textAlign: 'center', + color: 'red', + fontSize: '14px', + fontWeight: 500, + textDecoration: 'underline', + }); + }); + it('returns the ref returned from the getSlotProps function', () => { const ref = React.createRef(); const getSlotProps = () => ({ diff --git a/packages/mui-base/src/utils/mergeSlotProps.ts b/packages/mui-base/src/utils/mergeSlotProps.ts index 8c3ea96b2d43cf..b7fcc1fd41ba93 100644 --- a/packages/mui-base/src/utils/mergeSlotProps.ts +++ b/packages/mui-base/src/utils/mergeSlotProps.ts @@ -4,11 +4,9 @@ import { EventHandlers } from './types'; import extractEventHandlers from './extractEventHandlers'; import omitEventHandlers, { OmitEventHandlers } from './omitEventHandlers'; -export type WithClassName = T & { +export type WithCommonProps = T & { className?: string; -}; - -export type WithRef = T & { + style?: React.CSSProperties; ref?: React.Ref; }; @@ -23,20 +21,20 @@ export interface MergeSlotPropsParameters< * It accepts the event handlers passed into the component by the user * and is responsible for calling them where appropriate. */ - getSlotProps?: (other: EventHandlers) => WithClassName; + getSlotProps?: (other: EventHandlers) => WithCommonProps; /** * Props provided to the `componentsProps.*` of the unstyled component. */ - externalSlotProps?: WithClassName; + externalSlotProps?: WithCommonProps; /** * Extra props placed on the unstyled component that should be forwarded to the slot. * This should usually be used only for the root slot. */ - externalForwardedProps?: WithClassName; + externalForwardedProps?: WithCommonProps; /** * Additional props to be placed on the slot. */ - additionalProps?: WithClassName; + additionalProps?: WithCommonProps; /** * Extra class name(s) to be placed on the slot. */ @@ -53,7 +51,7 @@ export type MergeSlotPropsResult< SlotProps & OmitEventHandlers & OmitEventHandlers & - AdditionalProps & { className?: string } + AdditionalProps & { className?: string; style?: React.CSSProperties } >; internalRef: React.Ref | undefined; }; @@ -78,7 +76,7 @@ export default function mergeSlotProps< AdditionalProps, >( parameters: MergeSlotPropsParameters< - WithRef, + SlotProps, ExternalForwardedProps, ExternalSlotProps, AdditionalProps @@ -97,20 +95,29 @@ export default function mergeSlotProps< additionalProps?.className, ); + const mergedStyle = { + ...additionalProps?.style, + ...externalForwardedProps?.style, + ...externalSlotProps?.style, + }; + const props = { ...additionalProps, ...externalForwardedProps, ...externalSlotProps, - className: joinedClasses, - } as Simplify< - SlotProps & - ExternalForwardedProps & - ExternalSlotProps & - AdditionalProps & { className?: string } - >; - - if (joinedClasses.length === 0) { - delete props.className; + } as MergeSlotPropsResult< + SlotProps, + ExternalForwardedProps, + ExternalSlotProps, + AdditionalProps + >['props']; + + if (joinedClasses.length > 0) { + props.className = joinedClasses; + } + + if (Object.keys(mergedStyle).length > 0) { + props.style = mergedStyle; } return { @@ -135,20 +142,31 @@ export default function mergeSlotProps< internalSlotProps?.className, ); + const mergedStyle = { + ...internalSlotProps?.style, + ...additionalProps?.style, + ...externalForwardedProps?.style, + ...externalSlotProps?.style, + }; + const props = { ...internalSlotProps, ...additionalProps, ...otherPropsWithoutEventHandlers, ...componentsPropsWithoutEventHandlers, - className: joinedClasses, - } as Simplify< - SlotProps & - OmitEventHandlers & - OmitEventHandlers & - AdditionalProps & { className?: string } - >; - if (joinedClasses.length === 0) { - delete props.className; + } as MergeSlotPropsResult< + SlotProps, + ExternalForwardedProps, + ExternalSlotProps, + AdditionalProps + >['props']; + + if (joinedClasses.length > 0) { + props.className = joinedClasses; + } + + if (Object.keys(mergedStyle).length > 0) { + props.style = mergedStyle; } return { diff --git a/packages/mui-base/src/utils/useSlotProps.ts b/packages/mui-base/src/utils/useSlotProps.ts index 6c93acbc493126..eba03aa8899c25 100644 --- a/packages/mui-base/src/utils/useSlotProps.ts +++ b/packages/mui-base/src/utils/useSlotProps.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { unstable_useForkRef as useForkRef } from '@mui/utils'; import appendOwnerState from './appendOwnerState'; -import mergeSlotProps, { MergeSlotPropsParameters, WithRef } from './mergeSlotProps'; +import mergeSlotProps, { MergeSlotPropsParameters, WithCommonProps } from './mergeSlotProps'; import resolveComponentProps from './resolveComponentProps'; export type UseSlotPropsParameters< @@ -60,8 +60,8 @@ export default function useSlotProps< parameters: UseSlotPropsParameters< SlotProps, ExternalForwardedProps, - WithRef, - WithRef, + WithCommonProps, + WithCommonProps, OwnerState >, ) { From 033668c33ec3c6bca71c7d404f005980d6a7e1a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Mon, 13 Jun 2022 11:55:27 +0200 Subject: [PATCH 2/9] proptypes --- packages/mui-base/src/SliderUnstyled/SliderUnstyled.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js index fab3d1fe49504d..5457b6c9413273 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js @@ -473,10 +473,6 @@ SliderUnstyled.propTypes /* remove-proptypes */ = { * @param {number | number[]} value The new value. */ onChangeCommitted: PropTypes.func, - /** - * @ignore - */ - onMouseDown: PropTypes.func, /** * The component orientation. * @default 'horizontal' From f0a190ec201ca3b4e308f778b44cc60a1e39b57b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Mon, 13 Jun 2022 12:53:29 +0200 Subject: [PATCH 3/9] Update the types of componentsProps --- .../SliderUnstyled/SliderUnstyled.types.ts | 391 ++++++++++-------- 1 file changed, 213 insertions(+), 178 deletions(-) diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts index 35ebe3217780c1..ea0eb28eabae81 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts @@ -1,5 +1,6 @@ import { OverridableComponent, OverridableTypeMap, OverrideProps } from '@mui/types'; import React from 'react'; +import { SlotComponentProps } from '../utils'; import { SliderUnstyledClasses } from './sliderUnstyledClasses'; import SliderValueLabelUnstyled from './SliderValueLabelUnstyled'; import { @@ -33,185 +34,219 @@ export interface SliderValueLabelProps extends React.HTMLAttributes { - props: P & { - /** - * The label of the slider. - */ - 'aria-label'?: string; - /** - * The id of the element containing a label for the slider. - */ - 'aria-labelledby'?: string; - /** - * A string value that provides a user-friendly name for the current value of the slider. - */ - 'aria-valuetext'?: string; - /** - * Override or extend the styles applied to the component. - */ - classes?: Partial; - /** - * The components used for each slot inside the Slider. - * Either a string to use a HTML element or a component. - * @default {} - */ - components?: { - Root?: React.ElementType; - Track?: React.ElementType; - Rail?: React.ElementType; - Thumb?: React.ElementType; - Mark?: React.ElementType; - MarkLabel?: React.ElementType; - ValueLabel?: React.ElementType; - Input?: React.ElementType; - }; - /** - * The props used for each slot inside the Slider. - * @default {} - */ - componentsProps?: { - root?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; - track?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; - rail?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; - thumb?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; - mark?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; - markLabel?: React.ComponentPropsWithRef<'span'> & SliderUnstyledComponentsPropsOverrides; - valueLabel?: Partial> & - SliderUnstyledComponentsPropsOverrides; - input?: React.ComponentPropsWithRef<'input'> & SliderUnstyledComponentsPropsOverrides; - }; - /** - * The default value. Use when the component is not controlled. - */ - defaultValue?: number | number[]; - /** - * If `true`, the component is disabled. - * @default false - */ - disabled?: boolean; - /** - * If `true`, the active thumb doesn't swap when moving pointer over a thumb while dragging another thumb. - * @default false - */ - disableSwap?: boolean; - /** - * Accepts a function which returns a string value that provides a user-friendly name for the thumb labels of the slider. - * This is important for screen reader users. - * @param {number} index The thumb label's index to format. - * @returns {string} - */ - getAriaLabel?: (index: number) => string; - /** - * Accepts a function which returns a string value that provides a user-friendly name for the current value of the slider. - * This is important for screen reader users. - * @param {number} value The thumb label's value to format. - * @param {number} index The thumb label's index to format. - * @returns {string} - */ - getAriaValueText?: (value: number, index: number) => string; - /** - * Indicates whether the theme context has rtl direction. It is set automatically. - * @default false - */ - isRtl?: boolean; - /** - * Marks indicate predetermined values to which the user can move the slider. - * If `true` the marks are spaced according the value of the `step` prop. - * If an array, it should contain objects with `value` and an optional `label` keys. - * @default false - */ - marks?: boolean | Mark[]; - /** - * The maximum allowed value of the slider. - * Should not be equal to min. - * @default 100 - */ - max?: number; - /** - * The minimum allowed value of the slider. - * Should not be equal to max. - * @default 0 - */ - min?: number; - /** - * Name attribute of the hidden `input` element. - */ - name?: string; - /** - * Callback function that is fired when the slider's value changed. - * - * @param {Event} event The event source of the callback. - * You can pull out the new value by accessing `event.target.value` (any). - * **Warning**: This is a generic event not a change event. - * @param {number | number[]} value The new value. - * @param {number} activeThumb Index of the currently moved thumb. - */ - onChange?: (event: Event, value: number | number[], activeThumb: number) => void; - /** - * Callback function that is fired when the `mouseup` is triggered. - * - * @param {React.SyntheticEvent | Event} event The event source of the callback. **Warning**: This is a generic event not a change event. - * @param {number | number[]} value The new value. - */ - onChangeCommitted?: (event: React.SyntheticEvent | Event, value: number | number[]) => void; - /** - * The component orientation. - * @default 'horizontal' - */ - orientation?: 'horizontal' | 'vertical'; - /** - * A transformation function, to change the scale of the slider. - * @default (x) => x - */ - scale?: (value: number) => number; - /** - * The granularity with which the slider can step through values. (A "discrete" slider.) - * The `min` prop serves as the origin for the valid values. - * We recommend (max - min) to be evenly divisible by the step. - * - * When step is `null`, the thumb can only be slid onto marks provided with the `marks` prop. - * @default 1 - */ - step?: number | null; - /** - * Tab index attribute of the hidden `input` element. - */ - tabIndex?: number; - /** - * The track presentation: - * - * - `normal` the track will render a bar representing the slider value. - * - `inverted` the track will render a bar representing the remaining slider value. - * - `false` the track will render without a bar. - * @default 'normal' - */ - track?: 'normal' | false | 'inverted'; - /** - * The value of the slider. - * For ranged sliders, provide an array with two values. - */ - value?: number | number[]; - /** - * Controls when the value label is displayed: - * - * - `auto` the value label will display when the thumb is hovered or focused. - * - `on` will display persistently. - * - `off` will never display. - * @default 'off' - */ - valueLabelDisplay?: 'on' | 'auto' | 'off'; - /** - * The format function the value label's value. - * - * When a function is provided, it should have the following signature: - * - * - {number} value The value label's value to format - * - {number} index The value label's index to format - * @default (x) => x - */ - valueLabelFormat?: string | ((value: number, index: number) => React.ReactNode); +interface SliderUnstyledOwnProps { + /** + * The label of the slider. + */ + 'aria-label'?: string; + /** + * The id of the element containing a label for the slider. + */ + 'aria-labelledby'?: string; + /** + * A string value that provides a user-friendly name for the current value of the slider. + */ + 'aria-valuetext'?: string; + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + /** + * The components used for each slot inside the Slider. + * Either a string to use a HTML element or a component. + * @default {} + */ + components?: { + Root?: React.ElementType; + Track?: React.ElementType; + Rail?: React.ElementType; + Thumb?: React.ElementType; + Mark?: React.ElementType; + MarkLabel?: React.ElementType; + ValueLabel?: React.ElementType; + Input?: React.ElementType; + }; + /** + * The props used for each slot inside the Slider. + * @default {} + */ + componentsProps?: { + root?: SlotComponentProps< + 'span', + SliderUnstyledComponentsPropsOverrides, + SliderUnstyledOwnerState + >; + track?: SlotComponentProps< + 'span', + SliderUnstyledComponentsPropsOverrides, + SliderUnstyledOwnerState + >; + rail?: SlotComponentProps< + 'span', + SliderUnstyledComponentsPropsOverrides, + SliderUnstyledOwnerState + >; + thumb?: SlotComponentProps< + 'span', + SliderUnstyledComponentsPropsOverrides, + SliderUnstyledOwnerState + >; + mark?: SlotComponentProps< + 'span', + SliderUnstyledComponentsPropsOverrides, + SliderUnstyledOwnerState + >; + markLabel?: SlotComponentProps< + 'span', + SliderUnstyledComponentsPropsOverrides, + SliderUnstyledOwnerState + >; + valueLabel?: SlotComponentProps< + typeof SliderValueLabelUnstyled, + SliderUnstyledComponentsPropsOverrides, + SliderUnstyledOwnerState + >; + + input?: SlotComponentProps< + 'input', + SliderUnstyledComponentsPropsOverrides, + SliderUnstyledOwnerState + >; }; + /** + * The default value. Use when the component is not controlled. + */ + defaultValue?: number | number[]; + /** + * If `true`, the component is disabled. + * @default false + */ + disabled?: boolean; + /** + * If `true`, the active thumb doesn't swap when moving pointer over a thumb while dragging another thumb. + * @default false + */ + disableSwap?: boolean; + /** + * Accepts a function which returns a string value that provides a user-friendly name for the thumb labels of the slider. + * This is important for screen reader users. + * @param {number} index The thumb label's index to format. + * @returns {string} + */ + getAriaLabel?: (index: number) => string; + /** + * Accepts a function which returns a string value that provides a user-friendly name for the current value of the slider. + * This is important for screen reader users. + * @param {number} value The thumb label's value to format. + * @param {number} index The thumb label's index to format. + * @returns {string} + */ + getAriaValueText?: (value: number, index: number) => string; + /** + * Indicates whether the theme context has rtl direction. It is set automatically. + * @default false + */ + isRtl?: boolean; + /** + * Marks indicate predetermined values to which the user can move the slider. + * If `true` the marks are spaced according the value of the `step` prop. + * If an array, it should contain objects with `value` and an optional `label` keys. + * @default false + */ + marks?: boolean | Mark[]; + /** + * The maximum allowed value of the slider. + * Should not be equal to min. + * @default 100 + */ + max?: number; + /** + * The minimum allowed value of the slider. + * Should not be equal to max. + * @default 0 + */ + min?: number; + /** + * Name attribute of the hidden `input` element. + */ + name?: string; + /** + * Callback function that is fired when the slider's value changed. + * + * @param {Event} event The event source of the callback. + * You can pull out the new value by accessing `event.target.value` (any). + * **Warning**: This is a generic event not a change event. + * @param {number | number[]} value The new value. + * @param {number} activeThumb Index of the currently moved thumb. + */ + onChange?: (event: Event, value: number | number[], activeThumb: number) => void; + /** + * Callback function that is fired when the `mouseup` is triggered. + * + * @param {React.SyntheticEvent | Event} event The event source of the callback. **Warning**: This is a generic event not a change event. + * @param {number | number[]} value The new value. + */ + onChangeCommitted?: (event: React.SyntheticEvent | Event, value: number | number[]) => void; + /** + * The component orientation. + * @default 'horizontal' + */ + orientation?: 'horizontal' | 'vertical'; + /** + * A transformation function, to change the scale of the slider. + * @default (x) => x + */ + scale?: (value: number) => number; + /** + * The granularity with which the slider can step through values. (A "discrete" slider.) + * The `min` prop serves as the origin for the valid values. + * We recommend (max - min) to be evenly divisible by the step. + * + * When step is `null`, the thumb can only be slid onto marks provided with the `marks` prop. + * @default 1 + */ + step?: number | null; + /** + * Tab index attribute of the hidden `input` element. + */ + tabIndex?: number; + /** + * The track presentation: + * + * - `normal` the track will render a bar representing the slider value. + * - `inverted` the track will render a bar representing the remaining slider value. + * - `false` the track will render without a bar. + * @default 'normal' + */ + track?: 'normal' | false | 'inverted'; + /** + * The value of the slider. + * For ranged sliders, provide an array with two values. + */ + value?: number | number[]; + /** + * Controls when the value label is displayed: + * + * - `auto` the value label will display when the thumb is hovered or focused. + * - `on` will display persistently. + * - `off` will never display. + * @default 'off' + */ + valueLabelDisplay?: 'on' | 'auto' | 'off'; + /** + * The format function the value label's value. + * + * When a function is provided, it should have the following signature: + * + * - {number} value The value label's value to format + * - {number} index The value label's index to format + * @default (x) => x + */ + valueLabelFormat?: string | ((value: number, index: number) => React.ReactNode); +} + +export interface SliderUnstyledTypeMap

{ + props: P & SliderUnstyledOwnProps; defaultComponent: D; } From 40448e2701c8fd51eb7ebdb46ca5f31adb402955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Mon, 13 Jun 2022 13:22:01 +0200 Subject: [PATCH 4/9] proptypes, again --- docs/pages/base/api/slider-unstyled.json | 2 +- docs/pages/material-ui/api/slider.json | 2 +- .../src/SliderUnstyled/SliderUnstyled.js | 37 ++++++++++--------- packages/mui-joy/src/Slider/Slider.tsx | 37 ++++++++++--------- packages/mui-material/src/Slider/Slider.js | 37 ++++++++++--------- 5 files changed, 62 insertions(+), 53 deletions(-) diff --git a/docs/pages/base/api/slider-unstyled.json b/docs/pages/base/api/slider-unstyled.json index 0695b4652d2252..bf25331f577a0d 100644 --- a/docs/pages/base/api/slider-unstyled.json +++ b/docs/pages/base/api/slider-unstyled.json @@ -15,7 +15,7 @@ "componentsProps": { "type": { "name": "shape", - "description": "{ input?: object, mark?: object, markLabel?: object, rail?: object, root?: object, thumb?: object, track?: object, valueLabel?: { children?: element, className?: string, components?: { Root?: elementType }, open?: bool, style?: object, value?: number, valueLabelDisplay?: 'auto'
| 'off'
| 'on' } }" + "description": "{ input?: func
| object, mark?: func
| object, markLabel?: func
| object, rail?: func
| object, root?: func
| object, thumb?: func
| object, track?: func
| object, valueLabel?: func
| { children?: element, className?: string, components?: { Root?: elementType }, open?: bool, style?: object, value?: number, valueLabelDisplay?: 'auto'
| 'off'
| 'on' } }" }, "default": "{}" }, diff --git a/docs/pages/material-ui/api/slider.json b/docs/pages/material-ui/api/slider.json index e97ac7f7a898c1..02e4e0aad21685 100644 --- a/docs/pages/material-ui/api/slider.json +++ b/docs/pages/material-ui/api/slider.json @@ -21,7 +21,7 @@ "componentsProps": { "type": { "name": "shape", - "description": "{ input?: object, mark?: object, markLabel?: object, rail?: object, root?: object, thumb?: object, track?: object, valueLabel?: { children?: element, className?: string, components?: { Root?: elementType }, open?: bool, style?: object, value?: number, valueLabelDisplay?: 'auto'
| 'off'
| 'on' } }" + "description": "{ input?: func
| object, mark?: func
| object, markLabel?: func
| object, rail?: func
| object, root?: func
| object, thumb?: func
| object, track?: func
| object, valueLabel?: func
| { children?: element, className?: string, components?: { Root?: elementType }, open?: bool, style?: object, value?: number, valueLabelDisplay?: 'auto'
| 'off'
| 'on' } }" }, "default": "{}" }, diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js index 5457b6c9413273..02ed93324b392b 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js @@ -372,24 +372,27 @@ SliderUnstyled.propTypes /* remove-proptypes */ = { * @default {} */ componentsProps: PropTypes.shape({ - input: PropTypes.object, - mark: PropTypes.object, - markLabel: PropTypes.object, - rail: PropTypes.object, - root: PropTypes.object, - thumb: PropTypes.object, - track: PropTypes.object, - valueLabel: PropTypes.shape({ - children: PropTypes.element, - className: PropTypes.string, - components: PropTypes.shape({ - Root: PropTypes.elementType, + input: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + mark: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + markLabel: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + rail: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + thumb: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + track: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + valueLabel: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ + children: PropTypes.element, + className: PropTypes.string, + components: PropTypes.shape({ + Root: PropTypes.elementType, + }), + open: PropTypes.bool, + style: PropTypes.object, + value: PropTypes.number, + valueLabelDisplay: PropTypes.oneOf(['auto', 'off', 'on']), }), - open: PropTypes.bool, - style: PropTypes.object, - value: PropTypes.number, - valueLabelDisplay: PropTypes.oneOf(['auto', 'off', 'on']), - }), + ]), }), /** * The default value. Use when the component is not controlled. diff --git a/packages/mui-joy/src/Slider/Slider.tsx b/packages/mui-joy/src/Slider/Slider.tsx index dfb53c73fdbd99..a78a0b41edee3c 100644 --- a/packages/mui-joy/src/Slider/Slider.tsx +++ b/packages/mui-joy/src/Slider/Slider.tsx @@ -468,24 +468,27 @@ Slider.propTypes /* remove-proptypes */ = { * @default {} */ componentsProps: PropTypes.shape({ - input: PropTypes.object, - mark: PropTypes.object, - markLabel: PropTypes.object, - rail: PropTypes.object, - root: PropTypes.object, - thumb: PropTypes.object, - track: PropTypes.object, - valueLabel: PropTypes.shape({ - children: PropTypes.element, - className: PropTypes.string, - components: PropTypes.shape({ - Root: PropTypes.elementType, + input: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + mark: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + markLabel: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + rail: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + thumb: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + track: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + valueLabel: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ + children: PropTypes.element, + className: PropTypes.string, + components: PropTypes.shape({ + Root: PropTypes.elementType, + }), + open: PropTypes.bool, + style: PropTypes.object, + value: PropTypes.number, + valueLabelDisplay: PropTypes.oneOf(['auto', 'off', 'on']), }), - open: PropTypes.bool, - style: PropTypes.object, - value: PropTypes.number, - valueLabelDisplay: PropTypes.oneOf(['auto', 'off', 'on']), - }), + ]), }), /** * The size of the component. diff --git a/packages/mui-material/src/Slider/Slider.js b/packages/mui-material/src/Slider/Slider.js index 994fe85dd371a9..9ccfb5475afa88 100644 --- a/packages/mui-material/src/Slider/Slider.js +++ b/packages/mui-material/src/Slider/Slider.js @@ -610,24 +610,27 @@ Slider.propTypes /* remove-proptypes */ = { * @default {} */ componentsProps: PropTypes.shape({ - input: PropTypes.object, - mark: PropTypes.object, - markLabel: PropTypes.object, - rail: PropTypes.object, - root: PropTypes.object, - thumb: PropTypes.object, - track: PropTypes.object, - valueLabel: PropTypes.shape({ - children: PropTypes.element, - className: PropTypes.string, - components: PropTypes.shape({ - Root: PropTypes.elementType, + input: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + mark: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + markLabel: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + rail: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + thumb: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + track: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + valueLabel: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ + children: PropTypes.element, + className: PropTypes.string, + components: PropTypes.shape({ + Root: PropTypes.elementType, + }), + open: PropTypes.bool, + style: PropTypes.object, + value: PropTypes.number, + valueLabelDisplay: PropTypes.oneOf(['auto', 'off', 'on']), }), - open: PropTypes.bool, - style: PropTypes.object, - value: PropTypes.number, - valueLabelDisplay: PropTypes.oneOf(['auto', 'off', 'on']), - }), + ]), }), /** * The default value. Use when the component is not controlled. From 9c5caf817d1c4fcccb5170200579d55db1e23fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Wed, 15 Jun 2022 13:38:24 +0200 Subject: [PATCH 5/9] Rename focusVisible to focusedThumbIndex --- .../mui-base/src/SliderUnstyled/SliderUnstyled.js | 6 +++--- .../src/SliderUnstyled/SliderUnstyled.types.ts | 2 +- packages/mui-base/src/SliderUnstyled/useSlider.ts | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js index 02ed93324b392b..212a7705c5fcbc 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js @@ -100,7 +100,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { active, axis, range, - focusVisible, + focusedThumbIndex, dragging, marks, values, @@ -110,7 +110,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { ownerState.marked = marks.length > 0 && marks.some((mark) => mark.label); ownerState.dragging = dragging; - ownerState.focusVisible = focusVisible; + ownerState.focusedThumbIndex = focusedThumbIndex; const classes = useUtilityClasses(ownerState); @@ -268,7 +268,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { {...thumbProps} className={clsx(classes.thumb, thumbProps.className, { [classes.active]: active === index, - [classes.focusVisible]: focusVisible === index, + [classes.focusVisible]: focusedThumbIndex === index, })} style={{ ...style, diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts index ea0eb28eabae81..78eacfdf70e1c5 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts @@ -12,7 +12,7 @@ import { export type SliderUnstyledOwnerState = SliderUnstyledProps & { disabled: boolean; - focusVisible: boolean; + focusedThumbIndex: number; isRtl: boolean; mark: boolean | Mark[]; max: number; diff --git a/packages/mui-base/src/SliderUnstyled/useSlider.ts b/packages/mui-base/src/SliderUnstyled/useSlider.ts index 0e57074be29cbd..afcc9176a1c8a7 100644 --- a/packages/mui-base/src/SliderUnstyled/useSlider.ts +++ b/packages/mui-base/src/SliderUnstyled/useSlider.ts @@ -251,7 +251,7 @@ export default function useSlider(parameters: UseSliderParameters) { onFocus: handleFocusVisible, ref: focusVisibleRef, } = useIsFocusVisible(); - const [focusVisible, setFocusVisible] = React.useState(-1); + const [focusedThumbIndex, setFocusedThumbIndex] = React.useState(-1); const sliderRef = React.useRef(); const handleFocusRef = useForkRef(focusVisibleRef, sliderRef); @@ -262,7 +262,7 @@ export default function useSlider(parameters: UseSliderParameters) { const index = Number(event.currentTarget.getAttribute('data-index')); handleFocusVisible(event); if (isFocusVisibleRef.current === true) { - setFocusVisible(index); + setFocusedThumbIndex(index); } setOpen(index); otherHandlers?.onFocus?.(event); @@ -271,7 +271,7 @@ export default function useSlider(parameters: UseSliderParameters) { (otherHandlers: Record>) => (event: React.FocusEvent) => { handleBlurVisible(event); if (isFocusVisibleRef.current === false) { - setFocusVisible(-1); + setFocusedThumbIndex(-1); } setOpen(-1); otherHandlers?.onBlur?.(event); @@ -290,8 +290,8 @@ export default function useSlider(parameters: UseSliderParameters) { if (disabled && active !== -1) { setActive(-1); } - if (disabled && focusVisible !== -1) { - setFocusVisible(-1); + if (disabled && focusedThumbIndex !== -1) { + setFocusedThumbIndex(-1); } const createHandleHiddenInputChange = @@ -344,7 +344,7 @@ export default function useSlider(parameters: UseSliderParameters) { } setValueState(newValue); - setFocusVisible(index); + setFocusedThumbIndex(index); if (handleChange) { handleChange(event, newValue, index); @@ -684,7 +684,7 @@ export default function useSlider(parameters: UseSliderParameters) { axis, axisProps, dragging, - focusVisible, + focusedThumbIndex, getHiddenInputProps, getRootProps, getThumbProps, From 884faaeb41020448445e4e0145c33091af184414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Thu, 16 Jun 2022 14:13:31 +0200 Subject: [PATCH 6/9] Handle functional componentsProps in Joy's Slider --- packages/mui-base/src/utils/index.ts | 1 + packages/mui-joy/src/Slider/Slider.tsx | 31 +++++++++++++------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/mui-base/src/utils/index.ts b/packages/mui-base/src/utils/index.ts index 628e03491d72b9..0823837f41e0de 100644 --- a/packages/mui-base/src/utils/index.ts +++ b/packages/mui-base/src/utils/index.ts @@ -2,5 +2,6 @@ export { default as appendOwnerState } from './appendOwnerState'; export { default as areArraysEqual } from './areArraysEqual'; export { default as extractEventHandlers } from './extractEventHandlers'; export { default as isHostComponent } from './isHostComponent'; +export { default as resolveComponentProps } from './resolveComponentProps'; export { default as useSlotProps } from './useSlotProps'; export * from './types'; diff --git a/packages/mui-joy/src/Slider/Slider.tsx b/packages/mui-joy/src/Slider/Slider.tsx index a78a0b41edee3c..dfa83a0a2c47dc 100644 --- a/packages/mui-joy/src/Slider/Slider.tsx +++ b/packages/mui-joy/src/Slider/Slider.tsx @@ -7,6 +7,7 @@ import { SliderUnstyled, SliderValueLabelUnstyled, unstable_composeClasses as composeClasses, + resolveComponentProps, } from '@mui/base'; import { Theme, useThemeProps } from '../styles'; import styled from '../styles/styled'; @@ -393,37 +394,37 @@ const Slider = React.forwardRef(function Slider(inProps, ref) { }} componentsProps={{ ...componentsProps, - root: { - ...componentsProps.root, + root: (os) => ({ + ...resolveComponentProps(componentsProps.root, { ...os, ...ownerState }), ...(shouldSpreadAdditionalProps(components.Root) && { as: component, ownerState, }), - }, - thumb: { - ...componentsProps.thumb, + }), + thumb: (os) => ({ + ...resolveComponentProps(componentsProps.thumb, { ...os, ...ownerState }), ...(shouldSpreadAdditionalProps(components.Thumb) && { ownerState, }), - }, - track: { - ...componentsProps.track, + }), + track: (os) => ({ + ...resolveComponentProps(componentsProps.track, { ...os, ...ownerState }), ...(shouldSpreadAdditionalProps(components.Track) && { ownerState, }), - }, - valueLabel: { - ...componentsProps.valueLabel, + }), + valueLabel: (os) => ({ + ...resolveComponentProps(componentsProps.valueLabel, { ...os, ...ownerState }), ...(shouldSpreadAdditionalProps(components.ValueLabel) && { ownerState, }), - }, - markLabel: { - ...componentsProps.markLabel, + }), + markLabel: (os) => ({ + ...resolveComponentProps(componentsProps.markLabel, { ...os, ...ownerState }), ...(shouldSpreadAdditionalProps(components.ValueLabel) && { ownerState, }), - }, + }), }} /> ); From d3cb7c33465d73d51ca0499f0480011044ed3e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Tue, 21 Jun 2022 14:30:54 +0200 Subject: [PATCH 7/9] Add data-focusvisible attribute to thumbs --- packages/mui-base/src/SliderUnstyled/SliderUnstyled.js | 1 + packages/mui-base/src/SliderUnstyled/SliderUnstyled.spec.tsx | 2 +- packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts | 1 + packages/mui-base/src/SliderUnstyled/useSlider.ts | 5 +---- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js index 212a7705c5fcbc..94b862078f7b20 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.js @@ -265,6 +265,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { > , ) { - const { 'data-index': index, ownerState, ...other } = props; + const { 'data-index': index, 'data-focusvisible': focusVisible, ownerState, ...other } = props; return

; }); diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts index 78eacfdf70e1c5..65cf9c7d56e28a 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.types.ts @@ -286,6 +286,7 @@ export type SliderUnstyledRailSlotProps = { export type SliderUnstyledThumbSlotProps = UseSliderThumbSlotProps & { 'data-index': number; + 'data-focusvisible': boolean; children: React.ReactNode; className?: string; ownerState: SliderUnstyledOwnerState; diff --git a/packages/mui-base/src/SliderUnstyled/useSlider.ts b/packages/mui-base/src/SliderUnstyled/useSlider.ts index afcc9176a1c8a7..224e73acd37c53 100644 --- a/packages/mui-base/src/SliderUnstyled/useSlider.ts +++ b/packages/mui-base/src/SliderUnstyled/useSlider.ts @@ -633,13 +633,10 @@ export default function useSlider(parameters: UseSliderParameters) { onMouseLeave: createHandleMouseLeave(otherHandlers || {}), }; - const mergedEventHandlers = { + return { ...otherHandlers, ...ownEventHandlers, }; - return { - ...mergedEventHandlers, - }; }; const getHiddenInputProps = ( From 1ce900b6a7c5ec476aef719d2135778aed1bbad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Thu, 23 Jun 2022 08:31:23 +0200 Subject: [PATCH 8/9] proptypes --- packages/mui-joy/src/Slider/Slider.tsx | 4 ---- packages/mui-joy/src/Slider/SliderProps.ts | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/mui-joy/src/Slider/Slider.tsx b/packages/mui-joy/src/Slider/Slider.tsx index 95d693ecafbb1f..3771347cd59211 100644 --- a/packages/mui-joy/src/Slider/Slider.tsx +++ b/packages/mui-joy/src/Slider/Slider.tsx @@ -629,10 +629,6 @@ Slider.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 those theme colors that make sense for this component. * @default 'primary' diff --git a/packages/mui-joy/src/Slider/SliderProps.ts b/packages/mui-joy/src/Slider/SliderProps.ts index 63ace936d5c7c9..15816b124b096f 100644 --- a/packages/mui-joy/src/Slider/SliderProps.ts +++ b/packages/mui-joy/src/Slider/SliderProps.ts @@ -21,6 +21,10 @@ export interface SliderPropsSizeOverrides {} export interface SliderComponentsPropsOverrides {} interface SliderOwnProps { + /** + * The props used for each slot inside the Slider. + * @default {} + */ componentsProps?: { root?: SlotComponentProps<'span', SliderComponentsPropsOverrides, SliderOwnerState>; track?: SlotComponentProps<'span', SliderComponentsPropsOverrides, SliderOwnerState>; From c99ea5f1ea66f5b12bb4bfcfffe3cbbc58745827 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 9/9] 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 ++++-- packages/mui-joy/src/Slider/Slider.tsx | 74 +++++++++---------- 8 files changed, 137 insertions(+), 70 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 eba03aa8899c25..f219be2eff284a 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, WithCommonProps } 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, WithCommonProps, diff --git a/packages/mui-joy/src/Slider/Slider.tsx b/packages/mui-joy/src/Slider/Slider.tsx index 3771347cd59211..4499cd86d4329b 100644 --- a/packages/mui-joy/src/Slider/Slider.tsx +++ b/packages/mui-joy/src/Slider/Slider.tsx @@ -65,8 +65,8 @@ const SliderRoot = styled('span', { name: 'JoySlider', slot: 'Root', overridesResolver: (props, styles) => styles.root, -})<{ ownerState?: SliderOwnerState }>(({ theme, ownerState }) => { - const getColorVariables = sliderColorVariables({ theme, ownerState: ownerState! }); +})<{ ownerState: SliderOwnerState }>(({ theme, ownerState }) => { + const getColorVariables = sliderColorVariables({ theme, ownerState }); return [ { '--Slider-size': 'max(42px, max(var(--Slider-thumb-size), var(--Slider-track-size)))', // Reach 42px touch target, about ~8mm on screen. @@ -75,19 +75,19 @@ const SliderRoot = styled('span', { [`& .${sliderClasses.markActive}`]: { '--Slider-mark-background': 'var(--Slider-track-color)', }, - ...(ownerState!.size === 'sm' && { + ...(ownerState.size === 'sm' && { '--Slider-mark-size': '2px', '--Slider-track-size': '4px', '--Slider-thumb-size': '10px', '--Slider-valueLabel-arrowSize': '6px', }), - ...(ownerState!.size === 'md' && { + ...(ownerState.size === 'md' && { '--Slider-mark-size': '2px', '--Slider-track-size': '6px', '--Slider-thumb-size': '14px', '--Slider-valueLabel-arrowSize': '8px', }), - ...(ownerState!.size === 'lg' && { + ...(ownerState.size === 'lg' && { '--Slider-mark-size': '3px', '--Slider-track-size': '10px', '--Slider-thumb-size': '20px', @@ -115,11 +115,11 @@ const SliderRoot = styled('span', { cursor: 'pointer', touchAction: 'none', WebkitTapHighlightColor: 'transparent', - ...(ownerState!.orientation === 'horizontal' && { + ...(ownerState.orientation === 'horizontal' && { padding: 'calc(var(--Slider-size) / 2) 0', width: '100%', }), - ...(ownerState!.orientation === 'vertical' && { + ...(ownerState.orientation === 'vertical' && { padding: '0 calc(var(--Slider-size) / 2)', height: '100%', }), @@ -134,30 +134,30 @@ const SliderRail = styled('span', { name: 'JoySlider', slot: 'Rail', overridesResolver: (props, styles) => styles.rail, -})<{ ownerState?: SliderOwnerState }>(({ ownerState }) => [ +})<{ ownerState: SliderOwnerState }>(({ ownerState }) => [ { display: 'block', position: 'absolute', backgroundColor: - ownerState!.track === 'inverted' + ownerState.track === 'inverted' ? 'var(--Slider-track-background)' : 'var(--Slider-rail-background)', borderRadius: 'var(--Slider-track-radius)', - ...(ownerState!.orientation === 'horizontal' && { + ...(ownerState.orientation === 'horizontal' && { height: 'var(--Slider-track-size)', top: '50%', left: 0, right: 0, transform: 'translateY(-50%)', }), - ...(ownerState!.orientation === 'vertical' && { + ...(ownerState.orientation === 'vertical' && { width: 'var(--Slider-track-size)', top: 0, bottom: 0, left: '50%', transform: 'translateX(-50%)', }), - ...(ownerState!.track === 'inverted' && { + ...(ownerState.track === 'inverted' && { opacity: 1, }), }, @@ -167,32 +167,32 @@ const SliderTrack = styled('span', { name: 'JoySlider', slot: 'Track', overridesResolver: (props, styles) => styles.track, -})<{ ownerState?: SliderOwnerState }>(({ ownerState }) => { +})<{ ownerState: SliderOwnerState }>(({ ownerState }) => { return [ { display: 'block', position: 'absolute', color: 'var(--Slider-track-color)', backgroundColor: - ownerState!.track === 'inverted' + ownerState.track === 'inverted' ? 'var(--Slider-rail-background)' : 'var(--Slider-track-background)', // TODO: discuss the transition approach in a separate PR. This value is copied from mui-material Slider. transition: 'left 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, width 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, bottom 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, height 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', - ...(ownerState!.orientation === 'horizontal' && { + ...(ownerState.orientation === 'horizontal' && { height: 'var(--Slider-track-size)', top: '50%', transform: 'translateY(-50%)', borderRadius: 'var(--Slider-track-radius) 0 0 var(--Slider-track-radius)', }), - ...(ownerState!.orientation === 'vertical' && { + ...(ownerState.orientation === 'vertical' && { width: 'var(--Slider-track-size)', left: '50%', transform: 'translateX(-50%)', borderRadius: '0 0 var(--Slider-track-radius) var(--Slider-track-radius)', }), - ...(ownerState!.track === false && { + ...(ownerState.track === false && { display: 'none', }), }, @@ -203,7 +203,7 @@ const SliderThumb = styled('span', { name: 'JoySlider', slot: 'Thumb', overridesResolver: (props, styles) => styles.thumb, -})<{ ownerState?: SliderOwnerState }>(({ ownerState, theme }) => ({ +})<{ ownerState: SliderOwnerState }>(({ ownerState, theme }) => ({ position: 'absolute', boxSizing: 'border-box', outline: 0, @@ -222,11 +222,11 @@ const SliderThumb = styled('span', { transition: 'box-shadow 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,left 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,bottom 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', [theme.focus.selector]: theme.focus.default, - ...(ownerState!.orientation === 'horizontal' && { + ...(ownerState.orientation === 'horizontal' && { top: '50%', transform: 'translate(-50%, -50%)', }), - ...(ownerState!.orientation === 'vertical' && { + ...(ownerState.orientation === 'vertical' && { left: '50%', transform: 'translate(-50%, 50%)', }), @@ -236,30 +236,30 @@ const SliderMark = styled('span', { name: 'JoySlider', slot: 'Mark', overridesResolver: (props, styles) => styles.mark, -})<{ ownerState?: SliderOwnerState & { percent: number } }>(({ ownerState }) => { +})<{ ownerState: SliderOwnerState & { percent: number } }>(({ ownerState }) => { return { position: 'absolute', width: 'var(--Slider-mark-size)', height: 'var(--Slider-mark-size)', borderRadius: 'var(--Slider-mark-size)', backgroundColor: 'var(--Slider-mark-background)', - ...(ownerState!.orientation === 'horizontal' && { + ...(ownerState.orientation === 'horizontal' && { top: '50%', transform: `translate(calc(var(--Slider-mark-size) / -2), -50%)`, - ...(ownerState!.percent === 0 && { + ...(ownerState.percent === 0 && { transform: `translate(min(var(--Slider-mark-size), 3px), -50%)`, }), - ...(ownerState!.percent === 100 && { + ...(ownerState.percent === 100 && { transform: `translate(calc(var(--Slider-mark-size) * -1 - min(var(--Slider-mark-size), 3px)), -50%)`, }), }), - ...(ownerState!.orientation === 'vertical' && { + ...(ownerState.orientation === 'vertical' && { left: '50%', transform: 'translate(-50%, calc(var(--Slider-mark-size) / 2))', - ...(ownerState!.percent === 0 && { + ...(ownerState.percent === 0 && { transform: `translate(-50%, calc(min(var(--Slider-mark-size), 3px) * -1))`, }), - ...(ownerState!.percent === 100 && { + ...(ownerState.percent === 100 && { transform: `translate(-50%, calc(var(--Slider-mark-size) * 1 + min(var(--Slider-mark-size), 3px)))`, }), }), @@ -270,20 +270,20 @@ const SliderValueLabel = styled('span', { name: 'JoySlider', slot: 'ValueLabel', overridesResolver: (props, styles) => styles.valueLabel, -})<{ ownerState?: SliderOwnerState }>(({ theme, ownerState }) => ({ - ...(ownerState!.size === 'sm' && { +})<{ ownerState: SliderOwnerState }>(({ theme, ownerState }) => ({ + ...(ownerState.size === 'sm' && { fontSize: theme.fontSize.xs, lineHeight: theme.lineHeight.md, paddingInline: '0.25rem', minWidth: '20px', }), - ...(ownerState!.size === 'md' && { + ...(ownerState.size === 'md' && { fontSize: theme.fontSize.sm, lineHeight: theme.lineHeight.md, paddingInline: '0.375rem', minWidth: '24px', }), - ...(ownerState!.size === 'lg' && { + ...(ownerState.size === 'lg' && { fontSize: theme.fontSize.md, lineHeight: theme.lineHeight.md, paddingInline: '0.5rem', @@ -330,25 +330,25 @@ const SliderMarkLabel = styled('span', { name: 'JoySlider', slot: 'MarkLabel', overridesResolver: (props, styles) => styles.markLabel, -})<{ ownerState?: SliderOwnerState }>(({ theme, ownerState }) => ({ +})<{ ownerState: SliderOwnerState }>(({ theme, ownerState }) => ({ fontFamily: theme.vars.fontFamily.body, - ...(ownerState!.size === 'sm' && { + ...(ownerState.size === 'sm' && { fontSize: theme.vars.fontSize.xs, }), - ...(ownerState!.size === 'md' && { + ...(ownerState.size === 'md' && { fontSize: theme.vars.fontSize.sm, }), - ...(ownerState!.size === 'lg' && { + ...(ownerState.size === 'lg' && { fontSize: theme.vars.fontSize.md, }), color: theme.palette.text.tertiary, position: 'absolute', whiteSpace: 'nowrap', - ...(ownerState!.orientation === 'horizontal' && { + ...(ownerState.orientation === 'horizontal' && { top: 'calc(50% + 4px + (max(var(--Slider-track-size), var(--Slider-thumb-size)) / 2))', transform: 'translateX(-50%)', }), - ...(ownerState!.orientation === 'vertical' && { + ...(ownerState.orientation === 'vertical' && { left: 'calc(50% + 8px + (max(var(--Slider-track-size), var(--Slider-thumb-size)) / 2))', transform: 'translateY(50%)', }),