From 36682817f560c00c4d603b90bbe6de07c9eddddd Mon Sep 17 00:00:00 2001 From: nuria1110 Date: Fri, 22 Nov 2024 16:30:10 +0000 Subject: [PATCH] chore: add aria-describedby --- .../fieldset/fieldset.component.tsx | 4 ++ src/components/date/date-test.stories.tsx | 8 ++-- .../decimal/decimal-test.stories.tsx | 12 +++--- .../grouped-character-test.stories.tsx | 6 +-- src/components/number/number-test.stories.tsx | 4 +- .../numeral-date/numeral-date.component.tsx | 29 +++++++++++--- .../numeral-date/numeral-date.test.tsx | 40 +++++++++++++++++++ .../textbox/textbox-test.stories.tsx | 16 +++++--- src/components/textbox/textbox.component.tsx | 11 ++++- src/components/textbox/textbox.test.tsx | 9 +++++ 10 files changed, 111 insertions(+), 28 deletions(-) diff --git a/src/__internal__/fieldset/fieldset.component.tsx b/src/__internal__/fieldset/fieldset.component.tsx index 43f4012676..5ea58e43c0 100644 --- a/src/__internal__/fieldset/fieldset.component.tsx +++ b/src/__internal__/fieldset/fieldset.component.tsx @@ -58,6 +58,8 @@ export interface FieldsetProps extends MarginProps { id?: string; /** Content for the Help tooltip */ labelHelp?: React.ReactNode; + /** Id for the validation icon tooltip */ + validationId?: string; } const Fieldset = ({ @@ -76,6 +78,7 @@ const Fieldset = ({ isDisabled, isOptional, labelHelp, + validationId, ...rest }: FieldsetProps) => { const { validationRedesignOptIn } = useContext(NewValidationContext); @@ -102,6 +105,7 @@ const Fieldset = ({ warning={warning} info={info} tooltipFlipOverrides={["top", "bottom"]} + tooltipId={validationId} /> ); diff --git a/src/components/date/date-test.stories.tsx b/src/components/date/date-test.stories.tsx index 7134983460..a883e95c30 100644 --- a/src/components/date/date-test.stories.tsx +++ b/src/components/date/date-test.stories.tsx @@ -8,7 +8,7 @@ import { CommonTextboxArgs, commonTextboxArgTypes, getCommonTextboxArgs, - getCommonTextboxArgsWithSpecialCaracters, + getCommonTextboxArgsWithSpecialCharacters, } from "../textbox/textbox-test.stories"; import CarbonProvider from "../carbon-provider/carbon-provider.component"; import Box from "../box"; @@ -44,7 +44,7 @@ export const DateStory = (args: CommonTextboxArgs) => { action("onKeyDown")((ev.target as HTMLInputElement).value) } onClick={(ev) => action("onClick")((ev.target as HTMLInputElement).value)} - {...getCommonTextboxArgsWithSpecialCaracters(args)} + {...getCommonTextboxArgsWithSpecialCharacters(args)} /> ); }; @@ -79,7 +79,7 @@ export const NewValidationStory = (args: CommonTextboxArgs) => { onClick={(ev) => action("onClick")((ev.target as HTMLInputElement).value) } - {...getCommonTextboxArgsWithSpecialCaracters(args)} + {...getCommonTextboxArgsWithSpecialCharacters(args)} /> ); @@ -191,7 +191,7 @@ export const I18NStory = (args: I18nArgs) => { onClick={(ev) => action("onClick")((ev.target as HTMLInputElement).value) } - {...getCommonTextboxArgsWithSpecialCaracters(args)} + {...getCommonTextboxArgsWithSpecialCharacters(args)} /> diff --git a/src/components/decimal/decimal-test.stories.tsx b/src/components/decimal/decimal-test.stories.tsx index f5ad0179c1..94e2e5ded8 100644 --- a/src/components/decimal/decimal-test.stories.tsx +++ b/src/components/decimal/decimal-test.stories.tsx @@ -7,7 +7,7 @@ import { CommonTextboxArgs, commonTextboxArgTypes, getCommonTextboxArgs, - getCommonTextboxArgsWithSpecialCaracters, + getCommonTextboxArgsWithSpecialCharacters, } from "../textbox/textbox-test.stories"; import CarbonProvider from "../carbon-provider/carbon-provider.component"; @@ -59,7 +59,7 @@ export const DecimalStory = (args: CommonTextboxArgs) => { value={state} onChange={handleChange} onBlur={handleBlur} - {...getCommonTextboxArgsWithSpecialCaracters(args)} + {...getCommonTextboxArgsWithSpecialCharacters(args)} /> ); }; @@ -79,7 +79,7 @@ export const UncontrolledDecimalStory = (args: CommonTextboxArgs) => { defaultValue="0.03" onChange={handleChange} onBlur={handleBlur} - {...getCommonTextboxArgsWithSpecialCaracters(args)} + {...getCommonTextboxArgsWithSpecialCharacters(args)} /> ); }; @@ -125,7 +125,7 @@ export const PostStory = ({ value={state} onChange={handleChange} onBlur={handleBlur} - {...getCommonTextboxArgsWithSpecialCaracters(args)} + {...getCommonTextboxArgsWithSpecialCharacters(args)} /> @@ -150,7 +150,7 @@ export const NewValidationStory = (args: CommonTextboxArgs) => { m={2} onChange={handleChange} onBlur={handleBlur} - {...getCommonTextboxArgsWithSpecialCaracters(args)} + {...getCommonTextboxArgsWithSpecialCharacters(args)} /> ); @@ -180,7 +180,7 @@ export const DecimalCustomOnChangeStory = (args: CommonTextboxArgs) => { value={state} onChange={handleChange} onBlur={handleBlur} - {...getCommonTextboxArgsWithSpecialCaracters(args)} + {...getCommonTextboxArgsWithSpecialCharacters(args)} /> ); diff --git a/src/components/grouped-character/grouped-character-test.stories.tsx b/src/components/grouped-character/grouped-character-test.stories.tsx index 9708037b8d..0e9788cafa 100644 --- a/src/components/grouped-character/grouped-character-test.stories.tsx +++ b/src/components/grouped-character/grouped-character-test.stories.tsx @@ -3,7 +3,7 @@ import { action } from "@storybook/addon-actions"; import { commonTextboxArgTypes, getCommonTextboxArgs, - getCommonTextboxArgsWithSpecialCaracters, + getCommonTextboxArgsWithSpecialCharacters, CommonTextboxArgs, } from "../textbox/textbox-test.stories"; import GroupedCharacter, { CustomEvent } from "./grouped-character.component"; @@ -45,7 +45,7 @@ export const Default = ({ separator, groups, ...args }: StoryArgs) => { onChange={onChange} groups={groups} separator={separator || " "} - {...getCommonTextboxArgsWithSpecialCaracters(args)} + {...getCommonTextboxArgsWithSpecialCharacters(args)} /> ); }; @@ -71,7 +71,7 @@ export const NewValidation = ({ separator, groups, ...args }: StoryArgs) => { onChange={onChange} groups={groups} separator={separator || " "} - {...getCommonTextboxArgsWithSpecialCaracters(args)} + {...getCommonTextboxArgsWithSpecialCharacters(args)} /> ); diff --git a/src/components/number/number-test.stories.tsx b/src/components/number/number-test.stories.tsx index b83697079c..9cff644e97 100644 --- a/src/components/number/number-test.stories.tsx +++ b/src/components/number/number-test.stories.tsx @@ -6,7 +6,7 @@ import { CommonTextboxArgs, commonTextboxArgTypes, getCommonTextboxArgs, - getCommonTextboxArgsWithSpecialCaracters, + getCommonTextboxArgsWithSpecialCharacters, } from "../textbox/textbox-test.stories"; export default { @@ -49,7 +49,7 @@ export const Default = ({ onChangeDeferred={ onChangeDeferredEnabled ? action("onChangeDeferred") : undefined } - {...getCommonTextboxArgsWithSpecialCaracters(args)} + {...getCommonTextboxArgsWithSpecialCharacters(args)} /> ); }; diff --git a/src/components/numeral-date/numeral-date.component.tsx b/src/components/numeral-date/numeral-date.component.tsx index ae6f2b9df2..e7868a5409 100644 --- a/src/components/numeral-date/numeral-date.component.tsx +++ b/src/components/numeral-date/numeral-date.component.tsx @@ -23,6 +23,7 @@ import Logger from "../../__internal__/utils/logger"; import Locale from "../../locales/locale"; import FieldHelp from "../../__internal__/field-help"; import useIsAboveBreakpoint from "../../hooks/__internal__/useIsAboveBreakpoint"; +import useInputAccessibility from "../../hooks/__internal__/useInputAccessibility"; let deprecateUncontrolledWarnTriggered = false; @@ -263,6 +264,7 @@ export const NumeralDate = ({ const { current: uniqueId } = useRef(id || guid()); const inputIds = useRef({ dd: guid(), mm: guid(), yyyy: guid() }); + const inputHintId = useRef(guid()); const isControlled = useRef(value !== undefined); const initialValue = isControlled.current ? value : defaultValue; @@ -412,6 +414,20 @@ export const NumeralDate = ({ } }; + const { validationId, fieldHelpId, ariaDescribedBy } = useInputAccessibility({ + id: uniqueId, + // we still want to add the validationId to aria-describedby with the legacy validation + validationRedesignOptIn: true, + error: internalError, + warning: internalWarning, + info, + fieldHelp, + }); + + const combinedAriaDescribedBy = [inputHintId.current, ariaDescribedBy] + .filter(Boolean) + .join(" "); + const renderInputs = () => { const hasInfo = validationRedesignOptIn ? info : undefined; @@ -477,6 +493,7 @@ export const NumeralDate = ({ info, })} tooltipPosition={tooltipPosition} + tooltipId={validationId} /> @@ -507,11 +524,13 @@ export const NumeralDate = ({ legendAlign={labelAlign} legendWidth={labelWidth} legendSpacing={labelSpacing} + validationId={validationId} + aria-describedby={ariaDescribedBy} {...filterStyledSystemMarginProps(rest)} > {renderInputs()} - {fieldHelp && {fieldHelp}} + {fieldHelp && {fieldHelp}} @@ -529,13 +548,11 @@ export const NumeralDate = ({ isOptional={isOptional} isDisabled={disabled} name={name} - aria-describedby={labelHelp && `${uniqueId}-label-help`} + aria-describedby={combinedAriaDescribedBy} {...filterStyledSystemMarginProps(rest)} > {labelHelp && ( - - {labelHelp} - + {labelHelp} )} @@ -544,7 +561,7 @@ export const NumeralDate = ({ diff --git a/src/components/numeral-date/numeral-date.test.tsx b/src/components/numeral-date/numeral-date.test.tsx index 39a3bd4624..c159e098cc 100644 --- a/src/components/numeral-date/numeral-date.test.tsx +++ b/src/components/numeral-date/numeral-date.test.tsx @@ -2109,3 +2109,43 @@ test("should set the inputs to `readOnly` when the prop is passed", () => { expect(monthInput).toHaveAttribute("readonly"); expect(yearInput).toHaveAttribute("readonly"); }); + +test.each(["error", "warning", "info"])( + "should render with expected accessible description when '%s' and 'fieldHelp' props are passed and 'validationRedesignOptIn' is false", + async (validation) => { + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render( + {}} + fieldHelp="fieldHelp" + {...{ [validation]: "Message" }} + />, + ); + + const dayInput = screen.getByRole("textbox", { name: "Day" }); + await user.click(dayInput); + + const fieldset = screen.getByRole("group"); + expect(fieldset).toHaveAccessibleDescription("fieldHelp Message"); + }, +); + +test.each(["error", "warning"])( + "should render with expected accessible description when '%s' and 'labelHelp' props are passed and 'validationRedesignOptIn' is true", + (validation) => { + render( + + {}} + labelHelp="labelHelp" + {...{ [validation]: "Message" }} + /> + , + ); + + const fieldset = screen.getByRole("group"); + expect(fieldset).toHaveAccessibleDescription("labelHelp Message"); + }, +); diff --git a/src/components/textbox/textbox-test.stories.tsx b/src/components/textbox/textbox-test.stories.tsx index 2d48e62d77..cce63dd0b6 100644 --- a/src/components/textbox/textbox-test.stories.tsx +++ b/src/components/textbox/textbox-test.stories.tsx @@ -24,6 +24,7 @@ export const getCommonTextboxArgs = ( labelWidth: 30, inputWidth: 70, labelAlign: undefined, + tooltipId: "", }), size: "medium", inputIcon: undefined, @@ -42,7 +43,7 @@ export interface CommonTextboxArgs { placeholder: string; } -export const getCommonTextboxArgsWithSpecialCaracters = ( +export const getCommonTextboxArgsWithSpecialCharacters = ( args: CommonTextboxArgs, ) => { const { prefix, fieldHelp, label, labelHelp, placeholder } = args; @@ -93,6 +94,11 @@ export const commonTextboxArgTypes = (isNewValidation?: boolean) => ({ step: 1, }, }, + tooltipId: { + control: { + type: "text", + }, + }, }), adaptiveLabelBreakpoint: { control: { @@ -128,7 +134,7 @@ export const Default = (args: CommonTextboxArgs) => { iconOnClick={action("iconOnClick")} value={state} onChange={setValue} - {...getCommonTextboxArgsWithSpecialCaracters(args)} + {...getCommonTextboxArgsWithSpecialCharacters(args)} /> ); @@ -139,8 +145,8 @@ Default.args = getCommonTextboxArgs(); export const Multiple = (args: CommonTextboxArgs) => (
- - + +
); @@ -160,7 +166,7 @@ export const NewValidation = (args: CommonTextboxArgs) => { diff --git a/src/components/textbox/textbox.component.tsx b/src/components/textbox/textbox.component.tsx index 152a7c760c..481307d8ae 100644 --- a/src/components/textbox/textbox.component.tsx +++ b/src/components/textbox/textbox.component.tsx @@ -124,6 +124,8 @@ export interface CommonTextboxProps helpAriaLabel?: string; /** Flag to configure component as optional. */ isOptional?: boolean; + /** The id attribute for the validation tooltip */ + tooltipId?: string; } export interface TextboxProps extends CommonTextboxProps { @@ -190,6 +192,7 @@ export const Textbox = React.forwardRef( "data-role": dataRole, characterLimit, helpAriaLabel, + tooltipId, ...props }: TextboxProps, ref: React.ForwardedRef, @@ -299,7 +302,9 @@ export const Textbox = React.forwardRef( placeholder={disabled || readOnly ? "" : placeholder} readOnly={readOnly} value={typeof formattedValue === "string" ? formattedValue : value} - validationIconId={validationRedesignOptIn ? undefined : validationId} + validationIconId={ + validationRedesignOptIn ? undefined : tooltipId || validationId + } {...props} /> {children} @@ -318,7 +323,9 @@ export const Textbox = React.forwardRef( size={size} useValidationIcon={!(validationRedesignOptIn || validationOnLabel)} warning={warning} - validationIconId={validationRedesignOptIn ? undefined : validationId} + validationIconId={ + validationRedesignOptIn ? undefined : tooltipId || validationId + } /> ); diff --git a/src/components/textbox/textbox.test.tsx b/src/components/textbox/textbox.test.tsx index 7c80039192..94ad4e6bd1 100644 --- a/src/components/textbox/textbox.test.tsx +++ b/src/components/textbox/textbox.test.tsx @@ -507,6 +507,15 @@ test.each(validationTypes)( }, ); +test("renders validation tooltip with provided 'tooltipId' prop", async () => { + render(); + const input = screen.getByRole("textbox"); + input.focus(); + + expect(await screen.findByRole("tooltip")).toHaveAttribute("id", "foo"); + expect(input).toHaveAccessibleDescription("baz"); +}); + describe("when inputHint prop is present", () => { it("renders the hint", () => { render();