Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions docs/reference/generated/slider-root.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,6 @@
"description": "The component orientation.",
"detailedType": "'horizontal' | 'vertical' | undefined"
},
"inputRef": {
"type": "Ref<HTMLInputElement>",
"description": "A ref to access the hidden input element.",
"detailedType": "React.Ref<HTMLInputElement> | undefined"
},
"className": {
"type": "string | ((state: Slider.Root.State) => string)",
"description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state."
Expand Down
5 changes: 5 additions & 0 deletions docs/reference/generated/slider-thumb.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
"description": "Whether the thumb should ignore user interaction.",
"detailedType": "boolean | undefined"
},
"inputRef": {
"type": "Ref<HTMLInputElement>",
"description": "A ref to access the nested input element.",
"detailedType": "React.Ref<HTMLInputElement> | undefined"
},
"className": {
"type": "string | ((state: Slider.Thumb.State) => string)",
"description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state."
Expand Down
24 changes: 19 additions & 5 deletions docs/src/app/(private)/experiments/forms/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const schema = z.object({
input: z.string().min(1, 'This field is required'),
checkbox: z.enum(['on']),
switch: z.enum(['on']),
slider: z.number().min(40),
slider: z.number().max(90, 'Too loud'),
'range-slider': z.array(z.number()),
'number-field': z.number().min(0).max(100),
select: z.enum(['sans', 'serif', 'mono', 'cursive']),
Expand All @@ -58,7 +58,11 @@ export const settingsMetadata: SettingsMetadata<Settings> = {
},
};

async function submitForm(event: React.FormEvent<HTMLFormElement>, values: Values) {
async function submitForm(
event: React.FormEvent<HTMLFormElement>,
values: Values,
native: boolean,
) {
event.preventDefault();

const formData = new FormData(event.currentTarget);
Expand All @@ -70,6 +74,12 @@ async function submitForm(event: React.FormEvent<HTMLFormElement>, values: Value
entries['range-slider'] = formData.getAll('range-slider').map((v) => parseFloat(v as string));
entries['multi-select'] = formData.getAll('multi-select');

if (native) {
return {
errors: {},
};
}

const result = schema.safeParse(entries);

if (!result.success) {
Expand Down Expand Up @@ -102,9 +112,13 @@ export default function Page() {
errors={errors}
onClearErrors={setErrors}
onSubmit={async (event) => {
const response = await submitForm(event, {
numberField: numberFieldValueRef.current,
});
const response = await submitForm(
event,
{
numberField: numberFieldValueRef.current,
},
native,
);
setErrors(response.errors);
}}
>
Expand Down
7 changes: 5 additions & 2 deletions docs/src/app/(private)/experiments/forms/rhf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,13 +224,16 @@ export default function ExampleForm() {
<Slider.Root
value={field.value}
onValueChange={field.onChange}
inputRef={field.ref}
className={styles.Slider}
>
<Slider.Control className={styles.SliderControl}>
<Slider.Track className={styles.SliderTrack}>
<Slider.Indicator className={styles.SliderIndicator} />
<Slider.Thumb onBlur={field.onBlur} className={styles.SliderThumb} />
<Slider.Thumb
inputRef={field.ref}
onBlur={field.onBlur}
className={styles.SliderThumb}
/>
</Slider.Track>
</Slider.Control>
</Slider.Root>
Expand Down
5 changes: 2 additions & 3 deletions packages/react/src/slider/root/SliderRoot.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1799,7 +1799,7 @@ describe.skipIf(typeof Touch === 'undefined')('<Slider.Root />', () => {
});

it('should receive name prop from Field.Root', async () => {
const { container } = await render(
await render(
<Field.Root name="field-slider">
<Slider.Root>
<Slider.Control>
Expand All @@ -1809,8 +1809,7 @@ describe.skipIf(typeof Touch === 'undefined')('<Slider.Root />', () => {
</Field.Root>,
);

const hiddenInput = container.querySelector('input[aria-hidden="true"]');
expect(hiddenInput).to.have.attribute('name', 'field-slider');
expect(screen.getByRole('slider')).to.have.attribute('name', 'field-slider');
});

it('[data-touched]', async () => {
Expand Down
47 changes: 2 additions & 45 deletions packages/react/src/slider/root/SliderRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import * as React from 'react';
import { ownerDocument } from '@base-ui-components/utils/owner';
import { useControlled } from '@base-ui-components/utils/useControlled';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { useMergedRefs } from '@base-ui-components/utils/useMergedRefs';
import { useLatestRef } from '@base-ui-components/utils/useLatestRef';
import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect';
import { visuallyHidden } from '@base-ui-components/utils/visuallyHidden';
import { warn } from '@base-ui-components/utils/warn';
import type { BaseUIComponentProps, Orientation } from '../../utils/types';
import {
Expand Down Expand Up @@ -59,7 +57,6 @@ export const SliderRoot = React.forwardRef(function SliderRoot<
defaultValue,
disabled: disabledProp = false,
id: idProp,
inputRef: inputRefProp,
format,
largeStep = 10,
locale,
Expand Down Expand Up @@ -121,7 +118,6 @@ export const SliderRoot = React.forwardRef(function SliderRoot<
const controlRef = React.useRef<HTMLElement>(null);
const thumbRefs = React.useRef<(HTMLElement | null)[]>([]);
const pressedInputRef = React.useRef<HTMLInputElement>(null);
const inputRef = useMergedRefs(inputRefProp, fieldControlValidation.inputRef);
const lastChangedValueRef = React.useRef<number | readonly number[] | null>(null);
const formatOptionsRef = useLatestRef(format);

Expand Down Expand Up @@ -215,11 +211,6 @@ export const SliderRoot = React.forwardRef(function SliderRoot<
},
);

const handleHiddenInputFocus = useEventCallback(() => {
// focus the first thumb if the hidden input receives focus
thumbRefs.current?.[0]?.focus();
});

if (process.env.NODE_ENV !== 'production') {
if (min >= max) {
warn('Slider `max` must be greater than `min`.');
Expand Down Expand Up @@ -283,6 +274,7 @@ export const SliderRoot = React.forwardRef(function SliderRoot<
max,
min,
minStepsBetweenValues,
name,
onValueCommitted,
orientation,
range,
Expand Down Expand Up @@ -311,6 +303,7 @@ export const SliderRoot = React.forwardRef(function SliderRoot<
max,
min,
minStepsBetweenValues,
name,
onValueCommitted,
orientation,
range,
Expand Down Expand Up @@ -345,38 +338,6 @@ export const SliderRoot = React.forwardRef(function SliderRoot<
<SliderRootContext.Provider value={contextValue}>
<CompositeList elementsRef={thumbRefs} onMapChange={setThumbMap}>
{element}
{range ? (
values.map((value, index) => {
return (
<input
key={`${name}-input-${index}`}
{...fieldControlValidation.getInputValidationProps({
disabled,
name,
ref: inputRef,
value,
onFocus: handleHiddenInputFocus,
style: visuallyHidden,
tabIndex: -1,
'aria-hidden': true,
})}
/>
);
})
) : (
<input
{...fieldControlValidation.getInputValidationProps({
disabled,
name,
ref: inputRef,
value: valueUnwrapped,
onFocus: handleHiddenInputFocus,
style: visuallyHidden,
tabIndex: -1,
'aria-hidden': true,
})}
/>
)}
</CompositeList>
</SliderRootContext.Provider>
);
Expand Down Expand Up @@ -442,10 +403,6 @@ export namespace SliderRoot {
* Options to format the input value.
*/
format?: Intl.NumberFormatOptions;
/**
* A ref to access the hidden input element.
*/
inputRef?: React.Ref<HTMLInputElement>;
/**
* The locale used by `Intl.NumberFormat` when formatting the value.
* Defaults to the user's runtime locale.
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/slider/root/SliderRootContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface SliderRootContext {
* The minimum steps between values in a range slider.
*/
minStepsBetweenValues: number;
name: string;
/**
* Function to be called when drag ends and the pointer is released.
*/
Expand Down
31 changes: 31 additions & 0 deletions packages/react/src/slider/thumb/SliderThumb.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,37 @@ describe('<Slider.Thumb />', () => {
});
});

describe('prop: inputRef', () => {
it('can focus the input element', async () => {
function App() {
const inputRef = React.useRef<HTMLInputElement>(null);
return (
<React.Fragment>
<Slider.Root defaultValue={50}>
<Slider.Control>
<Slider.Thumb inputRef={inputRef} />
</Slider.Control>
</Slider.Root>
<button
onClick={() => {
if (inputRef.current) {
inputRef.current.focus();
}
}}
>
Button
</button>
</React.Fragment>
);
}
const { user } = await render(<App />);

expect(document.body).toHaveFocus();
await user.click(screen.getByText('Button'));
expect(screen.getByRole('slider')).toHaveFocus();
});
});

/**
* Browser tests render with 1024px width by default, so most tests here set
* the component to `width: 100px` to make the asserted values more readable.
Expand Down
16 changes: 13 additions & 3 deletions packages/react/src/slider/thumb/SliderThumb.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';
import * as React from 'react';
import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect';
import { useMergedRefs } from '@base-ui-components/utils/useMergedRefs';
import { visuallyHidden } from '@base-ui-components/utils/visuallyHidden';
import { BaseUIComponentProps } from '../../utils/types';
import { formatNumber } from '../../utils/formatNumber';
Expand Down Expand Up @@ -94,6 +95,7 @@ export const SliderThumb = React.forwardRef(function SliderThumb(
getAriaValueText: getAriaValueTextProp,
id: idProp,
index: indexProp,
inputRef: inputRefProp,
onBlur: onBlurProp,
onFocus: onFocusProp,
onKeyDown: onKeyDownProp,
Expand All @@ -117,6 +119,7 @@ export const SliderThumb = React.forwardRef(function SliderThumb(
max,
min,
minStepsBetweenValues,
name,
orientation,
setActive,
state,
Expand Down Expand Up @@ -212,6 +215,7 @@ export const SliderThumb = React.forwardRef(function SliderThumb(
id: inputId,
max,
min,
name,
onChange(event: React.ChangeEvent<HTMLInputElement>) {
handleInputChange(event.target.valueAsNumber, index, event);
},
Expand Down Expand Up @@ -315,16 +319,18 @@ export const SliderThumb = React.forwardRef(function SliderThumb(
type: 'range',
value: thumbValue ?? '',
},
fieldControlValidation.getValidationProps,
fieldControlValidation.getInputValidationProps,
);

const mergedInputRef = useMergedRefs(inputRef, fieldControlValidation.inputRef, inputRefProp);

const children = childrenProp ? (
<React.Fragment>
{childrenProp}
<input ref={inputRef} {...inputProps} />
<input ref={mergedInputRef} {...inputProps} />
</React.Fragment>
) : (
<input ref={inputRef} {...inputProps} />
<input ref={mergedInputRef} {...inputProps} />
);

const element = useRenderElement('div', componentProps, {
Expand Down Expand Up @@ -395,6 +401,10 @@ export namespace SliderThumb {
* ```
*/
index?: number | undefined;
/**
* A ref to access the nested input element.
*/
inputRef?: React.Ref<HTMLInputElement>;
/**
* A blur handler forwarded to the `input`.
*/
Expand Down
Loading