feat(web): base-ui slider (#34064)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh
2026-03-25 16:03:49 +08:00
committed by GitHub
parent 1789988be7
commit a8e1ff85db
43 changed files with 425 additions and 1068 deletions

View File

@ -93,7 +93,6 @@ const ConfigParamModal: FC<Props> = ({
className="mt-1"
value={(annotationConfig.score_threshold || ANNOTATION_DEFAULT.score_threshold) * 100}
onChange={(val) => {
/* v8 ignore next -- callback dispatch depends on react-slider drag mechanics that are flaky in jsdom. @preserve */
setAnnotationConfig({
...annotationConfig,
score_threshold: val / 100,

View File

@ -1,20 +1,9 @@
import { render, screen } from '@testing-library/react'
import ScoreSlider from '../index'
vi.mock('@/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider', () => ({
default: ({ value, onChange, min, max }: { value: number, onChange: (v: number) => void, min: number, max: number }) => (
<input
type="range"
data-testid="slider"
value={value}
min={min}
max={max}
onChange={e => onChange(Number(e.target.value))}
/>
),
}))
describe('ScoreSlider', () => {
const getSliderInput = () => screen.getByLabelText('appDebug.feature.annotation.scoreThreshold.title')
beforeEach(() => {
vi.clearAllMocks()
})
@ -22,7 +11,7 @@ describe('ScoreSlider', () => {
it('should render the slider', () => {
render(<ScoreSlider value={90} onChange={vi.fn()} />)
expect(screen.getByTestId('slider')).toBeInTheDocument()
expect(getSliderInput()).toBeInTheDocument()
})
it('should display easy match and accurate match labels', () => {
@ -37,14 +26,14 @@ describe('ScoreSlider', () => {
it('should render with custom className', () => {
const { container } = render(<ScoreSlider className="custom-class" value={90} onChange={vi.fn()} />)
// Verifying the component renders successfully with a custom className
expect(screen.getByTestId('slider')).toBeInTheDocument()
expect(getSliderInput()).toBeInTheDocument()
expect(container.firstChild).toHaveClass('custom-class')
})
it('should pass value to the slider', () => {
render(<ScoreSlider value={95} onChange={vi.fn()} />)
expect(screen.getByTestId('slider')).toHaveValue('95')
expect(getSliderInput()).toHaveValue('95')
expect(screen.getByText('0.95')).toBeInTheDocument()
})
})

View File

@ -1,50 +0,0 @@
import { render, screen } from '@testing-library/react'
import Slider from '../index'
describe('BaseSlider', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render the slider component', () => {
render(<Slider value={50} onChange={vi.fn()} />)
expect(screen.getByRole('slider')).toBeInTheDocument()
})
it('should display the formatted value in the thumb', () => {
render(<Slider value={85} onChange={vi.fn()} />)
expect(screen.getByText('0.85')).toBeInTheDocument()
})
it('should use default min/max/step when not provided', () => {
render(<Slider value={50} onChange={vi.fn()} />)
const slider = screen.getByRole('slider')
expect(slider).toHaveAttribute('aria-valuemin', '0')
expect(slider).toHaveAttribute('aria-valuemax', '100')
expect(slider).toHaveAttribute('aria-valuenow', '50')
})
it('should use custom min/max/step when provided', () => {
render(<Slider value={90} min={80} max={100} step={5} onChange={vi.fn()} />)
const slider = screen.getByRole('slider')
expect(slider).toHaveAttribute('aria-valuemin', '80')
expect(slider).toHaveAttribute('aria-valuemax', '100')
expect(slider).toHaveAttribute('aria-valuenow', '90')
})
it('should handle NaN value as 0', () => {
render(<Slider value={Number.NaN} onChange={vi.fn()} />)
expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0')
})
it('should pass disabled prop', () => {
render(<Slider value={50} disabled onChange={vi.fn()} />)
expect(screen.getByRole('slider')).toHaveAttribute('aria-disabled', 'true')
})
})

View File

@ -1,40 +0,0 @@
import ReactSlider from 'react-slider'
import { cn } from '@/utils/classnames'
import s from './style.module.css'
type ISliderProps = {
className?: string
value: number
max?: number
min?: number
step?: number
disabled?: boolean
onChange: (value: number) => void
}
const Slider: React.FC<ISliderProps> = ({ className, max, min, step, value, disabled, onChange }) => {
return (
<ReactSlider
disabled={disabled}
value={isNaN(value) ? 0 : value}
min={min || 0}
max={max || 100}
step={step || 1}
className={cn(className, s.slider)}
thumbClassName={cn(s['slider-thumb'], 'top-[-7px] h-[18px] w-2 cursor-pointer rounded-[36px] border !border-black/8 bg-white shadow-md')}
trackClassName={s['slider-track']}
onChange={onChange}
renderThumb={(props, state) => (
<div {...props}>
<div className="relative h-full w-full">
<div className="absolute left-[50%] top-[-16px] translate-x-[-50%] text-text-primary system-sm-semibold">
{(state.valueNow / 100).toFixed(2)}
</div>
</div>
</div>
)}
/>
)
}
export default Slider

View File

@ -1,20 +0,0 @@
.slider {
position: relative;
}
.slider.disabled {
opacity: 0.6;
}
.slider-thumb:focus {
outline: none;
}
.slider-track {
background-color: #528BFF;
height: 2px;
}
.slider-track-1 {
background-color: #E5E7EB;
}

View File

@ -2,7 +2,7 @@
import type { FC } from 'react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Slider from '@/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider'
import { Slider } from '@/app/components/base/ui/slider'
type Props = {
className?: string
@ -10,23 +10,42 @@ type Props = {
onChange: (value: number) => void
}
const clamp = (value: number, min: number, max: number) => {
if (!Number.isFinite(value))
return min
return Math.min(Math.max(value, min), max)
}
const ScoreSlider: FC<Props> = ({
className,
value,
onChange,
}) => {
const { t } = useTranslation()
const safeValue = clamp(value, 80, 100)
return (
<div className={className}>
<div className="mt-[14px] h-px">
<div className="relative mt-[14px]">
<Slider
max={100}
className="w-full"
value={safeValue}
min={80}
max={100}
step={1}
value={value}
onChange={onChange}
onValueChange={onChange}
aria-label={t('feature.annotation.scoreThreshold.title', { ns: 'appDebug' })}
/>
<div
className="pointer-events-none absolute top-[-16px] text-text-primary system-sm-semibold"
style={{
left: `calc(4px + ${(safeValue - 80) / 20} * (100% - 8px))`,
transform: 'translateX(-50%)',
}}
>
{(safeValue / 100).toFixed(2)}
</div>
</div>
<div className="mt-[10px] flex items-center justify-between system-xs-semibold-uppercase">
<div className="flex space-x-1 text-util-colors-cyan-cyan-500">