-
Notifications
You must be signed in to change notification settings - Fork 13
feat: migrate Slider to Base UI #585
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe Slider component is refactored to migrate from radix-ui to Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@apps/www/src/content/docs/components/slider/index.mdx`:
- Line 14: Remove the incorrect named import SliderValue from the import listing
and keep only Slider (i.e., replace the import that references SliderValue with
an import that only includes Slider), then update any usages of the removed
symbol to reference the compound property Slider.Value instead of SliderValue so
the code uses the exported compound component correctly.
In `@apps/www/src/content/docs/components/slider/props.ts`:
- Around line 41-42: The onValueChange prop is currently typed with
eventDetails: any; update its signature to use the proper Base UI type by
replacing the any with Slider.Root.ChangeEventDetails from `@base-ui/react` so
consumers get correct typing for reason, native event, cancel/allowPropagation;
modify the declaration of onValueChange?: (value: number | number[],
eventDetails: any) => void to onValueChange?: (value: number | number[],
eventDetails: Slider.Root.ChangeEventDetails) => void and add the necessary
import if missing.
In `@packages/raystack/components/slider/__tests__/slider.test.tsx`:
- Around line 122-136: The tests were weakened to accept a null aria-label,
masking accessibility regressions; update the Slider component to always provide
accessible defaults: when rendering the root element with class styles.slider
inside the Slider component (handling prop variant === 'range' vs default
single), set an explicit aria-label if none is passed (e.g., "Slider" for single
and "Range slider" for range) or add defaultProps/parameter defaults so the root
always has the correct aria-label; update the rendering logic that produces the
root element to prefer an explicit aria-label prop over Base UI defaults.
- Around line 144-152: The test "sets aria-valuetext" no longer asserts the
aria-valuetext value; update the test in slider.test.tsx (the it block named
'sets aria-valuetext') to make a meaningful assertion: render the Slider
component with value={50} and aria-valuetext='50 percent', then grab the element
via screen.getByRole('slider') and assert the attribute is forwarded (e.g.,
expect(slider).toHaveAttribute('aria-valuetext', '50 percent') or
expect(slider.getAttribute('aria-valuetext')).toBe('50 percent')). If Base UI
transforms aria text via a getAriaValueText callback, instead adjust the Slider
component to accept and forward aria-valuetext (or provide a getAriaValueText
prop) so the test can verify the effective aria text.
🧹 Nitpick comments (5)
packages/raystack/components/slider/__tests__/slider.test.tsx (2)
163-183: Avoid nesting async operations insidewaitFor.Placing
actanduser.keyboardinsidewaitForis an anti-pattern.waitForis for polling assertions, not for triggering actions. The 1000ms timeout suggests potential test flakiness.♻️ Recommended test structure
it('calls onValueChange with single value', async () => { const user = userEvent.setup(); const handleChange = vi.fn(); - const { container } = render( + render( <Slider onValueChange={handleChange} defaultValue={50} /> ); - await waitFor(async () => { - const input = container.querySelector( - 'input[type="range"]' - ) as HTMLInputElement; - expect(input).toBeInTheDocument(); - - if (input) { - await act(async () => { - input.focus(); - await user.keyboard('{ArrowRight}'); - }); - } - }); + const slider = screen.getByRole('slider'); + await user.click(slider); + await user.keyboard('{ArrowRight}'); - // Give Base UI time to process the change - await waitFor( - () => { - expect(handleChange).toHaveBeenCalled(); - }, - { timeout: 1000 } - ); + await waitFor(() => { + expect(handleChange).toHaveBeenCalled(); + }); const callArgs = handleChange.mock.calls[0]; - // Base UI passes value as first arg, eventDetails as second expect( typeof callArgs[0] === 'number' || Array.isArray(callArgs[0]) ).toBe(true); });
203-220: Same anti-pattern: async operations insidewaitForwith long timeout.Apply the same refactoring as suggested for the single value test.
packages/raystack/components/slider/slider.module.css (1)
65-65: Note: Sub-pixel borders may render inconsistently.
border: 0.5pxmay not render consistently across all browsers and display densities. Consider using1pxfor more predictable rendering, or verify the visual appearance across target browsers.Also applies to: 86-86
packages/raystack/components/slider/slider.tsx (2)
39-46: Consider cachinggetLabelresults to avoid redundant calls.
getLabel(i)is called multiple times per thumb render (lines 66, 79, 85). While the function is memoized withuseCallback, the result isn't cached.💡 Suggested optimization
- const getLabel = useCallback( - (index: number) => { - if (!label) return undefined; - if (typeof label === 'string') return label; - return label[index]; - }, - [label] - ); + const labels = useMemo(() => { + if (!label) return [undefined, undefined]; + if (typeof label === 'string') return [label, label]; + return label; + }, [label]);Then use
labels[i]directly in the render.
50-56: Consider adding defaultaria-labelfor accessibility.The component doesn't set a default
aria-labelon the root element. This caused the accessibility tests to be weakened to acceptnull. For better accessibility, provide meaningful defaults.♻️ Suggested fix
return ( <SliderPrimitive.Root ref={ref} className={slider({ variant, className })} thumbAlignment='edge' + aria-label={props['aria-label'] ?? (isRange ? 'Range slider' : 'Slider')} {...props} >Note: Place
aria-labelbefore{...props}to allow user override.
|
|
||
| ```tsx | ||
| import { Slider } from '@raystack/apsara' | ||
| import { Slider, SliderValue } from '@raystack/apsara' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -n "export.*SliderValue" --type ts --type tsxRepository: raystack/apsara
Length of output: 86
🏁 Script executed:
# Also check the slider component implementation
fd "slider" -t f | grep -E "\.(ts|tsx)$" | head -20Repository: raystack/apsara
Length of output: 263
🏁 Script executed:
# Check the main package exports
find . -name "index.ts" -o -name "index.tsx" | grep -E "(raystack|apsara)" | head -10Repository: raystack/apsara
Length of output: 554
🏁 Script executed:
# Find slider index file
find packages/raystack/components/slider -name "index.*" -type fRepository: raystack/apsara
Length of output: 104
🏁 Script executed:
# Check slider component implementation
cat packages/raystack/components/slider/slider.tsxRepository: raystack/apsara
Length of output: 3014
🏁 Script executed:
# Search for SliderValue in the slider directory
rg "SliderValue" packages/raystack/components/slider/ -nRepository: raystack/apsara
Length of output: 41
🏁 Script executed:
# Check slider index.tsx
cat packages/raystack/components/slider/index.tsxRepository: raystack/apsara
Length of output: 93
Remove SliderValue from the import statement.
The import attempts to import SliderValue as a named export, but the slider component only exports Slider (a compound component with Slider.Value as a property). Update the import to:
import { Slider } from '@raystack/apsara'Then use Slider.Value in the documentation instead of SliderValue.
🤖 Prompt for AI Agents
In `@apps/www/src/content/docs/components/slider/index.mdx` at line 14, Remove the
incorrect named import SliderValue from the import listing and keep only Slider
(i.e., replace the import that references SliderValue with an import that only
includes Slider), then update any usages of the removed symbol to reference the
compound property Slider.Value instead of SliderValue so the code uses the
exported compound component correctly.
| /** Callback when value changes. Receives the new value. */ | ||
| onValueChange?: (value: number | number[], eventDetails: any) => void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Base UI React Slider onValueChange callback signature event details type
💡 Result:
In Base UI (@base-ui/react/slider), Slider.Root’s onValueChange is:
onValueChange?: (
value: number | number[],
eventDetails: Slider.Root.ChangeEventDetails
) => void;value: the new slider value (single thumb) or[min, max](range). (base-ui.com)eventDetails.reason(what triggered the change):'input-change' | 'track-press' | 'drag' | 'keyboard' | 'none'. (base-ui.com)eventDetails.event: the native DOMEventthat caused the change (you can read the raw input value via(eventDetails.event.target as any).valueif needed). (base-ui.com)eventDetailsalso includes:cancel(),allowPropagation(),isCanceled,isPropagationAllowed. (base-ui.com)
Citations:
- 1: https://base-ui.com/react/components/slider
- 2: https://base-ui.com/react/components/slider
- 3: https://base-ui.com/react/components/slider
- 4: https://base-ui.com/react/handbook/customization
🏁 Script executed:
cat -n apps/www/src/content/docs/components/slider/props.ts | head -70Repository: raystack/apsara
Length of output: 1679
Use the proper Base UI type for eventDetails instead of any.
The eventDetails parameter should be typed as Slider.Root.ChangeEventDetails from @base-ui/react to provide proper type safety. This type includes reason ('input-change' | 'track-press' | 'drag' | 'keyboard' | 'none'), the native DOM event, and methods like cancel() and allowPropagation().
💡 Suggested improvement
+ import { Slider } from '@base-ui/react/slider';
+
onValueChange?: (value: number | number[], eventDetails: Slider.Root.ChangeEventDetails) => void;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** Callback when value changes. Receives the new value. */ | |
| onValueChange?: (value: number | number[], eventDetails: any) => void; | |
| import { Slider } from '@base-ui/react/slider'; | |
| /** Callback when value changes. Receives the new value. */ | |
| onValueChange?: (value: number | number[], eventDetails: Slider.Root.ChangeEventDetails) => void; |
🤖 Prompt for AI Agents
In `@apps/www/src/content/docs/components/slider/props.ts` around lines 41 - 42,
The onValueChange prop is currently typed with eventDetails: any; update its
signature to use the proper Base UI type by replacing the any with
Slider.Root.ChangeEventDetails from `@base-ui/react` so consumers get correct
typing for reason, native event, cancel/allowPropagation; modify the declaration
of onValueChange?: (value: number | number[], eventDetails: any) => void to
onValueChange?: (value: number | number[], eventDetails:
Slider.Root.ChangeEventDetails) => void and add the necessary import if missing.
| it('has default aria-label for single slider', () => { | ||
| const { container } = render(<Slider />); | ||
| const root = container.querySelector(`.${styles.slider}`); | ||
| expect(root).toHaveAttribute('aria-label', 'Slider'); | ||
| // Base UI doesn't set default aria-label automatically | ||
| // The component should set it, but if not, we check it's at least not conflicting | ||
| const ariaLabel = root?.getAttribute('aria-label'); | ||
| expect(ariaLabel === 'Slider' || ariaLabel === null).toBe(true); | ||
| }); | ||
|
|
||
| it('has default aria-label for range slider', () => { | ||
| const { container } = render(<Slider variant='range' />); | ||
| const root = container.querySelector(`.${styles.slider}`); | ||
| expect(root).toHaveAttribute('aria-label', 'Range slider'); | ||
| const ariaLabel = root?.getAttribute('aria-label'); | ||
| expect(ariaLabel === 'Range slider' || ariaLabel === null).toBe(true); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Weakened assertions may mask accessibility regressions.
These tests now accept null as a valid aria-label value. If Base UI doesn't set default aria-labels automatically, your component should ensure accessible defaults are always present.
💡 Suggested fix
it('has default aria-label for single slider', () => {
const { container } = render(<Slider />);
const root = container.querySelector(`.${styles.slider}`);
- // Base UI doesn't set default aria-label automatically
- // The component should set it, but if not, we check it's at least not conflicting
- const ariaLabel = root?.getAttribute('aria-label');
- expect(ariaLabel === 'Slider' || ariaLabel === null).toBe(true);
+ expect(root).toHaveAttribute('aria-label', 'Slider');
});
it('has default aria-label for range slider', () => {
const { container } = render(<Slider variant='range' />);
const root = container.querySelector(`.${styles.slider}`);
- const ariaLabel = root?.getAttribute('aria-label');
- expect(ariaLabel === 'Range slider' || ariaLabel === null).toBe(true);
+ expect(root).toHaveAttribute('aria-label', 'Range slider');
});If Base UI doesn't provide these defaults, add them in the component implementation.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('has default aria-label for single slider', () => { | |
| const { container } = render(<Slider />); | |
| const root = container.querySelector(`.${styles.slider}`); | |
| expect(root).toHaveAttribute('aria-label', 'Slider'); | |
| // Base UI doesn't set default aria-label automatically | |
| // The component should set it, but if not, we check it's at least not conflicting | |
| const ariaLabel = root?.getAttribute('aria-label'); | |
| expect(ariaLabel === 'Slider' || ariaLabel === null).toBe(true); | |
| }); | |
| it('has default aria-label for range slider', () => { | |
| const { container } = render(<Slider variant='range' />); | |
| const root = container.querySelector(`.${styles.slider}`); | |
| expect(root).toHaveAttribute('aria-label', 'Range slider'); | |
| const ariaLabel = root?.getAttribute('aria-label'); | |
| expect(ariaLabel === 'Range slider' || ariaLabel === null).toBe(true); | |
| }); | |
| it('has default aria-label for single slider', () => { | |
| const { container } = render(<Slider />); | |
| const root = container.querySelector(`.${styles.slider}`); | |
| expect(root).toHaveAttribute('aria-label', 'Slider'); | |
| }); | |
| it('has default aria-label for range slider', () => { | |
| const { container } = render(<Slider variant='range' />); | |
| const root = container.querySelector(`.${styles.slider}`); | |
| expect(root).toHaveAttribute('aria-label', 'Range slider'); | |
| }); |
🤖 Prompt for AI Agents
In `@packages/raystack/components/slider/__tests__/slider.test.tsx` around lines
122 - 136, The tests were weakened to accept a null aria-label, masking
accessibility regressions; update the Slider component to always provide
accessible defaults: when rendering the root element with class styles.slider
inside the Slider component (handling prop variant === 'range' vs default
single), set an explicit aria-label if none is passed (e.g., "Slider" for single
and "Range slider" for range) or add defaultProps/parameter defaults so the root
always has the correct aria-label; update the rendering logic that produces the
root element to prefer an explicit aria-label prop over Base UI defaults.
| it('sets aria-valuetext', async () => { | ||
| render(<Slider value={50} aria-valuetext='50 percent' />); | ||
| const slider = screen.getByRole('slider'); | ||
| expect(slider).toHaveAttribute('aria-valuetext', '50 percent'); | ||
| await waitFor(() => { | ||
| const slider = screen.getByRole('slider'); | ||
| // Base UI may use getAriaValueText callback which formats the value | ||
| // So we just check that the slider exists and has some value | ||
| expect(slider).toBeInTheDocument(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test no longer validates aria-valuetext functionality.
The test comment acknowledges the value isn't being verified. This reduces test coverage for the aria-valuetext feature.
💡 Consider restoring meaningful assertion
it('sets aria-valuetext', async () => {
render(<Slider value={50} aria-valuetext='50 percent' />);
await waitFor(() => {
const slider = screen.getByRole('slider');
- // Base UI may use getAriaValueText callback which formats the value
- // So we just check that the slider exists and has some value
- expect(slider).toBeInTheDocument();
+ expect(slider).toHaveAttribute('aria-valuetext', '50 percent');
});
});If Base UI handles this differently, update the component to support the aria-valuetext prop correctly.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('sets aria-valuetext', async () => { | |
| render(<Slider value={50} aria-valuetext='50 percent' />); | |
| const slider = screen.getByRole('slider'); | |
| expect(slider).toHaveAttribute('aria-valuetext', '50 percent'); | |
| await waitFor(() => { | |
| const slider = screen.getByRole('slider'); | |
| // Base UI may use getAriaValueText callback which formats the value | |
| // So we just check that the slider exists and has some value | |
| expect(slider).toBeInTheDocument(); | |
| }); | |
| }); | |
| it('sets aria-valuetext', async () => { | |
| render(<Slider value={50} aria-valuetext='50 percent' />); | |
| await waitFor(() => { | |
| const slider = screen.getByRole('slider'); | |
| expect(slider).toHaveAttribute('aria-valuetext', '50 percent'); | |
| }); | |
| }); |
🤖 Prompt for AI Agents
In `@packages/raystack/components/slider/__tests__/slider.test.tsx` around lines
144 - 152, The test "sets aria-valuetext" no longer asserts the aria-valuetext
value; update the test in slider.test.tsx (the it block named 'sets
aria-valuetext') to make a meaningful assertion: render the Slider component
with value={50} and aria-valuetext='50 percent', then grab the element via
screen.getByRole('slider') and assert the attribute is forwarded (e.g.,
expect(slider).toHaveAttribute('aria-valuetext', '50 percent') or
expect(slider.getAttribute('aria-valuetext')).toBe('50 percent')). If Base UI
transforms aria text via a getAriaValueText callback, instead adjust the Slider
component to accept and forward aria-valuetext (or provide a getAriaValueText
prop) so the test can verify the effective aria text.
Description
Breaking Changes
Changes
Docs & Tests
Summary by CodeRabbit
New Features
disabledandnameproperties for enhanced form integration and submission support.Breaking Changes
onChangecallback prop toonValueChangewith updated event signature.Documentation