diff --git a/docs/pages/material-ui/api/slider.json b/docs/pages/material-ui/api/slider.json index aa9ca8c8932c9e..3b9b5f1c2e8379 100644 --- a/docs/pages/material-ui/api/slider.json +++ b/docs/pages/material-ui/api/slider.json @@ -137,7 +137,7 @@ "spread": true, "forwardsRefTo": "HTMLSpanElement", "filename": "/packages/mui-material/src/Slider/Slider.js", - "inheritance": { "component": "SliderUnstyled", "pathname": "/base/api/slider-unstyled/" }, + "inheritance": null, "demos": "", "cssComponent": false } diff --git a/packages/mui-material/src/Slider/Slider.d.ts b/packages/mui-material/src/Slider/Slider.d.ts index ddf13dd5ff2416..1922ef01e6b711 100644 --- a/packages/mui-material/src/Slider/Slider.d.ts +++ b/packages/mui-material/src/Slider/Slider.d.ts @@ -84,7 +84,7 @@ export type SliderTypeMap< defaultComponent: D; }>; -export { SliderValueLabelProps } from '@mui/base/SliderUnstyled'; +export { SliderValueLabelProps }; type SliderRootProps = NonNullable['root']; type SliderMarkProps = NonNullable['mark']; @@ -110,7 +110,6 @@ export declare const SliderValueLabel: React.FC; * API: * * - [Slider API](https://mui.com/material-ui/api/slider/) - * - inherits [SliderUnstyled API](https://mui.com/base/api/slider-unstyled/) */ declare const Slider: ExtendSliderUnstyled; diff --git a/packages/mui-material/src/Slider/Slider.js b/packages/mui-material/src/Slider/Slider.js index 24573b1243930d..abc9f58cf1b99f 100644 --- a/packages/mui-material/src/Slider/Slider.js +++ b/packages/mui-material/src/Slider/Slider.js @@ -1,33 +1,25 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; +import { chainPropTypes } from '@mui/utils'; import { - chainPropTypes, - unstable_generateUtilityClasses as generateUtilityClasses, -} from '@mui/utils'; -import SliderUnstyled, { - SliderValueLabelUnstyled, - sliderUnstyledClasses, - getSliderUtilityClass, -} from '@mui/base/SliderUnstyled'; + isHostComponent, + useSlotProps, + unstable_composeClasses as composeClasses, +} from '@mui/base'; +import { useSlider, getSliderUtilityClass } from '@mui/base/SliderUnstyled'; import { alpha, lighten, darken } from '@mui/system'; import useThemeProps from '../styles/useThemeProps'; import styled, { slotShouldForwardProp } from '../styles/styled'; import useTheme from '../styles/useTheme'; import shouldSpreadAdditionalProps from '../utils/shouldSpreadAdditionalProps'; import capitalize from '../utils/capitalize'; +import SliderValueLabelComponent from './SliderValueLabel'; +import sliderClasses from './sliderClasses'; -export const sliderClasses = { - ...sliderUnstyledClasses, - ...generateUtilityClasses('MuiSlider', [ - 'colorPrimary', - 'colorSecondary', - 'thumbColorPrimary', - 'thumbColorSecondary', - 'sizeSmall', - 'thumbSizeSmall', - ]), -}; +const valueToPercent = (value, min, max) => ((value - min) * 100) / (max - min); + +const Identity = (x) => x; const SliderRoot = styled('span', { name: 'MuiSlider', @@ -304,7 +296,7 @@ SliderThumb.propTypes /* remove-proptypes */ = { export { SliderThumb }; -const SliderValueLabel = styled(SliderValueLabelUnstyled, { +const SliderValueLabel = styled(SliderValueLabelComponent, { name: 'MuiSlider', slot: 'ValueLabel', overridesResolver: (props, styles) => styles.valueLabel, @@ -460,28 +452,44 @@ SliderMarkLabel.propTypes /* remove-proptypes */ = { export { SliderMarkLabel }; -const extendUtilityClasses = (ownerState) => { - const { color, size, classes = {} } = ownerState; +const useUtilityClasses = (ownerState) => { + const { disabled, dragging, marked, orientation, track, classes, color, size } = ownerState; - return { - ...classes, - root: clsx( - classes.root, - getSliderUtilityClass(`color${capitalize(color)}`), - classes[`color${capitalize(color)}`], - size && getSliderUtilityClass(`size${capitalize(size)}`), - size && classes[`size${capitalize(size)}`], - ), - thumb: clsx( - classes.thumb, - getSliderUtilityClass(`thumbColor${capitalize(color)}`), - classes[`thumbColor${capitalize(color)}`], - size && getSliderUtilityClass(`thumbSize${capitalize(size)}`), - size && classes[`thumbSize${capitalize(size)}`], - ), + const slots = { + root: [ + 'root', + disabled && 'disabled', + dragging && 'dragging', + marked && 'marked', + orientation === 'vertical' && 'vertical', + track === 'inverted' && 'trackInverted', + track === false && 'trackFalse', + color && `color${capitalize(color)}`, + size && `size${capitalize(size)}`, + ], + rail: ['rail'], + track: ['track'], + mark: ['mark'], + markActive: ['markActive'], + markLabel: ['markLabel'], + markLabelActive: ['markLabelActive'], + valueLabel: ['valueLabel'], + thumb: [ + 'thumb', + disabled && 'disabled', + size && `thumbSize${capitalize(size)}`, + color && `thumbColor${capitalize(color)}`, + ], + active: ['active'], + disabled: ['disabled'], + focusVisible: ['focusVisible'], }; + + return composeClasses(slots, getSliderUtilityClass, classes); }; +const Forward = ({ children }) => children; + const Slider = React.forwardRef(function Slider(inputProps, ref) { const props = useThemeProps({ props: inputProps, name: 'MuiSlider' }); @@ -489,20 +497,82 @@ const Slider = React.forwardRef(function Slider(inputProps, ref) { const isRtl = theme.direction === 'rtl'; const { + 'aria-label': ariaLabel, + 'aria-valuetext': ariaValuetext, + 'aria-labelledby': ariaLabelledby, // eslint-disable-next-line react/prop-types component = 'span', components = {}, componentsProps = {}, color = 'primary', + classes: classesProp, + // eslint-disable-next-line react/prop-types + className, + disableSwap = false, + disabled = false, + getAriaLabel, + getAriaValueText, + marks: marksProp = false, + max = 100, + min = 0, + name, + onChange, + onChangeCommitted, + orientation = 'horizontal', size = 'medium', + step = 1, + scale = Identity, slotProps, slots, + tabIndex, + track = 'normal', + value: valueProp, + valueLabelDisplay = 'off', + valueLabelFormat = Identity, ...other } = props; - const ownerState = { ...props, color, size }; + const ownerState = { + ...props, + isRtl, + max, + min, + classes: classesProp, + disabled, + disableSwap, + orientation, + marks: marksProp, + color, + size, + step, + scale, + track, + valueLabelDisplay, + valueLabelFormat, + }; + + const { + axisProps, + getRootProps, + getHiddenInputProps, + getThumbProps, + open, + active, + axis, + focusedThumbIndex, + range, + dragging, + marks, + values, + trackOffset, + trackLeap, + } = useSlider({ ...ownerState, ref }); + + ownerState.marked = marks.length > 0 && marks.some((mark) => mark.label); + ownerState.dragging = dragging; + ownerState.focusedThumbIndex = focusedThumbIndex; - const classes = extendUtilityClasses(ownerState); + const classes = useUtilityClasses(ownerState); // support both `slots` and `components` for backward compatibility const RootSlot = slots?.root ?? components.Root ?? SliderRoot; @@ -512,7 +582,7 @@ const Slider = React.forwardRef(function Slider(inputProps, ref) { const ValueLabelSlot = slots?.valueLabel ?? components.ValueLabel ?? SliderValueLabel; const MarkSlot = slots?.mark ?? components.Mark ?? SliderMark; const MarkLabelSlot = slots?.markLabel ?? components.MarkLabel ?? SliderMarkLabel; - const InputSlot = slots?.input ?? components.Input; + const InputSlot = slots?.input ?? components.Input ?? 'input'; const rootSlotProps = slotProps?.root ?? componentsProps.root; const railSlotProps = slotProps?.rail ?? componentsProps.rail; @@ -523,55 +593,197 @@ const Slider = React.forwardRef(function Slider(inputProps, ref) { const markLabelSlotProps = slotProps?.markLabel ?? componentsProps.markLabel; const inputSlotProps = slotProps?.input ?? componentsProps.input; + const rootProps = useSlotProps({ + elementType: RootSlot, + getSlotProps: getRootProps, + externalSlotProps: rootSlotProps, + externalForwardedProps: other, + additionalProps: { + ...(shouldSpreadAdditionalProps(RootSlot) && { + as: component, + }), + }, + ownerState: { + ...ownerState, + ...rootSlotProps?.ownerState, + }, + className: [classes.root, className], + }); + + const railProps = useSlotProps({ + elementType: RailSlot, + externalSlotProps: railSlotProps, + ownerState, + className: classes.rail, + }); + + const trackProps = useSlotProps({ + elementType: TrackSlot, + externalSlotProps: trackSlotProps, + additionalProps: { + style: { + ...axisProps[axis].offset(trackOffset), + ...axisProps[axis].leap(trackLeap), + }, + }, + ownerState: { + ...ownerState, + ...trackSlotProps?.ownerState, + }, + className: classes.track, + }); + + const thumbProps = useSlotProps({ + elementType: ThumbSlot, + getSlotProps: getThumbProps, + externalSlotProps: thumbSlotProps, + ownerState: { + ...ownerState, + ...thumbSlotProps?.ownerState, + }, + }); + + const valueLabelProps = useSlotProps({ + elementType: ValueLabelSlot, + externalSlotProps: valueLabelSlotProps, + ownerState: { + ...ownerState, + ...valueLabelSlotProps?.ownerState, + }, + className: classes.valueLabel, + }); + + const markProps = useSlotProps({ + elementType: MarkSlot, + externalSlotProps: markSlotProps, + ownerState, + className: classes.mark, + }); + + const markLabelProps = useSlotProps({ + elementType: MarkLabelSlot, + externalSlotProps: markLabelSlotProps, + ownerState, + }); + + const inputSliderProps = useSlotProps({ + elementType: InputSlot, + getSlotProps: getHiddenInputProps, + externalSlotProps: inputSlotProps, + ownerState, + }); + return ( - + + + + {marks + .filter((mark) => mark.value >= min && mark.value <= max) + .map((mark, index) => { + const percent = valueToPercent(mark.value, min, max); + const style = axisProps[axis].offset(percent); + + let markActive; + if (track === false) { + markActive = values.indexOf(mark.value) !== -1; + } else { + markActive = + (track === 'normal' && + (range + ? mark.value >= values[0] && mark.value <= values[values.length - 1] + : mark.value <= values[0])) || + (track === 'inverted' && + (range + ? mark.value <= values[0] || mark.value >= values[values.length - 1] + : mark.value >= values[0])); + } + + return ( + + + {mark.label != null ? ( + + {mark.label} + + ) : null} + + ); + })} + {values.map((value, index) => { + const percent = valueToPercent(value, min, max); + const style = axisProps[axis].offset(percent); + + const ValueLabelComponent = valueLabelDisplay === 'off' ? Forward : ValueLabelSlot; + + return ( + + {/* TODO v6: Change component structure. It will help in avoiding the complicated React.cloneElement API added in SliderValueLabel component. Should be: Thumb -> Input, ValueLabel. Follow Joy UI's Slider structure. */} + + + + + + + ); + })} + ); }); diff --git a/packages/mui-material/src/Slider/Slider.test.js b/packages/mui-material/src/Slider/Slider.test.js index ea495be50ec8cc..d2d842e0106d13 100644 --- a/packages/mui-material/src/Slider/Slider.test.js +++ b/packages/mui-material/src/Slider/Slider.test.js @@ -33,7 +33,7 @@ describe('', () => { , () => ({ classes, - inheritComponent: SliderUnstyled, + inheritComponent: 'span', render, refInstanceof: window.HTMLSpanElement, muiName: 'MuiSlider', diff --git a/packages/mui-material/src/Slider/SliderValueLabel.tsx b/packages/mui-material/src/Slider/SliderValueLabel.tsx new file mode 100644 index 00000000000000..0b1ac785d0192f --- /dev/null +++ b/packages/mui-material/src/Slider/SliderValueLabel.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; +import { SliderValueLabelProps } from './SliderValueLabel.types'; +import sliderClasses from './sliderClasses'; + +const useValueLabelClasses = (props: SliderValueLabelProps) => { + const { open } = props; + + const utilityClasses = { + offset: clsx({ + [sliderClasses.valueLabelOpen]: open, + }), + circle: sliderClasses.valueLabelCircle, + label: sliderClasses.valueLabelLabel, + }; + + return utilityClasses; +}; + +/** + * @ignore - internal component. + */ +export default function SliderValueLabel(props: SliderValueLabelProps) { + const { children, className, value } = props; + const classes = useValueLabelClasses(props); + + return React.cloneElement( + children, + { + className: clsx(children.props.className), + }, + + {children.props.children} + + + {value} + + + , + ); +} + +SliderValueLabel.propTypes = { + children: PropTypes.element.isRequired, + className: PropTypes.string, + value: PropTypes.node, +}; diff --git a/packages/mui-material/src/Slider/SliderValueLabel.types.ts b/packages/mui-material/src/Slider/SliderValueLabel.types.ts new file mode 100644 index 00000000000000..21747c25bb3537 --- /dev/null +++ b/packages/mui-material/src/Slider/SliderValueLabel.types.ts @@ -0,0 +1,23 @@ +export interface SliderValueLabelProps { + children: React.ReactElement; + className?: string; + style?: React.CSSProperties; + /** + * If `true`, the value label is visible. + */ + open: boolean; + /** + * The value of the slider. + * For ranged sliders, provide an array with two values. + */ + value: 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'; +} diff --git a/packages/mui-material/src/Slider/index.d.ts b/packages/mui-material/src/Slider/index.d.ts index 006f966fe2404f..b367aa2014dc31 100644 --- a/packages/mui-material/src/Slider/index.d.ts +++ b/packages/mui-material/src/Slider/index.d.ts @@ -1,2 +1,3 @@ export { default } from './Slider'; export * from './Slider'; +export { default as sliderClasses } from './sliderClasses'; diff --git a/packages/mui-material/src/Slider/index.js b/packages/mui-material/src/Slider/index.js index 006f966fe2404f..b367aa2014dc31 100644 --- a/packages/mui-material/src/Slider/index.js +++ b/packages/mui-material/src/Slider/index.js @@ -1,2 +1,3 @@ export { default } from './Slider'; export * from './Slider'; +export { default as sliderClasses } from './sliderClasses'; diff --git a/packages/mui-material/src/Slider/sliderClasses.ts b/packages/mui-material/src/Slider/sliderClasses.ts new file mode 100644 index 00000000000000..36228115c31e2c --- /dev/null +++ b/packages/mui-material/src/Slider/sliderClasses.ts @@ -0,0 +1,16 @@ +import { unstable_generateUtilityClasses as generateUtilityClasses } from '@mui/utils'; +import { sliderUnstyledClasses } from '@mui/base/SliderUnstyled'; + +const sliderClasses = { + ...sliderUnstyledClasses, + ...generateUtilityClasses('MuiSlider', [ + 'colorPrimary', + 'colorSecondary', + 'thumbColorPrimary', + 'thumbColorSecondary', + 'sizeSmall', + 'thumbSizeSmall', + ]), +}; + +export default sliderClasses;