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,