From a8e1ff85db19a97a252007799ac8ac6a59dbeefa Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:03:49 +0800 Subject: [PATCH] feat(web): base-ui slider (#34064) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- web/.storybook/preview.tsx | 2 +- .../config/agent/agent-setting/index.spec.tsx | 8 +- .../config/agent/agent-setting/index.tsx | 5 +- .../params-config/config-content.spec.tsx | 6 +- .../params-config/weighted-score.css | 7 - .../params-config/weighted-score.spec.tsx | 11 +- .../params-config/weighted-score.tsx | 36 +- .../annotation-reply/config-param-modal.tsx | 1 - .../score-slider/__tests__/index.spec.tsx | 23 +- .../base-slider/__tests__/index.spec.tsx | 50 -- .../score-slider/base-slider/index.tsx | 40 -- .../score-slider/base-slider/style.module.css | 20 - .../annotation-reply/score-slider/index.tsx | 29 +- .../__tests__/index-slider.spec.tsx | 10 +- .../base/param-item/__tests__/index.spec.tsx | 22 +- .../__tests__/score-threshold-item.spec.tsx | 6 +- .../param-item/__tests__/top-k-item.spec.tsx | 14 +- web/app/components/base/param-item/index.tsx | 5 +- .../base/slider/__tests__/index.spec.tsx | 77 --- .../components/base/slider/index.stories.tsx | 635 ------------------ web/app/components/base/slider/index.tsx | 43 -- web/app/components/base/slider/style.css | 11 - .../base/ui/slider/__tests__/index.spec.tsx | 73 ++ .../base/ui/slider/index.stories.tsx | 92 +++ web/app/components/base/ui/slider/index.tsx | 100 +++ .../index-method/__tests__/index.spec.tsx | 5 +- .../__tests__/keyword-number.spec.tsx | 20 +- .../settings/index-method/keyword-number.tsx | 5 +- .../__tests__/parameter-item.spec.tsx | 6 +- .../model-parameter-modal/parameter-item.tsx | 13 +- .../__tests__/agent-strategy.spec.tsx | 2 +- .../nodes/_base/components/agent-strategy.tsx | 5 +- .../components/input-number-with-slider.tsx | 7 +- .../nodes/_base/components/memory-config.tsx | 7 +- .../_base/components/retry/retry-on-panel.tsx | 8 +- .../workflow/nodes/iteration/panel.tsx | 11 +- .../__tests__/index-method.spec.tsx | 2 +- .../components/index-method.tsx | 7 +- .../components/__tests__/integration.spec.tsx | 4 +- .../components/on-minute-selector.tsx | 5 +- web/eslint-suppressions.json | 35 - web/package.json | 2 - web/pnpm-lock.yaml | 23 - 43 files changed, 425 insertions(+), 1068 deletions(-) delete mode 100644 web/app/components/app/configuration/dataset-config/params-config/weighted-score.css delete mode 100644 web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/__tests__/index.spec.tsx delete mode 100644 web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/index.tsx delete mode 100644 web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/style.module.css delete mode 100644 web/app/components/base/slider/__tests__/index.spec.tsx delete mode 100644 web/app/components/base/slider/index.stories.tsx delete mode 100644 web/app/components/base/slider/index.tsx delete mode 100644 web/app/components/base/slider/style.css create mode 100644 web/app/components/base/ui/slider/__tests__/index.spec.tsx create mode 100644 web/app/components/base/ui/slider/index.stories.tsx create mode 100644 web/app/components/base/ui/slider/index.tsx diff --git a/web/.storybook/preview.tsx b/web/.storybook/preview.tsx index 5b38424776..072244c33f 100644 --- a/web/.storybook/preview.tsx +++ b/web/.storybook/preview.tsx @@ -7,7 +7,7 @@ import { I18nClientProvider as I18N } from '../app/components/provider/i18n' import commonEnUS from '../i18n/en-US/common.json' import '../app/styles/globals.css' -import '../app/styles/markdown.scss' +import '../app/styles/markdown.css' import './storybook.css' const queryClient = new QueryClient({ diff --git a/web/app/components/app/configuration/config/agent/agent-setting/index.spec.tsx b/web/app/components/app/configuration/config/agent/agent-setting/index.spec.tsx index b3a9bd7abc..1b8d64b911 100644 --- a/web/app/components/app/configuration/config/agent/agent-setting/index.spec.tsx +++ b/web/app/components/app/configuration/config/agent/agent-setting/index.spec.tsx @@ -12,15 +12,15 @@ vi.mock('ahooks', async (importOriginal) => { } }) -vi.mock('react-slider', () => ({ - default: (props: { className?: string, min?: number, max?: number, value: number, onChange: (value: number) => void }) => ( +vi.mock('@/app/components/base/ui/slider', () => ({ + Slider: (props: { className?: string, min?: number, max?: number, value: number, onValueChange: (value: number) => void }) => ( props.onChange(Number(e.target.value))} + onChange={e => props.onValueChange(Number(e.target.value))} /> ), })) diff --git a/web/app/components/app/configuration/config/agent/agent-setting/index.tsx b/web/app/components/app/configuration/config/agent/agent-setting/index.tsx index ec42e946dd..bce4e74aab 100644 --- a/web/app/components/app/configuration/config/agent/agent-setting/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-setting/index.tsx @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import { CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication' import { Unblur } from '@/app/components/base/icons/src/vender/solid/education' -import Slider from '@/app/components/base/slider' +import { Slider } from '@/app/components/base/ui/slider' import { DEFAULT_AGENT_PROMPT, MAX_ITERATIONS_NUM } from '@/config' import ItemPanel from './item-panel' @@ -105,12 +105,13 @@ const AgentSetting: FC = ({ min={maxIterationsMin} max={MAX_ITERATIONS_NUM} value={tempPayload.max_iteration} - onChange={(value) => { + onValueChange={(value) => { setTempPayload({ ...tempPayload, max_iteration: value, }) }} + aria-label={t('agent.setting.maximumIterations.name', { ns: 'appDebug' })} /> { />, ) - const weightedScoreSlider = screen.getAllByRole('slider') - .find(slider => slider.getAttribute('aria-valuemax') === '1') - expect(weightedScoreSlider).toBeDefined() - await user.click(weightedScoreSlider!) + const weightedScoreSlider = screen.getByLabelText('dataset.weightedScore.semantic') + weightedScoreSlider.focus() const callsBefore = onChange.mock.calls.length await user.keyboard('{ArrowRight}') diff --git a/web/app/components/app/configuration/dataset-config/params-config/weighted-score.css b/web/app/components/app/configuration/dataset-config/params-config/weighted-score.css deleted file mode 100644 index ef9350645a..0000000000 --- a/web/app/components/app/configuration/dataset-config/params-config/weighted-score.css +++ /dev/null @@ -1,7 +0,0 @@ -.weightedScoreSliderTrack { - background: var(--color-util-colors-blue-light-blue-light-500) !important; -} - -.weightedScoreSliderTrack-1 { - background: transparent !important; -} diff --git a/web/app/components/app/configuration/dataset-config/params-config/weighted-score.spec.tsx b/web/app/components/app/configuration/dataset-config/params-config/weighted-score.spec.tsx index 7729830348..8e9348c77a 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/weighted-score.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/weighted-score.spec.tsx @@ -3,6 +3,8 @@ import userEvent from '@testing-library/user-event' import WeightedScore from './weighted-score' describe('WeightedScore', () => { + const getSliderInput = () => screen.getByLabelText('dataset.weightedScore.semantic') + beforeEach(() => { vi.clearAllMocks() }) @@ -48,8 +50,8 @@ describe('WeightedScore', () => { render() // Act - await user.tab() - const slider = screen.getByRole('slider') + const slider = getSliderInput() + slider.focus() expect(slider).toHaveFocus() const callsBefore = onChange.mock.calls.length await user.keyboard('{ArrowRight}') @@ -69,9 +71,8 @@ describe('WeightedScore', () => { render() // Act - await user.tab() - const slider = screen.getByRole('slider') - expect(slider).toHaveFocus() + const slider = getSliderInput() + expect(slider).toBeDisabled() await user.keyboard('{ArrowRight}') // Assert diff --git a/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx b/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx index 40beef52e8..d4ce935a4d 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx @@ -1,9 +1,13 @@ +import type { CSSProperties } from 'react' import { noop } from 'es-toolkit/function' import { memo } from 'react' import { useTranslation } from 'react-i18next' -import Slider from '@/app/components/base/slider' -import { cn } from '@/utils/classnames' -import './weighted-score.css' +import { Slider } from '@/app/components/base/ui/slider' + +const weightedScoreSliderStyle: CSSProperties & Record<'--slider-track' | '--slider-range', string> = { + '--slider-track': 'var(--color-util-colors-teal-teal-500)', + '--slider-range': 'var(--color-util-colors-blue-light-blue-light-500)', +} const formatNumber = (value: number) => { if (value > 0 && value < 1) @@ -33,24 +37,26 @@ const WeightedScore = ({ return (
- !readonly && onChange({ value: [v, (10 - v * 10) / 10] })} - trackClassName="weightedScoreSliderTrack" - disabled={readonly} - /> +
+ !readonly && onChange({ value: [v, (10 - v * 10) / 10] })} + disabled={readonly} + aria-label={t('weightedScore.semantic', { ns: 'dataset' })} + /> +
-
+
{t('weightedScore.semantic', { ns: 'dataset' })}
{formatNumber(value.value[0])}
-
+
{formatNumber(value.value[1])}
{t('weightedScore.keyword', { ns: 'dataset' })} diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx index 332b87cb30..ac0b6d0f57 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx @@ -93,7 +93,6 @@ const ConfigParamModal: FC = ({ 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, diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/__tests__/index.spec.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/__tests__/index.spec.tsx index 2bc30e4ead..ffa9c33043 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/__tests__/index.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/__tests__/index.spec.tsx @@ -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 }) => ( - 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() - 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() - // 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() - expect(screen.getByTestId('slider')).toHaveValue('95') + expect(getSliderInput()).toHaveValue('95') + expect(screen.getByText('0.95')).toBeInTheDocument() }) }) diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/__tests__/index.spec.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/__tests__/index.spec.tsx deleted file mode 100644 index 815e8ffe49..0000000000 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/__tests__/index.spec.tsx +++ /dev/null @@ -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() - - expect(screen.getByRole('slider')).toBeInTheDocument() - }) - - it('should display the formatted value in the thumb', () => { - render() - - expect(screen.getByText('0.85')).toBeInTheDocument() - }) - - it('should use default min/max/step when not provided', () => { - render() - - 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() - - 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() - - expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0') - }) - - it('should pass disabled prop', () => { - render() - - expect(screen.getByRole('slider')).toHaveAttribute('aria-disabled', 'true') - }) -}) diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/index.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/index.tsx deleted file mode 100644 index 509426c08e..0000000000 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/index.tsx +++ /dev/null @@ -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 = ({ className, max, min, step, value, disabled, onChange }) => { - return ( - ( -
-
-
- {(state.valueNow / 100).toFixed(2)} -
-
-
- )} - /> - ) -} - -export default Slider diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/style.module.css b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/style.module.css deleted file mode 100644 index 8ef23b54b5..0000000000 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/style.module.css +++ /dev/null @@ -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; -} diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx index c6fb1a0b4e..0363eb2820 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx @@ -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 = ({ className, value, onChange, }) => { const { t } = useTranslation() + const safeValue = clamp(value, 80, 100) return (
-
+
+
+ {(safeValue / 100).toFixed(2)} +
diff --git a/web/app/components/base/param-item/__tests__/index-slider.spec.tsx b/web/app/components/base/param-item/__tests__/index-slider.spec.tsx index 0048b89644..6448835844 100644 --- a/web/app/components/base/param-item/__tests__/index-slider.spec.tsx +++ b/web/app/components/base/param-item/__tests__/index-slider.spec.tsx @@ -14,12 +14,14 @@ describe('ParamItem Slider onChange', () => { vi.clearAllMocks() }) + const getSlider = () => screen.getByLabelText('Test Param') + it('should divide slider value by 100 when max < 5', async () => { const user = userEvent.setup() render() - const slider = screen.getByRole('slider') + const slider = getSlider() - await user.click(slider) + slider.focus() await user.keyboard('{ArrowRight}') // max=1 < 5, so slider value change (50->51) becomes 0.51 @@ -29,9 +31,9 @@ describe('ParamItem Slider onChange', () => { it('should not divide slider value when max >= 5', async () => { const user = userEvent.setup() render() - const slider = screen.getByRole('slider') + const slider = getSlider() - await user.click(slider) + slider.focus() await user.keyboard('{ArrowRight}') // max=10 >= 5, so value remains raw (5->6) diff --git a/web/app/components/base/param-item/__tests__/index.spec.tsx b/web/app/components/base/param-item/__tests__/index.spec.tsx index 96591446c8..889662c87d 100644 --- a/web/app/components/base/param-item/__tests__/index.spec.tsx +++ b/web/app/components/base/param-item/__tests__/index.spec.tsx @@ -17,6 +17,8 @@ describe('ParamItem', () => { vi.clearAllMocks() }) + const getSlider = () => screen.getByLabelText('Test Param') + describe('Rendering', () => { it('should render the parameter name', () => { render() @@ -54,7 +56,7 @@ describe('ParamItem', () => { render() expect(screen.getByRole('textbox')).toBeInTheDocument() - expect(screen.getByRole('slider')).toBeInTheDocument() + expect(getSlider()).toBeInTheDocument() }) }) @@ -74,7 +76,7 @@ describe('ParamItem', () => { it('should disable Slider when enable is false', () => { render() - expect(screen.getByRole('slider')).toHaveAttribute('aria-disabled', 'true') + expect(getSlider()).toBeDisabled() }) it('should set switch value based on enable prop', () => { @@ -135,7 +137,7 @@ describe('ParamItem', () => { await user.clear(input) expect(defaultProps.onChange).toHaveBeenLastCalledWith('test_param', 0) - expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0') + expect(getSlider()).toHaveAttribute('aria-valuenow', '0') await user.tab() @@ -166,12 +168,12 @@ describe('ParamItem', () => { await user.type(input, '1.5') expect(defaultProps.onChange).toHaveBeenLastCalledWith('test_param', 1) - expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '100') + expect(getSlider()).toHaveAttribute('aria-valuenow', '100') }) it('should pass scaled value to slider when max < 5', () => { render() - const slider = screen.getByRole('slider') + const slider = getSlider() // When max < 5, slider value = value * 100 = 50 expect(slider).toHaveAttribute('aria-valuenow', '50') @@ -179,7 +181,7 @@ describe('ParamItem', () => { it('should pass raw value to slider when max >= 5', () => { render() - const slider = screen.getByRole('slider') + const slider = getSlider() // When max >= 5, slider value = value = 5 expect(slider).toHaveAttribute('aria-valuenow', '5') @@ -212,15 +214,15 @@ describe('ParamItem', () => { render() // Slider should get value * 100 = 50, min * 100 = 0, max * 100 = 100 - const slider = screen.getByRole('slider') - expect(slider).toHaveAttribute('aria-valuemax', '100') + const slider = getSlider() + expect(slider).toHaveAttribute('max', '100') }) it('should not scale slider value when max >= 5', () => { render() - const slider = screen.getByRole('slider') - expect(slider).toHaveAttribute('aria-valuemax', '10') + const slider = getSlider() + expect(slider).toHaveAttribute('max', '10') }) it('should expose default minimum of 0 when min is not provided', () => { diff --git a/web/app/components/base/param-item/__tests__/score-threshold-item.spec.tsx b/web/app/components/base/param-item/__tests__/score-threshold-item.spec.tsx index 54a13e1b74..ddc286942b 100644 --- a/web/app/components/base/param-item/__tests__/score-threshold-item.spec.tsx +++ b/web/app/components/base/param-item/__tests__/score-threshold-item.spec.tsx @@ -14,6 +14,8 @@ describe('ScoreThresholdItem', () => { vi.clearAllMocks() }) + const getSlider = () => screen.getByLabelText('appDebug.datasetConfig.score_threshold') + describe('Rendering', () => { it('should render the translated parameter name', () => { render() @@ -32,7 +34,7 @@ describe('ScoreThresholdItem', () => { render() expect(screen.getByRole('textbox')).toBeInTheDocument() - expect(screen.getByRole('slider')).toBeInTheDocument() + expect(getSlider()).toBeInTheDocument() }) }) @@ -63,7 +65,7 @@ describe('ScoreThresholdItem', () => { render() expect(screen.getByRole('textbox')).toBeDisabled() - expect(screen.getByRole('slider')).toHaveAttribute('aria-disabled', 'true') + expect(getSlider()).toBeDisabled() }) }) diff --git a/web/app/components/base/param-item/__tests__/top-k-item.spec.tsx b/web/app/components/base/param-item/__tests__/top-k-item.spec.tsx index 1b8555213b..c84fd50518 100644 --- a/web/app/components/base/param-item/__tests__/top-k-item.spec.tsx +++ b/web/app/components/base/param-item/__tests__/top-k-item.spec.tsx @@ -19,6 +19,8 @@ describe('TopKItem', () => { vi.clearAllMocks() }) + const getSlider = () => screen.getByLabelText('appDebug.datasetConfig.top_k') + describe('Rendering', () => { it('should render the translated parameter name', () => { render() @@ -37,7 +39,7 @@ describe('TopKItem', () => { render() expect(screen.getByRole('textbox')).toBeInTheDocument() - expect(screen.getByRole('slider')).toBeInTheDocument() + expect(getSlider()).toBeInTheDocument() }) }) @@ -52,7 +54,7 @@ describe('TopKItem', () => { render() expect(screen.getByRole('textbox')).toBeDisabled() - expect(screen.getByRole('slider')).toHaveAttribute('aria-disabled', 'true') + expect(getSlider()).toBeDisabled() }) }) @@ -77,10 +79,10 @@ describe('TopKItem', () => { it('should render slider with max >= 5 so no scaling is applied', () => { render() - const slider = screen.getByRole('slider') + const slider = getSlider() // max=10 >= 5 so slider shows raw values - expect(slider).toHaveAttribute('aria-valuemax', '10') + expect(slider).toHaveAttribute('max', '10') }) it('should not render a switch (no hasSwitch prop)', () => { @@ -116,9 +118,9 @@ describe('TopKItem', () => { it('should call onChange with integer value when slider changes', async () => { const user = userEvent.setup() render() - const slider = screen.getByRole('slider') + const slider = getSlider() - await user.click(slider) + slider.focus() await user.keyboard('{ArrowRight}') expect(defaultProps.onChange).toHaveBeenLastCalledWith('top_k', 3) diff --git a/web/app/components/base/param-item/index.tsx b/web/app/components/base/param-item/index.tsx index 63af4bca84..56999fc6ea 100644 --- a/web/app/components/base/param-item/index.tsx +++ b/web/app/components/base/param-item/index.tsx @@ -1,8 +1,8 @@ 'use client' import type { FC } from 'react' -import Slider from '@/app/components/base/slider' import Switch from '@/app/components/base/switch' import Tooltip from '@/app/components/base/tooltip' +import { Slider } from '@/app/components/base/ui/slider' import { NumberField, NumberFieldControls, @@ -78,7 +78,8 @@ const ParamItem: FC = ({ className, id, name, noTooltip, tip, step = 0.1, value={max < 5 ? value * 100 : value} min={min < 1 ? min * 100 : min} max={max < 5 ? max * 100 : max} - onChange={value => onChange(id, value / (max < 5 ? 100 : 1))} + onValueChange={value => onChange(id, value / (max < 5 ? 100 : 1))} + aria-label={name} />
diff --git a/web/app/components/base/slider/__tests__/index.spec.tsx b/web/app/components/base/slider/__tests__/index.spec.tsx deleted file mode 100644 index bb1f030689..0000000000 --- a/web/app/components/base/slider/__tests__/index.spec.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { act, render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { describe, expect, it, vi } from 'vitest' -import Slider from '../index' - -describe('Slider Component', () => { - it('should render with correct default ARIA limits and current value', () => { - render() - - 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 apply custom min, max, and step values', () => { - render() - - const slider = screen.getByRole('slider') - expect(slider).toHaveAttribute('aria-valuemin', '5') - expect(slider).toHaveAttribute('aria-valuemax', '20') - expect(slider).toHaveAttribute('aria-valuenow', '10') - }) - - it('should default to 0 if the value prop is NaN', () => { - render() - - const slider = screen.getByRole('slider') - expect(slider).toHaveAttribute('aria-valuenow', '0') - }) - - it('should call onChange when arrow keys are pressed', async () => { - const user = userEvent.setup() - const onChange = vi.fn() - - render() - - const slider = screen.getByRole('slider') - - await act(async () => { - slider.focus() - await user.keyboard('{ArrowRight}') - }) - - expect(onChange).toHaveBeenCalledTimes(1) - expect(onChange).toHaveBeenCalledWith(21, 0) - }) - - it('should not trigger onChange when disabled', async () => { - const user = userEvent.setup() - const onChange = vi.fn() - render() - - const slider = screen.getByRole('slider') - - expect(slider).toHaveAttribute('aria-disabled', 'true') - - await act(async () => { - slider.focus() - await user.keyboard('{ArrowRight}') - }) - - expect(onChange).not.toHaveBeenCalled() - }) - - it('should apply custom class names', () => { - render( - , - ) - - const sliderWrapper = screen.getByRole('slider').closest('.outer-test') - expect(sliderWrapper).toBeInTheDocument() - - const thumb = screen.getByRole('slider') - expect(thumb).toHaveClass('thumb-test') - }) -}) diff --git a/web/app/components/base/slider/index.stories.tsx b/web/app/components/base/slider/index.stories.tsx deleted file mode 100644 index bde937ffad..0000000000 --- a/web/app/components/base/slider/index.stories.tsx +++ /dev/null @@ -1,635 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/nextjs-vite' -import { useState } from 'react' -import Slider from '.' - -const meta = { - title: 'Base/Data Entry/Slider', - component: Slider, - parameters: { - layout: 'centered', - docs: { - description: { - component: 'Slider component for selecting a numeric value within a range. Built on react-slider with customizable min/max/step values.', - }, - }, - }, - tags: ['autodocs'], - argTypes: { - value: { - control: 'number', - description: 'Current slider value', - }, - min: { - control: 'number', - description: 'Minimum value (default: 0)', - }, - max: { - control: 'number', - description: 'Maximum value (default: 100)', - }, - step: { - control: 'number', - description: 'Step increment (default: 1)', - }, - disabled: { - control: 'boolean', - description: 'Disabled state', - }, - }, - args: { - onChange: (value) => { - console.log('Slider value:', value) - }, - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -// Interactive demo wrapper -const SliderDemo = (args: any) => { - const [value, setValue] = useState(args.value || 50) - - return ( -
- { - setValue(v) - console.log('Slider value:', v) - }} - /> -
- Value: - {' '} - {value} -
-
- ) -} - -// Default state -export const Default: Story = { - render: args => , - args: { - value: 50, - min: 0, - max: 100, - step: 1, - disabled: false, - }, -} - -// With custom range -export const CustomRange: Story = { - render: args => , - args: { - value: 25, - min: 0, - max: 50, - step: 1, - disabled: false, - }, -} - -// With step increment -export const WithStepIncrement: Story = { - render: args => , - args: { - value: 50, - min: 0, - max: 100, - step: 10, - disabled: false, - }, -} - -// Decimal values -export const DecimalValues: Story = { - render: args => , - args: { - value: 2.5, - min: 0, - max: 5, - step: 0.5, - disabled: false, - }, -} - -// Disabled state -export const Disabled: Story = { - render: args => , - args: { - value: 75, - min: 0, - max: 100, - step: 1, - disabled: true, - }, -} - -// Real-world example - Volume control -const VolumeControlDemo = () => { - const [volume, setVolume] = useState(70) - - const getVolumeIcon = (vol: number) => { - if (vol === 0) - return '🔇' - if (vol < 33) - return '🔈' - if (vol < 66) - return '🔉' - return '🔊' - } - - return ( -
-
-

Volume Control

- {getVolumeIcon(volume)} -
- -
- Mute - - {volume} - % - - Max -
-
- ) -} - -export const VolumeControl: Story = { - render: () => , - parameters: { controls: { disable: true } }, -} as unknown as Story - -// Real-world example - Brightness control -const BrightnessControlDemo = () => { - const [brightness, setBrightness] = useState(80) - - return ( -
-
-

Screen Brightness

- ☀️ -
- -
-
- Preview at - {' '} - {brightness} - % brightness -
-
-
- ) -} - -export const BrightnessControl: Story = { - render: () => , - parameters: { controls: { disable: true } }, -} as unknown as Story - -// Real-world example - Price range filter -const PriceRangeFilterDemo = () => { - const [maxPrice, setMaxPrice] = useState(500) - const minPrice = 0 - - const products = [ - { name: 'Product A', price: 150 }, - { name: 'Product B', price: 350 }, - { name: 'Product C', price: 600 }, - { name: 'Product D', price: 250 }, - { name: 'Product E', price: 450 }, - ] - - const filteredProducts = products.filter(p => p.price >= minPrice && p.price <= maxPrice) - - return ( -
-

Filter by Price

-
-
- Maximum Price - - $ - {maxPrice} - -
- -
-
-
- Showing - {' '} - {filteredProducts.length} - {' '} - of - {' '} - {products.length} - {' '} - products -
-
- {filteredProducts.map(product => ( -
- {product.name} - - $ - {product.price} - -
- ))} -
-
-
- ) -} - -export const PriceRangeFilter: Story = { - render: () => , - parameters: { controls: { disable: true } }, -} as unknown as Story - -// Real-world example - Temperature selector -const TemperatureSelectorDemo = () => { - const [temperature, setTemperature] = useState(22) - const fahrenheit = ((temperature * 9) / 5 + 32).toFixed(1) - - return ( -
-

Thermostat Control

-
- -
-
-
-
Celsius
-
- {temperature} - °C -
-
-
-
Fahrenheit
-
- {fahrenheit} - °F -
-
-
-
- {temperature < 18 && '🥶 Too cold'} - {temperature >= 18 && temperature <= 24 && '😊 Comfortable'} - {temperature > 24 && '🥵 Too warm'} -
-
- ) -} - -export const TemperatureSelector: Story = { - render: () => , - parameters: { controls: { disable: true } }, -} as unknown as Story - -// Real-world example - Progress/completion slider -const ProgressSliderDemo = () => { - const [progress, setProgress] = useState(65) - - return ( -
-

Project Completion

- -
-
- Progress - - {progress} - % - -
-
-
- = 25 ? '✅' : '⏳'}>Planning - 25% -
-
- = 50 ? '✅' : '⏳'}>Development - 50% -
-
- = 75 ? '✅' : '⏳'}>Testing - 75% -
-
- = 100 ? '✅' : '⏳'}>Deployment - 100% -
-
-
-
- ) -} - -export const ProgressSlider: Story = { - render: () => , - parameters: { controls: { disable: true } }, -} as unknown as Story - -// Real-world example - Zoom control -const ZoomControlDemo = () => { - const [zoom, setZoom] = useState(100) - - return ( -
-

Zoom Level

-
- -
- -
- -
-
- 50% - - {zoom} - % - - 200% -
-
-
Preview content
-
-
- ) -} - -export const ZoomControl: Story = { - render: () => , - parameters: { controls: { disable: true } }, -} as unknown as Story - -// Real-world example - AI model parameters -const AIModelParametersDemo = () => { - const [temperature, setTemperature] = useState(0.7) - const [maxTokens, setMaxTokens] = useState(2000) - const [topP, setTopP] = useState(0.9) - - return ( -
-

Model Configuration

-
-
-
- - {temperature} -
- -

- Controls randomness. Lower is more focused, higher is more creative. -

-
- -
-
- - {maxTokens} -
- -

- Maximum length of generated response. -

-
- -
-
- - {topP} -
- -

- Nucleus sampling threshold. -

-
-
-
-
- Temperature: - {' '} - {temperature} -
-
- Max Tokens: - {' '} - {maxTokens} -
-
- Top P: - {' '} - {topP} -
-
-
- ) -} - -export const AIModelParameters: Story = { - render: () => , - parameters: { controls: { disable: true } }, -} as unknown as Story - -// Real-world example - Image quality selector -const ImageQualitySelectorDemo = () => { - const [quality, setQuality] = useState(80) - - const getQualityLabel = (q: number) => { - if (q < 50) - return 'Low' - if (q < 70) - return 'Medium' - if (q < 90) - return 'High' - return 'Maximum' - } - - const estimatedSize = Math.round((quality / 100) * 5) - - return ( -
-

Image Export Quality

- -
-
-
Quality
-
{getQualityLabel(quality)}
-
- {quality} - % -
-
-
-
File Size
-
- ~ - {estimatedSize} - {' '} - MB -
-
Estimated
-
-
-
- ) -} - -export const ImageQualitySelector: Story = { - render: () => , - parameters: { controls: { disable: true } }, -} as unknown as Story - -// Multiple sliders -const MultipleSlidersDemo = () => { - const [red, setRed] = useState(128) - const [green, setGreen] = useState(128) - const [blue, setBlue] = useState(128) - - const rgbColor = `rgb(${red}, ${green}, ${blue})` - - return ( -
-

RGB Color Picker

-
-
-
- - {red} -
- -
-
-
- - {green} -
- -
-
-
- - {blue} -
- -
-
-
-
-
-
Color Value
-
{rgbColor}
-
- # - {red.toString(16).padStart(2, '0')} - {green.toString(16).padStart(2, '0')} - {blue.toString(16).padStart(2, '0')} -
-
-
-
- ) -} - -export const MultipleSliders: Story = { - render: () => , - parameters: { controls: { disable: true } }, -} as unknown as Story - -// Interactive playground -export const Playground: Story = { - render: args => , - args: { - value: 50, - min: 0, - max: 100, - step: 1, - disabled: false, - }, -} diff --git a/web/app/components/base/slider/index.tsx b/web/app/components/base/slider/index.tsx deleted file mode 100644 index 4e4656f590..0000000000 --- a/web/app/components/base/slider/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import ReactSlider from 'react-slider' -import { cn } from '@/utils/classnames' -import './style.css' - -type ISliderProps = { - className?: string - thumbClassName?: string - trackClassName?: string - value: number - max?: number - min?: number - step?: number - disabled?: boolean - onChange: (value: number) => void -} - -const Slider: React.FC = ({ - className, - thumbClassName, - trackClassName, - max, - min, - step, - value, - disabled, - onChange, -}) => { - return ( - - ) -} - -export default Slider diff --git a/web/app/components/base/slider/style.css b/web/app/components/base/slider/style.css deleted file mode 100644 index 5d87fb0897..0000000000 --- a/web/app/components/base/slider/style.css +++ /dev/null @@ -1,11 +0,0 @@ -.slider.disabled { - opacity: 0.6; -} - -.slider-track { - background-color: var(--color-components-slider-range); -} - -.slider-track-1 { - background-color: var(--color-components-slider-track); -} diff --git a/web/app/components/base/ui/slider/__tests__/index.spec.tsx b/web/app/components/base/ui/slider/__tests__/index.spec.tsx new file mode 100644 index 0000000000..f34de5010d --- /dev/null +++ b/web/app/components/base/ui/slider/__tests__/index.spec.tsx @@ -0,0 +1,73 @@ +import { act, render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { describe, expect, it, vi } from 'vitest' +import { Slider } from '../index' + +describe('Slider', () => { + const getSliderInput = () => screen.getByLabelText('Value') + + it('should render with correct default ARIA limits and current value', () => { + render() + + const slider = getSliderInput() + expect(slider).toHaveAttribute('min', '0') + expect(slider).toHaveAttribute('max', '100') + expect(slider).toHaveAttribute('aria-valuenow', '50') + }) + + it('should apply custom min, max, and step values', () => { + render() + + const slider = getSliderInput() + expect(slider).toHaveAttribute('min', '5') + expect(slider).toHaveAttribute('max', '20') + expect(slider).toHaveAttribute('aria-valuenow', '10') + }) + + it('should clamp non-finite values to min', () => { + render() + + expect(getSliderInput()).toHaveAttribute('aria-valuenow', '5') + }) + + it('should call onValueChange when arrow keys are pressed', async () => { + const user = userEvent.setup() + const onValueChange = vi.fn() + + render() + + const slider = getSliderInput() + + await act(async () => { + slider.focus() + await user.keyboard('{ArrowRight}') + }) + + expect(onValueChange).toHaveBeenCalledTimes(1) + expect(onValueChange).toHaveBeenLastCalledWith(21, expect.anything()) + }) + + it('should not trigger onValueChange when disabled', async () => { + const user = userEvent.setup() + const onValueChange = vi.fn() + render() + + const slider = getSliderInput() + + expect(slider).toBeDisabled() + + await act(async () => { + slider.focus() + await user.keyboard('{ArrowRight}') + }) + + expect(onValueChange).not.toHaveBeenCalled() + }) + + it('should apply custom class names on root', () => { + const { container } = render() + + const sliderWrapper = container.querySelector('.outer-test') + expect(sliderWrapper).toBeInTheDocument() + }) +}) diff --git a/web/app/components/base/ui/slider/index.stories.tsx b/web/app/components/base/ui/slider/index.stories.tsx new file mode 100644 index 0000000000..b61a6cb288 --- /dev/null +++ b/web/app/components/base/ui/slider/index.stories.tsx @@ -0,0 +1,92 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite' +import type * as React from 'react' +import { useState } from 'react' +import { Slider } from '.' + +const meta = { + title: 'Base UI/Data Entry/Slider', + component: Slider, + parameters: { + layout: 'centered', + docs: { + description: { + component: 'Single-value horizontal slider built on Base UI.', + }, + }, + }, + tags: ['autodocs'], + argTypes: { + value: { + control: 'number', + }, + min: { + control: 'number', + }, + max: { + control: 'number', + }, + step: { + control: 'number', + }, + disabled: { + control: 'boolean', + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +function SliderDemo({ + value: initialValue = 50, + defaultValue: _defaultValue, + ...args +}: React.ComponentProps) { + const [value, setValue] = useState(initialValue) + + return ( +
+ +
+ {value} +
+
+ ) +} + +export const Default: Story = { + render: args => , + args: { + value: 50, + min: 0, + max: 100, + step: 1, + }, +} + +export const Decimal: Story = { + render: args => , + args: { + value: 0.5, + min: 0, + max: 1, + step: 0.1, + }, +} + +export const Disabled: Story = { + render: args => , + args: { + value: 75, + min: 0, + max: 100, + step: 1, + disabled: true, + }, +} diff --git a/web/app/components/base/ui/slider/index.tsx b/web/app/components/base/ui/slider/index.tsx new file mode 100644 index 0000000000..8e1dc969bc --- /dev/null +++ b/web/app/components/base/ui/slider/index.tsx @@ -0,0 +1,100 @@ +'use client' + +import { Slider as BaseSlider } from '@base-ui/react/slider' +import * as React from 'react' +import { cn } from '@/utils/classnames' + +type SliderRootProps = BaseSlider.Root.Props +type SliderThumbProps = BaseSlider.Thumb.Props + +type SliderBaseProps = Pick< + SliderRootProps, + 'onValueChange' | 'min' | 'max' | 'step' | 'disabled' | 'name' +> & Pick & { + className?: string +} + +type ControlledSliderProps = SliderBaseProps & { + value: number + defaultValue?: never +} + +type UncontrolledSliderProps = SliderBaseProps & { + value?: never + defaultValue?: number +} + +export type SliderProps = ControlledSliderProps | UncontrolledSliderProps + +const sliderRootClassName = 'group/slider relative inline-flex w-full data-[disabled]:opacity-30' +const sliderControlClassName = cn( + 'relative flex h-5 w-full touch-none select-none items-center', + 'data-[disabled]:cursor-not-allowed', +) +const sliderTrackClassName = cn( + 'relative h-1 w-full overflow-hidden rounded-full', + 'bg-[var(--slider-track,var(--color-components-slider-track))]', +) +const sliderIndicatorClassName = cn( + 'h-full rounded-full', + 'bg-[var(--slider-range,var(--color-components-slider-range))]', +) +const sliderThumbClassName = cn( + 'block h-5 w-2 shrink-0 rounded-[3px] border-[0.5px]', + 'border-[var(--slider-knob-border,var(--color-components-slider-knob-border))]', + 'bg-[var(--slider-knob,var(--color-components-slider-knob))] shadow-sm', + 'transition-[background-color,border-color,box-shadow,opacity] motion-reduce:transition-none', + 'hover:bg-[var(--slider-knob-hover,var(--color-components-slider-knob-hover))]', + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-components-slider-knob-border-hover focus-visible:ring-offset-0', + 'active:shadow-md', + 'group-data-[disabled]/slider:bg-[var(--slider-knob-disabled,var(--color-components-slider-knob-disabled))]', + 'group-data-[disabled]/slider:border-[var(--slider-knob-border,var(--color-components-slider-knob-border))]', + 'group-data-[disabled]/slider:shadow-none', +) + +const getSafeValue = (value: number | undefined, min: number) => { + if (value === undefined) + return undefined + + return Number.isFinite(value) ? value : min +} + +export function Slider({ + value, + defaultValue, + onValueChange, + min = 0, + max = 100, + step = 1, + disabled = false, + name, + className, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledby, +}: SliderProps) { + return ( + + + + + + + + + ) +} diff --git a/web/app/components/datasets/settings/index-method/__tests__/index.spec.tsx b/web/app/components/datasets/settings/index-method/__tests__/index.spec.tsx index ae2c17d880..7441274155 100644 --- a/web/app/components/datasets/settings/index-method/__tests__/index.spec.tsx +++ b/web/app/components/datasets/settings/index-method/__tests__/index.spec.tsx @@ -14,6 +14,8 @@ describe('IndexMethod', () => { vi.clearAllMocks() }) + const getKeywordSlider = () => screen.getByLabelText('datasetSettings.form.numberOfKeywords') + describe('Rendering', () => { it('should render without crashing', () => { render() @@ -123,8 +125,7 @@ describe('IndexMethod', () => { describe('KeywordNumber', () => { it('should render KeywordNumber component inside Economy option', () => { render() - // KeywordNumber has a slider - expect(screen.getByRole('slider')).toBeInTheDocument() + expect(getKeywordSlider()).toBeInTheDocument() }) it('should pass keywordNumber to KeywordNumber component', () => { diff --git a/web/app/components/datasets/settings/index-method/__tests__/keyword-number.spec.tsx b/web/app/components/datasets/settings/index-method/__tests__/keyword-number.spec.tsx index e7ba8af6f1..cd0d56bbeb 100644 --- a/web/app/components/datasets/settings/index-method/__tests__/keyword-number.spec.tsx +++ b/web/app/components/datasets/settings/index-method/__tests__/keyword-number.spec.tsx @@ -11,6 +11,8 @@ describe('KeyWordNumber', () => { vi.clearAllMocks() }) + const getSlider = () => screen.getByLabelText('datasetSettings.form.numberOfKeywords') + describe('Rendering', () => { it('should render without crashing', () => { render() @@ -31,8 +33,7 @@ describe('KeyWordNumber', () => { it('should render slider', () => { render() - // Slider has a slider role - expect(screen.getByRole('slider')).toBeInTheDocument() + expect(getSlider()).toBeInTheDocument() }) it('should render input number field', () => { @@ -61,7 +62,7 @@ describe('KeyWordNumber', () => { it('should pass correct value to slider', () => { render() - const slider = screen.getByRole('slider') + const slider = getSlider() expect(slider).toHaveAttribute('aria-valuenow', '30') }) }) @@ -71,8 +72,7 @@ describe('KeyWordNumber', () => { const handleChange = vi.fn() render() - const slider = screen.getByRole('slider') - // Verify slider is rendered and interactive + const slider = getSlider() expect(slider).toBeInTheDocument() expect(slider).not.toBeDisabled() }) @@ -109,14 +109,14 @@ describe('KeyWordNumber', () => { describe('Slider Configuration', () => { it('should have max value of 50', () => { render() - const slider = screen.getByRole('slider') - expect(slider).toHaveAttribute('aria-valuemax', '50') + const slider = getSlider() + expect(slider).toHaveAttribute('max', '50') }) it('should have min value of 0', () => { render() - const slider = screen.getByRole('slider') - expect(slider).toHaveAttribute('aria-valuemin', '0') + const slider = getSlider() + expect(slider).toHaveAttribute('min', '0') }) }) @@ -162,7 +162,7 @@ describe('KeyWordNumber', () => { describe('Accessibility', () => { it('should have accessible slider', () => { render() - const slider = screen.getByRole('slider') + const slider = getSlider() expect(slider).toBeInTheDocument() }) diff --git a/web/app/components/datasets/settings/index-method/keyword-number.tsx b/web/app/components/datasets/settings/index-method/keyword-number.tsx index 95810d7d49..feb63c1d65 100644 --- a/web/app/components/datasets/settings/index-method/keyword-number.tsx +++ b/web/app/components/datasets/settings/index-method/keyword-number.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import Slider from '@/app/components/base/slider' import Tooltip from '@/app/components/base/tooltip' import { NumberField, @@ -11,6 +10,7 @@ import { NumberFieldIncrement, NumberFieldInput, } from '@/app/components/base/ui/number-field' +import { Slider } from '@/app/components/base/ui/slider' const MIN_KEYWORD_NUMBER = 0 const MAX_KEYWORD_NUMBER = 50 @@ -47,7 +47,8 @@ const KeyWordNumber = ({ value={keywordNumber} min={MIN_KEYWORD_NUMBER} max={MAX_KEYWORD_NUMBER} - onChange={onKeywordNumberChange} + onValueChange={onKeywordNumberChange} + aria-label={t('form.numberOfKeywords', { ns: 'datasetSettings' })} /> ({ useLanguage: () => 'en_US', })) -vi.mock('@/app/components/base/slider', () => ({ - default: ({ onChange }: { onChange: (v: number) => void }) => ( - +vi.mock('@/app/components/base/ui/slider', () => ({ + Slider: ({ onValueChange }: { onValueChange: (v: number) => void }) => ( + ), })) diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item.tsx index 01e3f45371..162e39d162 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item.tsx @@ -7,10 +7,10 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import PromptEditor from '@/app/components/base/prompt-editor' import Radio from '@/app/components/base/radio' -import Slider from '@/app/components/base/slider' import Switch from '@/app/components/base/switch' import TagInput from '@/app/components/base/tag-input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/app/components/base/ui/select' +import { Slider } from '@/app/components/base/ui/slider' import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip' import { BlockEnum } from '@/app/components/workflow/types' import { cn } from '@/utils/classnames' @@ -78,6 +78,7 @@ function ParameterItem({ } const renderValue = value ?? localValue ?? getDefaultValue() + const sliderLabel = parameterRule.label[language] || parameterRule.label.en_US const handleInputChange = (newValue: ParameterValue) => { setLocalValue(newValue) @@ -170,7 +171,8 @@ function ParameterItem({ min={parameterRule.min} max={parameterRule.max} step={step} - onChange={handleSlideChange} + onValueChange={handleSlideChange} + aria-label={sliderLabel} /> )} )} - {parameterRule.label[language] || parameterRule.label.en_US} + {sliderLabel}
{ parameterRule.help && ( diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy.spec.tsx index c3738ca260..fb63a30030 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy.spec.tsx @@ -145,7 +145,7 @@ describe('AgentStrategy', () => { />, ) - expect(screen.getByRole('slider')).toBeInTheDocument() + expect(screen.getByLabelText('Count')).toBeInTheDocument() expect(screen.getByRole('textbox')).toBeInTheDocument() }) diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx index 70c480892b..b70bcd0173 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx @@ -9,7 +9,6 @@ import { memo } from 'react' import { useTranslation } from 'react-i18next' import { Agent } from '@/app/components/base/icons/src/vender/workflow' import ListEmpty from '@/app/components/base/list-empty' -import Slider from '@/app/components/base/slider' import { NumberField, NumberFieldControls, @@ -18,6 +17,7 @@ import { NumberFieldIncrement, NumberFieldInput, } from '@/app/components/base/ui/number-field' +import { Slider } from '@/app/components/base/ui/slider' import { FormTypeEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' @@ -147,10 +147,11 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
= ({ onChange, }) => { const handleBlur = useCallback(() => { - if (value === undefined || value === null) { + if (value === undefined || value === null || Number.isNaN(value)) { onChange(defaultValue) return } @@ -57,8 +57,9 @@ const InputNumberWithSlider: FC = ({ min={min} max={max} step={1} - onChange={onChange} + onValueChange={onChange} disabled={readonly} + aria-label="Number input slider" />
) diff --git a/web/app/components/workflow/nodes/_base/components/memory-config.tsx b/web/app/components/workflow/nodes/_base/components/memory-config.tsx index ac82162915..9ee522e54f 100644 --- a/web/app/components/workflow/nodes/_base/components/memory-config.tsx +++ b/web/app/components/workflow/nodes/_base/components/memory-config.tsx @@ -6,8 +6,8 @@ import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' -import Slider from '@/app/components/base/slider' import Switch from '@/app/components/base/switch' +import { Slider } from '@/app/components/base/ui/slider' import Field from '@/app/components/workflow/nodes/_base/components/field' import { cn } from '@/utils/classnames' import { MemoryRole } from '../../../types' @@ -154,7 +154,7 @@ const MemoryConfig: FC = ({ size="md" disabled={readonly} /> -
{t(`${i18nPrefix}.windowSize`, { ns: 'workflow' })}
+
{t(`${i18nPrefix}.windowSize`, { ns: 'workflow' })}
= ({ min={WINDOW_SIZE_MIN} max={WINDOW_SIZE_MAX} step={1} - onChange={handleWindowSizeChange} + onValueChange={handleWindowSizeChange} disabled={readonly || !payload.window?.enabled} + aria-label={t(`${i18nPrefix}.windowSize`, { ns: 'workflow' })} /> > = ({ title={t(`${i18nPrefix}.input`, { ns: 'workflow' })} required operations={( -
Array
+
Array
)} > > = ({ title={t(`${i18nPrefix}.output`, { ns: 'workflow' })} required operations={( -
Array
+
Array
)} > > = ({ { changeParallelNums(Number(e.target.value)) }} />
diff --git a/web/app/components/workflow/nodes/knowledge-base/components/__tests__/index-method.spec.tsx b/web/app/components/workflow/nodes/knowledge-base/components/__tests__/index-method.spec.tsx index a11f93e0b0..583c1c8966 100644 --- a/web/app/components/workflow/nodes/knowledge-base/components/__tests__/index-method.spec.tsx +++ b/web/app/components/workflow/nodes/knowledge-base/components/__tests__/index-method.spec.tsx @@ -39,7 +39,7 @@ describe('IndexMethod', () => { fireEvent.change(container.querySelector('input') as HTMLInputElement, { target: { value: '7' } }) - expect(onKeywordNumberChange).toHaveBeenCalledWith(7) + expect(onKeywordNumberChange).toHaveBeenCalledWith(7, expect.anything()) }) it('should disable keyword controls when readonly is enabled', () => { diff --git a/web/app/components/workflow/nodes/knowledge-base/components/index-method.tsx b/web/app/components/workflow/nodes/knowledge-base/components/index-method.tsx index b692e35ed2..a223c5ab24 100644 --- a/web/app/components/workflow/nodes/knowledge-base/components/index-method.tsx +++ b/web/app/components/workflow/nodes/knowledge-base/components/index-method.tsx @@ -9,8 +9,8 @@ import { HighQuality, } from '@/app/components/base/icons/src/vender/knowledge' import Input from '@/app/components/base/input' -import Slider from '@/app/components/base/slider' import Tooltip from '@/app/components/base/tooltip' +import { Slider } from '@/app/components/base/ui/slider' import { Field } from '@/app/components/workflow/nodes/_base/components/layout' import { cn } from '@/utils/classnames' import { @@ -94,7 +94,7 @@ const IndexMethod = ({ >
-
+
{t('form.numberOfKeywords', { ns: 'datasetSettings' })}
{ const onChange = vi.fn() render() - const slider = screen.getByRole('slider') + const slider = screen.getByLabelText('workflow.nodes.triggerSchedule.onMinute') slider.focus() await user.keyboard('{ArrowRight}') - expect(onChange).toHaveBeenCalledWith(16, 0) + expect(onChange).toHaveBeenCalledWith(16, expect.objectContaining({ activeThumbIndex: 0 })) }) it('should keep at least one weekday selected', async () => { diff --git a/web/app/components/workflow/nodes/trigger-schedule/components/on-minute-selector.tsx b/web/app/components/workflow/nodes/trigger-schedule/components/on-minute-selector.tsx index 188a630151..e0a46d1c68 100644 --- a/web/app/components/workflow/nodes/trigger-schedule/components/on-minute-selector.tsx +++ b/web/app/components/workflow/nodes/trigger-schedule/components/on-minute-selector.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import Slider from '@/app/components/base/slider' +import { Slider } from '@/app/components/base/ui/slider' type OnMinuteSelectorProps = { value?: number @@ -27,7 +27,8 @@ const OnMinuteSelector = ({ value = 0, onChange }: OnMinuteSelectorProps) => { min={0} max={59} step={1} - onChange={onChange} + onValueChange={onChange} + aria-label={t('nodes.triggerSchedule.onMinute', { ns: 'workflow' })} />
diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index 38835cf3a5..d1c4312e35 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -965,11 +965,6 @@ "count": 1 } }, - "app/components/app/configuration/dataset-config/params-config/weighted-score.tsx": { - "tailwindcss/enforce-consistent-class-order": { - "count": 2 - } - }, "app/components/app/configuration/dataset-config/select-dataset/index.spec.tsx": { "ts/no-explicit-any": { "count": 2 @@ -2043,11 +2038,6 @@ "count": 3 } }, - "app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider/index.tsx": { - "unicorn/prefer-number-properties": { - "count": 1 - } - }, "app/components/base/features/new-feature-panel/annotation-reply/type.ts": { "erasable-syntax-only/enums": { "count": 1 @@ -2878,19 +2868,6 @@ "count": 3 } }, - "app/components/base/slider/index.stories.tsx": { - "no-console": { - "count": 2 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, - "app/components/base/slider/index.tsx": { - "tailwindcss/no-unnecessary-whitespace": { - "count": 1 - } - }, "app/components/base/sort/index.tsx": { "tailwindcss/enforce-consistent-class-order": { "count": 3 @@ -7041,9 +7018,6 @@ } }, "app/components/workflow/nodes/_base/components/memory-config.tsx": { - "tailwindcss/enforce-consistent-class-order": { - "count": 1 - }, "unicorn/prefer-number-properties": { "count": 1 } @@ -7872,12 +7846,6 @@ "app/components/workflow/nodes/iteration/panel.tsx": { "no-restricted-imports": { "count": 1 - }, - "tailwindcss/enforce-consistent-class-order": { - "count": 2 - }, - "tailwindcss/no-unnecessary-whitespace": { - "count": 1 } }, "app/components/workflow/nodes/iteration/use-config.ts": { @@ -7906,9 +7874,6 @@ "app/components/workflow/nodes/knowledge-base/components/index-method.tsx": { "no-restricted-imports": { "count": 1 - }, - "tailwindcss/enforce-consistent-class-order": { - "count": 1 } }, "app/components/workflow/nodes/knowledge-base/components/option-card.tsx": { diff --git a/web/package.json b/web/package.json index 7ce1d2a530..1b573d84eb 100644 --- a/web/package.json +++ b/web/package.json @@ -140,7 +140,6 @@ "react-multi-email": "1.0.25", "react-papaparse": "4.4.0", "react-pdf-highlighter": "8.0.0-rc.0", - "react-slider": "2.0.6", "react-sortablejs": "6.1.4", "react-syntax-highlighter": "15.6.6", "react-textarea-autosize": "8.5.9", @@ -202,7 +201,6 @@ "@types/qs": "6.15.0", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", - "@types/react-slider": "1.3.6", "@types/react-syntax-highlighter": "15.5.13", "@types/react-window": "1.8.8", "@types/sortablejs": "1.15.9", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 933870a79b..9945c6f893 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -307,9 +307,6 @@ importers: react-pdf-highlighter: specifier: 8.0.0-rc.0 version: 8.0.0-rc.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react-slider: - specifier: 2.0.6 - version: 2.0.6(react@19.2.4) react-sortablejs: specifier: 6.1.4 version: 6.1.4(@types/sortablejs@1.15.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sortablejs@1.15.7) @@ -488,9 +485,6 @@ importers: '@types/react-dom': specifier: 19.2.3 version: 19.2.3(@types/react@19.2.14) - '@types/react-slider': - specifier: 1.3.6 - version: 1.3.6 '@types/react-syntax-highlighter': specifier: 15.5.13 version: 15.5.13 @@ -3537,9 +3531,6 @@ packages: peerDependencies: '@types/react': ^19.2.0 - '@types/react-slider@1.3.6': - resolution: {integrity: sha512-RS8XN5O159YQ6tu3tGZIQz1/9StMLTg/FCIPxwqh2gwVixJnlfIodtVx+fpXVMZHe7A58lAX1Q4XTgAGOQaCQg==} - '@types/react-syntax-highlighter@15.5.13': resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} @@ -6885,11 +6876,6 @@ packages: react-dom: ^19.2.4 webpack: ^5.59.0 - react-slider@2.0.6: - resolution: {integrity: sha512-gJxG1HwmuMTJ+oWIRCmVWvgwotNCbByTwRkFZC6U4MBsHqJBmxwbYRJUmxy4Tke1ef8r9jfXjgkmY/uHOCEvbA==} - peerDependencies: - react: ^16 || ^17 || ^18 - react-sortablejs@6.1.4: resolution: {integrity: sha512-fc7cBosfhnbh53Mbm6a45W+F735jwZ1UFIYSrIqcO/gRIFoDyZeMtgKlpV4DdyQfbCzdh5LoALLTDRxhMpTyXQ==} peerDependencies: @@ -10939,10 +10925,6 @@ snapshots: dependencies: '@types/react': 19.2.14 - '@types/react-slider@1.3.6': - dependencies: - '@types/react': 19.2.14 - '@types/react-syntax-highlighter@15.5.13': dependencies: '@types/react': 19.2.14 @@ -14937,11 +14919,6 @@ snapshots: webpack: 5.105.4(esbuild@0.27.2)(uglify-js@3.19.3) webpack-sources: 3.3.4 - react-slider@2.0.6(react@19.2.4): - dependencies: - prop-types: 15.8.1 - react: 19.2.4 - react-sortablejs@6.1.4(@types/sortablejs@1.15.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sortablejs@1.15.7): dependencies: '@types/sortablejs': 1.15.9