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 9a10794dc7df11..4536cbe2c1cb26 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, @@ -101,7 +100,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { active, axis, range, - focusVisible, + focusedThumbIndex, dragging, marks, values, @@ -111,50 +110,84 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { ownerState.marked = marks.length > 0 && marks.some((mark) => mark.label); ownerState.dragging = dragging; + ownerState.focusedThumbIndex = focusedThumbIndex; + + 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, + className: classes.mark, + }); 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) => { @@ -185,7 +218,7 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { markActive, })} style={{ ...style, ...markProps.style }} - className={clsx(classes.mark, markProps.className, { + className={clsx(markProps.className, { [classes.markActive]: markActive, })} /> @@ -233,11 +266,11 @@ const SliderUnstyled = React.forwardRef(function SliderUnstyled(props, ref) { > @@ -346,24 +371,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. @@ -447,10 +475,6 @@ SliderUnstyled.propTypes /* remove-proptypes */ = { * @param {number | number[]} value The new value. */ onChangeCommitted: PropTypes.func, - /** - * @ignore - */ - onMouseDown: PropTypes.func, /** * The component orientation. * @default 'horizontal' diff --git a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.spec.tsx b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.spec.tsx index 48ca367482c47c..d94110e23042f1 100644 --- a/packages/mui-base/src/SliderUnstyled/SliderUnstyled.spec.tsx +++ b/packages/mui-base/src/SliderUnstyled/SliderUnstyled.spec.tsx @@ -38,7 +38,7 @@ const Thumb = React.forwardRef(function Thumb( props: SliderUnstyledThumbSlotProps, ref: React.ForwardedRef, ) { - 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 355772f61783d4..65cf9c7d56e28a 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 { @@ -11,6 +12,7 @@ import { export type SliderUnstyledOwnerState = SliderUnstyledProps & { disabled: boolean; + focusedThumbIndex: number; isRtl: boolean; mark: boolean | Mark[]; max: number; @@ -32,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; } @@ -250,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 6622a043d93282..c60319d32c643e 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); @@ -633,13 +633,10 @@ export default function useSlider(parameters: UseSliderParameters) { onMouseLeave: createHandleMouseLeave(otherHandlers || {}), }; - const mergedEventHandlers = { + return { ...otherHandlers, ...ownEventHandlers, }; - return { - ...mergedEventHandlers, - }; }; const getHiddenInputProps = ( @@ -666,7 +663,7 @@ export default function useSlider(parameters: UseSliderParameters) { type: 'range', min: parameters.min, max: parameters.max, - step: parameters.step, + step: parameters.step ?? undefined, disabled, ...mergedEventHandlers, style: { @@ -684,7 +681,7 @@ export default function useSlider(parameters: UseSliderParameters) { axis: axis as keyof typeof axisProps, axisProps, dragging, - focusVisible, + focusedThumbIndex, getHiddenInputProps, getRootProps, getThumbProps, diff --git a/packages/mui-base/src/SliderUnstyled/useSlider.types.ts b/packages/mui-base/src/SliderUnstyled/useSlider.types.ts index 324b781fb3621f..2742b4c5795cf1 100644 --- a/packages/mui-base/src/SliderUnstyled/useSlider.types.ts +++ b/packages/mui-base/src/SliderUnstyled/useSlider.types.ts @@ -51,7 +51,7 @@ type UseSliderHiddenInputOwnProps = { onBlur: React.FocusEventHandler; onChange: React.ChangeEventHandler; onFocus: React.FocusEventHandler; - step?: number | null; + step?: number; style: React.CSSProperties; tabIndex?: number; type?: React.InputHTMLAttributes['type']; 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-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 8092cd32d6fad1..59f7c4205b47f9 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 e544b6ae089646..f219be2eff284a 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, { AppendOwnerStateReturnType } from './appendOwnerState'; -import mergeSlotProps, { MergeSlotPropsParameters, WithRef } from './mergeSlotProps'; +import mergeSlotProps, { MergeSlotPropsParameters, WithCommonProps } from './mergeSlotProps'; import resolveComponentProps from './resolveComponentProps'; export type UseSlotPropsParameters< @@ -67,8 +67,8 @@ export default function useSlotProps< ElementType, SlotProps, ExternalForwardedProps, - WithRef, - WithRef, + WithCommonProps, + WithCommonProps, OwnerState >, ) { diff --git a/packages/mui-joy/src/Slider/Slider.tsx b/packages/mui-joy/src/Slider/Slider.tsx index 7833cc462133c0..8f3cf7570e699b 100644 --- a/packages/mui-joy/src/Slider/Slider.tsx +++ b/packages/mui-joy/src/Slider/Slider.tsx @@ -7,21 +7,17 @@ import { } from '@mui/utils'; import { OverridableComponent } from '@mui/types'; import { useSlider } from '@mui/base/SliderUnstyled'; +import { useSlotProps } from '@mui/base/utils'; import { useThemeProps, styled, Theme } from '../styles'; import sliderClasses, { getSliderUtilityClass } from './sliderClasses'; -import { SliderProps, SliderTypeMap } from './SliderProps'; - -type OwnerState = SliderProps & { - dragging: boolean; - marked: boolean; -}; +import { SliderProps, SliderTypeMap, SliderOwnerState } from './SliderProps'; const valueToPercent = (value: number, min: number, max: number) => ((value - min) * 100) / (max - min); const Identity = (x: any) => x; -const useUtilityClasses = (ownerState: OwnerState) => { +const useUtilityClasses = (ownerState: SliderOwnerState) => { const { disabled, dragging, marked, orientation, track, color, size } = ownerState; const slots = { @@ -69,7 +65,7 @@ const SliderRoot = styled('span', { name: 'JoySlider', slot: 'Root', overridesResolver: (props, styles) => styles.root, -})<{ ownerState: SliderProps }>(({ theme, ownerState }) => { +})<{ ownerState: SliderOwnerState }>(({ theme, ownerState }) => { const getColorVariables = sliderColorVariables({ theme, ownerState }); return [ { @@ -138,7 +134,7 @@ const SliderRail = styled('span', { name: 'JoySlider', slot: 'Rail', overridesResolver: (props, styles) => styles.rail, -})<{ ownerState: SliderProps }>(({ ownerState }) => [ +})<{ ownerState: SliderOwnerState }>(({ ownerState }) => [ { display: 'block', position: 'absolute', @@ -171,7 +167,7 @@ const SliderTrack = styled('span', { name: 'JoySlider', slot: 'Track', overridesResolver: (props, styles) => styles.track, -})<{ ownerState: SliderProps }>(({ ownerState }) => { +})<{ ownerState: SliderOwnerState }>(({ ownerState }) => { return [ { display: 'block', @@ -207,7 +203,7 @@ const SliderThumb = styled('span', { name: 'JoySlider', slot: 'Thumb', overridesResolver: (props, styles) => styles.thumb, -})<{ ownerState: SliderProps }>(({ ownerState, theme }) => ({ +})<{ ownerState: SliderOwnerState }>(({ ownerState, theme }) => ({ position: 'absolute', boxSizing: 'border-box', outline: 0, @@ -240,7 +236,7 @@ const SliderMark = styled('span', { name: 'JoySlider', slot: 'Mark', overridesResolver: (props, styles) => styles.mark, -})<{ ownerState: SliderProps & { percent: number } }>(({ ownerState }) => { +})<{ ownerState: SliderOwnerState & { percent: number } }>(({ ownerState }) => { return { position: 'absolute', width: 'var(--Slider-mark-size)', @@ -274,7 +270,7 @@ const SliderValueLabel = styled('span', { name: 'JoySlider', slot: 'ValueLabel', overridesResolver: (props, styles) => styles.valueLabel, -})<{ ownerState: SliderProps }>(({ theme, ownerState }) => ({ +})<{ ownerState: SliderOwnerState }>(({ theme, ownerState }) => ({ ...(ownerState.size === 'sm' && { fontSize: theme.fontSize.xs, lineHeight: theme.lineHeight.md, @@ -334,7 +330,7 @@ const SliderMarkLabel = styled('span', { name: 'JoySlider', slot: 'MarkLabel', overridesResolver: (props, styles) => styles.markLabel, -})<{ ownerState: SliderProps }>(({ theme, ownerState }) => ({ +})<{ ownerState: SliderOwnerState }>(({ theme, ownerState }) => ({ fontFamily: theme.vars.fontFamily.body, ...(ownerState.size === 'sm' && { fontSize: theme.vars.fontSize.xs, @@ -362,7 +358,7 @@ const SliderInput = styled('input', { name: 'JoySlider', slot: 'Input', overridesResolver: (props, styles) => styles.input, -})<{ ownerState: SliderProps }>({}); +})<{ ownerState?: SliderOwnerState }>({}); const Slider = React.forwardRef(function Slider(inProps, ref) { const props = useThemeProps({ @@ -373,7 +369,6 @@ const Slider = React.forwardRef(function Slider(inProps, ref) { const { 'aria-label': ariaLabel, 'aria-valuetext': ariaValuetext, - className, component, componentsProps = {}, classes: classesProp, @@ -420,7 +415,7 @@ const Slider = React.forwardRef(function Slider(inProps, ref) { valueLabelFormat, color, size, - } as OwnerState; + } as SliderOwnerState; const { axisProps, @@ -430,8 +425,8 @@ const Slider = React.forwardRef(function Slider(inProps, ref) { open, active, axis, + focusedThumbIndex, range, - focusVisible, dragging, marks, values, @@ -447,29 +442,77 @@ const Slider = React.forwardRef(function Slider(inProps, ref) { ...axisProps[axis].leap(trackLeap), }; - const hiddenInputProps = getHiddenInputProps(); - const classes = useUtilityClasses(ownerState); + const rootProps = useSlotProps({ + elementType: SliderRoot, + getSlotProps: getRootProps, + externalSlotProps: componentsProps.root, + externalForwardedProps: other, + additionalProps: { + as: component, + }, + ownerState, + className: classes.root, + }); + + const railProps = useSlotProps({ + elementType: SliderRail, + externalSlotProps: componentsProps.rail, + ownerState, + className: classes.rail, + }); + + const trackProps = useSlotProps({ + elementType: SliderTrack, + externalSlotProps: componentsProps.track, + additionalProps: { + style: trackStyle, + }, + ownerState, + className: classes.track, + }); + + const markProps = useSlotProps({ + elementType: SliderMark, + externalSlotProps: componentsProps.mark, + ownerState, + className: classes.mark, + }); + + const markLabelProps = useSlotProps({ + elementType: SliderMarkLabel, + externalSlotProps: componentsProps.markLabel, + ownerState, + className: classes.markLabel, + }); + + const thumbProps = useSlotProps({ + elementType: SliderThumb, + getSlotProps: getThumbProps, + externalSlotProps: componentsProps.thumb, + ownerState, + className: classes.thumb, + }); + + const inputProps = useSlotProps({ + elementType: SliderInput, + getSlotProps: getHiddenInputProps, + externalSlotProps: componentsProps.input, + ownerState, + }); + + const valueLabelProps = useSlotProps({ + elementType: SliderValueLabel, + externalSlotProps: componentsProps.valueLabel, + ownerState, + className: classes.valueLabel, + }); + return ( - - - + + + {marks .filter((mark) => mark.value >= min && mark.value <= max) .map((mark, index) => { @@ -495,10 +538,10 @@ const Slider = React.forwardRef(function Slider(inProps, ref) { @@ -506,10 +549,10 @@ const Slider = React.forwardRef(function Slider(inProps, ref) { @@ -526,23 +569,18 @@ const Slider = React.forwardRef(function Slider(inProps, ref) { - {/* @ts-expect-error TODO: revisit the null type in useSlider */} {valueLabelDisplay !== 'off' ? ( ; + track?: SlotComponentProps<'span', SliderComponentsPropsOverrides, SliderOwnerState>; + rail?: SlotComponentProps<'span', SliderComponentsPropsOverrides, SliderOwnerState>; + thumb?: SlotComponentProps<'span', SliderComponentsPropsOverrides, SliderOwnerState>; + mark?: SlotComponentProps<'span', SliderComponentsPropsOverrides, SliderOwnerState>; + markLabel?: SlotComponentProps<'span', SliderComponentsPropsOverrides, SliderOwnerState>; + valueLabel?: SlotComponentProps< + typeof SliderValueLabelUnstyled, + SliderComponentsPropsOverrides, + SliderOwnerState + >; + + input?: SlotComponentProps<'input', SliderComponentsPropsOverrides, SliderOwnerState>; + }; + /** + * The color of the component. It supports those theme colors that make sense for this component. + * @default 'primary' + */ + color?: OverridableStringUnion; + /** + * The size of the component. + * It accepts theme values between 'sm' and 'lg'. + * @default 'md' + */ + size?: OverridableStringUnion<'sm' | 'md' | 'lg', SliderPropsSizeOverrides>; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; +} + export type SliderTypeMap< D extends React.ElementType = 'span', P = {}, > = ExtendSliderUnstyledTypeMap<{ - props: P & { - /** - * The color of the component. It supports those theme colors that make sense for this component. - * @default 'primary' - */ - color?: OverridableStringUnion; - /** - * The size of the component. - * It accepts theme values between 'sm' and 'lg'. - * @default 'md' - */ - size?: OverridableStringUnion<'sm' | 'md' | 'lg', SliderPropsSizeOverrides>; - /** - * The system prop that allows defining system overrides as well as additional CSS styles. - */ - sx?: SxProps; - }; + props: P & SliderOwnProps; defaultComponent: D; }>; @@ -45,3 +69,8 @@ export type SliderProps< D extends React.ElementType = SliderTypeMap['defaultComponent'], P = { component?: React.ElementType }, > = OverrideProps, D>; + +export type SliderOwnerState = SliderProps & { + dragging: boolean; + marked: boolean; +}; diff --git a/packages/mui-material/src/Slider/Slider.js b/packages/mui-material/src/Slider/Slider.js index 8453781872dff2..be38a6ebf021f2 100644 --- a/packages/mui-material/src/Slider/Slider.js +++ b/packages/mui-material/src/Slider/Slider.js @@ -618,24 +618,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.