diff --git a/docs/data/joy/components/slider/EdgeLabelSlider.js b/docs/data/joy/components/slider/EdgeLabelSlider.js new file mode 100644 index 00000000000000..e9cff3393c1fe3 --- /dev/null +++ b/docs/data/joy/components/slider/EdgeLabelSlider.js @@ -0,0 +1,63 @@ +import * as React from 'react'; +import Box from '@mui/joy/Box'; +import Slider, { sliderClasses } from '@mui/joy/Slider'; + +function valueText(value) { + return `${value}°C`; +} + +export default function EdgeLabelSlider() { + return ( + + 'Amount'} + getAriaValueText={valueText} + marks={[ + { + value: 0, + label: '0°C', + }, + { + value: 100, + label: '100°C', + }, + ]} + valueLabelDisplay="on" + sx={{ + // Need both of the selectors to make it works on the server-side and client-side + [`& [style*="left:0%"], & [style*="left: 0%"]`]: { + [`&.${sliderClasses.markLabel}`]: { + transform: 'none', + }, + [`& .${sliderClasses.valueLabel}`]: { + left: 'calc(var(--Slider-thumb-size) / 2 - 2px)', // 2px is the thumb border width + borderBottomLeftRadius: 0, + '&::before': { + left: 0, + transform: 'translateY(100%)', + borderLeftColor: 'currentColor', + }, + }, + }, + [`& [style*="left:100%"], & [style*="left: 100%"]`]: { + [`&.${sliderClasses.markLabel}`]: { + transform: 'translateX(-100%)', + }, + [`& .${sliderClasses.valueLabel}`]: { + right: 'calc(var(--Slider-thumb-size) / 2 - 2px)', // 2px is the thumb border width + borderBottomRightRadius: 0, + '&::before': { + left: 'initial', + right: 0, + transform: 'translateY(100%)', + borderRightColor: 'currentColor', + }, + }, + }, + }} + /> + + ); +} diff --git a/docs/data/joy/components/slider/EdgeLabelSlider.tsx b/docs/data/joy/components/slider/EdgeLabelSlider.tsx new file mode 100644 index 00000000000000..506f92174f7386 --- /dev/null +++ b/docs/data/joy/components/slider/EdgeLabelSlider.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import Box from '@mui/joy/Box'; +import Slider, { sliderClasses } from '@mui/joy/Slider'; + +function valueText(value: number) { + return `${value}°C`; +} + +export default function EdgeLabelSlider() { + return ( + + 'Amount'} + getAriaValueText={valueText} + marks={[ + { + value: 0, + label: '0°C', + }, + { + value: 100, + label: '100°C', + }, + ]} + valueLabelDisplay="on" + sx={{ + // Need both of the selectors to make it works on the server-side and client-side + [`& [style*="left:0%"], & [style*="left: 0%"]`]: { + [`&.${sliderClasses.markLabel}`]: { + transform: 'none', + }, + [`& .${sliderClasses.valueLabel}`]: { + left: 'calc(var(--Slider-thumb-size) / 2 - 2px)', // 2px is the thumb border width + borderBottomLeftRadius: 0, + '&::before': { + left: 0, + transform: 'translateY(100%)', + borderLeftColor: 'currentColor', + }, + }, + }, + [`& [style*="left:100%"], & [style*="left: 100%"]`]: { + [`&.${sliderClasses.markLabel}`]: { + transform: 'translateX(-100%)', + }, + [`& .${sliderClasses.valueLabel}`]: { + right: 'calc(var(--Slider-thumb-size) / 2 - 2px)', // 2px is the thumb border width + borderBottomRightRadius: 0, + '&::before': { + left: 'initial', + right: 0, + transform: 'translateY(100%)', + borderRightColor: 'currentColor', + }, + }, + }, + }} + /> + + ); +} diff --git a/docs/data/joy/components/slider/slider.md b/docs/data/joy/components/slider/slider.md index 683aef8fc015bf..cf007b2052b50b 100644 --- a/docs/data/joy/components/slider/slider.md +++ b/docs/data/joy/components/slider/slider.md @@ -31,6 +31,12 @@ You can force the thumb label to be always visible with `valueLabelDisplay="on"` {{"demo": "AlwaysVisibleLabelSlider.js"}} +### Keep label at edges + +For horizontal slider on mobile viewports, the value label might be offset from the track. Apply the style to keep the label at the start/end edges: + +{{"demo": "EdgeLabelSlider.js"}} + ## Range slider By passing an array of values to the `value` prop, you can use the `Slider` to set the start and end of a range. diff --git a/packages/mui-base/src/SliderUnstyled/useSlider.ts b/packages/mui-base/src/SliderUnstyled/useSlider.ts index 0e57074be29cbd..6622a043d93282 100644 --- a/packages/mui-base/src/SliderUnstyled/useSlider.ts +++ b/packages/mui-base/src/SliderUnstyled/useSlider.ts @@ -681,14 +681,14 @@ export default function useSlider(parameters: UseSliderParameters) { return { active, - axis, + axis: axis as keyof typeof axisProps, axisProps, dragging, focusVisible, getHiddenInputProps, getRootProps, getThumbProps, - marks, + marks: marks as Mark[], open, range, trackLeap, diff --git a/packages/mui-joy/src/Slider/Slider.tsx b/packages/mui-joy/src/Slider/Slider.tsx index dfb53c73fdbd99..898e08cceb71f2 100644 --- a/packages/mui-joy/src/Slider/Slider.tsx +++ b/packages/mui-joy/src/Slider/Slider.tsx @@ -1,26 +1,35 @@ -import PropTypes from 'prop-types'; import * as React from 'react'; -import { OverridableComponent } from '@mui/types'; -import { unstable_capitalize as capitalize } from '@mui/utils'; -import { shouldForwardProp } from '@mui/system'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; import { - SliderUnstyled, - SliderValueLabelUnstyled, unstable_composeClasses as composeClasses, -} from '@mui/base'; -import { Theme, useThemeProps } from '../styles'; -import styled from '../styles/styled'; -import shouldSpreadAdditionalProps from '../utils/shouldSpreadAdditionalProps'; + unstable_capitalize as capitalize, +} from '@mui/utils'; +import { OverridableComponent } from '@mui/types'; +import { useSlider } from '@mui/base/SliderUnstyled'; +import { useThemeProps, styled, Theme } from '../styles'; import sliderClasses, { getSliderUtilityClass } from './sliderClasses'; import { SliderProps, SliderTypeMap } from './SliderProps'; -const useUtilityClasses = (ownerState: SliderProps) => { - const { disabled, orientation, track, size, color } = ownerState; +type OwnerState = SliderProps & { + dragging: boolean; + marked: boolean; +}; + +const valueToPercent = (value: number, min: number, max: number) => + ((value - min) * 100) / (max - min); + +const Identity = (x: any) => x; + +const useUtilityClasses = (ownerState: OwnerState) => { + const { disabled, dragging, marked, orientation, track, color, size } = ownerState; const slots = { root: [ 'root', disabled && 'disabled', + dragging && 'dragging', + marked && 'marked', orientation === 'vertical' && 'vertical', track === 'inverted' && 'trackInverted', track === false && 'trackFalse', @@ -30,9 +39,14 @@ const useUtilityClasses = (ownerState: SliderProps) => { rail: ['rail'], track: ['track'], mark: ['mark'], + markActive: ['markActive'], markLabel: ['markLabel'], + markLabelActive: ['markLabelActive'], valueLabel: ['valueLabel'], + valueLabelOpen: ['valueLabelOpen'], thumb: ['thumb', disabled && 'disabled'], + active: ['active'], + focusVisible: ['focusVisible'], }; return composeClasses(slots, getSliderUtilityClass, {}); @@ -193,81 +207,70 @@ const SliderThumb = styled('span', { name: 'JoySlider', slot: 'Thumb', overridesResolver: (props, styles) => styles.thumb, -})<{ ownerState: SliderProps }>(({ ownerState }) => { - return [ - { - position: 'absolute', - boxSizing: 'border-box', - outline: 0, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: 'var(--Slider-thumb-width)', - height: 'var(--Slider-thumb-size)', - borderRadius: 'var(--Slider-thumb-radius)', - boxShadow: 'var(--Slider-thumb-shadow)', - border: '2px solid', - borderColor: 'var(--Slider-thumb-color)', - color: 'var(--Slider-thumb-color)', - backgroundColor: 'var(--Slider-thumb-background)', - // TODO: discuss the transition approach in a separate PR. This value is copied from mui-material Slider. - 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', - ...(ownerState.orientation === 'horizontal' && { - top: '50%', - transform: 'translate(-50%, -50%)', - }), - ...(ownerState.orientation === 'vertical' && { - left: '50%', - transform: 'translate(-50%, 50%)', - }), - }, - ]; -}); +})<{ ownerState: SliderProps }>(({ ownerState, theme }) => ({ + position: 'absolute', + boxSizing: 'border-box', + outline: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 'var(--Slider-thumb-width)', + height: 'var(--Slider-thumb-size)', + borderRadius: 'var(--Slider-thumb-radius)', + boxShadow: 'var(--Slider-thumb-shadow)', + border: '2px solid', + borderColor: 'var(--Slider-thumb-color)', + color: 'var(--Slider-thumb-color)', + backgroundColor: 'var(--Slider-thumb-background)', + // TODO: discuss the transition approach in a separate PR. This value is copied from mui-material Slider. + 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' && { + top: '50%', + transform: 'translate(-50%, -50%)', + }), + ...(ownerState.orientation === 'vertical' && { + left: '50%', + transform: 'translate(-50%, 50%)', + }), +})); const SliderMark = styled('span', { name: 'JoySlider', slot: 'Mark', - // `markActive` is injected by SliderUnstyled, should not spread to DOM - shouldForwardProp: (prop) => shouldForwardProp(prop) && prop !== 'markActive', overridesResolver: (props, styles) => styles.mark, -})<{ ownerState: SliderProps; 'data-index': number; style: React.CSSProperties }>( - ({ ownerState, ...props }) => { - 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' && { - top: '50%', - transform: `translate(calc(var(--Slider-mark-size) / -2), -50%)`, - ...(props['data-index'] === 0 && { - // data-index is from SliderUnstyled - transform: `translate(min(var(--Slider-mark-size), 3px), -50%)`, - }), - ...(props.style?.left === '100%' && { - // workaround for detecting last mark - transform: `translate(calc(var(--Slider-mark-size) * -1 - min(var(--Slider-mark-size), 3px)), -50%)`, - }), +})<{ ownerState: SliderProps & { 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' && { + top: '50%', + transform: `translate(calc(var(--Slider-mark-size) / -2), -50%)`, + ...(ownerState.percent === 0 && { + transform: `translate(min(var(--Slider-mark-size), 3px), -50%)`, }), - ...(ownerState.orientation === 'vertical' && { - left: '50%', - transform: 'translate(-50%, calc(var(--Slider-mark-size) / 2))', - ...(props['data-index'] === 0 && { - // data-index is from SliderUnstyled - transform: `translate(-50%, min(var(--Slider-mark-size), 3px))`, - }), - ...(props.style?.left === '100%' && { - // workaround for detecting last mark - transform: `translate(-50%, calc(var(--Slider-mark-size) * -1 - min(var(--Slider-mark-size), 3px)))`, - }), + ...(ownerState.percent === 100 && { + transform: `translate(calc(var(--Slider-mark-size) * -1 - min(var(--Slider-mark-size), 3px)), -50%)`, }), - }; - }, -); + }), + ...(ownerState.orientation === 'vertical' && { + left: '50%', + transform: 'translate(-50%, calc(var(--Slider-mark-size) / 2))', + ...(ownerState.percent === 0 && { + transform: `translate(-50%, calc(min(var(--Slider-mark-size), 3px) * -1))`, + }), + ...(ownerState.percent === 100 && { + transform: `translate(-50%, calc(var(--Slider-mark-size) * 1 + min(var(--Slider-mark-size), 3px)))`, + }), + }), + }; +}); -const SliderValueLabel = styled(SliderValueLabelUnstyled, { +const SliderValueLabel = styled('span', { name: 'JoySlider', slot: 'ValueLabel', overridesResolver: (props, styles) => styles.valueLabel, @@ -297,7 +300,7 @@ const SliderValueLabel = styled(SliderValueLabelUnstyled, { whiteSpace: 'nowrap', fontFamily: theme.vars.fontFamily.body, fontWeight: theme.vars.fontWeight.md, - bottom: '2px', + bottom: 0, transformOrigin: 'bottom center', transform: 'translateY(calc((var(--Slider-thumb-size) + var(--Slider-valueLabel-arrowSize)) * -1)) scale(0)', @@ -310,28 +313,26 @@ const SliderValueLabel = styled(SliderValueLabelUnstyled, { display: 'var(--Slider-valueLabel-arrowDisplay)', position: 'absolute', content: '""', + color: theme.vars.palette.background.tooltip, bottom: 0, - width: 'var(--Slider-valueLabel-arrowSize)', - height: 'var(--Slider-valueLabel-arrowSize)', + border: 'calc(var(--Slider-valueLabel-arrowSize) / 2) solid', + borderColor: 'currentColor', + borderRightColor: 'transparent', + borderBottomColor: 'transparent', + borderLeftColor: 'transparent', left: '50%', - transform: 'translate(-50%, 50%) rotate(45deg)', - backgroundColor: 'inherit', + transform: 'translate(-50%, 100%)', + backgroundColor: 'transparent', }, [`&.${sliderClasses.valueLabelOpen}`]: { transform: 'translateY(calc((var(--Slider-thumb-size) + var(--Slider-valueLabel-arrowSize)) * -1)) scale(1)', }, - [`& .${sliderClasses.valueLabelCircle}`]: { - display: 'inline-flex', - zIndex: 1, - }, })); const SliderMarkLabel = styled('span', { name: 'JoySlider', slot: 'MarkLabel', - // `markLabelActive` is injected by SliderUnstyled, should not spread to DOM - shouldForwardProp: (prop) => shouldForwardProp(prop) && prop !== 'markLabelActive', overridesResolver: (props, styles) => styles.markLabel, })<{ ownerState: SliderProps }>(({ theme, ownerState }) => ({ fontFamily: theme.vars.fontFamily.body, @@ -348,21 +349,55 @@ const SliderMarkLabel = styled('span', { position: 'absolute', whiteSpace: 'nowrap', ...(ownerState.orientation === 'horizontal' && { - top: 'calc(50% + (max(var(--Slider-track-size), var(--Slider-thumb-size)) / 2))', + top: 'calc(50% + 4px + (max(var(--Slider-track-size), var(--Slider-thumb-size)) / 2))', transform: 'translateX(-50%)', }), ...(ownerState.orientation === 'vertical' && { - left: 36, + left: 'calc(50% + 8px + (max(var(--Slider-track-size), var(--Slider-thumb-size)) / 2))', transform: 'translateY(50%)', }), })); +const SliderInput = styled('input', { + name: 'JoySlider', + slot: 'Input', + overridesResolver: (props, styles) => styles.input, +})<{ ownerState: SliderProps }>({}); + const Slider = React.forwardRef(function Slider(inProps, ref) { - const props = useThemeProps({ props: inProps, name: 'JoySlider' }); + const props = useThemeProps({ + props: inProps, + name: 'JoySlider', + }); + const { - component = 'span', - components = {}, + 'aria-label': ariaLabel, + 'aria-valuetext': ariaValuetext, + className, + component, componentsProps = {}, + classes: classesProp, + disableSwap = false, + disabled = false, + defaultValue, + getAriaLabel, + getAriaValueText, + marks: marksProp = false, + max = 100, + min = 0, + name, + onChange, + onChangeCommitted, + onMouseDown, + orientation = 'horizontal', + scale = Identity, + step = 1, + tabIndex, + track = 'normal', + value: valueProp, + valueLabelDisplay = 'off', + valueLabelFormat = Identity, + isRtl = false, color = 'primary', size = 'md', ...other @@ -370,62 +405,173 @@ const Slider = React.forwardRef(function Slider(inProps, ref) { const ownerState = { ...props, - size, + marks: marksProp, + classes: classesProp, + disabled, + defaultValue, + isRtl, + max, + min, + orientation, + scale, + step, + track, + valueLabelDisplay, + valueLabelFormat, color, + size, + } as OwnerState; + + const { + axisProps, + getRootProps, + getHiddenInputProps, + getThumbProps, + open, + active, + axis, + range, + focusVisible, + dragging, + marks, + values, + trackOffset, + trackLeap, + } = useSlider({ ...ownerState, ref }); + + ownerState.marked = marks.length > 0 && marks.some((mark) => mark.label); + ownerState.dragging = dragging; + + const trackStyle = { + ...axisProps[axis].offset(trackOffset), + ...axisProps[axis].leap(trackLeap), }; + const hiddenInputProps = getHiddenInputProps(); + const classes = useUtilityClasses(ownerState); return ( - + {...getRootProps(onMouseDown ? { onMouseDown } : {})} + as={component} + ownerState={ownerState} + className={clsx(classes.root, className)} + > + + + {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); + return ( + + {/* @ts-expect-error TODO: revisit the null type in useSlider */} + + {valueLabelDisplay !== 'off' ? ( + + {value} + + ) : null} + + ); + })} + ); }) as OverridableComponent; @@ -434,10 +580,26 @@ Slider.propTypes /* remove-proptypes */ = { // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit TypeScript types and run "yarn proptypes" | // ---------------------------------------------------------------------- + /** + * The label of the slider. + */ + 'aria-label': PropTypes.string, + /** + * A string value that provides a user-friendly name for the current value of the slider. + */ + 'aria-valuetext': PropTypes.string, /** * @ignore */ children: PropTypes.node, + /** + * 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' @@ -448,21 +610,6 @@ Slider.propTypes /* remove-proptypes */ = { * Either a string to use a HTML element or a component. */ component: PropTypes.elementType, - /** - * The components used for each slot inside the Slider. - * Either a string to use a HTML element or a component. - * @default {} - */ - components: PropTypes.shape({ - Input: PropTypes.elementType, - Mark: PropTypes.elementType, - MarkLabel: PropTypes.elementType, - Rail: PropTypes.elementType, - Root: PropTypes.elementType, - Thumb: PropTypes.elementType, - Track: PropTypes.elementType, - ValueLabel: PropTypes.elementType, - }), /** * The props used for each slot inside the Slider. * @default {} @@ -487,12 +634,117 @@ Slider.propTypes /* remove-proptypes */ = { valueLabelDisplay: PropTypes.oneOf(['auto', 'off', 'on']), }), }), + /** + * The default value. Use when the component is not controlled. + */ + defaultValue: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]), + /** + * If `true`, the component is disabled. + * @default false + */ + disabled: PropTypes.bool, + /** + * If `true`, the active thumb doesn't swap when moving pointer over a thumb while dragging another thumb. + * @default false + */ + disableSwap: PropTypes.bool, + /** + * 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: PropTypes.func, + /** + * 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: PropTypes.func, + /** + * Indicates whether the theme context has rtl direction. It is set automatically. + * @default false + */ + isRtl: PropTypes.bool, + /** + * 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: PropTypes.oneOfType([ + PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.node, + value: PropTypes.number.isRequired, + }), + ), + PropTypes.bool, + ]), + /** + * The maximum allowed value of the slider. + * Should not be equal to min. + * @default 100 + */ + max: PropTypes.number, + /** + * The minimum allowed value of the slider. + * Should not be equal to max. + * @default 0 + */ + min: PropTypes.number, + /** + * Name attribute of the hidden `input` element. + */ + name: PropTypes.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: PropTypes.func, + /** + * 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: PropTypes.func, + /** + * @ignore + */ + onMouseDown: PropTypes.func, + /** + * The component orientation. + * @default 'horizontal' + */ + orientation: PropTypes.oneOf(['horizontal', 'vertical']), + /** + * A transformation function, to change the scale of the slider. + * @default (x) => x + */ + scale: PropTypes.func, /** * The size of the component. * It accepts theme values between 'sm' and 'lg'. * @default 'md' */ size: PropTypes.oneOf(['sm', 'md', 'lg']), + /** + * 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: PropTypes.number, /** * The system prop that allows defining system overrides as well as additional CSS styles. */ @@ -501,6 +753,43 @@ Slider.propTypes /* remove-proptypes */ = { PropTypes.func, PropTypes.object, ]), + /** + * Tab index attribute of the hidden `input` element. + */ + tabIndex: PropTypes.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: PropTypes.oneOf(['inverted', 'normal', false]), + /** + * The value of the slider. + * For ranged sliders, provide an array with two values. + */ + value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.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: PropTypes.oneOf(['auto', 'off', 'on']), + /** + * 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: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), } as any; export default Slider; diff --git a/packages/mui-joy/src/Slider/SliderProps.ts b/packages/mui-joy/src/Slider/SliderProps.ts index 8c40676b12b84b..105f297a06df03 100644 --- a/packages/mui-joy/src/Slider/SliderProps.ts +++ b/packages/mui-joy/src/Slider/SliderProps.ts @@ -3,9 +3,18 @@ import { OverridableStringUnion, OverrideProps } from '@mui/types'; import * as React from 'react'; import { ColorPaletteProp, SxProps } from '../styles/types'; -export type SliderSlot = 'root' | 'mark' | 'markLabel' | 'rail' | 'track' | 'thumb' | 'valueLabel'; +export type SliderSlot = + | 'root' + | 'mark' + | 'markLabel' + | 'rail' + | 'track' + | 'thumb' + | 'valueLabel' + | 'input'; export interface SliderPropsColorOverrides {} + export interface SliderPropsSizeOverrides {} export type SliderTypeMap< @@ -32,18 +41,6 @@ export type SliderTypeMap< defaultComponent: D; }>; -export type SliderRootProps = NonNullable['root']; -export type SliderMarkProps = NonNullable['mark']; -export type SliderMarkLabelProps = NonNullable< - SliderTypeMap['props']['componentsProps'] ->['markLabel']; -export type SliderRailProps = NonNullable['rail']; -export type SliderTrackProps = NonNullable['track']; -export type SliderThumbProps = NonNullable['thumb']; -export type SliderValueLabelProps = NonNullable< - SliderTypeMap['props']['componentsProps'] ->['valueLabel']; - export type SliderProps< D extends React.ElementType = SliderTypeMap['defaultComponent'], P = { component?: React.ElementType }, diff --git a/packages/mui-joy/src/Slider/sliderClasses.ts b/packages/mui-joy/src/Slider/sliderClasses.ts index cf16691afb96e7..8ab9451a2a23f1 100644 --- a/packages/mui-joy/src/Slider/sliderClasses.ts +++ b/packages/mui-joy/src/Slider/sliderClasses.ts @@ -25,10 +25,6 @@ export interface SliderClasses { valueLabel: string; /** Class name applied to the thumb label element if it's open. */ valueLabelOpen: string; - /** Class name applied to the thumb label's circle element. */ - valueLabelCircle: string; - /** Class name applied to the thumb label's label element. */ - valueLabelLabel: string; /** Class name applied to the mark element. */ mark: string; /** Class name applied to the mark element when it is active. */ @@ -58,10 +54,10 @@ export interface SliderClasses { export type SliderClassKey = keyof SliderClasses; export function getSliderUtilityClass(slot: string): string { - return generateUtilityClass('MuiSlider', slot); + return generateUtilityClass('JoySlider', slot); } -const sliderClasses: SliderClasses = generateUtilityClasses('MuiSlider', [ +const sliderClasses: SliderClasses = generateUtilityClasses('JoySlider', [ 'root', 'disabled', 'dragging', @@ -75,10 +71,10 @@ const sliderClasses: SliderClasses = generateUtilityClasses('MuiSlider', [ 'markActive', 'markLabel', 'thumb', + 'thumbStart', + 'thumbEnd', 'valueLabel', 'valueLabelOpen', - 'valueLabelCircle', - 'valueLabelLabel', 'colorPrimary', 'colorNeutral', 'colorDanger', diff --git a/test/regressions/index.js b/test/regressions/index.js index e88bdf8240997a..2ea37a842ac992 100644 --- a/test/regressions/index.js +++ b/test/regressions/index.js @@ -28,7 +28,7 @@ importRegressionFixtures.keys().forEach((path) => { }, []); const blacklist = [ - 'docs-joy-templates/TemplateCollection.png', + 'docs-joy-getting-started-templates/TemplateCollection.png', 'docs-joy-core-features-automatic-adjustment/ListThemes.png', 'docs-components-alert/TransitionAlerts.png', // Needs interaction 'docs-components-app-bar/BackToTop.png', // Needs interaction