diff --git a/packages/mui-base/src/Slider/Slider.test.tsx b/packages/mui-base/src/Slider/Slider.test.tsx
index 30c2b14b1431a1..d07e944359c388 100644
--- a/packages/mui-base/src/Slider/Slider.test.tsx
+++ b/packages/mui-base/src/Slider/Slider.test.tsx
@@ -7,6 +7,7 @@ import { expect } from 'chai';
import * as React from 'react';
import { spy, stub } from 'sinon';
import {
+ act,
createRenderer,
createMount,
describeConformanceUnstyled,
@@ -372,5 +373,78 @@ describe('', () => {
expect(screen.getByTestId('value-label')).to.have.text('20');
});
+
+ it('should provide focused state to the slotProps.thumb', () => {
+ const { getByTestId } = render(
+ ({
+ 'data-testid': `thumb-${index}`,
+ 'data-focused': focused,
+ 'data-active': active,
+ }),
+ }}
+ />,
+ );
+
+ const firstThumb = getByTestId('thumb-0');
+ const secondThumb = getByTestId('thumb-1');
+
+ fireEvent.keyDown(document.body, { key: 'TAB' });
+ act(() => {
+ (firstThumb.firstChild as HTMLInputElement).focus();
+ });
+ expect(firstThumb.getAttribute('data-focused')).to.equal('true');
+ expect(secondThumb.getAttribute('data-focused')).to.equal('false');
+
+ act(() => {
+ (secondThumb.firstChild as HTMLInputElement).focus();
+ });
+ expect(firstThumb.getAttribute('data-focused')).to.equal('false');
+ expect(secondThumb.getAttribute('data-focused')).to.equal('true');
+ });
+
+ it('should provide active state to the slotProps.thumb', function test() {
+ // TODO: Don't skip once a fix for https://github.com/jsdom/jsdom/issues/3029 is released.
+ if (/jsdom/.test(window.navigator.userAgent)) {
+ this.skip();
+ }
+
+ const { getByTestId } = render(
+ ({
+ 'data-testid': `thumb-${index}`,
+ 'data-focused': focused,
+ 'data-active': active,
+ }),
+ }}
+ data-testid="slider-root"
+ />,
+ );
+
+ const sliderRoot = getByTestId('slider-root');
+
+ stub(sliderRoot, 'getBoundingClientRect').callsFake(() => ({
+ width: 100,
+ height: 10,
+ bottom: 10,
+ left: 0,
+ x: 0,
+ y: 0,
+ top: 0,
+ right: 0,
+ toJSON() {},
+ }));
+ fireEvent.touchStart(sliderRoot, createTouches([{ identifier: 1, clientX: 21, clientY: 0 }]));
+
+ const firstThumb = getByTestId('thumb-0');
+ const secondThumb = getByTestId('thumb-1');
+
+ expect(firstThumb.getAttribute('data-active')).to.equal('true');
+ expect(secondThumb.getAttribute('data-active')).to.equal('false');
+ });
});
});
diff --git a/packages/mui-base/src/Slider/Slider.tsx b/packages/mui-base/src/Slider/Slider.tsx
index 46ac1d3163a5b0..06e866e046abc0 100644
--- a/packages/mui-base/src/Slider/Slider.tsx
+++ b/packages/mui-base/src/Slider/Slider.tsx
@@ -8,6 +8,7 @@ import composeClasses from '../composeClasses';
import { getSliderUtilityClass } from './sliderClasses';
import useSlider, { valueToPercent } from '../useSlider';
import useSlotProps from '../utils/useSlotProps';
+import resolveComponentProps from '../utils/resolveComponentProps';
import { SliderOwnerState, SliderProps, SliderTypeMap } from './Slider.types';
import { useClassNamesOverride } from '../utils/ClassNameConfigurator';
@@ -170,6 +171,7 @@ const Slider = React.forwardRef(function Slider {
const percent = valueToPercent(value, min, max);
const style = axisProps[axis].offset(percent);
-
+ const resolvedSlotProps = resolveComponentProps(slotProps.thumb, ownerState, {
+ index,
+ focused: focusedThumbIndex === index,
+ active: active === index,
+ });
return (
{
/**
* The label of the slider.
@@ -66,7 +72,12 @@ export interface SliderOwnProps extends Omit {
root?: SlotComponentProps<'span', SliderRootSlotPropsOverrides, SliderOwnerState>;
track?: SlotComponentProps<'span', SliderTrackSlotPropsOverrides, SliderOwnerState>;
rail?: SlotComponentProps<'span', SliderRailSlotPropsOverrides, SliderOwnerState>;
- thumb?: SlotComponentProps<'span', SliderThumbSlotPropsOverrides, SliderOwnerState>;
+ thumb?: SlotComponentPropsWithSlotState<
+ 'span',
+ SliderThumbSlotPropsOverrides,
+ SliderOwnerState,
+ SliderThumbSlotState
+ >;
mark?: SlotComponentProps<'span', SliderMarkSlotPropsOverrides, SliderOwnerState>;
markLabel?: SlotComponentProps<'span', SliderMarkLabelSlotPropsOverrides, SliderOwnerState>;
valueLabel?: SlotComponentProps<
diff --git a/packages/mui-base/src/utils/resolveComponentProps.ts b/packages/mui-base/src/utils/resolveComponentProps.ts
index 192714cf0fd710..f409584a403af7 100644
--- a/packages/mui-base/src/utils/resolveComponentProps.ts
+++ b/packages/mui-base/src/utils/resolveComponentProps.ts
@@ -2,12 +2,19 @@
* If `componentProps` is a function, calls it with the provided `ownerState`.
* Otherwise, just returns `componentProps`.
*/
-export default function resolveComponentProps(
- componentProps: TProps | ((ownerState: TOwnerState) => TProps) | undefined,
+export default function resolveComponentProps(
+ componentProps:
+ | TProps
+ | ((ownerState: TOwnerState, slotState?: TSlotState) => TProps)
+ | undefined,
ownerState: TOwnerState,
+ slotState?: TSlotState,
): TProps | undefined {
if (typeof componentProps === 'function') {
- return (componentProps as (ownerState: TOwnerState) => TProps)(ownerState);
+ return (componentProps as (ownerState: TOwnerState, slotState?: TSlotState) => TProps)(
+ ownerState,
+ slotState,
+ );
}
return componentProps;
diff --git a/packages/mui-base/src/utils/types.ts b/packages/mui-base/src/utils/types.ts
index ead43556174e39..78027f2b3964bc 100644
--- a/packages/mui-base/src/utils/types.ts
+++ b/packages/mui-base/src/utils/types.ts
@@ -10,3 +10,15 @@ export type SlotComponentProps Partial> & TOverrides);
+
+export type SlotComponentPropsWithSlotState<
+ TSlotComponent extends React.ElementType,
+ TOverrides,
+ TOwnerState,
+ TSlotState,
+> =
+ | (Partial> & TOverrides)
+ | ((
+ ownerState: TOwnerState,
+ slotState: TSlotState,
+ ) => Partial> & TOverrides);
diff --git a/packages/mui-base/src/utils/useSlotProps.test.tsx b/packages/mui-base/src/utils/useSlotProps.test.tsx
index cc9997aff2042f..c5de1a29d8bd9f 100644
--- a/packages/mui-base/src/utils/useSlotProps.test.tsx
+++ b/packages/mui-base/src/utils/useSlotProps.test.tsx
@@ -261,4 +261,41 @@ describe('useSlotProps', () => {
foo: 'bar',
});
});
+
+ it('should call externalSlotProps with ownerState if skipResolvingSlotProps is not provided', () => {
+ const externalSlotProps = spy();
+ const ownerState = { foo: 'bar' };
+
+ const getSlotProps = () => ({
+ skipResolvingSlotProps: true,
+ });
+
+ callUseSlotProps({
+ elementType: 'div',
+ getSlotProps,
+ externalSlotProps,
+ ownerState,
+ });
+
+ expect(externalSlotProps.callCount).to.not.equal(0);
+ expect(externalSlotProps.args[0][0]).to.deep.equal(ownerState);
+ });
+
+ it('should not call externalSlotProps if skipResolvingSlotProps is true', () => {
+ const externalSlotProps = spy();
+
+ const getSlotProps = () => ({
+ skipResolvingSlotProps: true,
+ });
+
+ callUseSlotProps({
+ elementType: 'div',
+ getSlotProps,
+ externalSlotProps,
+ skipResolvingSlotProps: true,
+ ownerState: undefined,
+ });
+
+ expect(externalSlotProps.callCount).to.equal(0);
+ });
});
diff --git a/packages/mui-base/src/utils/useSlotProps.ts b/packages/mui-base/src/utils/useSlotProps.ts
index ba08727bec3f93..dfbd4bf651784f 100644
--- a/packages/mui-base/src/utils/useSlotProps.ts
+++ b/packages/mui-base/src/utils/useSlotProps.ts
@@ -34,6 +34,10 @@ export type UseSlotPropsParameters<
* The ownerState of the Base UI component.
*/
ownerState: OwnerState;
+ /**
+ * Set to true if the slotProps callback should receive more props.
+ */
+ skipResolvingSlotProps?: boolean;
};
export type UseSlotPropsResult<
@@ -72,8 +76,16 @@ export default function useSlotProps<
OwnerState
>,
) {
- const { elementType, externalSlotProps, ownerState, ...rest } = parameters;
- const resolvedComponentsProps = resolveComponentProps(externalSlotProps, ownerState);
+ const {
+ elementType,
+ externalSlotProps,
+ ownerState,
+ skipResolvingSlotProps = false,
+ ...rest
+ } = parameters;
+ const resolvedComponentsProps = skipResolvingSlotProps
+ ? {}
+ : resolveComponentProps(externalSlotProps, ownerState);
const { props: mergedProps, internalRef } = mergeSlotProps({
...rest,
externalSlotProps: resolvedComponentsProps,