diff --git a/packages/orbit-components/src/InputField/__tests__/index.test.tsx b/packages/orbit-components/src/InputField/__tests__/index.test.tsx index 7ecfbf51d9..4e7cca2742 100644 --- a/packages/orbit-components/src/InputField/__tests__/index.test.tsx +++ b/packages/orbit-components/src/InputField/__tests__/index.test.tsx @@ -59,6 +59,7 @@ describe("InputField", () => { expect(input).toHaveAttribute("data-recording-ignore"); expect(input).toHaveAttribute("id", "id"); expect(input).toHaveAttribute("data-state", "ok"); + expect(input).not.toBeInvalid(); expect(screen.getByTestId("test")).toBeInTheDocument(); expect(screen.getByTestId("prefix")).toBeInTheDocument(); expect(screen.getByTestId("suffix")).toBeInTheDocument(); @@ -142,10 +143,12 @@ describe("InputField", () => { expect(input).toHaveAttribute("min", "1"); expect(input).toHaveAttribute("max", "5"); expect(input).toHaveAttribute("data-state", "error"); + expect(input).toBeInvalid(); userEvent.tab(); expect(screen.queryByTestId("help")).not.toBeInTheDocument(); expect(screen.getByTestId("error")).toBeInTheDocument(); + expect(input).toHaveDescription("Something went wrong."); // Needs to flush async `floating-ui` hooks // https://github.com/floating-ui/floating-ui/issues/1520 await act(async () => {}); diff --git a/packages/orbit-components/src/InputField/index.tsx b/packages/orbit-components/src/InputField/index.tsx index 60472c8601..35bd8b95a1 100644 --- a/packages/orbit-components/src/InputField/index.tsx +++ b/packages/orbit-components/src/InputField/index.tsx @@ -255,12 +255,26 @@ interface StyledInputProps extends Partial { autoCorrect: string; autoCapitalize: string; ariaLabelledby?: string; + ariaDescribedby?: string; + ariaInvalid?: boolean; } export const Input = styled( React.forwardRef( ( - { type, size, error, help, inlineLabel, dataAttrs, required, ariaLabelledby, ...props }, + { + type, + size, + error, + help, + inlineLabel, + dataAttrs, + required, + ariaLabelledby, + ariaDescribedby, + ariaInvalid, + ...props + }, ref, ) => { return ( @@ -275,6 +289,8 @@ export const Input = styled( aria-required={required} // in case when there is no label aria-labelledby={ariaLabelledby} + aria-describedby={ariaDescribedby} + aria-invalid={ariaInvalid} /> ); }, @@ -501,6 +517,8 @@ const InputField = React.forwardRef((props, ref) => { ref={ref} tabIndex={tabIndex} ariaLabelledby={!label ? inputId : undefined} + ariaDescribedby={shown ? `${inputId}-feedback` : undefined} + ariaInvalid={error ? true : undefined} inlineLabel={inlineLabel} readOnly={readOnly} autoCapitalize="off" @@ -517,7 +535,7 @@ const InputField = React.forwardRef((props, ref) => { {!insideInputGroup && ( { expect(select).toHaveAttribute("data-state", "ok"); expect(select).toHaveAttribute("data-recording-ignore"); expect(select).toHaveAttribute("name", name); + expect(select).not.toBeInvalid(); expect(screen.getByLabelText(label)).toBeInTheDocument(); expect(screen.getByText(placeholder)).toBeInTheDocument(); @@ -82,16 +83,37 @@ describe("Select", () => { }); it("should have error message", async () => { - render(, + ); + const select = screen.getByTestId("error-select"); + userEvent.tab(); expect(screen.getByText("error")).toBeInTheDocument(); + expect(select).toBeInvalid(); + expect(select).toHaveDescription("error"); await act(async () => {}); }); it("should have help message", async () => { - render(, + ); + const select = screen.getByTestId("help-select"); + userEvent.tab(); expect(screen.getByText("help")).toBeInTheDocument(); + expect(select).toHaveDescription("help"); await act(async () => {}); }); diff --git a/packages/orbit-components/src/Select/index.tsx b/packages/orbit-components/src/Select/index.tsx index 5767825712..9f70851040 100644 --- a/packages/orbit-components/src/Select/index.tsx +++ b/packages/orbit-components/src/Select/index.tsx @@ -14,6 +14,7 @@ import getFieldDataState from "../common/getFieldDataState"; import useErrorTooltip from "../ErrorFormTooltip/hooks/useErrorTooltip"; import formElementFocus from "../InputField/helpers/formElementFocus"; import mq from "../utils/mediaQuery"; +import useRandomId from "../hooks/useRandomId"; import type { Props } from "./types"; const getSelectSize = ({ theme, size }: { theme: Theme; size: Size }) => { @@ -42,6 +43,8 @@ interface StyledSelectType extends Partial, DataAttrs { children: React.ReactNode; error?: React.ReactNode; filled: boolean; + ariaDescribedby?: string; + ariaInvalid?: boolean; } const StyledSelect = styled( @@ -62,6 +65,8 @@ const StyledSelect = styled( id, dataAttrs, readOnly, + ariaDescribedby, + ariaInvalid, }, ref, ) => ( @@ -80,6 +85,8 @@ const StyledSelect = styled( name={name} tabIndex={tabIndex ? Number(tabIndex) : undefined} ref={ref} + aria-describedby={ariaDescribedby} + aria-invalid={ariaInvalid} {...dataAttrs} > {children} @@ -287,6 +294,9 @@ const Select = React.forwardRef((props, ref) => { } = props; const filled = !(value == null || value === ""); + const forID = useRandomId(); + const selectId = id || forID; + const { tooltipShown, tooltipShownHover, @@ -339,11 +349,13 @@ const Select = React.forwardRef((props, ref) => { filled={filled} customValueText={customValueText} tabIndex={tabIndex ? Number(tabIndex) : undefined} - id={id} + id={selectId} readOnly={readOnly} required={required} ref={ref} dataAttrs={dataAttrs} + ariaDescribedby={shown ? `${selectId}-feedback` : undefined} + ariaInvalid={error ? true : undefined} > {placeholder && (