From 8912e543c1f8b45bd7840cc94b148e12233e5c8d Mon Sep 17 00:00:00 2001 From: Ferit Cem Kahraman Date: Wed, 21 Oct 2026 08:10:52 +0300 Subject: [PATCH] feat: support aria-invalid on handles --- README.md | 2 ++ README.zh-CN.md | 2 ++ src/Handles/Handle.tsx | 3 +++ src/Slider.tsx | 9 +++++++++ src/context.ts | 4 +++- src/index.tsx | 1 + src/interface.ts | 2 ++ tests/Range.test.tsx | 26 ++++++++++++++++++++++++++ tests/Slider.test.js | 16 ++++++++++++++++ 9 files changed, 64 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d313ba99..867d84745 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,8 @@ Then open `http://localhost:8000`. | `ariaLabelledByForHandle` | string \| string[] | - | `aria-labelledby` for handle elements. | | `ariaDescribedByForHandle` | string \| string[] | - | `aria-describedby` for handle elements. | | `ariaRequired` | boolean | - | `aria-required` for handle elements. | +| `ariaInvalid` | boolean | - | `aria-invalid` for handle elements. | +| `ariaInvalidForHandle` | `AriaInvalid \| AriaInvalid[]` | - | `aria-invalid` for handle elements. | | `ariaValueTextFormatterForHandle` | `((value: number) => string) \| ((value: number) => string)[]` | - | Formatter for `aria-valuetext`. | | `autoFocus` | boolean | - | Focus the slider on mount. | | `className` | string | - | Additional class name. | diff --git a/README.zh-CN.md b/README.zh-CN.md index 9c2b7b6a2..590b2268c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -76,6 +76,8 @@ npm start | `ariaLabelledByForHandle` | string \| string[] | - | `aria-labelledby` for handle elements. | | `ariaDescribedByForHandle` | string \| string[] | - | 句柄元素的 `aria-describedby`。 | | `ariaRequired` | boolean | - | 句柄元素的 `aria-required`。 | +| `ariaInvalid` | boolean | - | 句柄元素的 `aria-invalid`。 | +| `ariaInvalidForHandle` | `AriaInvalid \| AriaInvalid[]` | - | 句柄元素的 `aria-invalid`。 | | `ariaValueTextFormatterForHandle` | `((value: number) => string) \| ((value: number) => string)[]` | - | `aria-valuetext` 格式化函数。 | | `autoFocus` | boolean | - | 挂载后自动聚焦滑块。 | | `className` | string | - | 附加 className。 | diff --git a/src/Handles/Handle.tsx b/src/Handles/Handle.tsx index d246d01a9..02ba153aa 100644 --- a/src/Handles/Handle.tsx +++ b/src/Handles/Handle.tsx @@ -62,6 +62,8 @@ const Handle = React.forwardRef((props, ref) => { ariaLabelledByForHandle, ariaDescribedByForHandle, ariaRequired, + ariaInvalid, + ariaInvalidForHandle, ariaValueTextFormatterForHandle, styles, classNames, @@ -176,6 +178,7 @@ const Handle = React.forwardRef((props, ref) => { 'aria-labelledby': getIndex(ariaLabelledByForHandle, valueIndex), 'aria-describedby': getIndex(ariaDescribedByForHandle, valueIndex), 'aria-required': getIndex(ariaRequired, valueIndex), + 'aria-invalid': getIndex(ariaInvalidForHandle ?? ariaInvalid, valueIndex), 'aria-valuetext': getIndex(ariaValueTextFormatterForHandle, valueIndex)?.(value), 'aria-orientation': direction === 'ltr' || direction === 'rtl' ? 'horizontal' : 'vertical', onMouseDown: onInternalStartMove, diff --git a/src/Slider.tsx b/src/Slider.tsx index 713b0152d..7a5de7c0e 100644 --- a/src/Slider.tsx +++ b/src/Slider.tsx @@ -17,6 +17,7 @@ import useDrag from './hooks/useDrag'; import useOffset, { getClosestEnabledHandleIndex } from './hooks/useOffset'; import useRange from './hooks/useRange'; import type { + AriaInvalid, AriaValueFormat, Direction, OnStartMove, @@ -115,6 +116,8 @@ export interface SliderProps { ariaLabelledByForHandle?: string | string[]; ariaDescribedByForHandle?: string | string[]; ariaRequired?: boolean; + ariaInvalid?: boolean; + ariaInvalidForHandle?: AriaInvalid | AriaInvalid[]; ariaValueTextFormatterForHandle?: AriaValueFormat | AriaValueFormat[]; } @@ -184,6 +187,8 @@ const Slider = React.forwardRef>((prop ariaLabelledByForHandle, ariaDescribedByForHandle, ariaRequired, + ariaInvalid, + ariaInvalidForHandle, ariaValueTextFormatterForHandle, } = props; @@ -594,6 +599,8 @@ const Slider = React.forwardRef>((prop ariaLabelledByForHandle, ariaDescribedByForHandle, ariaRequired, + ariaInvalid, + ariaInvalidForHandle, ariaValueTextFormatterForHandle, styles: styles || {}, classNames: classNames || {}, @@ -615,6 +622,8 @@ const Slider = React.forwardRef>((prop ariaLabelledByForHandle, ariaDescribedByForHandle, ariaRequired, + ariaInvalid, + ariaInvalidForHandle, ariaValueTextFormatterForHandle, styles, classNames, diff --git a/src/context.ts b/src/context.ts index 1356ff575..7c91c4d74 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import type { AriaValueFormat, Direction, SliderClassNames, SliderStyles } from './interface'; +import type { AriaInvalid, AriaValueFormat, Direction, SliderClassNames, SliderStyles } from './interface'; export interface SliderContextProps { min: number; @@ -17,6 +17,8 @@ export interface SliderContextProps { ariaLabelledByForHandle?: string | string[]; ariaDescribedByForHandle?: string | string[]; ariaRequired?: boolean; + ariaInvalid?: boolean; + ariaInvalidForHandle?: AriaInvalid | AriaInvalid[]; ariaValueTextFormatterForHandle?: AriaValueFormat | AriaValueFormat[]; classNames: SliderClassNames; styles: SliderStyles; diff --git a/src/index.tsx b/src/index.tsx index 4d7a41ca4..ff2b69ee9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,7 @@ import Slider, { type SliderProps, type SliderRef } from './Slider'; export { UnstableContext } from './context'; +export type { AriaInvalid } from './interface'; export type { SliderProps, SliderRef }; export default Slider; diff --git a/src/interface.ts b/src/interface.ts index ffaab9dae..5a999db06 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -10,6 +10,8 @@ export type OnStartMove = ( export type AriaValueFormat = (value: number) => string; +export type AriaInvalid = boolean | 'grammar' | 'spelling'; + export type SemanticName = 'tracks' | 'track' | 'rail' | 'handle'; export type SliderClassNames = Partial>; diff --git a/tests/Range.test.tsx b/tests/Range.test.tsx index be22e791f..c9fd0ff2a 100644 --- a/tests/Range.test.tsx +++ b/tests/Range.test.tsx @@ -549,6 +549,32 @@ describe('Range', () => { ); }); + it('sets aria-invalid on the handles', () => { + const { container } = render( + , + ); + expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute( + 'aria-invalid', + 'true', + ); + expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveAttribute( + 'aria-invalid', + 'false', + ); + }); + + it('sets aria-invalid on the handles using ariaInvalid prop', () => { + const { container } = render(); + expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute( + 'aria-invalid', + 'true', + ); + expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveAttribute( + 'aria-invalid', + 'true', + ); + }); + it('sets aria-valuetext on the handles', () => { const { container } = render( { ); }); + it('sets aria-invalid on the handle', () => { + const { container } = render(); + expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute( + 'aria-invalid', + 'true', + ); + }); + + it('sets aria-invalid on the handle with ariaInvalidForHandle', () => { + const { container } = render(); + expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute( + 'aria-invalid', + 'grammar', + ); + }); + it('sets aria-valuetext on the handle', () => { const { container } = render(