Skip to content

Commit

Permalink
[base][slider] Provide slot state to Slider's thumb slot props callba…
Browse files Browse the repository at this point in the history
…ck (#37749)
  • Loading branch information
mnajdova authored Jul 3, 2023
1 parent 9fe770e commit e6beb95
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 9 deletions.
74 changes: 74 additions & 0 deletions packages/mui-base/src/Slider/Slider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { expect } from 'chai';
import * as React from 'react';
import { spy, stub } from 'sinon';
import {
act,
createRenderer,
createMount,
describeConformanceUnstyled,
Expand Down Expand Up @@ -372,5 +373,78 @@ describe('<Slider />', () => {

expect(screen.getByTestId('value-label')).to.have.text('20');
});

it('should provide focused state to the slotProps.thumb', () => {
const { getByTestId } = render(
<Slider
defaultValue={[20, 40]}
slotProps={{
thumb: (_, { index, focused, active }) => ({
'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(
<Slider
defaultValue={[20, 40]}
slotProps={{
thumb: (_, { index, focused, active }) => ({
'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');
});
});
});
12 changes: 10 additions & 2 deletions packages/mui-base/src/Slider/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -170,6 +171,7 @@ const Slider = React.forwardRef(function Slider<RootComponentType extends React.
getSlotProps: getThumbProps,
externalSlotProps: slotProps.thumb,
ownerState,
skipResolvingSlotProps: true,
});

const ValueLabel = slots.valueLabel;
Expand Down Expand Up @@ -262,20 +264,26 @@ const Slider = React.forwardRef(function Slider<RootComponentType extends React.
{values.map((value, index) => {
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 (
<Thumb
key={index}
data-index={index}
{...thumbProps}
className={clsx(classes.thumb, thumbProps.className, {
{...resolvedSlotProps}
className={clsx(classes.thumb, thumbProps.className, resolvedSlotProps?.className, {
[classes.active]: active === index,
[classes.focusVisible]: focusedThumbIndex === index,
})}
style={{
...style,
...getThumbStyle(index),
...thumbProps.style,
...resolvedSlotProps?.style,
}}
>
<Input
Expand Down
15 changes: 13 additions & 2 deletions packages/mui-base/src/Slider/Slider.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { OverridableComponent, OverridableTypeMap, Simplify } from '@mui/types';
import * as React from 'react';
import { PolymorphicProps, SlotComponentProps } from '../utils';
import { PolymorphicProps, SlotComponentProps, SlotComponentPropsWithSlotState } from '../utils';
import {
UseSliderHiddenInputProps,
UseSliderParameters,
Expand Down Expand Up @@ -34,6 +34,12 @@ export interface SliderMarkLabelSlotPropsOverrides {}
export interface SliderValueLabelSlotPropsOverrides {}
export interface SliderInputSlotPropsOverrides {}

export interface SliderThumbSlotState {
focused: boolean;
active: boolean;
index: number;
}

export interface SliderOwnProps extends Omit<UseSliderParameters, 'ref'> {
/**
* The label of the slider.
Expand Down Expand Up @@ -66,7 +72,12 @@ export interface SliderOwnProps extends Omit<UseSliderParameters, 'ref'> {
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<
Expand Down
13 changes: 10 additions & 3 deletions packages/mui-base/src/utils/resolveComponentProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@
* If `componentProps` is a function, calls it with the provided `ownerState`.
* Otherwise, just returns `componentProps`.
*/
export default function resolveComponentProps<TProps, TOwnerState>(
componentProps: TProps | ((ownerState: TOwnerState) => TProps) | undefined,
export default function resolveComponentProps<TProps, TOwnerState, TSlotState>(
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;
Expand Down
12 changes: 12 additions & 0 deletions packages/mui-base/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,15 @@ export type SlotComponentProps<TSlotComponent extends React.ElementType, TOverri
| ((
ownerState: TOwnerState,
) => Partial<React.ComponentPropsWithRef<TSlotComponent>> & TOverrides);

export type SlotComponentPropsWithSlotState<
TSlotComponent extends React.ElementType,
TOverrides,
TOwnerState,
TSlotState,
> =
| (Partial<React.ComponentPropsWithRef<TSlotComponent>> & TOverrides)
| ((
ownerState: TOwnerState,
slotState: TSlotState,
) => Partial<React.ComponentPropsWithRef<TSlotComponent>> & TOverrides);
37 changes: 37 additions & 0 deletions packages/mui-base/src/utils/useSlotProps.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
16 changes: 14 additions & 2 deletions packages/mui-base/src/utils/useSlotProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit e6beb95

Please sign in to comment.