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