From 6017c2669a82c184308dfcc44c1f864394dc438f Mon Sep 17 00:00:00 2001 From: Valerii Sidorenko Date: Wed, 19 Jun 2024 20:57:54 +0200 Subject: [PATCH 1/3] feat(Slider)!: support form --- .../Slider/BaseSlider/BaseSlider.tsx | 5 +- src/components/Slider/Slider.tsx | 139 +++++++++--------- .../Slider/__tetsts__/Slider.form.test.tsx | 123 ++++++++++++++++ src/components/Slider/types.ts | 18 ++- 4 files changed, 205 insertions(+), 80 deletions(-) create mode 100644 src/components/Slider/__tetsts__/Slider.form.test.tsx diff --git a/src/components/Slider/BaseSlider/BaseSlider.tsx b/src/components/Slider/BaseSlider/BaseSlider.tsx index ef18676dd2..02b3988020 100644 --- a/src/components/Slider/BaseSlider/BaseSlider.tsx +++ b/src/components/Slider/BaseSlider/BaseSlider.tsx @@ -13,7 +13,7 @@ import './BaseSlider.scss'; const b = block('base-slider'); type BaseSliderProps = {stateModifiers: StateModifiers} & Omit< - SliderProps, + SliderProps, 'classNames' | 'prefixCls' | 'className' | 'pushable' | 'keyboard' >; @@ -22,6 +22,7 @@ export const BaseSlider = React.forwardRef(function ref: React.ForwardedRef, ) { return ( + // @ts-expect-error Slider value type is (number | number[]) but we use (number | [number, number]) (function pushable={false} dots={false} keyboard={true} - > + /> ); }); diff --git a/src/components/Slider/Slider.tsx b/src/components/Slider/Slider.tsx index 4251043e73..bcc73fde64 100644 --- a/src/components/Slider/Slider.tsx +++ b/src/components/Slider/Slider.tsx @@ -2,20 +2,15 @@ import React from 'react'; -import debounce from 'lodash/debounce'; - +import {useControlledState} from '../../hooks'; +import {useFormResetHandler} from '../../hooks/private'; import {useDirection} from '../theme'; import {block} from '../utils/cn'; +import {filterDOMProps} from '../utils/filterDOMProps'; import {BaseSlider} from './BaseSlider/BaseSlider'; import {HandleWithTooltip} from './HandleWithTooltip/HandleWithTooltip'; -import type { - HandleWithTooltipProps, - RcSliderValueType, - SliderProps, - SliderValue, - StateModifiers, -} from './types'; +import type {HandleWithTooltipProps, SliderProps, StateModifiers} from './types'; import {prepareSliderInnerState} from './utils'; import './Slider.scss'; @@ -40,7 +35,6 @@ export const Slider = React.forwardRef(function Slider( errorMessage, validationState, disabled = false, - debounceDelay = 0, onBlur, onUpdate, onUpdateComplete, @@ -48,38 +42,18 @@ export const Slider = React.forwardRef(function Slider( autoFocus = false, tabIndex, className, + style, qa, apiRef, 'aria-label': ariaLabelForHandle, 'aria-labelledby': ariaLabelledByForHandle, + name, + form, + ...otherProps }: SliderProps, ref: React.ForwardedRef, ) { const direction = useDirection(); - // eslint-disable-next-line react-hooks/exhaustive-deps - const handleUpdate = React.useCallback( - debounce( - (changedValue: RcSliderValueType) => onUpdate?.(changedValue as SliderValue), - debounceDelay, - ), - [onUpdate, debounceDelay], - ); - - // eslint-disable-next-line react-hooks/exhaustive-deps - const handleUpdateComplete = React.useCallback( - debounce( - (changedValue: RcSliderValueType) => onUpdateComplete?.(changedValue as SliderValue), - debounceDelay, - ), - [onUpdateComplete, debounceDelay], - ); - - React.useEffect(() => { - return () => { - handleUpdate.cancel(); - handleUpdateComplete.cancel(); - }; - }, [handleUpdate, handleUpdateComplete]); const innerState = prepareSliderInnerState({ availableValues, @@ -95,47 +69,71 @@ export const Slider = React.forwardRef(function Slider( tooltipDisplay, tooltipFormat, }); - - const stateModifiers: StateModifiers = React.useMemo( - () => ({ - size, - error: validationState === 'invalid' && !disabled, - disabled, - 'tooltip-display': innerState.tooltipDisplay, - rtl: direction === 'rtl', - 'no-marks': Array.isArray(marks) ? marks.length === 0 : marks === 0, - }), - [direction, disabled, innerState.tooltipDisplay, size, validationState, marks], + const [innerValue, setValue] = useControlledState( + innerState.value, + innerState.defaultValue ?? min, + onUpdate, ); - const handleRender = React.useMemo( - () => - innerState.tooltipDisplay === 'off' - ? undefined - : ( - originHandle: HandleWithTooltipProps['originHandle'], - originHandleProps: HandleWithTooltipProps['originHandleProps'], - ) => ( - - ), - [innerState.tooltipDisplay, innerState.tooltipFormat, stateModifiers], + const handleReset = React.useCallback( + (v: number | [number, number]) => { + setValue(v); + onUpdateComplete?.(v); + }, + [onUpdateComplete, setValue], ); + const inputRef = useFormResetHandler({initialValue: innerValue, onReset: handleReset}); + + const stateModifiers: StateModifiers = { + size, + error: validationState === 'invalid' && !disabled, + disabled, + 'tooltip-display': innerState.tooltipDisplay, + rtl: direction === 'rtl', + 'no-marks': Array.isArray(marks) ? marks.length === 0 : marks === 0, + }; + + const handleRender = ( + originHandle: HandleWithTooltipProps['originHandle'], + originHandleProps: HandleWithTooltipProps['originHandleProps'], + ) => { + const handle = + innerState.tooltipDisplay === 'off' ? ( + originHandle + ) : ( + + ); + + return ( + + {handle} + + + ); + }; + return ( -
+
{/* use this block to reserve place for tooltip */}
+ /> {stateModifiers.error && errorMessage && (
{errorMessage}
)}
); -}); +}) as ( + p: SliderProps & {ref?: React.Ref}, +) => React.ReactElement; diff --git a/src/components/Slider/__tetsts__/Slider.form.test.tsx b/src/components/Slider/__tetsts__/Slider.form.test.tsx new file mode 100644 index 0000000000..7130612826 --- /dev/null +++ b/src/components/Slider/__tetsts__/Slider.form.test.tsx @@ -0,0 +1,123 @@ +import React from 'react'; + +import userEvent from '@testing-library/user-event'; + +import {fireEvent, render, screen} from '../../../../test-utils/utils'; +import {Slider} from '../Slider'; + +describe('Slider form', () => { + it('should submit empty option by default', async () => { + let value; + const onSubmit = jest.fn((e) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + value = [...formData.entries()]; + }); + render( +
+ + + , + ); + await userEvent.click(screen.getByTestId('submit')); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(value).toEqual([['slider', '0']]); + }); + + it('should submit default option', async () => { + let value; + const onSubmit = jest.fn((e) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + value = [...formData.entries()]; + }); + render( +
+ + + , + ); + await userEvent.click(screen.getByTestId('submit')); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(value).toEqual([['slider', '5']]); + }); + + it('should submit multiple option', async () => { + let value; + const onSubmit = jest.fn((e) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + value = [...formData.entries()]; + }); + render( +
+ + + , + ); + await userEvent.click(screen.getByTestId('submit')); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(value).toEqual([ + ['slider', '5'], + ['slider', '10'], + ]); + }); + + it('supports form reset', async () => { + function Test() { + const [value, setValue] = React.useState(5); + return ( +
+ + + + ); + } + + render(); + const form = screen.getByTestId('form'); + expect(form).toHaveFormValues({slider: '5'}); + + const sliderHandle = screen.getAllByRole('slider')[0]; + fireEvent.keyDown(sliderHandle, {key: 'ArrowRight', keyCode: 39, code: 'ArrowRight'}); + fireEvent.keyDown(sliderHandle, {key: 'ArrowRight', keyCode: 39, code: 'ArrowRight'}); + + expect(form).toHaveFormValues({slider: '7'}); + + const button = screen.getByTestId('reset'); + await userEvent.click(button); + expect(form).toHaveFormValues({slider: '5'}); + }); + + it('supports form reset range value', async () => { + function Test() { + const [value, setValue] = React.useState<[number, number]>([5, 10]); + return ( +
+ + + + ); + } + + render(); + const form = screen.getByTestId('form'); + expect(form).toHaveFormValues({slider: ['5', '10']}); + + const sliderHandle = screen.getAllByRole('slider')[1]; + fireEvent.keyDown(sliderHandle, {key: 'ArrowRight', keyCode: 39, code: 'ArrowRight'}); + fireEvent.keyDown(sliderHandle, {key: 'ArrowRight', keyCode: 39, code: 'ArrowRight'}); + + expect(form).toHaveFormValues({slider: ['5', '12']}); + + const button = screen.getByTestId('reset'); + await userEvent.click(button); + expect(form).toHaveFormValues({slider: ['5', '10']}); + }); +}); diff --git a/src/components/Slider/types.ts b/src/components/Slider/types.ts index 6d4279978f..a553bab476 100644 --- a/src/components/Slider/types.ts +++ b/src/components/Slider/types.ts @@ -53,10 +53,6 @@ export type SliderProps = { /** Describes the validation state */ validationState?: 'invalid'; - /** Specifies the delay (in milliseconds) before the processing function is called - * @deprecated use external debouncing. - */ - debounceDelay?: number; /** Fires when the control gets focus. Provides focus event as a callback's argument */ onFocus?: (e: React.FocusEvent) => void; /** Fires when the control lost focus. Provides focus event as a callback's argument */ @@ -72,15 +68,21 @@ export type SliderProps = { tabIndex?: ValueType; /** Ref to Slider's component props of focus and blur */ apiRef?: React.RefObject; - 'aria-label'?: string; - 'aria-labelledby'?: string; -} & Pick & + 'aria-label'?: string | [string, string]; + 'aria-labelledby'?: string | [string, string]; + id?: string; + /** Name attribute of the hidden input element. */ + name?: string; + form?: string; +} & DOMProps & QAProps; export type SliderInnerState = { max: number; min: number; -} & Pick & + value?: number | [number, number]; + defaultValue?: number | [number, number]; +} & Pick & Pick; export type StateModifiers = { From c23285dae8567fe6d809232cdfd06077aac37321 Mon Sep 17 00:00:00 2001 From: Valerii Sidorenko Date: Thu, 20 Jun 2024 12:12:33 +0200 Subject: [PATCH 2/3] fix(BaseSlider): replace ts-expect error with a type assertion --- src/components/Slider/BaseSlider/BaseSlider.tsx | 11 +++++------ src/components/Slider/types.ts | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/Slider/BaseSlider/BaseSlider.tsx b/src/components/Slider/BaseSlider/BaseSlider.tsx index 02b3988020..35a3f1e477 100644 --- a/src/components/Slider/BaseSlider/BaseSlider.tsx +++ b/src/components/Slider/BaseSlider/BaseSlider.tsx @@ -12,17 +12,16 @@ import './BaseSlider.scss'; const b = block('base-slider'); -type BaseSliderProps = {stateModifiers: StateModifiers} & Omit< - SliderProps, +type BaseSliderProps = {stateModifiers: StateModifiers} & Omit< + SliderProps, 'classNames' | 'prefixCls' | 'className' | 'pushable' | 'keyboard' >; export const BaseSlider = React.forwardRef(function BaseSlider( - {stateModifiers, ...otherProps}: BaseSliderProps, - ref: React.ForwardedRef, + {stateModifiers, ...otherProps}, + ref, ) { return ( - // @ts-expect-error Slider value type is (number | number[]) but we use (number | [number, number]) (function keyboard={true} /> ); -}); +}) as (p: BaseSliderProps & {ref?: React.Ref}) => React.ReactElement; diff --git a/src/components/Slider/types.ts b/src/components/Slider/types.ts index a553bab476..30847b9536 100644 --- a/src/components/Slider/types.ts +++ b/src/components/Slider/types.ts @@ -67,7 +67,7 @@ export type SliderProps = { /** The control's tabIndex attribute */ tabIndex?: ValueType; /** Ref to Slider's component props of focus and blur */ - apiRef?: React.RefObject; + apiRef?: React.Ref; 'aria-label'?: string | [string, string]; 'aria-labelledby'?: string | [string, string]; id?: string; From ee044da23957e7b61c78617b95f21bbb9781f0ee Mon Sep 17 00:00:00 2001 From: Valerii Sidorenko Date: Wed, 4 Dec 2024 10:26:26 +0100 Subject: [PATCH 3/3] fix: remove debounceDelay from readme --- src/components/Slider/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Slider/README.md b/src/components/Slider/README.md index 81dcf851f0..273ac6c6b8 100644 --- a/src/components/Slider/README.md +++ b/src/components/Slider/README.md @@ -309,7 +309,6 @@ You are able to change display format of tooltip value by using `tooltipFormat` | autoFocus | The control's `autofocus` attribute | `boolean` | | | [availableValues](#define-available-values) | (deprecated) use `marks` and `step` === null instead. Specifies the array of available values for the slider | `number[]` | | | className | The control's wrapper class name | `string` | | -| debounceDelay | (deprecated) use external debouncing. Specifies the delay (in milliseconds) before the processing function is called | `number` | `0` | | [defaultValue](#slider-variants) | The control's default value, used when the component is not controlled | `number` `[number, number]` | `0` | | [disabled](#disabled) | Indicates that the user cannot interact with the control | `boolean` | `false` | | [errorMessage](#error) | Text of an error to show | `string` | |