diff --git a/docs/app/experiments/slider-change-committed-lag.tsx b/docs/app/experiments/slider-change-committed-lag.tsx
index 673b39ad78..885b3e9597 100644
--- a/docs/app/experiments/slider-change-committed-lag.tsx
+++ b/docs/app/experiments/slider-change-committed-lag.tsx
@@ -3,7 +3,7 @@
// to cross check whether this issue would still occur in the new API
import * as React from 'react';
import { Slider } from '@base_ui/react/Slider';
-import { Styles } from './slider';
+import classes from './slider.module.css';
export default function App() {
const [val1, setVal1] = React.useState(80);
@@ -17,23 +17,22 @@ export default function App() {
}}
>
setVal1(newValue as number)}
onValueCommitted={(newValue) => setVal2(newValue as number)}
>
-
-
-
-
-
+
+
+
+
+
onValueChange value: {val1}
onValueCommitted value: {val2}
-
);
}
diff --git a/docs/app/experiments/slider-marks.tsx b/docs/app/experiments/slider-marks.tsx
index 04194347ca..3aca9f1326 100644
--- a/docs/app/experiments/slider-marks.tsx
+++ b/docs/app/experiments/slider-marks.tsx
@@ -1,6 +1,7 @@
'use client';
import * as React from 'react';
import { Slider } from '@base_ui/react/Slider';
+import { useSliderContext } from '../../../packages/mui-base/src/Slider/Root/SliderContext';
const STOPS = [
{
@@ -32,7 +33,7 @@ function getSliderThumbAriaValueText(value: number) {
// for "inverted track", the track/rail can be styled with CSS but a prop is needed to flip the "mark active" state
function MarkWithLabel(props: { index: number; value: number; label: string; inverted?: boolean }) {
const { index, value, label, inverted = false } = props;
- const { direction, values } = Slider.useSliderContext();
+ const { direction, values } = useSliderContext();
const isRtl = direction === 'rtl';
const isFilled = inverted ? value >= values[0] : values[0] >= value;
return (
diff --git a/docs/app/experiments/slider-template.tsx b/docs/app/experiments/slider-template.tsx
deleted file mode 100644
index 7472d3a329..0000000000
--- a/docs/app/experiments/slider-template.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-'use client';
-import * as React from 'react';
-import { Slider } from '@base_ui/react/Slider';
-import { Styles } from './slider';
-
-export default function App() {
- return (
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/docs/app/experiments/slider-tooltip.tsx b/docs/app/experiments/slider-tooltip.tsx
index 2d8b140855..28d89d6855 100644
--- a/docs/app/experiments/slider-tooltip.tsx
+++ b/docs/app/experiments/slider-tooltip.tsx
@@ -3,6 +3,7 @@ import * as React from 'react';
import { useTheme } from '@mui/system';
import { Slider } from '@base_ui/react/Slider';
import { Tooltip } from '@base_ui/react/Tooltip';
+import { useSliderContext } from '../../../packages/mui-base/src/Slider/Root/SliderContext';
function useIsDarkMode() {
const theme = useTheme();
@@ -73,7 +74,7 @@ export default function App() {
const SliderMark = React.forwardRef(function SliderMark(props: any, ref: React.ForwardedRef) {
const { index, style, ...otherProps } = props;
- const { percentageValues } = Slider.useSliderContext();
+ const { percentageValues } = useSliderContext();
const isFilled = percentageValues[0] >= index * 10;
return (
- Uncontrolled
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Controlled
-
- {
- setVal1(newValue as number);
- }}
- >
-
-
-
-
-
-
-
-
-
- {
- setVal2(newValue as number[]);
- }}
- >
-
-
-
-
-
-
-
-
-
-
- {
- setVal3(newValue as number[]);
- }}
- >
-
-
-
- {val3.map((_val, idx) => (
-
- ))}
-
-
-
-
-
- With custom labels
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Volume Range
-
-
-
-
-
-
-
-
-
-
-
-
- Vertical
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- RTL
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Price Range
-
-
-
-
- );
-}
-
-function Label(props: any) {
- const { id: idProp, ...otherProps } = props;
- const defaultId = React.useId();
- const labelId = idProp ?? defaultId;
-
- const { subitems } = Slider.useSliderContext();
-
- const htmlFor = Array.from(subitems.values())
- .reduce((acc, item) => {
- return `${acc} ${item.inputId}`;
- }, '')
- .trim();
-
- return ;
-}
-
-function LabelRange(props: any) {
- const { id: idProp, ...otherProps } = props;
-
- const defaultId = React.useId();
- const labelId = idProp ?? defaultId;
-
- return ;
-}
-
-const grey = {
- 50: '#F3F6F9',
- 100: '#E5EAF2',
- 200: '#DAE2ED',
- 300: '#C7D0DD',
- 400: '#B0B8C4',
- 500: '#9DA8B7',
- 600: '#6B7A90',
- 700: '#434D5B',
- 800: '#303740',
- 900: '#1C2025',
-};
-
-function useIsDarkMode() {
- const theme = useTheme();
- return theme.palette.mode === 'dark';
-}
-
-export function Styles() {
- const isDarkMode = useIsDarkMode();
- return (
-
- );
-}
diff --git a/docs/data/api/slider-root.json b/docs/data/api/slider-root.json
index 9bf64ced01..077f5d8052 100644
--- a/docs/data/api/slider-root.json
+++ b/docs/data/api/slider-root.json
@@ -10,12 +10,8 @@
"default": "'ltr'"
},
"disabled": { "type": { "name": "bool" }, "default": "false" },
- "id": { "type": { "name": "string" } },
"largeStep": { "type": { "name": "number" }, "default": "10" },
- "max": { "type": { "name": "number" }, "default": "100" },
- "min": { "type": { "name": "number" }, "default": "0" },
"minStepsBetweenValues": { "type": { "name": "number" }, "default": "0" },
- "name": { "type": { "name": "string" } },
"onValueChange": {
"type": { "name": "func" },
"signature": {
@@ -35,8 +31,6 @@
"default": "'horizontal'"
},
"render": { "type": { "name": "union", "description": "element
| func" } },
- "step": { "type": { "name": "number" }, "default": "1" },
- "tabIndex": { "type": { "name": "number" } },
"value": {
"type": { "name": "union", "description": "Array<number>
| number" }
}
diff --git a/docs/data/components/slider/RangeSlider.js b/docs/data/components/slider/RangeSlider.js
index 43c1a01582..d464db388e 100644
--- a/docs/data/components/slider/RangeSlider.js
+++ b/docs/data/components/slider/RangeSlider.js
@@ -1,7 +1,8 @@
'use client';
import * as React from 'react';
-import { styled, useTheme, Box } from '@mui/system';
-import { Slider as BaseSlider } from '@base_ui/react/Slider';
+import { useTheme } from '@mui/system';
+import { Slider } from '@base_ui/react/Slider';
+import classes from './styles.module.css';
export default function RangeSlider() {
// Replace this with your app logic for determining dark mode
@@ -9,160 +10,59 @@ export default function RangeSlider() {
const [value, setValue] = React.useState([20, 37]);
return (
-
{/* controlled: */}
-
-
-
-
-
-
-
-
-
-
-
+ {/*
+ we can't use a
+
+
+ Uncontrolled Range
+
+
+
+
+
+
+
+
+
+
+
);
}
-const grey = {
- 50: '#F3F6F9',
- 100: '#E5EAF2',
- 200: '#DAE2ED',
- 300: '#C7D0DD',
- 400: '#B0B8C4',
- 500: '#9DA8B7',
- 600: '#6B7A90',
- 700: '#434D5B',
- 800: '#303740',
- 900: '#1C2025',
-};
-
function useIsDarkMode() {
const theme = useTheme();
return theme.palette.mode === 'dark';
}
-
-const Slider = styled(BaseSlider.Root)`
- font-family: 'IBM Plex Sans', sans-serif;
- font-size: 1rem;
- width: 100%;
- align-items: center;
- position: relative;
- -webkit-tap-highlight-color: transparent;
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 1rem;
-`;
-
-const SliderOutput = styled(BaseSlider.Output)`
- text-align: right;
-`;
-
-const SliderControl = styled(BaseSlider.Control)`
- grid-column: 1/3;
- display: flex;
- align-items: center;
- position: relative;
- width: 100%;
- height: 16px;
- border-radius: 9999px;
- touch-action: none;
-
- &[data-disabled] {
- cursor: not-allowed;
- }
-`;
-
-const SliderTrack = styled(BaseSlider.Track)`
- width: 100%;
- height: 2px;
- border-radius: 9999px;
- background-color: ${grey[400]};
- touch-action: none;
-
- .dark & {
- background-color: ${grey[700]};
- }
-`;
-
-const SliderIndicator = styled(BaseSlider.Indicator)`
- border-radius: 9999px;
- background-color: black;
-
- .dark & {
- background-color: ${grey[100]};
- }
-`;
-
-const SliderThumb = styled(BaseSlider.Thumb)`
- position: absolute;
- width: 16px;
- height: 16px;
- box-sizing: border-box;
- border-radius: 50%;
- background-color: black;
- transform: translateX(-50%);
- touch-action: none;
-
- &:focus-visible {
- outline: 2px solid black;
- outline-offset: 2px;
- }
-
- .dark & {
- background-color: ${grey[300]};
- }
-
- .dark &:focus-visible {
- outline-color: ${grey[300]};
- outline-width: 1px;
- outline-offset: 3px;
- }
-
- &[data-dragging='true'] {
- background-color: pink;
- }
-
- &[data-disabled],
- .dark &[data-disabled] {
- background-color: ${grey[600]};
- }
-
- .dark &[data-dragging='true'] {
- background-color: pink;
- }
-`;
-
-// we can't use a element in a range slider since the `for` attribute
-// cannot reference more than one element
-// the html spec doesn't forbid without `for` https://html.spec.whatwg.org/multipage/forms.html#the-label-element
-// but eslint complains by default and a11y validators may complain e.g. WAVE
-const Label = styled('span')`
- cursor: unset;
- font-weight: bold;
-
- &[data-disabled='true'] {
- color: ${grey[600]};
- }
-`;
diff --git a/docs/data/components/slider/RangeSlider.tsx b/docs/data/components/slider/RangeSlider.tsx
index 53279eade9..e05f50db87 100644
--- a/docs/data/components/slider/RangeSlider.tsx
+++ b/docs/data/components/slider/RangeSlider.tsx
@@ -1,7 +1,8 @@
'use client';
import * as React from 'react';
-import { styled, useTheme, Box } from '@mui/system';
-import { Slider as BaseSlider } from '@base_ui/react/Slider';
+import { useTheme } from '@mui/system';
+import { Slider } from '@base_ui/react/Slider';
+import classes from './styles.module.css';
export default function RangeSlider() {
// Replace this with your app logic for determining dark mode
@@ -9,160 +10,59 @@ export default function RangeSlider() {
const [value, setValue] = React.useState([20, 37]);
return (
-
{/* controlled: */}
-
- Controlled Range
-
-
-
-
-
-
-
-
-
+ {/*
+ we can't use a element in a range slider since the `for` attribute
+ cannot reference more than one element
+ although the html spec doesn't forbid without `for`:
+ https://html.spec.whatwg.org/multipage/forms.html#the-label-element
+ eslint complains by default and a11y validators may complain e.g. WAVE
+ */}
+
+ Controlled Range
+
+
+
+
+
+
+
+
+
+
{/* uncontrolled: */}
-
- Uncontrolled Range
-
-
-
-
-
-
-
-
-
-
+
+
+ Uncontrolled Range
+
+
+
+
+
+
+
+
+
+
+
);
}
-const grey = {
- 50: '#F3F6F9',
- 100: '#E5EAF2',
- 200: '#DAE2ED',
- 300: '#C7D0DD',
- 400: '#B0B8C4',
- 500: '#9DA8B7',
- 600: '#6B7A90',
- 700: '#434D5B',
- 800: '#303740',
- 900: '#1C2025',
-};
-
function useIsDarkMode() {
const theme = useTheme();
return theme.palette.mode === 'dark';
}
-
-const Slider = styled(BaseSlider.Root)`
- font-family: 'IBM Plex Sans', sans-serif;
- font-size: 1rem;
- width: 100%;
- align-items: center;
- position: relative;
- -webkit-tap-highlight-color: transparent;
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 1rem;
-`;
-
-const SliderOutput = styled(BaseSlider.Output)`
- text-align: right;
-`;
-
-const SliderControl = styled(BaseSlider.Control)`
- grid-column: 1/3;
- display: flex;
- align-items: center;
- position: relative;
- width: 100%;
- height: 16px;
- border-radius: 9999px;
- touch-action: none;
-
- &[data-disabled] {
- cursor: not-allowed;
- }
-`;
-
-const SliderTrack = styled(BaseSlider.Track)`
- width: 100%;
- height: 2px;
- border-radius: 9999px;
- background-color: ${grey[400]};
- touch-action: none;
-
- .dark & {
- background-color: ${grey[700]};
- }
-`;
-
-const SliderIndicator = styled(BaseSlider.Indicator)`
- border-radius: 9999px;
- background-color: black;
-
- .dark & {
- background-color: ${grey[100]};
- }
-`;
-
-const SliderThumb = styled(BaseSlider.Thumb)`
- position: absolute;
- width: 16px;
- height: 16px;
- box-sizing: border-box;
- border-radius: 50%;
- background-color: black;
- transform: translateX(-50%);
- touch-action: none;
-
- &:focus-visible {
- outline: 2px solid black;
- outline-offset: 2px;
- }
-
- .dark & {
- background-color: ${grey[300]};
- }
-
- .dark &:focus-visible {
- outline-color: ${grey[300]};
- outline-width: 1px;
- outline-offset: 3px;
- }
-
- &[data-dragging='true'] {
- background-color: pink;
- }
-
- &[data-disabled],
- .dark &[data-disabled] {
- background-color: ${grey[600]};
- }
-
- .dark &[data-dragging='true'] {
- background-color: pink;
- }
-`;
-
-// we can't use a element in a range slider since the `for` attribute
-// cannot reference more than one element
-// the html spec doesn't forbid without `for` https://html.spec.whatwg.org/multipage/forms.html#the-label-element
-// but eslint complains by default and a11y validators may complain e.g. WAVE
-const Label = styled('span')`
- cursor: unset;
- font-weight: bold;
-
- &[data-disabled='true'] {
- color: ${grey[600]};
- }
-`;
diff --git a/docs/data/components/slider/RtlSlider.js b/docs/data/components/slider/RtlSlider.js
index 472dce4697..7d3674a823 100644
--- a/docs/data/components/slider/RtlSlider.js
+++ b/docs/data/components/slider/RtlSlider.js
@@ -1,159 +1,55 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
-import { Slider as BaseSlider } from '@base_ui/react/Slider';
-import { styled, useTheme, Box } from '@mui/system';
+import { Slider } from '@base_ui/react/Slider';
+import { useTheme } from '@mui/system';
+import classes from './styles.module.css';
export default function RtlSlider() {
// Replace this with your app logic for determining dark mode
const isDarkMode = useIsDarkMode();
return (
-
-
- Volume (RTL)
-
-
-
-
-
-
-
-
-
+
+
+
+ Volume (RTL)
+
+
+
+
+
+
+
+
+
+
);
}
-function BaseLabel(props) {
- const { id, ...otherProps } = props;
- const { subitems, disabled } = BaseSlider.useSliderContext();
+function Label(props) {
+ const { id, htmlFor, ...otherProps } = props;
- const htmlFor = Array.from(subitems.values())
- .reduce((acc, item) => {
- return `${acc} ${item.inputId}`;
- }, '')
- .trim();
-
- return (
-
- );
+ return ;
}
-BaseLabel.propTypes = {
+Label.propTypes = {
+ htmlFor: PropTypes.string,
id: PropTypes.string,
};
-const grey = {
- 50: '#F3F6F9',
- 100: '#E5EAF2',
- 200: '#DAE2ED',
- 300: '#C7D0DD',
- 400: '#B0B8C4',
- 500: '#9DA8B7',
- 600: '#6B7A90',
- 700: '#434D5B',
- 800: '#303740',
- 900: '#1C2025',
-};
-
function useIsDarkMode() {
const theme = useTheme();
return theme.palette.mode === 'dark';
}
-
-const Slider = styled(BaseSlider.Root)`
- font-family: 'IBM Plex Sans', sans-serif;
- font-size: 1rem;
- width: 100%;
- align-items: center;
- position: relative;
- -webkit-tap-highlight-color: transparent;
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 1rem;
-`;
-
-const SliderOutput = styled(BaseSlider.Output)`
- text-align: left;
-`;
-
-const SliderControl = styled(BaseSlider.Control)`
- grid-column: 1/3;
- display: flex;
- align-items: center;
- position: relative;
- width: 100%;
- height: 16px;
- border-radius: 9999px;
- touch-action: none;
-
- &[data-disabled='true'] {
- cursor: not-allowed;
- }
-`;
-
-const SliderTrack = styled(BaseSlider.Track)`
- width: 100%;
- height: 2px;
- border-radius: 9999px;
- background-color: ${grey[400]};
- touch-action: none;
-
- .dark & {
- background-color: ${grey[700]};
- }
-`;
-
-const SliderIndicator = styled(BaseSlider.Indicator)`
- border-radius: 9999px;
- background-color: black;
-
- .dark & {
- background-color: ${grey[100]};
- }
-`;
-
-const SliderThumb = styled(BaseSlider.Thumb)`
- width: 16px;
- height: 16px;
- box-sizing: border-box;
- border-radius: 50%;
- background-color: black;
- touch-action: none;
-
- &:focus-visible {
- outline: 2px solid black;
- outline-offset: 2px;
- }
-
- .dark & {
- background-color: ${grey[100]};
- }
-
- .dark &:focus-visible {
- outline-color: ${grey[300]};
- outline-width: 1px;
- outline-offset: 3px;
- }
-
- &[data-dragging='true'] {
- background-color: pink;
- }
-
- &[data-disabled='true'],
- .dark &[data-disabled='true'] {
- background-color: ${grey[600]};
- }
-
- .dark &[data-dragging='true'] {
- background-color: pink;
- }
-`;
-
-const Label = styled(BaseLabel)`
- cursor: unset;
- font-weight: bold;
-
- &[data-disabled='true'] {
- color: ${grey[600]};
- }
-`;
diff --git a/docs/data/components/slider/RtlSlider.tsx b/docs/data/components/slider/RtlSlider.tsx
index 31961f6289..6571841947 100644
--- a/docs/data/components/slider/RtlSlider.tsx
+++ b/docs/data/components/slider/RtlSlider.tsx
@@ -1,154 +1,49 @@
'use client';
import * as React from 'react';
-import { Slider as BaseSlider } from '@base_ui/react/Slider';
-import { styled, useTheme, Box } from '@mui/system';
+import { Slider } from '@base_ui/react/Slider';
+import { useTheme } from '@mui/system';
+import classes from './styles.module.css';
export default function RtlSlider() {
// Replace this with your app logic for determining dark mode
const isDarkMode = useIsDarkMode();
return (
-
-
- Volume (RTL)
-
-
-
-
-
-
-
-
-
+
+
+
+ Volume (RTL)
+
+
+
+
+
+
+
+
+
+
);
}
-function BaseLabel(props: React.HTMLAttributes) {
- const { id, ...otherProps } = props;
- const { subitems, disabled } = BaseSlider.useSliderContext();
+function Label(props: React.LabelHTMLAttributes) {
+ const { id, htmlFor, ...otherProps } = props;
- const htmlFor = Array.from(subitems.values())
- .reduce((acc, item) => {
- return `${acc} ${item.inputId}`;
- }, '')
- .trim();
-
- return (
-
- );
+ return ;
}
-const grey = {
- 50: '#F3F6F9',
- 100: '#E5EAF2',
- 200: '#DAE2ED',
- 300: '#C7D0DD',
- 400: '#B0B8C4',
- 500: '#9DA8B7',
- 600: '#6B7A90',
- 700: '#434D5B',
- 800: '#303740',
- 900: '#1C2025',
-};
-
function useIsDarkMode() {
const theme = useTheme();
return theme.palette.mode === 'dark';
}
-
-const Slider = styled(BaseSlider.Root)`
- font-family: 'IBM Plex Sans', sans-serif;
- font-size: 1rem;
- width: 100%;
- align-items: center;
- position: relative;
- -webkit-tap-highlight-color: transparent;
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 1rem;
-`;
-
-const SliderOutput = styled(BaseSlider.Output)`
- text-align: left;
-`;
-
-const SliderControl = styled(BaseSlider.Control)`
- grid-column: 1/3;
- display: flex;
- align-items: center;
- position: relative;
- width: 100%;
- height: 16px;
- border-radius: 9999px;
- touch-action: none;
-
- &[data-disabled='true'] {
- cursor: not-allowed;
- }
-`;
-
-const SliderTrack = styled(BaseSlider.Track)`
- width: 100%;
- height: 2px;
- border-radius: 9999px;
- background-color: ${grey[400]};
- touch-action: none;
-
- .dark & {
- background-color: ${grey[700]};
- }
-`;
-
-const SliderIndicator = styled(BaseSlider.Indicator)`
- border-radius: 9999px;
- background-color: black;
-
- .dark & {
- background-color: ${grey[100]};
- }
-`;
-
-const SliderThumb = styled(BaseSlider.Thumb)`
- width: 16px;
- height: 16px;
- box-sizing: border-box;
- border-radius: 50%;
- background-color: black;
- touch-action: none;
-
- &:focus-visible {
- outline: 2px solid black;
- outline-offset: 2px;
- }
-
- .dark & {
- background-color: ${grey[100]};
- }
-
- .dark &:focus-visible {
- outline-color: ${grey[300]};
- outline-width: 1px;
- outline-offset: 3px;
- }
-
- &[data-dragging='true'] {
- background-color: pink;
- }
-
- &[data-disabled='true'],
- .dark &[data-disabled='true'] {
- background-color: ${grey[600]};
- }
-
- .dark &[data-dragging='true'] {
- background-color: pink;
- }
-`;
-
-const Label = styled(BaseLabel)`
- cursor: unset;
- font-weight: bold;
-
- &[data-disabled='true'] {
- color: ${grey[600]};
- }
-`;
diff --git a/docs/data/components/slider/RtlSlider.tsx.preview b/docs/data/components/slider/RtlSlider.tsx.preview
deleted file mode 100644
index ec8ede2676..0000000000
--- a/docs/data/components/slider/RtlSlider.tsx.preview
+++ /dev/null
@@ -1,10 +0,0 @@
-
- Volume (RTL)
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/data/components/slider/UnstyledSliderIntroduction/css-modules/index.js b/docs/data/components/slider/UnstyledSliderIntroduction/css-modules/index.js
index c5c0546f44..d9913a4dfc 100644
--- a/docs/data/components/slider/UnstyledSliderIntroduction/css-modules/index.js
+++ b/docs/data/components/slider/UnstyledSliderIntroduction/css-modules/index.js
@@ -3,6 +3,7 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import { useTheme } from '@mui/system';
import { Slider } from '@base_ui/react/Slider';
+import classes from '../../styles.module.css';
export default function UnstyledSliderIntroduction() {
// Replace this with your app logic for determining dark mode
@@ -13,163 +14,41 @@ export default function UnstyledSliderIntroduction() {
style={{ display: 'flex', flexDirection: 'column', gap: '4rem', width: 320 }}
>
-
+
Volume
-
-
-
-
-
+
+
+
+
+
-
);
}
function Label(props) {
- const { id, ...otherProps } = props;
- const { subitems, disabled } = Slider.useSliderContext();
+ const { id, htmlFor, ...otherProps } = props;
- const htmlFor = Array.from(subitems.values())
- .reduce((acc, item) => {
- return `${acc} ${item.inputId}`;
- }, '')
- .trim();
-
- return (
-
- );
+ return ;
}
Label.propTypes = {
+ htmlFor: PropTypes.string,
id: PropTypes.string,
};
-const grey = {
- 50: '#F3F6F9',
- 100: '#E5EAF2',
- 200: '#DAE2ED',
- 300: '#C7D0DD',
- 400: '#B0B8C4',
- 500: '#9DA8B7',
- 600: '#6B7A90',
- 700: '#434D5B',
- 800: '#303740',
- 900: '#1C2025',
-};
-
function useIsDarkMode() {
const theme = useTheme();
return theme.palette.mode === 'dark';
}
-
-function Styles() {
- return (
-
- );
-}
diff --git a/docs/data/components/slider/UnstyledSliderIntroduction/css-modules/index.tsx b/docs/data/components/slider/UnstyledSliderIntroduction/css-modules/index.tsx
index a7ca9c845a..bfde075ee8 100644
--- a/docs/data/components/slider/UnstyledSliderIntroduction/css-modules/index.tsx
+++ b/docs/data/components/slider/UnstyledSliderIntroduction/css-modules/index.tsx
@@ -2,6 +2,7 @@
import * as React from 'react';
import { useTheme } from '@mui/system';
import { Slider } from '@base_ui/react/Slider';
+import classes from '../../styles.module.css';
export default function UnstyledSliderIntroduction() {
// Replace this with your app logic for determining dark mode
@@ -12,159 +13,36 @@ export default function UnstyledSliderIntroduction() {
style={{ display: 'flex', flexDirection: 'column', gap: '4rem', width: 320 }}
>
-
+
Volume
-
-
-
-
-
+
+
+
+
+
-
);
}
-function Label(props: React.HTMLAttributes) {
- const { id, ...otherProps } = props;
- const { subitems, disabled } = Slider.useSliderContext();
+function Label(props: React.LabelHTMLAttributes) {
+ const { id, htmlFor, ...otherProps } = props;
- const htmlFor = Array.from(subitems.values())
- .reduce((acc, item) => {
- return `${acc} ${item.inputId}`;
- }, '')
- .trim();
-
- return (
-
- );
+ return ;
}
-const grey = {
- 50: '#F3F6F9',
- 100: '#E5EAF2',
- 200: '#DAE2ED',
- 300: '#C7D0DD',
- 400: '#B0B8C4',
- 500: '#9DA8B7',
- 600: '#6B7A90',
- 700: '#434D5B',
- 800: '#303740',
- 900: '#1C2025',
-};
-
function useIsDarkMode() {
const theme = useTheme();
return theme.palette.mode === 'dark';
}
-
-function Styles() {
- return (
-
- );
-}
diff --git a/docs/data/components/slider/UnstyledSliderIntroduction/system/index.js b/docs/data/components/slider/UnstyledSliderIntroduction/system/index.js
index 61da914e59..0c74f1518f 100644
--- a/docs/data/components/slider/UnstyledSliderIntroduction/system/index.js
+++ b/docs/data/components/slider/UnstyledSliderIntroduction/system/index.js
@@ -13,12 +13,14 @@ export default function UnstyledSliderIntroduction() {
sx={{ display: 'flex', flexDirection: 'column', gap: '4rem', width: 320 }}
>
- Volume
+
+ Volume
+
-
+
@@ -27,21 +29,13 @@ export default function UnstyledSliderIntroduction() {
}
function BaseLabel(props) {
- const { id, ...otherProps } = props;
- const { subitems, disabled } = BaseSlider.useSliderContext();
+ const { id, htmlFor, ...otherProps } = props;
- const htmlFor = Array.from(subitems.values())
- .reduce((acc, item) => {
- return `${acc} ${item.inputId}`;
- }, '')
- .trim();
-
- return (
-
- );
+ return ;
}
BaseLabel.propTypes = {
+ htmlFor: PropTypes.string,
id: PropTypes.string,
};
diff --git a/docs/data/components/slider/UnstyledSliderIntroduction/system/index.tsx b/docs/data/components/slider/UnstyledSliderIntroduction/system/index.tsx
index 3e07ad98a9..330a8d5dd2 100644
--- a/docs/data/components/slider/UnstyledSliderIntroduction/system/index.tsx
+++ b/docs/data/components/slider/UnstyledSliderIntroduction/system/index.tsx
@@ -12,12 +12,14 @@ export default function UnstyledSliderIntroduction() {
sx={{ display: 'flex', flexDirection: 'column', gap: '4rem', width: 320 }}
>
- Volume
+
+ Volume
+
-
+
@@ -25,19 +27,10 @@ export default function UnstyledSliderIntroduction() {
);
}
-function BaseLabel(props: React.HTMLAttributes) {
- const { id, ...otherProps } = props;
- const { subitems, disabled } = BaseSlider.useSliderContext();
+function BaseLabel(props: React.LabelHTMLAttributes) {
+ const { id, htmlFor, ...otherProps } = props;
- const htmlFor = Array.from(subitems.values())
- .reduce((acc, item) => {
- return `${acc} ${item.inputId}`;
- }, '')
- .trim();
-
- return (
-
- );
+ return ;
}
const grey = {
diff --git a/docs/data/components/slider/UnstyledSliderIntroduction/system/index.tsx.preview b/docs/data/components/slider/UnstyledSliderIntroduction/system/index.tsx.preview
index 754a05be71..4a4e7cdbbf 100644
--- a/docs/data/components/slider/UnstyledSliderIntroduction/system/index.tsx.preview
+++ b/docs/data/components/slider/UnstyledSliderIntroduction/system/index.tsx.preview
@@ -1,10 +1,12 @@
- Volume
+
+ Volume
+
-
+
\ No newline at end of file
diff --git a/docs/data/components/slider/UnstyledSliderIntroduction/tailwind/index.js b/docs/data/components/slider/UnstyledSliderIntroduction/tailwind/index.js
index 5e06ad386d..d1f6449969 100644
--- a/docs/data/components/slider/UnstyledSliderIntroduction/tailwind/index.js
+++ b/docs/data/components/slider/UnstyledSliderIntroduction/tailwind/index.js
@@ -23,7 +23,9 @@ export default function UnstyledSliderIntroduction() {
style={{ display: 'flex', flexDirection: 'column', gap: '4rem', width: 320 }}
>
- Volume
+
+ Volume
+
@@ -43,7 +45,7 @@ const Slider = React.forwardRef(function Slider(props, ref) {
ref={ref}
className={(state) =>
classNames(
- 'relative w-full items-center grid grid-cols-2 gap-4',
+ 'font-sans relative w-full items-center grid grid-cols-2 gap-4',
typeof props.className === 'function'
? props.className(state)
: props.className,
@@ -184,25 +186,12 @@ SliderIndicator.propTypes = {
};
function Label(props) {
- const { id, ...otherProps } = props;
- const { subitems, disabled } = BaseSlider.useSliderContext();
+ const { id, htmlFor, ...otherProps } = props;
- const htmlFor = Array.from(subitems.values())
- .reduce((acc, item) => {
- return `${acc} ${item.inputId}`;
- }, '')
- .trim();
-
- return (
-
- );
+ return ;
}
Label.propTypes = {
+ htmlFor: PropTypes.string,
id: PropTypes.string,
};
diff --git a/docs/data/components/slider/UnstyledSliderIntroduction/tailwind/index.tsx b/docs/data/components/slider/UnstyledSliderIntroduction/tailwind/index.tsx
index 232b324362..dd34f204e9 100644
--- a/docs/data/components/slider/UnstyledSliderIntroduction/tailwind/index.tsx
+++ b/docs/data/components/slider/UnstyledSliderIntroduction/tailwind/index.tsx
@@ -22,7 +22,9 @@ export default function UnstyledSliderIntroduction() {
style={{ display: 'flex', flexDirection: 'column', gap: '4rem', width: 320 }}
>
- Volume
+
+ Volume
+
@@ -36,7 +38,7 @@ export default function UnstyledSliderIntroduction() {
}
const Slider = React.forwardRef(function Slider(
- props: BaseSlider.RootProps,
+ props: BaseSlider.Root.Props,
ref: React.ForwardedRef,
) {
return (
@@ -45,7 +47,7 @@ const Slider = React.forwardRef(function Slider(
ref={ref}
className={(state) =>
classNames(
- 'relative w-full items-center grid grid-cols-2 gap-4',
+ 'font-sans relative w-full items-center grid grid-cols-2 gap-4',
typeof props.className === 'function'
? props.className(state)
: props.className,
@@ -56,7 +58,7 @@ const Slider = React.forwardRef(function Slider(
});
const SliderOutput = React.forwardRef(function SliderOutput(
- props: BaseSlider.OutputProps,
+ props: BaseSlider.Output.Props,
ref: React.ForwardedRef,
) {
return (
@@ -76,7 +78,7 @@ const SliderOutput = React.forwardRef(function SliderOutput(
});
const SliderControl = React.forwardRef(function SliderControl(
- props: BaseSlider.ControlProps,
+ props: BaseSlider.Control.Props,
ref: React.ForwardedRef,
) {
return (
@@ -97,7 +99,7 @@ const SliderControl = React.forwardRef(function SliderControl(
});
const SliderTrack = React.forwardRef(function SliderTrack(
- props: BaseSlider.TrackProps,
+ props: BaseSlider.Track.Props,
ref: React.ForwardedRef,
) {
return (
@@ -117,7 +119,7 @@ const SliderTrack = React.forwardRef(function SliderTrack(
});
const SliderThumb = React.forwardRef(function SliderThumb(
- props: BaseSlider.ThumbProps,
+ props: BaseSlider.Thumb.Props,
ref: React.ForwardedRef,
) {
return (
@@ -139,7 +141,7 @@ const SliderThumb = React.forwardRef(function SliderThumb(
});
const SliderIndicator = React.forwardRef(function SliderIndicator(
- props: BaseSlider.IndicatorProps,
+ props: BaseSlider.Indicator.Props,
ref: React.ForwardedRef,
) {
return (
@@ -158,22 +160,8 @@ const SliderIndicator = React.forwardRef(function SliderIndicator(
);
});
-function Label(props: React.HTMLAttributes) {
- const { id, ...otherProps } = props;
- const { subitems, disabled } = BaseSlider.useSliderContext();
+function Label(props: React.LabelHTMLAttributes) {
+ const { id, htmlFor, ...otherProps } = props;
- const htmlFor = Array.from(subitems.values())
- .reduce((acc, item) => {
- return `${acc} ${item.inputId}`;
- }, '')
- .trim();
-
- return (
-
- );
+ return ;
}
diff --git a/docs/data/components/slider/UnstyledSliderIntroduction/tailwind/index.tsx.preview b/docs/data/components/slider/UnstyledSliderIntroduction/tailwind/index.tsx.preview
index 754a05be71..497ae0f0b3 100644
--- a/docs/data/components/slider/UnstyledSliderIntroduction/tailwind/index.tsx.preview
+++ b/docs/data/components/slider/UnstyledSliderIntroduction/tailwind/index.tsx.preview
@@ -1,5 +1,7 @@
- Volume
+
+ Volume
+
diff --git a/docs/data/components/slider/VerticalSlider.js b/docs/data/components/slider/VerticalSlider.js
index 0548448e78..6da07b57c1 100644
--- a/docs/data/components/slider/VerticalSlider.js
+++ b/docs/data/components/slider/VerticalSlider.js
@@ -3,6 +3,7 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import { Slider } from '@base_ui/react/Slider';
import { useTheme } from '@mui/system';
+import classes from './vertical.module.css';
export default function VerticalSlider() {
// Replace this with your app logic for determining dark mode
@@ -13,145 +14,38 @@ export default function VerticalSlider() {
defaultValue={50}
orientation="vertical"
aria-labelledby="VolumeSliderLabel"
- className="VerticalSlider"
+ className={classes.slider}
>
-
+
Volume
-
-
-
-
+
+
+
+
-
+
-
);
}
-function Styles() {
- return (
-
- );
-}
-
function Label(props) {
- const { id, ...otherProps } = props;
- const { subitems, disabled } = Slider.useSliderContext();
-
- const htmlFor = Array.from(subitems.values())
- .reduce((acc, item) => {
- return `${acc} ${item.inputId}`;
- }, '')
- .trim();
+ const { id, htmlFor, ...otherProps } = props;
- return (
-
- );
+ return ;
}
Label.propTypes = {
+ htmlFor: PropTypes.string,
id: PropTypes.string,
};
@@ -159,16 +53,3 @@ function useIsDarkMode() {
const theme = useTheme();
return theme.palette.mode === 'dark';
}
-
-const grey = {
- 50: '#F3F6F9',
- 100: '#E5EAF2',
- 200: '#DAE2ED',
- 300: '#C7D0DD',
- 400: '#B0B8C4',
- 500: '#9DA8B7',
- 600: '#6B7A90',
- 700: '#434D5B',
- 800: '#303740',
- 900: '#1C2025',
-};
diff --git a/docs/data/components/slider/VerticalSlider.tsx b/docs/data/components/slider/VerticalSlider.tsx
index e38e7c64a9..7ca6ec8a84 100644
--- a/docs/data/components/slider/VerticalSlider.tsx
+++ b/docs/data/components/slider/VerticalSlider.tsx
@@ -2,6 +2,7 @@
import * as React from 'react';
import { Slider } from '@base_ui/react/Slider';
import { useTheme } from '@mui/system';
+import classes from './vertical.module.css';
export default function VerticalSlider() {
// Replace this with your app logic for determining dark mode
@@ -12,158 +13,37 @@ export default function VerticalSlider() {
defaultValue={50}
orientation="vertical"
aria-labelledby="VolumeSliderLabel"
- className="VerticalSlider"
+ className={classes.slider}
>
-
+
Volume
-
-
-
-
+
+
+
+
-
+
-
);
}
-function Styles() {
- return (
-
- );
-}
-
-function Label(props: React.HTMLAttributes) {
- const { id, ...otherProps } = props;
- const { subitems, disabled } = Slider.useSliderContext();
-
- const htmlFor = Array.from(subitems.values())
- .reduce((acc, item) => {
- return `${acc} ${item.inputId}`;
- }, '')
- .trim();
-
- return (
-
- );
+ return ;
}
function useIsDarkMode() {
const theme = useTheme();
return theme.palette.mode === 'dark';
}
-
-const grey = {
- 50: '#F3F6F9',
- 100: '#E5EAF2',
- 200: '#DAE2ED',
- 300: '#C7D0DD',
- 400: '#B0B8C4',
- 500: '#9DA8B7',
- 600: '#6B7A90',
- 700: '#434D5B',
- 800: '#303740',
- 900: '#1C2025',
-};
diff --git a/docs/data/components/slider/slider.mdx b/docs/data/components/slider/slider.mdx
index f9c106aff6..3d60a072f5 100644
--- a/docs/data/components/slider/slider.mdx
+++ b/docs/data/components/slider/slider.mdx
@@ -171,6 +171,18 @@ To create vertical sliders, set the `orientation` prop to `"vertical"`. This wil
+
+ Chrome versions below 124 does not implement `aria-orientation` correctly for vertical sliders ([Chromium issue #40736841](https://issues.chromium.org/issues/40736841)), and exposes them in the accessibility tree as `horizontal`.
+
+ The `-webkit-appearance: slider-vertical` CSS property can be used to correct this, though it will trigger a console warning in newer Chrome versions:
+ ```css
+ .MySlider-thumb > input {
+ -webkit-appearance: slider-vertical;
+ }
+ ```
+ The `Slider.Thumb` subcomponent automatically sets the CSS [`writing-mode`](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_writing_modes/Vertical_controls#range_sliders_meters_and_progress_bars) property that fixes this bug for Chrome 124 and newer.
+
+
## RTL
Set the `direction` prop to `'rtl'` to change the slider's direction for right-to-left languages:
diff --git a/docs/data/components/slider/styles.module.css b/docs/data/components/slider/styles.module.css
new file mode 100644
index 0000000000..d91cbd99ac
--- /dev/null
+++ b/docs/data/components/slider/styles.module.css
@@ -0,0 +1,103 @@
+.slider {
+ font-family:
+ IBM Plex Sans,
+ sans-serif;
+ font-size: 1rem;
+ width: 100%;
+ align-items: center;
+ position: relative;
+ -webkit-tap-highlight-color: transparent;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+}
+
+.output {
+ text-align: right;
+}
+
+[dir='rtl'] .output {
+ text-align: left;
+}
+
+.control {
+ grid-column: 1/3;
+ display: flex;
+ align-items: center;
+ position: relative;
+ width: 100%;
+ height: 16px;
+ border-radius: 9999px;
+ touch-action: none;
+}
+
+.track {
+ width: 100%;
+ height: 2px;
+ border-radius: 9999px;
+ background-color: var(--gray-400);
+ touch-action: none;
+}
+
+.dark .track {
+ background-color: var(--gray-700);
+}
+
+.control[data-disabled] {
+ cursor: not-allowed;
+}
+
+.indicator {
+ height: 2px;
+ border-radius: 9999px;
+ background-color: var(--gray-900);
+}
+
+.dark .indicator {
+ background-color: var(--gray-100);
+}
+
+.thumb {
+ width: 16px;
+ height: 16px;
+ box-sizing: border-box;
+ border-radius: 50%;
+ background-color: var(--gray-900);
+ touch-action: none;
+
+ &:focus-visible {
+ outline: 2px solid var(--gray-900);
+ outline-offset: 2px;
+ }
+
+ &[data-dragging] {
+ background-color: pink;
+ }
+
+ &[data-disabled] {
+ background-color: var(--gray-300);
+ }
+}
+
+.dark .thumb {
+ background-color: var(--gray-100);
+
+ &:focus-visible {
+ outline-color: var(--gray-300);
+ outline-width: 1px;
+ outline-offset: 3px;
+ }
+
+ &[data-disabled] {
+ background-color: var(--gray-600);
+ }
+}
+
+.label {
+ cursor: unset;
+ font-weight: bold;
+}
+
+.label[data-disabled='true'] {
+ color: var(--gray-600);
+}
diff --git a/docs/data/components/slider/vertical.module.css b/docs/data/components/slider/vertical.module.css
new file mode 100644
index 0000000000..e4b21c8497
--- /dev/null
+++ b/docs/data/components/slider/vertical.module.css
@@ -0,0 +1,106 @@
+.slider {
+ font-family: 'IBM Plex Sans', sans-serif;
+ font-size: 1rem;
+ height: 100%;
+ width: 5rem;
+ align-items: center;
+ position: relative;
+ -webkit-tap-highlight-color: transparent;
+ display: flex;
+ flex-flow: column-reverse nowrap;
+ gap: 1rem;
+}
+
+.output {
+ font-size: 1.125rem;
+}
+
+.control {
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: center;
+ position: relative;
+ width: 16px;
+ height: 300px;
+ border-radius: 9999px;
+ touch-action: none;
+}
+
+.track {
+ height: 100%;
+ width: 2px;
+ border-radius: 9999px;
+ background-color: var(--gray-400);
+ touch-action: none;
+}
+
+.dark .track {
+ background-color: var(--gray-700);
+}
+
+.control[data-disabled] {
+ cursor: not-allowed;
+}
+
+.indicator {
+ border-radius: 9999px;
+ background-color: var(--gray-900);
+}
+
+.dark .indicator {
+ position: absolute;
+ width: 16px;
+ height: 16px;
+ box-sizing: border-box;
+ border-radius: 50%;
+ background-color: var(--gray-900);
+ transform: translateY(50%);
+ touch-action: none;
+}
+
+.thumb {
+ width: 16px;
+ height: 16px;
+ box-sizing: border-box;
+ border-radius: 50%;
+ background-color: var(--gray-900);
+ touch-action: none;
+
+ &:focus-visible {
+ outline: 2px solid var(--gray-900);
+ outline-offset: 2px;
+ }
+
+ &[data-dragging] {
+ background-color: pink;
+ }
+
+ &[data-disabled] {
+ background-color: var(--gray-300);
+ }
+}
+
+.dark .thumb {
+ background-color: var(--gray-100);
+
+ &:focus-visible {
+ outline-color: var(--gray-300);
+ outline-width: 1px;
+ outline-offset: 3px;
+ }
+
+ &[data-disabled] {
+ background-color: var(--gray-600);
+ }
+}
+
+.label {
+ cursor: unset;
+ font-weight: 700;
+ font-size: 1rem;
+ color: inherit;
+}
+
+.label[data-disabled='true'] {
+ color: var(--gray-600);
+}
diff --git a/docs/data/translations/api-docs/slider-root/slider-root.json b/docs/data/translations/api-docs/slider-root/slider-root.json
index 392d883f71..0d195e281c 100644
--- a/docs/data/translations/api-docs/slider-root/slider-root.json
+++ b/docs/data/translations/api-docs/slider-root/slider-root.json
@@ -14,20 +14,12 @@
"description": "Sets the direction. For right-to-left languages, the lowest value is on the right-hand side."
},
"disabled": { "description": "If true
, the component is disabled." },
- "id": { "description": "The id of the slider element." },
"largeStep": {
"description": "The granularity with which the slider can step through values when using Page Up/Page Down or Shift + Arrow Up/Arrow Down."
},
- "max": {
- "description": "The maximum allowed value of the slider. Should not be equal to min."
- },
- "min": {
- "description": "The minimum allowed value of the slider. Should not be equal to max."
- },
"minStepsBetweenValues": {
"description": "The minimum steps between values in a range slider."
},
- "name": { "description": "Name attribute of the hidden input
element." },
"onValueChange": {
"description": "Callback function that is fired when the slider's value changed.",
"typeDescriptions": {
@@ -45,12 +37,6 @@
},
"orientation": { "description": "The component orientation." },
"render": { "description": "A function to customize rendering of the component." },
- "step": {
- "description": "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."
- },
- "tabIndex": {
- "description": "Tab index attribute of the Thumb component's input
element."
- },
"value": {
"description": "The value of the slider. For ranged sliders, provide an array with two values."
}
diff --git a/packages/mui-base/src/Slider/Control/SliderControl.test.tsx b/packages/mui-base/src/Slider/Control/SliderControl.test.tsx
index c09ba78a14..6fd2866642 100644
--- a/packages/mui-base/src/Slider/Control/SliderControl.test.tsx
+++ b/packages/mui-base/src/Slider/Control/SliderControl.test.tsx
@@ -1,65 +1,66 @@
import * as React from 'react';
import { Slider } from '@base_ui/react/Slider';
-import { SliderProvider, type SliderProviderValue } from '@base_ui/react/Slider/index.parts';
import { createRenderer, describeConformance } from '#test-utils';
+import { SliderContext } from '../Root/SliderContext';
+import type { SliderRoot } from '../Root/SliderRoot';
+import { NOOP } from '../../utils/noop';
-const NOOP = () => {};
-
-describe('', () => {
- const { render } = createRenderer();
-
- const testProviderValue: SliderProviderValue = {
- active: -1,
- areValuesEqual: () => true,
- axis: 'horizontal',
- changeValue: NOOP,
- compoundComponentContextValue: {
- registerItem: () => ({ id: 0, deregister: () => {} }),
- getItemIndex: () => 0,
- totalSubitemCount: 1,
- },
- dragging: false,
+const testRootContext: SliderRoot.Context = {
+ active: -1,
+ areValuesEqual: () => true,
+ axis: 'horizontal',
+ changeValue: NOOP,
+ direction: 'ltr',
+ dragging: false,
+ disabled: false,
+ getFingerNewValue: () => ({
+ newValue: 0,
+ activeIndex: 0,
+ newPercentageValue: 0,
+ }),
+ handleValueChange: NOOP,
+ largeStep: 10,
+ inputIdMap: new Map(),
+ max: 100,
+ min: 0,
+ minStepsBetweenValues: 0,
+ orientation: 'horizontal',
+ ownerState: {
+ activeThumbIndex: -1,
disabled: false,
- getFingerNewValue: () => ({
- newValue: 0,
- activeIndex: 0,
- newPercentageValue: 0,
- }),
- handleValueChange: NOOP,
+ dragging: false,
direction: 'ltr',
- largeStep: 10,
max: 100,
min: 0,
minStepsBetweenValues: 0,
orientation: 'horizontal',
- ownerState: {
- activeThumbIndex: -1,
- disabled: false,
- dragging: false,
- direction: 'ltr',
- max: 100,
- min: 0,
- minStepsBetweenValues: 0,
- orientation: 'horizontal',
- step: 1,
- values: [0],
- valid: null,
- dirty: false,
- touched: false,
- },
- percentageValues: [0],
- registerSliderControl: NOOP,
- setActive: NOOP,
- setDragging: NOOP,
- setValueState: NOOP,
step: 1,
- subitems: new Map(),
values: [0],
- };
+ valid: null,
+ dirty: false,
+ touched: false,
+ },
+ percentageValues: [0],
+ registerInputId: () => ({
+ deregister: NOOP,
+ }),
+ registerSliderControl: NOOP,
+ setActive: NOOP,
+ setDragging: NOOP,
+ setValueState: NOOP,
+ step: 1,
+ thumbRefs: { current: [] },
+ values: [0],
+};
+
+describe('', () => {
+ const { render } = createRenderer();
describeConformance(, () => ({
render: (node) => {
- return render({node});
+ return render(
+ {node},
+ );
},
refInstanceof: window.HTMLSpanElement,
}));
diff --git a/packages/mui-base/src/Slider/Control/SliderControl.tsx b/packages/mui-base/src/Slider/Control/SliderControl.tsx
index a2e6cac4d7..3622e16921 100644
--- a/packages/mui-base/src/Slider/Control/SliderControl.tsx
+++ b/packages/mui-base/src/Slider/Control/SliderControl.tsx
@@ -1,12 +1,12 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
+import type { BaseUIComponentProps } from '../../utils/types';
import { useComponentRenderer } from '../../utils/useComponentRenderer';
-import { useSliderContext } from '../Root/SliderProvider';
+import { useSliderContext } from '../Root/SliderContext';
import { sliderStyleHookMapping } from '../Root/styleHooks';
-import { SliderControlProps } from './SliderControl.types';
+import type { SliderRoot } from '../Root/SliderRoot';
import { useSliderControl } from './useSliderControl';
-
/**
*
* Demos:
@@ -18,7 +18,7 @@ import { useSliderControl } from './useSliderControl';
* - [SliderControl API](https://base-ui.netlify.app/components/react-slider/#api-reference-SliderControl)
*/
const SliderControl = React.forwardRef(function SliderControl(
- props: SliderControlProps,
+ props: SliderControl.Props,
forwardedRef: React.ForwardedRef,
) {
const { render: renderProp, className, ...otherProps } = props;
@@ -38,7 +38,7 @@ const SliderControl = React.forwardRef(function SliderControl(
setDragging,
setValueState,
step,
- subitems,
+ thumbRefs,
} = useSliderContext();
const { getRootProps } = useSliderControl({
@@ -56,7 +56,7 @@ const SliderControl = React.forwardRef(function SliderControl(
setDragging,
setValueState,
step,
- subitems,
+ thumbRefs,
});
const { renderElement } = useComponentRenderer({
@@ -71,6 +71,12 @@ const SliderControl = React.forwardRef(function SliderControl(
return renderElement();
});
+export namespace SliderControl {
+ export interface Props extends BaseUIComponentProps<'span', SliderRoot.OwnerState> {}
+}
+
+export { SliderControl };
+
SliderControl.propTypes /* remove-proptypes */ = {
// ┌────────────────────────────── Warning ──────────────────────────────┐
// │ These PropTypes are generated from the TypeScript type definitions. │
@@ -89,5 +95,3 @@ SliderControl.propTypes /* remove-proptypes */ = {
*/
render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
} as any;
-
-export { SliderControl };
diff --git a/packages/mui-base/src/Slider/Control/SliderControl.types.ts b/packages/mui-base/src/Slider/Control/SliderControl.types.ts
deleted file mode 100644
index b7b469d09d..0000000000
--- a/packages/mui-base/src/Slider/Control/SliderControl.types.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { BaseUIComponentProps } from '../../utils/types';
-import { SliderRootOwnerState, UseSliderReturnValue } from '../Root/SliderRoot.types';
-
-export interface SliderControlProps extends BaseUIComponentProps<'span', SliderRootOwnerState> {}
-
-export interface UseSliderControlParameters
- extends Pick<
- UseSliderReturnValue,
- | 'areValuesEqual'
- | 'disabled'
- | 'dragging'
- | 'getFingerNewValue'
- | 'handleValueChange'
- | 'minStepsBetweenValues'
- | 'onValueCommitted'
- | 'percentageValues'
- | 'registerSliderControl'
- | 'setActive'
- | 'setDragging'
- | 'setValueState'
- | 'step'
- | 'subitems'
- > {
- /**
- * The ref attached to the control area of the Slider.
- */
- rootRef?: React.Ref;
-}
-
-export interface UseSliderControlReturnValue {
- getRootProps: (
- externalProps?: React.ComponentPropsWithRef<'span'>,
- ) => React.ComponentPropsWithRef<'span'>;
-}
diff --git a/packages/mui-base/src/Slider/Control/useSliderControl.ts b/packages/mui-base/src/Slider/Control/useSliderControl.ts
index 039dba32af..32fcc0c5f1 100644
--- a/packages/mui-base/src/Slider/Control/useSliderControl.ts
+++ b/packages/mui-base/src/Slider/Control/useSliderControl.ts
@@ -2,17 +2,22 @@
import * as React from 'react';
import { mergeReactProps } from '../../utils/mergeReactProps';
import { ownerDocument } from '../../utils/owner';
+import type { GenericHTMLProps } from '../../utils/types';
import { useForkRef } from '../../utils/useForkRef';
import { useEventCallback } from '../../utils/useEventCallback';
-import { focusThumb, trackFinger, validateMinimumDistance } from '../Root/useSliderRoot';
-import { UseSliderControlParameters, UseSliderControlReturnValue } from './SliderControl.types';
+import {
+ focusThumb,
+ trackFinger,
+ type useSliderRoot,
+ validateMinimumDistance,
+} from '../Root/useSliderRoot';
import { useFieldControlValidation } from '../../Field/Control/useFieldControlValidation';
const INTENTIONAL_DRAG_COUNT_THRESHOLD = 2;
export function useSliderControl(
- parameters: UseSliderControlParameters,
-): UseSliderControlReturnValue {
+ parameters: useSliderControl.Parameters,
+): useSliderControl.ReturnValue {
const {
areValuesEqual,
disabled,
@@ -28,7 +33,7 @@ export function useSliderControl(
setDragging,
setValueState,
step,
- subitems,
+ thumbRefs,
} = parameters;
const { commitValidation } = useFieldControlValidation();
@@ -47,13 +52,6 @@ export function useSliderControl(
// 2. the exact intersection of the center of the thumb and the track
const offsetRef = React.useRef(0);
- const thumbRefs = React.useMemo(() => {
- return Array.from(subitems).map((subitem) => {
- const { ref } = subitem[1];
- return ref.current;
- });
- }, [subitems]);
-
const handleTouchMove = useEventCallback((nativeEvent: TouchEvent | PointerEvent) => {
const finger = trackFinger(nativeEvent, touchIdRef);
@@ -238,7 +236,7 @@ export function useSliderControl(
// if the event lands on a thumb, don't change the value, just get the
// percentageValue difference represented by the distance between the click origin
// and the coordinates of the value on the track area
- if (thumbRefs.includes(event.target as HTMLElement)) {
+ if (thumbRefs.current.includes(event.target as HTMLElement)) {
const targetThumbIndex = (event.target as HTMLElement).getAttribute('data-index');
const offset = percentageValues[Number(targetThumbIndex)] / 100 - newPercentageValue;
@@ -283,3 +281,33 @@ export function useSliderControl(
[getRootProps],
);
}
+
+export namespace useSliderControl {
+ export interface Parameters
+ extends Pick<
+ useSliderRoot.ReturnValue,
+ | 'areValuesEqual'
+ | 'disabled'
+ | 'dragging'
+ | 'getFingerNewValue'
+ | 'handleValueChange'
+ | 'minStepsBetweenValues'
+ | 'onValueCommitted'
+ | 'percentageValues'
+ | 'registerSliderControl'
+ | 'setActive'
+ | 'setDragging'
+ | 'setValueState'
+ | 'step'
+ | 'thumbRefs'
+ > {
+ /**
+ * The ref attached to the control area of the Slider.
+ */
+ rootRef?: React.Ref;
+ }
+
+ export interface ReturnValue {
+ getRootProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps;
+ }
+}
diff --git a/packages/mui-base/src/Slider/Indicator/SliderIndicator.test.tsx b/packages/mui-base/src/Slider/Indicator/SliderIndicator.test.tsx
index 979b288ed8..658c9a54f8 100644
--- a/packages/mui-base/src/Slider/Indicator/SliderIndicator.test.tsx
+++ b/packages/mui-base/src/Slider/Indicator/SliderIndicator.test.tsx
@@ -1,65 +1,66 @@
import * as React from 'react';
import { Slider } from '@base_ui/react/Slider';
-import { SliderProvider, type SliderProviderValue } from '@base_ui/react/Slider/index.parts';
import { createRenderer, describeConformance } from '#test-utils';
+import { SliderContext } from '../Root/SliderContext';
+import type { SliderRoot } from '../Root/SliderRoot';
+import { NOOP } from '../../utils/noop';
-const NOOP = () => {};
-
-describe('', () => {
- const { render } = createRenderer();
-
- const testProviderValue: SliderProviderValue = {
- active: -1,
- areValuesEqual: () => true,
- axis: 'horizontal',
- changeValue: NOOP,
- compoundComponentContextValue: {
- registerItem: () => ({ id: 0, deregister: () => {} }),
- getItemIndex: () => 0,
- totalSubitemCount: 1,
- },
- dragging: false,
+const testRootContext: SliderRoot.Context = {
+ active: -1,
+ areValuesEqual: () => true,
+ axis: 'horizontal',
+ changeValue: NOOP,
+ direction: 'ltr',
+ dragging: false,
+ disabled: false,
+ getFingerNewValue: () => ({
+ newValue: 0,
+ activeIndex: 0,
+ newPercentageValue: 0,
+ }),
+ handleValueChange: NOOP,
+ largeStep: 10,
+ inputIdMap: new Map(),
+ max: 100,
+ min: 0,
+ minStepsBetweenValues: 0,
+ orientation: 'horizontal',
+ ownerState: {
+ activeThumbIndex: -1,
disabled: false,
- getFingerNewValue: () => ({
- newValue: 0,
- activeIndex: 0,
- newPercentageValue: 0,
- }),
- handleValueChange: NOOP,
+ dragging: false,
direction: 'ltr',
- largeStep: 10,
max: 100,
min: 0,
minStepsBetweenValues: 0,
orientation: 'horizontal',
- ownerState: {
- activeThumbIndex: -1,
- disabled: false,
- dragging: false,
- direction: 'ltr',
- max: 100,
- min: 0,
- minStepsBetweenValues: 0,
- orientation: 'horizontal',
- step: 1,
- values: [0],
- valid: null,
- dirty: false,
- touched: false,
- },
- percentageValues: [0],
- registerSliderControl: NOOP,
- setActive: NOOP,
- setDragging: NOOP,
- setValueState: NOOP,
step: 1,
- subitems: new Map(),
values: [0],
- };
+ valid: null,
+ dirty: false,
+ touched: false,
+ },
+ percentageValues: [0],
+ registerInputId: () => ({
+ deregister: NOOP,
+ }),
+ registerSliderControl: NOOP,
+ setActive: NOOP,
+ setDragging: NOOP,
+ setValueState: NOOP,
+ step: 1,
+ thumbRefs: { current: [] },
+ values: [0],
+};
+
+describe('', () => {
+ const { render } = createRenderer();
describeConformance(, () => ({
render: (node) => {
- return render({node});
+ return render(
+ {node},
+ );
},
refInstanceof: window.HTMLSpanElement,
}));
diff --git a/packages/mui-base/src/Slider/Indicator/SliderIndicator.tsx b/packages/mui-base/src/Slider/Indicator/SliderIndicator.tsx
index 60b85841b8..e28914e3f1 100644
--- a/packages/mui-base/src/Slider/Indicator/SliderIndicator.tsx
+++ b/packages/mui-base/src/Slider/Indicator/SliderIndicator.tsx
@@ -1,12 +1,12 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
+import type { BaseUIComponentProps } from '../../utils/types';
import { useComponentRenderer } from '../../utils/useComponentRenderer';
-import { useSliderContext } from '../Root/SliderProvider';
+import { useSliderContext } from '../Root/SliderContext';
import { sliderStyleHookMapping } from '../Root/styleHooks';
-import { SliderIndicatorProps } from './SliderIndicator.types';
+import type { SliderRoot } from '../Root/SliderRoot';
import { useSliderIndicator } from './useSliderIndicator';
-
/**
*
* Demos:
@@ -18,8 +18,8 @@ import { useSliderIndicator } from './useSliderIndicator';
* - [SliderIndicator API](https://base-ui.netlify.app/components/react-slider/#api-reference-SliderIndicator)
*/
const SliderIndicator = React.forwardRef(function SliderIndicator(
- props: SliderIndicatorProps,
- forwardedRef: React.ForwardedRef,
+ props: SliderIndicator.Props,
+ forwardedRef: React.ForwardedRef,
) {
const { render, className, ...otherProps } = props;
@@ -47,6 +47,12 @@ const SliderIndicator = React.forwardRef(function SliderIndicator(
return renderElement();
});
+export namespace SliderIndicator {
+ export interface Props extends BaseUIComponentProps<'span', SliderRoot.OwnerState> {}
+}
+
+export { SliderIndicator };
+
SliderIndicator.propTypes /* remove-proptypes */ = {
// ┌────────────────────────────── Warning ──────────────────────────────┐
// │ These PropTypes are generated from the TypeScript type definitions. │
@@ -65,5 +71,3 @@ SliderIndicator.propTypes /* remove-proptypes */ = {
*/
render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
} as any;
-
-export { SliderIndicator };
diff --git a/packages/mui-base/src/Slider/Indicator/SliderIndicator.types.ts b/packages/mui-base/src/Slider/Indicator/SliderIndicator.types.ts
deleted file mode 100644
index f8916b0d35..0000000000
--- a/packages/mui-base/src/Slider/Indicator/SliderIndicator.types.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { BaseUIComponentProps } from '../../utils/types';
-import { SliderRootOwnerState, UseSliderReturnValue } from '../Root/SliderRoot.types';
-
-export interface SliderIndicatorProps extends BaseUIComponentProps<'span', SliderRootOwnerState> {}
-
-export interface UseSliderIndicatorParameters
- extends Pick<
- UseSliderReturnValue,
- 'axis' | 'direction' | 'disabled' | 'orientation' | 'percentageValues'
- > {}
-
-export interface UseSliderIndicatorReturnValue {
- getRootProps: (
- externalProps?: React.ComponentPropsWithRef<'span'>,
- ) => React.ComponentPropsWithRef<'span'>;
-}
diff --git a/packages/mui-base/src/Slider/Indicator/useSliderIndicator.ts b/packages/mui-base/src/Slider/Indicator/useSliderIndicator.ts
index a7322e43be..0c0255c767 100644
--- a/packages/mui-base/src/Slider/Indicator/useSliderIndicator.ts
+++ b/packages/mui-base/src/Slider/Indicator/useSliderIndicator.ts
@@ -1,10 +1,8 @@
'use client';
import * as React from 'react';
import { mergeReactProps } from '../../utils/mergeReactProps';
-import {
- UseSliderIndicatorParameters,
- UseSliderIndicatorReturnValue,
-} from './SliderIndicator.types';
+import type { GenericHTMLProps } from '../../utils/types';
+import type { useSliderRoot } from '../Root/useSliderRoot';
const axisProps = {
horizontal: {
@@ -21,9 +19,19 @@ const axisProps = {
},
};
-function useSliderIndicator(
- parameters: UseSliderIndicatorParameters,
-): UseSliderIndicatorReturnValue {
+/**
+ *
+ * Demos:
+ *
+ * - [Slider](https://mui.com/base-ui/react-slider/#hooks)
+ *
+ * API:
+ *
+ * - [useSliderIndicator API](https://mui.com/base-ui/react-slider/hooks-api/#use-slider-indicator)
+ */
+export function useSliderIndicator(
+ parameters: useSliderIndicator.Parameters,
+): useSliderIndicator.ReturnValue {
const { axis, direction, orientation, percentageValues } = parameters;
const isRange = percentageValues.length > 1;
@@ -74,4 +82,14 @@ function useSliderIndicator(
);
}
-export { useSliderIndicator };
+export namespace useSliderIndicator {
+ export interface Parameters
+ extends Pick<
+ useSliderRoot.ReturnValue,
+ 'axis' | 'direction' | 'disabled' | 'orientation' | 'percentageValues'
+ > {}
+
+ export interface ReturnValue {
+ getRootProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps;
+ }
+}
diff --git a/packages/mui-base/src/Slider/Output/SliderOutput.test.tsx b/packages/mui-base/src/Slider/Output/SliderOutput.test.tsx
index 62d26e9c52..500877d604 100644
--- a/packages/mui-base/src/Slider/Output/SliderOutput.test.tsx
+++ b/packages/mui-base/src/Slider/Output/SliderOutput.test.tsx
@@ -1,66 +1,67 @@
import * as React from 'react';
import { expect } from 'chai';
import { Slider } from '@base_ui/react/Slider';
-import { SliderProvider, type SliderProviderValue } from '@base_ui/react/Slider/index.parts';
import { createRenderer, describeConformance } from '#test-utils';
+import { SliderContext } from '../Root/SliderContext';
+import { NOOP } from '../../utils/noop';
+import type { SliderRoot } from '../Root/SliderRoot';
-const NOOP = () => {};
-
-describe('', () => {
- const { render } = createRenderer();
-
- const testProviderValue: SliderProviderValue = {
- active: -1,
- areValuesEqual: () => true,
- axis: 'horizontal',
- changeValue: NOOP,
- compoundComponentContextValue: {
- registerItem: () => ({ id: 0, deregister: () => {} }),
- getItemIndex: () => 0,
- totalSubitemCount: 1,
- },
- dragging: false,
+const testRootContext: SliderRoot.Context = {
+ active: -1,
+ areValuesEqual: () => true,
+ axis: 'horizontal',
+ changeValue: NOOP,
+ direction: 'ltr',
+ dragging: false,
+ disabled: false,
+ getFingerNewValue: () => ({
+ newValue: 0,
+ activeIndex: 0,
+ newPercentageValue: 0,
+ }),
+ handleValueChange: NOOP,
+ largeStep: 10,
+ inputIdMap: new Map(),
+ max: 100,
+ min: 0,
+ minStepsBetweenValues: 0,
+ orientation: 'horizontal',
+ ownerState: {
+ activeThumbIndex: -1,
disabled: false,
- getFingerNewValue: () => ({
- newValue: 0,
- activeIndex: 0,
- newPercentageValue: 0,
- }),
- handleValueChange: NOOP,
+ dragging: false,
direction: 'ltr',
- largeStep: 10,
max: 100,
min: 0,
minStepsBetweenValues: 0,
orientation: 'horizontal',
- ownerState: {
- activeThumbIndex: -1,
- disabled: false,
- dragging: false,
- direction: 'ltr',
- max: 100,
- min: 0,
- minStepsBetweenValues: 0,
- orientation: 'horizontal',
- step: 1,
- values: [0],
- valid: null,
- dirty: false,
- touched: false,
- },
- percentageValues: [0],
- registerSliderControl: NOOP,
- setActive: NOOP,
- setDragging: NOOP,
- setValueState: NOOP,
step: 1,
- subitems: new Map(),
values: [0],
- };
+ valid: null,
+ dirty: false,
+ touched: false,
+ },
+ percentageValues: [0],
+ registerInputId: () => ({
+ deregister: NOOP,
+ }),
+ registerSliderControl: NOOP,
+ setActive: NOOP,
+ setDragging: NOOP,
+ setValueState: NOOP,
+ step: 1,
+ thumbRefs: { current: [] },
+ values: [0],
+};
+
+describe('', () => {
+ const { render } = createRenderer();
describeConformance(, () => ({
render: (node) => {
- return render({node});
+ return render(
+ {node},
+ );
},
refInstanceof: window.HTMLOutputElement,
}));
diff --git a/packages/mui-base/src/Slider/Output/SliderOutput.tsx b/packages/mui-base/src/Slider/Output/SliderOutput.tsx
index 0f69309611..a2d80a44ee 100644
--- a/packages/mui-base/src/Slider/Output/SliderOutput.tsx
+++ b/packages/mui-base/src/Slider/Output/SliderOutput.tsx
@@ -1,12 +1,12 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
+import type { BaseUIComponentProps } from '../../utils/types';
import { useComponentRenderer } from '../../utils/useComponentRenderer';
-import { useSliderContext } from '../Root/SliderProvider';
+import { useSliderContext } from '../Root/SliderContext';
import { sliderStyleHookMapping } from '../Root/styleHooks';
-import { SliderOutputProps } from './SliderOutput.types';
+import type { SliderRoot } from '../Root/SliderRoot';
import { useSliderOutput } from './useSliderOutput';
-
/**
*
* Demos:
@@ -18,15 +18,15 @@ import { useSliderOutput } from './useSliderOutput';
* - [SliderOutput API](https://base-ui.netlify.app/components/react-slider/#api-reference-SliderOutput)
*/
const SliderOutput = React.forwardRef(function SliderOutput(
- props: SliderOutputProps,
+ props: SliderOutput.Props,
forwardedRef: React.ForwardedRef,
) {
const { render, className, ...otherProps } = props;
- const { ownerState, subitems, values } = useSliderContext();
+ const { inputIdMap, ownerState, values } = useSliderContext();
const { getRootProps } = useSliderOutput({
- subitems,
+ inputIdMap,
});
const { renderElement } = useComponentRenderer({
@@ -45,6 +45,12 @@ const SliderOutput = React.forwardRef(function SliderOutput(
return renderElement();
});
+export namespace SliderOutput {
+ export interface Props extends BaseUIComponentProps<'output', SliderRoot.OwnerState> {}
+}
+
+export { SliderOutput };
+
SliderOutput.propTypes /* remove-proptypes */ = {
// ┌────────────────────────────── Warning ──────────────────────────────┐
// │ These PropTypes are generated from the TypeScript type definitions. │
@@ -63,5 +69,3 @@ SliderOutput.propTypes /* remove-proptypes */ = {
*/
render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
} as any;
-
-export { SliderOutput };
diff --git a/packages/mui-base/src/Slider/Output/SliderOutput.types.ts b/packages/mui-base/src/Slider/Output/SliderOutput.types.ts
deleted file mode 100644
index a93f2cf1ae..0000000000
--- a/packages/mui-base/src/Slider/Output/SliderOutput.types.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { BaseUIComponentProps } from '../../utils/types';
-import { SliderRootOwnerState, UseSliderReturnValue } from '../Root/SliderRoot.types';
-
-export interface SliderOutputProps extends BaseUIComponentProps<'output', SliderRootOwnerState> {}
-
-export interface UseSliderOutputParameters extends Pick {
- 'aria-live'?: React.AriaAttributes['aria-live'];
-}
-
-export interface UseSliderOutputReturnValue {
- getRootProps: (
- externalProps?: React.ComponentPropsWithRef<'output'>,
- ) => React.ComponentPropsWithRef<'output'>;
-}
diff --git a/packages/mui-base/src/Slider/Output/useSliderOutput.ts b/packages/mui-base/src/Slider/Output/useSliderOutput.ts
index 7ad3534ec9..638f62d6ec 100644
--- a/packages/mui-base/src/Slider/Output/useSliderOutput.ts
+++ b/packages/mui-base/src/Slider/Output/useSliderOutput.ts
@@ -1,14 +1,25 @@
'use client';
import * as React from 'react';
import { mergeReactProps } from '../../utils/mergeReactProps';
-import { UseSliderOutputParameters, UseSliderOutputReturnValue } from './SliderOutput.types';
+import type { useSliderRoot } from '../Root/useSliderRoot';
-function useSliderOutput(parameters: UseSliderOutputParameters): UseSliderOutputReturnValue {
- const { 'aria-live': ariaLive = 'off', subitems } = parameters;
+export function useSliderOutput(
+ parameters: useSliderOutput.Parameters,
+): useSliderOutput.ReturnValue {
+ const { 'aria-live': ariaLive = 'off', inputIdMap } = parameters;
- const outputFor = Array.from(subitems.values()).reduce((acc, item) => {
- return `${acc} ${item.inputId}`;
- }, '');
+ const outputFor = React.useMemo(() => {
+ const size = inputIdMap.size;
+ let htmlFor = '';
+ for (let i = 0; i < size; i += 1) {
+ const inputId = inputIdMap.get(i);
+ if (!inputId) {
+ break;
+ }
+ htmlFor += `${inputId} `;
+ }
+ return htmlFor.trim() === '' ? undefined : htmlFor.trim();
+ }, [inputIdMap]);
const getRootProps = React.useCallback(
(externalProps = {}) => {
@@ -16,7 +27,7 @@ function useSliderOutput(parameters: UseSliderOutputParameters): UseSliderOutput
// off by default because it will keep announcing when the slider is being dragged
// and also when the value is changing (but not yet committed)
'aria-live': ariaLive,
- htmlFor: outputFor.trim(),
+ htmlFor: outputFor,
});
},
[ariaLive, outputFor],
@@ -30,4 +41,14 @@ function useSliderOutput(parameters: UseSliderOutputParameters): UseSliderOutput
);
}
-export { useSliderOutput };
+export namespace useSliderOutput {
+ export interface Parameters extends Pick {
+ 'aria-live'?: React.AriaAttributes['aria-live'];
+ }
+
+ export interface ReturnValue {
+ getRootProps: (
+ externalProps?: React.ComponentPropsWithRef<'output'>,
+ ) => React.ComponentPropsWithRef<'output'>;
+ }
+}
diff --git a/packages/mui-base/src/Slider/Root/SliderContext.tsx b/packages/mui-base/src/Slider/Root/SliderContext.tsx
new file mode 100644
index 0000000000..33cb14bfe7
--- /dev/null
+++ b/packages/mui-base/src/Slider/Root/SliderContext.tsx
@@ -0,0 +1,20 @@
+'use client';
+import * as React from 'react';
+import type { SliderRoot } from './SliderRoot';
+
+/**
+ * @ignore - internal component.
+ */
+export const SliderContext = React.createContext(undefined);
+
+if (process.env.NODE_ENV !== 'production') {
+ SliderContext.displayName = 'SliderContext';
+}
+
+export function useSliderContext() {
+ const context = React.useContext(SliderContext);
+ if (context === undefined) {
+ throw new Error('useSliderContext must be used inside a Slider component');
+ }
+ return context;
+}
diff --git a/packages/mui-base/src/Slider/Root/SliderProvider.tsx b/packages/mui-base/src/Slider/Root/SliderProvider.tsx
deleted file mode 100644
index 12444b470c..0000000000
--- a/packages/mui-base/src/Slider/Root/SliderProvider.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-'use client';
-import * as React from 'react';
-import PropTypes from 'prop-types';
-import { CompoundComponentContext } from '../../useCompound';
-import { SliderContextValue, SliderProviderValue } from './SliderRoot.types';
-
-export interface SliderProviderProps {
- value: SliderProviderValue;
- children: React.ReactNode;
-}
-
-export const SliderContext = React.createContext(undefined);
-
-if (process.env.NODE_ENV !== 'production') {
- SliderContext.displayName = 'SliderContext';
-}
-
-export function useSliderContext() {
- const context = React.useContext(SliderContext);
- if (context === undefined) {
- throw new Error('useSliderContext must be used inside a Slider component');
- }
- return context;
-}
-
-/**
- * Sets up contexts for the Slider and its subcomponents.
- *
- * @ignore - do not document.
- */
-const SliderProvider: React.FC = function SliderProvider(props) {
- const { value: valueProp, children } = props;
-
- const { compoundComponentContextValue, ...contextValue } = valueProp;
-
- return (
-
- {children}
-
- );
-};
-
-SliderProvider.propTypes /* remove-proptypes */ = {
- // ┌────────────────────────────── Warning ──────────────────────────────┐
- // │ These PropTypes are generated from the TypeScript type definitions. │
- // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
- // └─────────────────────────────────────────────────────────────────────┘
- /**
- * @ignore
- */
- children: PropTypes.node,
- /**
- * @ignore
- */
- value: PropTypes /* @typescript-to-proptypes-ignore */.shape({
- active: PropTypes.number.isRequired,
- areValuesEqual: PropTypes.func.isRequired,
- 'aria-labelledby': PropTypes.string,
- axis: PropTypes.oneOf(['horizontal-reverse', 'horizontal', 'vertical']).isRequired,
- changeValue: PropTypes.func.isRequired,
- compoundComponentContextValue: PropTypes.shape({
- getItemIndex: PropTypes.func.isRequired,
- registerItem: PropTypes.func.isRequired,
- totalSubitemCount: PropTypes.number.isRequired,
- }).isRequired,
- direction: PropTypes.oneOf(['ltr', 'rtl']).isRequired,
- disabled: PropTypes.bool.isRequired,
- dragging: PropTypes.bool.isRequired,
- getFingerNewValue: PropTypes.func.isRequired,
- handleValueChange: PropTypes.func.isRequired,
- largeStep: PropTypes.number.isRequired,
- max: PropTypes.number.isRequired,
- min: PropTypes.number.isRequired,
- minStepsBetweenValues: PropTypes.number.isRequired,
- name: PropTypes.string,
- onValueCommitted: PropTypes.func,
- orientation: PropTypes.oneOf(['horizontal', 'vertical']).isRequired,
- ownerState: PropTypes.shape({
- activeThumbIndex: PropTypes.number.isRequired,
- direction: PropTypes.oneOf(['ltr', 'rtl']).isRequired,
- disabled: PropTypes.bool.isRequired,
- dragging: PropTypes.bool.isRequired,
- max: PropTypes.number.isRequired,
- min: PropTypes.number.isRequired,
- minStepsBetweenValues: PropTypes.number.isRequired,
- orientation: PropTypes.oneOf(['horizontal', 'vertical']).isRequired,
- step: PropTypes.number.isRequired,
- values: PropTypes.arrayOf(PropTypes.number).isRequired,
- }).isRequired,
- percentageValues: PropTypes.arrayOf(PropTypes.number).isRequired,
- registerSliderControl: PropTypes.func.isRequired,
- setActive: PropTypes.func.isRequired,
- setDragging: PropTypes.func.isRequired,
- setValueState: PropTypes.func.isRequired,
- step: PropTypes.number.isRequired,
- subitems: PropTypes.object.isRequired,
- tabIndex: PropTypes.number,
- values: PropTypes.arrayOf(PropTypes.number).isRequired,
- }).isRequired,
-} as any;
-
-export { SliderProvider };
diff --git a/packages/mui-base/src/Slider/Root/SliderRoot.test.tsx b/packages/mui-base/src/Slider/Root/SliderRoot.test.tsx
index e6ca53c50f..2073b8415f 100644
--- a/packages/mui-base/src/Slider/Root/SliderRoot.test.tsx
+++ b/packages/mui-base/src/Slider/Root/SliderRoot.test.tsx
@@ -4,7 +4,7 @@ import { spy, stub } from 'sinon';
import { act, fireEvent, screen } from '@mui/internal-test-utils';
import { Slider } from '@base_ui/react/Slider';
import { createRenderer, describeConformance } from '#test-utils';
-import type { SliderRootProps } from './SliderRoot.types';
+import type { SliderRoot } from './SliderRoot';
type Touches = Array>;
@@ -32,7 +32,7 @@ function createTouches(touches: Touches) {
};
}
-function TestSlider(props: SliderRootProps) {
+function TestSlider(props: SliderRoot.Props) {
return (
@@ -46,7 +46,7 @@ function TestSlider(props: SliderRootProps) {
);
}
-function TestRangeSlider(props: SliderRootProps) {
+function TestRangeSlider(props: SliderRoot.Props) {
return (
diff --git a/packages/mui-base/src/Slider/Root/SliderRoot.tsx b/packages/mui-base/src/Slider/Root/SliderRoot.tsx
index da75649129..274855b2a3 100644
--- a/packages/mui-base/src/Slider/Root/SliderRoot.tsx
+++ b/packages/mui-base/src/Slider/Root/SliderRoot.tsx
@@ -1,13 +1,14 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
+import type { BaseUIComponentProps } from '../../utils/types';
import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import type { FieldRoot } from '../../Field/Root/FieldRoot';
+import { CompositeList } from '../../Composite/List/CompositeList';
import { sliderStyleHookMapping } from './styleHooks';
import { useSliderRoot } from './useSliderRoot';
-import { SliderProvider } from './SliderProvider';
-import { SliderRootProps, SliderRootOwnerState } from './SliderRoot.types';
+import { SliderContext } from './SliderContext';
import { useFieldRootContext } from '../../Field/Root/FieldRootContext';
-
/**
*
* Demos:
@@ -19,7 +20,7 @@ import { useFieldRootContext } from '../../Field/Root/FieldRootContext';
* - [SliderRoot API](https://base-ui.netlify.app/components/react-slider/#api-reference-SliderRoot)
*/
const SliderRoot = React.forwardRef(function SliderRoot(
- props: SliderRootProps,
+ props: SliderRoot.Props,
forwardedRef: React.ForwardedRef,
) {
const {
@@ -56,7 +57,7 @@ const SliderRoot = React.forwardRef(function SliderRoot(
...otherProps,
});
- const ownerState: SliderRootOwnerState = React.useMemo(
+ const ownerState: SliderRoot.OwnerState = React.useMemo(
() => ({
...fieldOwnerState,
activeThumbIndex: slider.active,
@@ -102,9 +103,78 @@ const SliderRoot = React.forwardRef(function SliderRoot(
customStyleHookMapping: sliderStyleHookMapping,
});
- return {renderElement()};
+ return (
+
+ {renderElement()}
+
+ );
});
+export namespace SliderRoot {
+ export interface Context
+ extends Omit {
+ ownerState: OwnerState;
+ }
+
+ export interface OwnerState extends FieldRoot.OwnerState {
+ /**
+ * The index of the active thumb.
+ */
+ activeThumbIndex: number;
+ /**
+ * If `true`, the component is disabled.
+ */
+ disabled: boolean;
+ /**
+ * If `true`, a thumb is being dragged by a pointer.
+ */
+ dragging: boolean;
+ direction: useSliderRoot.Direction;
+ max: number;
+ min: number;
+ /**
+ * The minimum steps between values in a range slider.
+ * @default 0
+ */
+ minStepsBetweenValues: number;
+ /**
+ * The component orientation.
+ */
+ orientation: useSliderRoot.Orientation;
+ /**
+ * The step increment of the slider when incrementing or decrementing. It will snap
+ * to multiples of this value. Decimal values are supported.
+ * @default 1
+ */
+ step: number;
+ /**
+ * The raw number value of the slider.
+ */
+ values: ReadonlyArray;
+ }
+
+ export interface Props
+ extends Omit,
+ Omit, 'defaultValue' | 'onChange' | 'values'> {
+ /**
+ * The default value of the slider. Use when the component is not controlled.
+ */
+ defaultValue?: number | ReadonlyArray;
+ /**
+ * If `true`, the component is disabled.
+ * @default false
+ */
+ disabled?: boolean;
+ /**
+ * The value of the slider.
+ * For ranged sliders, provide an array with two values.
+ */
+ value?: number | ReadonlyArray;
+ }
+}
+
+export { SliderRoot };
+
SliderRoot.propTypes /* remove-proptypes */ = {
// ┌────────────────────────────── Warning ──────────────────────────────┐
// │ These PropTypes are generated from the TypeScript type definitions. │
@@ -136,36 +206,16 @@ SliderRoot.propTypes /* remove-proptypes */ = {
* @default false
*/
disabled: PropTypes.bool,
- /**
- * The id of the slider element.
- */
- id: PropTypes.string,
/**
* The granularity with which the slider can step through values when using Page Up/Page Down or Shift + Arrow Up/Arrow Down.
* @default 10
*/
largeStep: PropTypes.number,
- /**
- * 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,
/**
* The minimum steps between values in a range slider.
* @default 0
*/
minStepsBetweenValues: PropTypes.number,
- /**
- * Name attribute of the hidden `input` element.
- */
- name: PropTypes.string,
/**
* Callback function that is fired when the slider's value changed.
*
@@ -193,22 +243,9 @@ SliderRoot.propTypes /* remove-proptypes */ = {
* A function to customize rendering of the component.
*/
render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
- /**
- * 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.
- * @default 1
- */
- step: PropTypes.number,
- /**
- * Tab index attribute of the Thumb component's `input` element.
- */
- tabIndex: PropTypes.number,
/**
* The value of the slider.
* For ranged sliders, provide an array with two values.
*/
value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]),
} as any;
-
-export { SliderRoot };
diff --git a/packages/mui-base/src/Slider/Root/SliderRoot.types.ts b/packages/mui-base/src/Slider/Root/SliderRoot.types.ts
deleted file mode 100644
index 1cb4b53fa5..0000000000
--- a/packages/mui-base/src/Slider/Root/SliderRoot.types.ts
+++ /dev/null
@@ -1,286 +0,0 @@
-import type { BaseUIComponentProps } from '../../utils/types';
-import type { CompoundComponentContextValue } from '../../useCompound';
-import type { FieldRoot } from '../../Field/Root/FieldRoot';
-
-export interface SliderThumbMetadata {
- inputId: string;
- ref: React.RefObject;
- inputRef: React.RefObject;
-}
-
-export type SliderContextValue = Omit<
- UseSliderReturnValue,
- 'compoundComponentContextValue' | 'getRootProps'
-> & {
- ownerState: SliderRootOwnerState;
-};
-
-export type SliderProviderValue = SliderContextValue & {
- compoundComponentContextValue: CompoundComponentContextValue;
-};
-
-export type SliderDirection = 'ltr' | 'rtl';
-
-export type SliderOrientation = 'horizontal' | 'vertical';
-
-export interface SliderRootOwnerState extends FieldRoot.OwnerState {
- /**
- * The index of the active thumb.
- */
- activeThumbIndex: number;
- /**
- * If `true`, the component is disabled.
- */
- disabled: boolean;
- /**
- * If `true`, a thumb is being dragged by a pointer.
- */
- dragging: boolean;
- direction: SliderDirection;
- max: number;
- min: number;
- /**
- * The minimum steps between values in a range slider.
- * @default 0
- */
- minStepsBetweenValues: number;
- /**
- * The component orientation.
- */
- orientation: SliderOrientation;
- /**
- * The step increment of the slider when incrementing or decrementing. It will snap
- * to multiples of this value. Decimal values are supported.
- * @default 1
- */
- step: number;
- /**
- * The raw number value of the slider.
- */
- values: ReadonlyArray;
-}
-
-export interface SliderRootProps
- extends Omit,
- Omit<
- BaseUIComponentProps<'span', SliderRootOwnerState>,
- 'defaultValue' | 'onChange' | 'values'
- > {
- /**
- * The default value of the slider. Use when the component is not controlled.
- */
- defaultValue?: number | ReadonlyArray;
- /**
- * If `true`, the component is disabled.
- * @default false
- */
- disabled?: boolean;
- /**
- * The value of the slider.
- * For ranged sliders, provide an array with two values.
- */
- value?: number | ReadonlyArray;
-}
-
-export interface UseSliderParameters {
- /**
- * The id of the slider element.
- */
- id?: string;
- /**
- * The id of the element containing a label for the slider.
- */
- 'aria-labelledby'?: string;
- /**
- * The default value. Use when the component is not controlled.
- */
- defaultValue?: number | ReadonlyArray;
- /**
- * Sets the direction. For right-to-left languages, the lowest value is on the right-hand side.
- * @default 'ltr'
- */
- direction?: SliderDirection;
- /**
- * If `true`, the component is disabled.
- * @default false
- */
- disabled?: boolean;
- /**
- * 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;
- /**
- * The minimum steps between values in a range slider.
- * @default 0
- */
- minStepsBetweenValues?: number;
- /**
- * Name attribute of the hidden `input` element.
- */
- name?: string;
- /**
- * Callback function that is fired when the slider's value changed.
- *
- * @param {number | number[]} value The new value.
- * @param {number} activeThumb Index of the currently moved thumb.
- * @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.
- */
- onValueChange?: (value: number | number[], activeThumb: number, event: Event) => void;
- /**
- * Callback function that is fired when the `pointerup` is triggered.
- *
- * @param {number | number[]} value The new value.
- * @param {Event} event The event source of the callback.
- * **Warning**: This is a generic event not a change event.
- */
- onValueCommitted?: (value: number | number[], event: Event) => void;
- /**
- * The component orientation.
- * @default 'horizontal'
- */
- orientation?: SliderOrientation;
- /**
- * The ref attached to the root of the Slider.
- */
- rootRef?: React.Ref;
- /**
- * The granularity with which the slider can step through values when using Page Up/Page Down or Shift + Arrow Up/Arrow Down.
- * @default 10
- */
- largeStep?: 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.
- * @default 1
- */
- step?: number;
- /**
- * Tab index attribute of the Thumb component's `input` element.
- */
- tabIndex?: number;
- /**
- * The value of the slider.
- * For ranged sliders, provide an array with two values.
- */
- value?: number | ReadonlyArray;
-}
-
-export type Axis = SliderOrientation | 'horizontal-reverse';
-
-export interface AxisProps {
- offset: (
- percent: number,
- ) => T extends 'horizontal'
- ? { left: string }
- : T extends 'vertical'
- ? { bottom: string }
- : T extends 'horizontal-reverse'
- ? { right: string }
- : never;
- leap: (
- percent: number,
- ) => T extends 'horizontal' | 'horizontal-reverse'
- ? { width: string }
- : T extends 'vertical'
- ? { height: string }
- : never;
-}
-
-export interface UseSliderReturnValue {
- getRootProps: (
- externalProps?: React.ComponentPropsWithRef<'span'>,
- ) => React.ComponentPropsWithRef<'span'>;
- /**
- * The index of the active thumb.
- */
- active: number;
- /**
- * A function that compares a new value with the internal value of the slider.
- * The internal value is potentially unsorted, e.g. to support frozen arrays: https://github.com/mui/material-ui/pull/28472
- */
- areValuesEqual: (newValue: number | ReadonlyArray) => boolean;
- 'aria-labelledby'?: string;
- /**
- * The orientation of the slider.
- */
- axis: Axis;
- changeValue: (
- valueInput: number,
- index: number,
- event: React.KeyboardEvent | React.ChangeEvent,
- ) => void;
- compoundComponentContextValue: CompoundComponentContextValue;
- dragging: boolean;
- direction: SliderDirection;
- disabled: boolean;
- getFingerNewValue: (args: {
- finger: { x: number; y: number };
- move?: boolean;
- offset?: number;
- activeIndex?: number;
- }) => { newValue: number | number[]; activeIndex: number; newPercentageValue: number } | null;
- handleValueChange: (
- value: number | number[],
- activeThumb: number,
- event: React.SyntheticEvent | Event,
- ) => void;
- /**
- * The large step value of the slider when incrementing or decrementing while the shift key is held,
- * or when using Page-Up or Page-Down keys. Snaps to multiples of this value.
- * @default 10
- */
- largeStep: number;
- /**
- * The maximum allowed value of the slider.
- */
- max: number;
- /**
- * The minimum allowed value of the slider.
- */
- min: number;
- /**
- * The minimum steps between values in a range slider.
- */
- minStepsBetweenValues: number;
- name?: string;
- onValueCommitted?: (value: number | number[], event: Event) => void;
- /**
- * The component orientation.
- * @default 'horizontal'
- */
- orientation: SliderOrientation;
- registerSliderControl: (element: HTMLElement | null) => void;
- /**
- * The value(s) of the slider as percentages
- */
- percentageValues: readonly number[];
- setActive: (activeIndex: number) => void;
- setDragging: (isDragging: boolean) => void;
- setValueState: (newValue: number | number[]) => void;
- /**
- * The step increment of the slider when incrementing or decrementing. It will snap
- * to multiples of this value. Decimal values are supported.
- * @default 1
- */
- step: number;
- /**
- * A map containing all the Thumb components registered to the slider
- */
- subitems: Map;
- tabIndex?: number;
- /**
- * The value(s) of the slider
- */
- values: readonly number[];
-}
diff --git a/packages/mui-base/src/Slider/Root/styleHooks.ts b/packages/mui-base/src/Slider/Root/styleHooks.ts
index e90a3d172c..72b3b2879e 100644
--- a/packages/mui-base/src/Slider/Root/styleHooks.ts
+++ b/packages/mui-base/src/Slider/Root/styleHooks.ts
@@ -1,7 +1,7 @@
import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps';
-import type { SliderRootOwnerState } from './SliderRoot.types';
+import type { SliderRoot } from './SliderRoot';
-export const sliderStyleHookMapping: CustomStyleHookMapping = {
+export const sliderStyleHookMapping: CustomStyleHookMapping = {
activeThumbIndex: () => null,
direction: () => null,
max: () => null,
diff --git a/packages/mui-base/src/Slider/Root/useSliderRoot.ts b/packages/mui-base/src/Slider/Root/useSliderRoot.ts
index ecce38de00..1984aa5ac4 100644
--- a/packages/mui-base/src/Slider/Root/useSliderRoot.ts
+++ b/packages/mui-base/src/Slider/Root/useSliderRoot.ts
@@ -7,10 +7,8 @@ import { mergeReactProps } from '../../utils/mergeReactProps';
import { ownerDocument } from '../../utils/owner';
import { useControlled } from '../../utils/useControlled';
import { useForkRef } from '../../utils/useForkRef';
-import { useCompoundParent } from '../../useCompound';
import { useEnhancedEffect } from '../../utils/useEnhancedEffect';
import { percentToValue, roundValueToStep, valueToPercent } from '../utils';
-import { SliderThumbMetadata, UseSliderParameters, UseSliderReturnValue } from './SliderRoot.types';
import { useFieldRootContext } from '../../Field/Root/FieldRootContext';
import { useId } from '../../utils/useId';
import { useFieldControlValidation } from '../../Field/Control/useFieldControlValidation';
@@ -120,7 +118,7 @@ export function trackFinger(
*
* - [useSliderRoot API](https://mui.com/base-ui/react-slider/hooks-api/#use-slider-root)
*/
-function useSliderRoot(parameters: UseSliderParameters): UseSliderReturnValue {
+export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRoot.ReturnValue {
const {
'aria-labelledby': ariaLabelledby,
id: idProp,
@@ -190,10 +188,31 @@ function useSliderRoot(parameters: UseSliderParameters): UseSliderReturnValue {
[inputValidationRef],
);
- const { contextValue: compoundComponentContextValue, subitems } = useCompoundParent<
- string,
- SliderThumbMetadata
- >();
+ const thumbRefs = React.useRef<(HTMLElement | null)[]>([]);
+
+ // Map with index (DOM position) as the key and the id attribute of each thumb element as the value
+ const [inputIdMap, setInputMap] = React.useState(() => new Map());
+
+ const deregisterInputId = React.useCallback((index: number) => {
+ setInputMap((prevMap) => {
+ const nextMap = new Map(prevMap);
+ nextMap.delete(index);
+ return nextMap;
+ });
+ }, []);
+
+ const registerInputId = React.useCallback(
+ (index: number, inputId: string | undefined) => {
+ if (index > -1 && inputId !== undefined) {
+ setInputMap((prevMap) => new Map(prevMap).set(index, inputId));
+ }
+
+ return {
+ deregister: deregisterInputId,
+ };
+ },
+ [deregisterInputId],
+ );
const handleValueChange = React.useCallback(
(value: number | number[], thumbIndex: number, event: Event | React.SyntheticEvent) => {
@@ -391,7 +410,7 @@ function useSliderRoot(parameters: UseSliderParameters): UseSliderReturnValue {
setActive(-1);
}
- const getRootProps: UseSliderReturnValue['getRootProps'] = React.useCallback(
+ const getRootProps: useSliderRoot.ReturnValue['getRootProps'] = React.useCallback(
(externalProps = {}) =>
mergeReactProps(getValidationProps(externalProps), {
'aria-labelledby': ariaLabelledby,
@@ -410,12 +429,12 @@ function useSliderRoot(parameters: UseSliderParameters): UseSliderReturnValue {
'aria-labelledby': ariaLabelledby,
axis,
changeValue,
- compoundComponentContextValue,
- dragging,
direction,
disabled,
+ dragging,
getFingerNewValue,
handleValueChange,
+ inputIdMap,
largeStep,
max,
min,
@@ -424,14 +443,15 @@ function useSliderRoot(parameters: UseSliderParameters): UseSliderReturnValue {
onValueCommitted,
orientation,
percentageValues: values.map((v) => valueToPercent(v, min, max)),
+ range,
+ registerInputId,
registerSliderControl,
setActive,
setDragging,
setValueState,
step,
- subitems,
tabIndex,
- range,
+ thumbRefs,
values,
}),
[
@@ -441,12 +461,12 @@ function useSliderRoot(parameters: UseSliderParameters): UseSliderReturnValue {
ariaLabelledby,
axis,
changeValue,
- compoundComponentContextValue,
- dragging,
direction,
disabled,
+ dragging,
getFingerNewValue,
handleValueChange,
+ inputIdMap,
largeStep,
max,
min,
@@ -454,17 +474,207 @@ function useSliderRoot(parameters: UseSliderParameters): UseSliderReturnValue {
name,
onValueCommitted,
orientation,
+ range,
+ registerInputId,
registerSliderControl,
setActive,
setDragging,
setValueState,
step,
- subitems,
tabIndex,
- range,
+ thumbRefs,
values,
],
);
}
-export { useSliderRoot };
+export namespace useSliderRoot {
+ export type Direction = 'ltr' | 'rtl';
+
+ export type Orientation = 'horizontal' | 'vertical';
+
+ export interface Parameters {
+ /**
+ * The id of the slider element.
+ */
+ id?: string;
+ /**
+ * The id of the element containing a label for the slider.
+ */
+ 'aria-labelledby'?: string;
+ /**
+ * The default value. Use when the component is not controlled.
+ */
+ defaultValue?: number | ReadonlyArray;
+ /**
+ * Sets the direction. For right-to-left languages, the lowest value is on the right-hand side.
+ * @default 'ltr'
+ */
+ direction?: Direction;
+ /**
+ * If `true`, the component is disabled.
+ * @default false
+ */
+ disabled?: boolean;
+ /**
+ * 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;
+ /**
+ * The minimum steps between values in a range slider.
+ * @default 0
+ */
+ minStepsBetweenValues?: number;
+ /**
+ * Name attribute of the hidden `input` element.
+ */
+ name?: string;
+ /**
+ * Callback function that is fired when the slider's value changed.
+ *
+ * @param {number | number[]} value The new value.
+ * @param {number} activeThumb Index of the currently moved thumb.
+ * @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.
+ */
+ onValueChange?: (value: number | number[], activeThumb: number, event: Event) => void;
+ /**
+ * Callback function that is fired when the `pointerup` is triggered.
+ *
+ * @param {number | number[]} value The new value.
+ * @param {Event} event The event source of the callback.
+ * **Warning**: This is a generic event not a change event.
+ */
+ onValueCommitted?: (value: number | number[], event: Event) => void;
+ /**
+ * The component orientation.
+ * @default 'horizontal'
+ */
+ orientation?: Orientation;
+ /**
+ * The ref attached to the root of the Slider.
+ */
+ rootRef?: React.Ref;
+ /**
+ * The granularity with which the slider can step through values when using Page Up/Page Down or Shift + Arrow Up/Arrow Down.
+ * @default 10
+ */
+ largeStep?: 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.
+ * @default 1
+ */
+ step?: number;
+ /**
+ * Tab index attribute of the Thumb component's `input` element.
+ */
+ tabIndex?: number;
+ /**
+ * The value of the slider.
+ * For ranged sliders, provide an array with two values.
+ */
+ value?: number | ReadonlyArray;
+ }
+
+ export interface ReturnValue {
+ getRootProps: (
+ externalProps?: React.ComponentPropsWithRef<'span'>,
+ ) => React.ComponentPropsWithRef<'span'>;
+ /**
+ * The index of the active thumb.
+ */
+ active: number;
+ /**
+ * A function that compares a new value with the internal value of the slider.
+ * The internal value is potentially unsorted, e.g. to support frozen arrays: https://github.com/mui/material-ui/pull/28472
+ */
+ areValuesEqual: (newValue: number | ReadonlyArray) => boolean;
+ 'aria-labelledby'?: string;
+ /**
+ * The orientation of the slider.
+ */
+ axis: Orientation | 'horizontal-reverse';
+ changeValue: (
+ valueInput: number,
+ index: number,
+ event: React.KeyboardEvent | React.ChangeEvent,
+ ) => void;
+ dragging: boolean;
+ direction: Direction;
+ disabled: boolean;
+ getFingerNewValue: (args: {
+ finger: { x: number; y: number };
+ move?: boolean;
+ offset?: number;
+ activeIndex?: number;
+ }) => { newValue: number | number[]; activeIndex: number; newPercentageValue: number } | null;
+ handleValueChange: (
+ value: number | number[],
+ activeThumb: number,
+ event: React.SyntheticEvent | Event,
+ ) => void;
+ inputIdMap: Map;
+ /**
+ * The large step value of the slider when incrementing or decrementing while the shift key is held,
+ * or when using Page-Up or Page-Down keys. Snaps to multiples of this value.
+ * @default 10
+ */
+ largeStep: number;
+ /**
+ * The maximum allowed value of the slider.
+ */
+ max: number;
+ /**
+ * The minimum allowed value of the slider.
+ */
+ min: number;
+ /**
+ * The minimum steps between values in a range slider.
+ */
+ minStepsBetweenValues: number;
+ name?: string;
+ onValueCommitted?: (value: number | number[], event: Event) => void;
+ /**
+ * The component orientation.
+ * @default 'horizontal'
+ */
+ orientation: Orientation;
+ registerInputId: (
+ index: number,
+ id: string | undefined,
+ ) => {
+ deregister: (index: number) => void;
+ };
+ registerSliderControl: (element: HTMLElement | null) => void;
+ /**
+ * The value(s) of the slider as percentages
+ */
+ percentageValues: readonly number[];
+ setActive: (activeIndex: number) => void;
+ setDragging: (isDragging: boolean) => void;
+ setValueState: (newValue: number | number[]) => void;
+ /**
+ * The step increment of the slider when incrementing or decrementing. It will snap
+ * to multiples of this value. Decimal values are supported.
+ * @default 1
+ */
+ step: number;
+ thumbRefs: React.MutableRefObject<(HTMLElement | null)[]>;
+ tabIndex?: number;
+ /**
+ * The value(s) of the slider
+ */
+ values: readonly number[];
+ }
+}
diff --git a/packages/mui-base/src/Slider/Thumb/SliderThumb.test.tsx b/packages/mui-base/src/Slider/Thumb/SliderThumb.test.tsx
index c04cf8180d..ac65120027 100644
--- a/packages/mui-base/src/Slider/Thumb/SliderThumb.test.tsx
+++ b/packages/mui-base/src/Slider/Thumb/SliderThumb.test.tsx
@@ -1,65 +1,66 @@
import * as React from 'react';
import { Slider } from '@base_ui/react/Slider';
-import { SliderProvider, type SliderProviderValue } from '@base_ui/react/Slider/index.parts';
import { createRenderer, describeConformance } from '#test-utils';
+import { SliderContext } from '../Root/SliderContext';
+import { NOOP } from '../../utils/noop';
+import type { SliderRoot } from '../Root/SliderRoot';
-const NOOP = () => {};
-
-describe('', () => {
- const { render } = createRenderer();
-
- const testProviderValue: SliderProviderValue = {
- active: -1,
- areValuesEqual: () => true,
- axis: 'horizontal',
- changeValue: NOOP,
- compoundComponentContextValue: {
- registerItem: () => ({ id: 0, deregister: () => {} }),
- getItemIndex: () => 0,
- totalSubitemCount: 1,
- },
- dragging: false,
+const testRootContext: SliderRoot.Context = {
+ active: -1,
+ areValuesEqual: () => true,
+ axis: 'horizontal',
+ changeValue: NOOP,
+ direction: 'ltr',
+ dragging: false,
+ disabled: false,
+ getFingerNewValue: () => ({
+ newValue: 0,
+ activeIndex: 0,
+ newPercentageValue: 0,
+ }),
+ handleValueChange: NOOP,
+ largeStep: 10,
+ inputIdMap: new Map(),
+ max: 100,
+ min: 0,
+ minStepsBetweenValues: 0,
+ orientation: 'horizontal',
+ ownerState: {
+ activeThumbIndex: -1,
disabled: false,
- getFingerNewValue: () => ({
- newValue: 0,
- activeIndex: 0,
- newPercentageValue: 0,
- }),
- handleValueChange: NOOP,
+ dragging: false,
direction: 'ltr',
- largeStep: 10,
max: 100,
min: 0,
minStepsBetweenValues: 0,
orientation: 'horizontal',
- ownerState: {
- activeThumbIndex: -1,
- disabled: false,
- dragging: false,
- direction: 'ltr',
- max: 100,
- min: 0,
- minStepsBetweenValues: 0,
- orientation: 'horizontal',
- step: 1,
- values: [0],
- valid: null,
- dirty: false,
- touched: false,
- },
- percentageValues: [0],
- registerSliderControl: NOOP,
- setActive: NOOP,
- setDragging: NOOP,
- setValueState: NOOP,
step: 1,
- subitems: new Map(),
values: [0],
- };
+ valid: null,
+ dirty: false,
+ touched: false,
+ },
+ percentageValues: [0],
+ registerInputId: () => ({
+ deregister: NOOP,
+ }),
+ registerSliderControl: NOOP,
+ setActive: NOOP,
+ setDragging: NOOP,
+ setValueState: NOOP,
+ step: 1,
+ thumbRefs: { current: [] },
+ values: [0],
+};
+
+describe('', () => {
+ const { render } = createRenderer();
describeConformance(, () => ({
render: (node) => {
- return render({node});
+ return render(
+ {node},
+ );
},
refInstanceof: window.HTMLSpanElement,
}));
diff --git a/packages/mui-base/src/Slider/Thumb/SliderThumb.tsx b/packages/mui-base/src/Slider/Thumb/SliderThumb.tsx
index 8a712fceb3..c9dc22214e 100644
--- a/packages/mui-base/src/Slider/Thumb/SliderThumb.tsx
+++ b/packages/mui-base/src/Slider/Thumb/SliderThumb.tsx
@@ -4,9 +4,10 @@ import PropTypes from 'prop-types';
import { getStyleHookProps } from '../../utils/getStyleHookProps';
import { mergeReactProps } from '../../utils/mergeReactProps';
import { resolveClassName } from '../../utils/resolveClassName';
+import { BaseUIComponentProps } from '../../utils/types';
import { useForkRef } from '../../utils/useForkRef';
-import { useSliderContext } from '../Root/SliderProvider';
-import { SliderThumbProps } from './SliderThumb.types';
+import type { SliderRoot } from '../Root/SliderRoot';
+import { useSliderContext } from '../Root/SliderContext';
import { useSliderThumb } from './useSliderThumb';
import { isReactVersionAtLeast } from '../../utils/reactVersion';
@@ -22,7 +23,6 @@ function defaultRender(
);
}
-
/**
*
* Demos:
@@ -34,7 +34,7 @@ function defaultRender(
* - [SliderThumb API](https://base-ui.netlify.app/components/react-slider/#api-reference-SliderThumb)
*/
const SliderThumb = React.forwardRef(function SliderThumb(
- props: SliderThumbProps,
+ props: SliderThumb.Props,
forwardedRef: React.ForwardedRef,
) {
const {
@@ -66,6 +66,7 @@ const SliderThumb = React.forwardRef(function SliderThumb(
orientation,
ownerState,
percentageValues,
+ registerInputId,
step,
tabIndex,
values,
@@ -97,6 +98,7 @@ const SliderThumb = React.forwardRef(function SliderThumb(
name,
orientation,
percentageValues,
+ registerInputId,
rootRef: mergedRef,
step,
tabIndex,
@@ -130,6 +132,7 @@ const SliderThumb = React.forwardRef(function SliderThumb(
...thumbProps,
children: (
+ {/* @ts-ignore */}
{typeof children === 'function' ? children() : children}
@@ -140,6 +143,32 @@ const SliderThumb = React.forwardRef(function SliderThumb(
});
});
+export namespace SliderThumb {
+ export interface OwnerState extends SliderRoot.OwnerState {}
+
+ export interface Props
+ extends Partial>,
+ Omit, 'render'> {
+ onPointerLeave?: React.PointerEventHandler;
+ onPointerOver?: React.PointerEventHandler;
+ onBlur?: React.FocusEventHandler;
+ onFocus?: React.FocusEventHandler;
+ onKeyDown?: React.KeyboardEventHandler;
+ /**
+ * A function to customize rendering of the component.
+ */
+ render?:
+ | ((
+ props: React.ComponentPropsWithRef<'span'>,
+ inputProps: React.ComponentPropsWithRef<'input'>,
+ state: OwnerState,
+ ) => React.ReactElement)
+ | (React.ReactElement & { ref: React.Ref });
+ }
+}
+
+export { SliderThumb };
+
SliderThumb.propTypes /* remove-proptypes */ = {
// ┌────────────────────────────── Warning ──────────────────────────────┐
// │ These PropTypes are generated from the TypeScript type definitions. │
@@ -211,5 +240,3 @@ SliderThumb.propTypes /* remove-proptypes */ = {
PropTypes.node,
]),
} as any;
-
-export { SliderThumb };
diff --git a/packages/mui-base/src/Slider/Thumb/SliderThumb.types.ts b/packages/mui-base/src/Slider/Thumb/SliderThumb.types.ts
deleted file mode 100644
index 4511630217..0000000000
--- a/packages/mui-base/src/Slider/Thumb/SliderThumb.types.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { BaseUIComponentProps } from '../../utils/types';
-import { SliderRootOwnerState, UseSliderReturnValue } from '../Root/SliderRoot.types';
-
-export interface SliderThumbOwnerState extends SliderRootOwnerState {}
-
-export interface SliderThumbProps
- extends Partial>,
- Omit, 'render'> {
- onPointerLeave?: React.PointerEventHandler;
- onPointerOver?: React.PointerEventHandler;
- onBlur?: React.FocusEventHandler;
- onFocus?: React.FocusEventHandler;
- onKeyDown?: React.KeyboardEventHandler;
- /**
- * A function to customize rendering of the component.
- */
- render?:
- | ((
- props: React.ComponentPropsWithRef<'span'>,
- inputProps: React.ComponentPropsWithRef<'input'>,
- state: SliderThumbOwnerState,
- ) => React.ReactElement>)
- | (React.ReactElement> & { ref: React.Ref });
-}
-
-export interface UseSliderThumbParameters
- extends Pick<
- UseSliderReturnValue,
- | 'active'
- | 'aria-labelledby'
- | 'axis'
- | 'changeValue'
- | 'direction'
- | 'largeStep'
- | 'max'
- | 'min'
- | 'minStepsBetweenValues'
- | 'name'
- | 'orientation'
- | 'percentageValues'
- | 'step'
- | 'tabIndex'
- | 'values'
- > {
- /**
- * The label for the input element.
- */
- 'aria-label'?: string;
- /**
- * A string value that provides a user-friendly name for the current value of the slider.
- */
- 'aria-valuetext'?: string;
- /**
- * Accepts a function which returns a string value that provides a user-friendly name for the input associated with the thumb
- * @param {number} index The index of the input
- * @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;
- id?: string;
- disabled?: boolean;
- onBlur?: React.FocusEventHandler;
- onFocus?: React.FocusEventHandler;
- onKeyDown?: React.KeyboardEventHandler;
- rootRef?: React.Ref;
-}
-
-export interface UseSliderThumbReturnValue {
- getRootProps: (
- externalProps?: React.ComponentPropsWithRef,
- ) => React.ComponentPropsWithRef;
- getThumbInputProps: (
- externalProps?: React.ComponentPropsWithRef<'input'>,
- ) => React.ComponentPropsWithRef<'input'>;
- /**
- * Resolver for the thumb slot's style prop.
- * @param index of the currently moved thumb
- * @returns props that should be spread on the style prop of thumb slot
- */
- getThumbStyle: (index: number) => Record;
- index: number;
-}
diff --git a/packages/mui-base/src/Slider/Thumb/useSliderThumb.ts b/packages/mui-base/src/Slider/Thumb/useSliderThumb.ts
index a14a728403..e1bc15ee48 100644
--- a/packages/mui-base/src/Slider/Thumb/useSliderThumb.ts
+++ b/packages/mui-base/src/Slider/Thumb/useSliderThumb.ts
@@ -1,19 +1,16 @@
'use client';
import * as React from 'react';
import { mergeReactProps } from '../../utils/mergeReactProps';
+import { GenericHTMLProps } from '../../utils/types';
+import { useEnhancedEffect } from '../../utils/useEnhancedEffect';
import { useForkRef } from '../../utils/useForkRef';
import { useId } from '../../utils/useId';
import { visuallyHidden } from '../../utils/visuallyHidden';
-import { useCompoundItem } from '../../useCompound';
-import { SliderThumbMetadata } from '../Root/SliderRoot.types';
-import { UseSliderThumbParameters, UseSliderThumbReturnValue } from './SliderThumb.types';
+import { useCompositeListItem } from '../../Composite/List/useCompositeListItem';
import { useFieldControlValidation } from '../../Field/Control/useFieldControlValidation';
import { useFieldRootContext } from '../../Field/Root/FieldRootContext';
import { getSliderValue } from '../utils/getSliderValue';
-
-function idGenerator(existingKeys: Set) {
- return `thumb-${existingKeys.size}`;
-}
+import type { useSliderRoot } from '../Root/useSliderRoot';
function getNewValue(
thumbValue: number,
@@ -41,7 +38,7 @@ function getDefaultAriaValueText(values: readonly number[], index: number): stri
return undefined;
}
-export function useSliderThumb(parameters: UseSliderThumbParameters) {
+export function useSliderThumb(parameters: useSliderThumb.Parameters): useSliderThumb.ReturnValue {
const {
active: activeIndex,
'aria-label': ariaLabel,
@@ -54,16 +51,18 @@ export function useSliderThumb(parameters: UseSliderThumbParameters) {
getAriaLabel,
getAriaValueText,
id: idParam,
+ inputId: inputIdParam,
largeStep,
max,
min,
minStepsBetweenValues,
name,
orientation,
+ percentageValues,
+ registerInputId,
rootRef: externalRef,
step,
tabIndex,
- percentageValues,
values: sliderValues,
} = parameters;
@@ -79,17 +78,20 @@ export function useSliderThumb(parameters: UseSliderThumbParameters) {
const inputRef = React.useRef(null);
const mergedInputRef = useForkRef(inputRef, inputValidationRef);
- const handleRef = useForkRef(externalRef, thumbRef);
- const thumbMetadata: SliderThumbMetadata = React.useMemo(
- () => ({ inputId: thumbId ? `${thumbId}-input` : '', ref: thumbRef, inputRef }),
- [thumbId],
- );
+ const { ref: listItemRef, index } = useCompositeListItem();
- const { id: compoundItemId, index } = useCompoundItem(
- (thumbId ? `${thumbId}-input` : thumbId) ?? idGenerator,
- thumbMetadata,
- );
+ const mergedThumbRef = useForkRef(externalRef, listItemRef, thumbRef);
+
+ const inputId = useId(inputIdParam);
+
+ useEnhancedEffect(() => {
+ const { deregister } = registerInputId(index, inputId);
+
+ return () => {
+ deregister(index);
+ };
+ }, [index, inputId, registerInputId]);
const thumbValue = sliderValues[index];
@@ -120,11 +122,11 @@ export function useSliderThumb(parameters: UseSliderThumbParameters) {
};
}, [activeIndex, axis, isRtl, orientation, percent, index]);
- const getRootProps: UseSliderThumbReturnValue['getRootProps'] = React.useCallback(
+ const getRootProps: useSliderThumb.ReturnValue['getRootProps'] = React.useCallback(
(externalProps = {}) => {
return mergeReactProps(externalProps, {
'data-index': index,
- id: idParam,
+ id: thumbId,
onBlur() {
if (!thumbRef.current) {
return;
@@ -202,7 +204,7 @@ export function useSliderThumb(parameters: UseSliderThumbParameters) {
event.preventDefault();
}
},
- ref: handleRef,
+ ref: mergedThumbRef,
style: {
...getThumbStyle(),
},
@@ -210,28 +212,32 @@ export function useSliderThumb(parameters: UseSliderThumbParameters) {
});
},
[
- index,
- idParam,
- handleRef,
- getThumbStyle,
- tabIndex,
- disabled,
- setTouched,
+ changeValue,
commitValidation,
- thumbValue,
- sliderValues,
+ disabled,
+ getThumbStyle,
+ index,
+ isRtl,
largeStep,
- step,
- min,
max,
- isRtl,
+ mergedThumbRef,
+ min,
minStepsBetweenValues,
- changeValue,
+ setTouched,
+ sliderValues,
+ step,
+ tabIndex,
+ thumbId,
+ thumbValue,
],
);
- const getThumbInputProps: UseSliderThumbReturnValue['getThumbInputProps'] = React.useCallback(
+ const getThumbInputProps: useSliderThumb.ReturnValue['getThumbInputProps'] = React.useCallback(
(externalProps = {}) => {
+ let cssWritingMode;
+ if (orientation === 'vertical') {
+ cssWritingMode = isRtl ? 'vertical-rl' : 'vertical-lr';
+ }
return mergeReactProps(getInputValidationProps(externalProps), {
'aria-label': getAriaLabel ? getAriaLabel(index) : ariaLabel,
'aria-labelledby': ariaLabelledby,
@@ -244,7 +250,7 @@ export function useSliderThumb(parameters: UseSliderThumbParameters) {
: ariaValuetext ?? getDefaultAriaValueText(sliderValues, index),
'data-index': index,
disabled,
- id: compoundItemId,
+ id: inputId,
max,
min,
name,
@@ -260,6 +266,7 @@ export function useSliderThumb(parameters: UseSliderThumbParameters) {
// So that VoiceOver's focus indicator matches the thumb's dimensions
width: '100%',
height: '100%',
+ writingMode: cssWritingMode,
},
tabIndex: -1,
type: 'range',
@@ -271,21 +278,21 @@ export function useSliderThumb(parameters: UseSliderThumbParameters) {
ariaLabelledby,
ariaValuetext,
changeValue,
- compoundItemId,
disabled,
getAriaLabel,
getAriaValueText,
+ getInputValidationProps,
index,
isRtl,
max,
+ mergedInputRef,
min,
name,
orientation,
sliderValues,
step,
+ inputId,
thumbValue,
- getInputValidationProps,
- mergedInputRef,
],
);
@@ -293,9 +300,71 @@ export function useSliderThumb(parameters: UseSliderThumbParameters) {
() => ({
getRootProps,
getThumbInputProps,
- index,
disabled,
+ index,
}),
- [getRootProps, getThumbInputProps, index, disabled],
+ [getRootProps, getThumbInputProps, disabled, index],
);
}
+
+export namespace useSliderThumb {
+ export interface Parameters
+ extends Pick<
+ useSliderRoot.ReturnValue,
+ | 'active'
+ | 'aria-labelledby'
+ | 'axis'
+ | 'changeValue'
+ | 'direction'
+ | 'largeStep'
+ | 'max'
+ | 'min'
+ | 'minStepsBetweenValues'
+ | 'name'
+ | 'orientation'
+ | 'percentageValues'
+ | 'registerInputId'
+ | 'step'
+ | 'tabIndex'
+ | 'values'
+ > {
+ /**
+ * The label for the input element.
+ */
+ 'aria-label'?: string;
+ /**
+ * A string value that provides a user-friendly name for the current value of the slider.
+ */
+ 'aria-valuetext'?: string;
+ /**
+ * Accepts a function which returns a string value that provides a user-friendly name for the input associated with the thumb
+ * @param {number} index The index of the input
+ * @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;
+ id?: React.HTMLAttributes['id'];
+ inputId?: React.HTMLAttributes['id'];
+ disabled: boolean;
+ onBlur?: React.FocusEventHandler;
+ onFocus?: React.FocusEventHandler;
+ onKeyDown?: React.KeyboardEventHandler;
+ rootRef?: React.Ref;
+ }
+
+ export interface ReturnValue {
+ getRootProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps;
+ getThumbInputProps: (
+ externalProps?: React.ComponentPropsWithRef<'input'>,
+ ) => React.ComponentPropsWithRef<'input'>;
+ index: number;
+ disabled: boolean;
+ }
+}
diff --git a/packages/mui-base/src/Slider/Track/SliderTrack.test.tsx b/packages/mui-base/src/Slider/Track/SliderTrack.test.tsx
index 382583c216..88165fc91d 100644
--- a/packages/mui-base/src/Slider/Track/SliderTrack.test.tsx
+++ b/packages/mui-base/src/Slider/Track/SliderTrack.test.tsx
@@ -1,65 +1,66 @@
import * as React from 'react';
import { Slider } from '@base_ui/react/Slider';
-import { SliderProvider, type SliderProviderValue } from '@base_ui/react/Slider/index.parts';
import { createRenderer, describeConformance } from '#test-utils';
+import { SliderContext } from '../Root/SliderContext';
+import { NOOP } from '../../utils/noop';
+import type { SliderRoot } from '../Root/SliderRoot';
-const NOOP = () => {};
-
-describe('', () => {
- const { render } = createRenderer();
-
- const testProviderValue: SliderProviderValue = {
- active: -1,
- areValuesEqual: () => true,
- axis: 'horizontal',
- changeValue: NOOP,
- compoundComponentContextValue: {
- registerItem: () => ({ id: 0, deregister: () => {} }),
- getItemIndex: () => 0,
- totalSubitemCount: 1,
- },
- dragging: false,
+const testRootContext: SliderRoot.Context = {
+ active: -1,
+ areValuesEqual: () => true,
+ axis: 'horizontal',
+ changeValue: NOOP,
+ direction: 'ltr',
+ dragging: false,
+ disabled: false,
+ getFingerNewValue: () => ({
+ newValue: 0,
+ activeIndex: 0,
+ newPercentageValue: 0,
+ }),
+ handleValueChange: NOOP,
+ largeStep: 10,
+ inputIdMap: new Map(),
+ max: 100,
+ min: 0,
+ minStepsBetweenValues: 0,
+ orientation: 'horizontal',
+ ownerState: {
+ activeThumbIndex: -1,
disabled: false,
- getFingerNewValue: () => ({
- newValue: 0,
- activeIndex: 0,
- newPercentageValue: 0,
- }),
- handleValueChange: NOOP,
+ dragging: false,
direction: 'ltr',
- largeStep: 10,
max: 100,
min: 0,
minStepsBetweenValues: 0,
orientation: 'horizontal',
- ownerState: {
- activeThumbIndex: -1,
- disabled: false,
- dragging: false,
- direction: 'ltr',
- max: 100,
- min: 0,
- minStepsBetweenValues: 0,
- orientation: 'horizontal',
- step: 1,
- values: [0],
- valid: null,
- dirty: false,
- touched: false,
- },
- percentageValues: [0],
- registerSliderControl: NOOP,
- setActive: NOOP,
- setDragging: NOOP,
- setValueState: NOOP,
step: 1,
- subitems: new Map(),
values: [0],
- };
+ valid: null,
+ dirty: false,
+ touched: false,
+ },
+ percentageValues: [0],
+ registerInputId: () => ({
+ deregister: NOOP,
+ }),
+ registerSliderControl: NOOP,
+ setActive: NOOP,
+ setDragging: NOOP,
+ setValueState: NOOP,
+ step: 1,
+ thumbRefs: { current: [] },
+ values: [0],
+};
+
+describe('', () => {
+ const { render } = createRenderer();
describeConformance(, () => ({
render: (node) => {
- return render({node});
+ return render(
+ {node},
+ );
},
refInstanceof: window.HTMLSpanElement,
}));
diff --git a/packages/mui-base/src/Slider/Track/SliderTrack.tsx b/packages/mui-base/src/Slider/Track/SliderTrack.tsx
index 33726a84a3..7fa0de2216 100644
--- a/packages/mui-base/src/Slider/Track/SliderTrack.tsx
+++ b/packages/mui-base/src/Slider/Track/SliderTrack.tsx
@@ -1,11 +1,11 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
+import { BaseUIComponentProps } from '../../utils/types';
import { useComponentRenderer } from '../../utils/useComponentRenderer';
-import { useSliderContext } from '../Root/SliderProvider';
+import { useSliderContext } from '../Root/SliderContext';
+import type { SliderRoot } from '../Root/SliderRoot';
import { sliderStyleHookMapping } from '../Root/styleHooks';
-import { SliderTrackProps } from './SliderTrack.types';
-
/**
*
* Demos:
@@ -17,8 +17,8 @@ import { SliderTrackProps } from './SliderTrack.types';
* - [SliderTrack API](https://base-ui.netlify.app/components/react-slider/#api-reference-SliderTrack)
*/
const SliderTrack = React.forwardRef(function SliderTrack(
- props: SliderTrackProps,
- forwardedRef: React.ForwardedRef,
+ props: SliderTrack.Props,
+ forwardedRef: React.ForwardedRef,
) {
const { render, className, ...otherProps } = props;
@@ -36,6 +36,12 @@ const SliderTrack = React.forwardRef(function SliderTrack(
return renderElement();
});
+export namespace SliderTrack {
+ export interface Props extends BaseUIComponentProps<'span', SliderRoot.OwnerState> {}
+}
+
+export { SliderTrack };
+
SliderTrack.propTypes /* remove-proptypes */ = {
// ┌────────────────────────────── Warning ──────────────────────────────┐
// │ These PropTypes are generated from the TypeScript type definitions. │
@@ -54,5 +60,3 @@ SliderTrack.propTypes /* remove-proptypes */ = {
*/
render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
} as any;
-
-export { SliderTrack };
diff --git a/packages/mui-base/src/Slider/Track/SliderTrack.types.ts b/packages/mui-base/src/Slider/Track/SliderTrack.types.ts
deleted file mode 100644
index edda2fcbf6..0000000000
--- a/packages/mui-base/src/Slider/Track/SliderTrack.types.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { BaseUIComponentProps } from '../../utils/types';
-import { SliderRootOwnerState } from '../Root/SliderRoot.types';
-
-export interface SliderTrackProps extends BaseUIComponentProps<'span', SliderRootOwnerState> {}
diff --git a/packages/mui-base/src/Slider/index.parts.ts b/packages/mui-base/src/Slider/index.parts.ts
index be5d9c2dc2..fc64fa908e 100644
--- a/packages/mui-base/src/Slider/index.parts.ts
+++ b/packages/mui-base/src/Slider/index.parts.ts
@@ -1,49 +1,6 @@
export { SliderRoot as Root } from './Root/SliderRoot';
-export type {
- SliderRootOwnerState as SliderOwnerState,
- SliderRootProps as RootProps,
- UseSliderParameters,
- UseSliderReturnValue,
- SliderContextValue,
- SliderProviderValue,
- SliderThumbMetadata,
- Axis,
-} from './Root/SliderRoot.types';
-export { useSliderRoot } from './Root/useSliderRoot';
-export * from './Root/SliderProvider';
-
export { SliderOutput as Output } from './Output/SliderOutput';
-export type {
- SliderOutputProps as OutputProps,
- UseSliderOutputParameters,
- UseSliderOutputReturnValue,
-} from './Output/SliderOutput.types';
-export { useSliderOutput } from './Output/useSliderOutput';
-
export { SliderControl as Control } from './Control/SliderControl';
-export type {
- SliderControlProps as ControlProps,
- UseSliderControlParameters,
- UseSliderControlReturnValue,
-} from './Control/SliderControl.types';
-export { useSliderControl } from './Control/useSliderControl';
-
export { SliderTrack as Track } from './Track/SliderTrack';
-export type { SliderTrackProps as TrackProps } from './Track/SliderTrack.types';
-
export { SliderThumb as Thumb } from './Thumb/SliderThumb';
-export type {
- SliderThumbOwnerState as ThumbOwnerState,
- SliderThumbProps as ThumbProps,
- UseSliderThumbParameters,
- UseSliderThumbReturnValue,
-} from './Thumb/SliderThumb.types';
-export { useSliderThumb } from './Thumb/useSliderThumb';
-
export { SliderIndicator as Indicator } from './Indicator/SliderIndicator';
-export type {
- SliderIndicatorProps as IndicatorProps,
- UseSliderIndicatorParameters,
- UseSliderIndicatorReturnValue,
-} from './Indicator/SliderIndicator.types';
-export { useSliderIndicator } from './Indicator/useSliderIndicator';