Skip to content

Commit

Permalink
[Slider] Add ability to name each thumb for more flexibility (#2766)
Browse files Browse the repository at this point in the history
Fixes #2454
  • Loading branch information
benoitgrelard committed Mar 12, 2024
1 parent be80c2a commit 8b38903
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .yarn/versions/b20a66e3.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
releases:
"@radix-ui/react-slider": minor

declined:
- primitives
30 changes: 28 additions & 2 deletions packages/react/slider/src/Slider.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,20 @@ export const SmallSteps = () => {
};

export const WithinForm = () => {
const [data, setData] = React.useState({ single: [0], multiple: [10, 15, 20, 80] });
const [data, setData] = React.useState({
single: [0],
multiple: [10, 15, 20, 80],
price: {
min: 30,
max: 70,
},
});
return (
<form
onSubmit={(event) => event.preventDefault()}
onSubmit={(event) => {
event.preventDefault();
console.log(serialize(event.currentTarget, { hash: true }));
}}
onChange={(event) => {
const formData = serialize(event.currentTarget, { hash: true });
setData(formData as any);
Expand Down Expand Up @@ -296,6 +306,22 @@ export const WithinForm = () => {
<Slider.Thumb className={thumbClass()} />
</Slider.Root>
</fieldset>

<br />
<br />

<fieldset>
<legend>Multiple values (with named thumbs): {JSON.stringify(data.price)}</legend>
<Slider.Root defaultValue={[data.price.min, data.price.max]} className={rootClass()}>
<Slider.Track className={trackClass()}>
<Slider.Range className={rangeClass()} />
</Slider.Track>
<Slider.Thumb className={thumbClass()} name="price[min]" />
<Slider.Thumb className={thumbClass()} name="price[max]" />
</Slider.Root>
</fieldset>

<button type="submit">Submit</button>
</form>
);
};
Expand Down
32 changes: 18 additions & 14 deletions packages/react/slider/src/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const [createSliderContext, createSliderScope] = createContextScope(SLIDER_NAME,
]);

type SliderContextValue = {
name?: string;
disabled?: boolean;
min: number;
max: number;
Expand Down Expand Up @@ -90,13 +91,9 @@ const Slider = React.forwardRef<SliderElement, SliderProps>(
inverted = false,
...sliderProps
} = props;
const [slider, setSlider] = React.useState<HTMLSpanElement | null>(null);
const composedRefs = useComposedRefs(forwardedRef, (node) => setSlider(node));
const thumbRefs = React.useRef<SliderContextValue['thumbs']>(new Set());
const valueIndexToChangeRef = React.useRef<number>(0);
const isHorizontal = orientation === 'horizontal';
// We set this to true by default so that events bubble to forms without JS (SSR)
const isFormControl = slider ? Boolean(slider.closest('form')) : true;
const SliderOrientation = isHorizontal ? SliderHorizontal : SliderVertical;

const [values = [], setValues] = useControllableState({
Expand Down Expand Up @@ -147,6 +144,7 @@ const Slider = React.forwardRef<SliderElement, SliderProps>(
return (
<SliderProvider
scope={props.__scopeSlider}
name={name}
disabled={disabled}
min={min}
max={max}
Expand All @@ -161,7 +159,7 @@ const Slider = React.forwardRef<SliderElement, SliderProps>(
aria-disabled={disabled}
data-disabled={disabled ? '' : undefined}
{...sliderProps}
ref={composedRefs}
ref={forwardedRef}
onPointerDown={composeEventHandlers(sliderProps.onPointerDown, () => {
if (!disabled) valuesBeforeSlideStartRef.current = values;
})}
Expand Down Expand Up @@ -189,14 +187,6 @@ const Slider = React.forwardRef<SliderElement, SliderProps>(
/>
</Collection.Slot>
</Collection.Provider>
{isFormControl &&
values.map((value, index) => (
<BubbleInput
key={index}
name={name ? name + (values.length > 1 ? '[]' : '') : undefined}
value={value}
/>
))}
</SliderProvider>
);
}
Expand Down Expand Up @@ -556,15 +546,18 @@ const SliderThumb = React.forwardRef<SliderThumbElement, SliderThumbProps>(
type SliderThumbImplElement = React.ElementRef<typeof Primitive.span>;
interface SliderThumbImplProps extends PrimitiveSpanProps {
index: number;
name?: string;
}

const SliderThumbImpl = React.forwardRef<SliderThumbImplElement, SliderThumbImplProps>(
(props: ScopedProps<SliderThumbImplProps>, forwardedRef) => {
const { __scopeSlider, index, ...thumbProps } = props;
const { __scopeSlider, index, name, ...thumbProps } = props;
const context = useSliderContext(THUMB_NAME, __scopeSlider);
const orientation = useSliderOrientationContext(THUMB_NAME, __scopeSlider);
const [thumb, setThumb] = React.useState<HTMLSpanElement | null>(null);
const composedRefs = useComposedRefs(forwardedRef, (node) => setThumb(node));
// We set this to true by default so that events bubble to forms without JS (SSR)
const isFormControl = thumb ? Boolean(thumb.closest('form')) : true;
const size = useSize(thumb);
// We cast because index could be `-1` which would return undefined
const value = context.values[index] as number | undefined;
Expand Down Expand Up @@ -618,6 +611,17 @@ const SliderThumbImpl = React.forwardRef<SliderThumbImplElement, SliderThumbImpl
})}
/>
</Collection.ItemSlot>

{isFormControl && (
<BubbleInput
key={index}
name={
name ??
(context.name ? context.name + (context.values.length > 1 ? '[]' : '') : undefined)
}
value={value}
/>
)}
</span>
);
}
Expand Down

0 comments on commit 8b38903

Please sign in to comment.