Skip to content

Commit

Permalink
Add aria-invalid and aria-describedby to form components (#3706)
Browse files Browse the repository at this point in the history
* feat(InputField): add aria-invalid and aria-describedby

Accessibility properties added to the correct states.

* feat(Select): add aria-invalid and aria-describedby

Accessibility properties added to the correct states

* feat(Textarea): add aria-invalid and aria-describedby

Accessibility properties added to the correct states
  • Loading branch information
DSil committed Jan 5, 2023
1 parent a9ab2aa commit 8a29fe6
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 7 deletions.
Expand Up @@ -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();
Expand Down Expand Up @@ -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 () => {});
Expand Down
22 changes: 20 additions & 2 deletions packages/orbit-components/src/InputField/index.tsx
Expand Up @@ -255,12 +255,26 @@ interface StyledInputProps extends Partial<Props> {
autoCorrect: string;
autoCapitalize: string;
ariaLabelledby?: string;
ariaDescribedby?: string;
ariaInvalid?: boolean;
}

export const Input = styled(
React.forwardRef<HTMLInputElement, StyledInputProps>(
(
{ type, size, error, help, inlineLabel, dataAttrs, required, ariaLabelledby, ...props },
{
type,
size,
error,
help,
inlineLabel,
dataAttrs,
required,
ariaLabelledby,
ariaDescribedby,
ariaInvalid,
...props
},
ref,
) => {
return (
Expand All @@ -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}
/>
);
},
Expand Down Expand Up @@ -501,6 +517,8 @@ const InputField = React.forwardRef<HTMLInputElement, Props>((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"
Expand All @@ -517,7 +535,7 @@ const InputField = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
{!insideInputGroup && (
<ErrorFormTooltip
help={help}
id={inputId}
id={`${inputId}-feedback`}
shown={shown}
helpClosable={helpClosable}
onShown={setTooltipShown}
Expand Down
26 changes: 24 additions & 2 deletions packages/orbit-components/src/Select/__tests__/Select.test.tsx
Expand Up @@ -57,6 +57,7 @@ describe("Select", () => {
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();

Expand All @@ -82,16 +83,37 @@ describe("Select", () => {
});

it("should have error message", async () => {
render(<Select error="error" readOnly options={[{ value: "1", label: "One" }]} />);
render(
<Select
dataTest="error-select"
error="error"
readOnly
options={[{ value: "1", label: "One" }]}
/>,
);
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(<Select help="help" readOnly options={[{ value: "1", label: "One" }]} />);
render(
<Select
dataTest="help-select"
help="help"
readOnly
options={[{ value: "1", label: "One" }]}
/>,
);
const select = screen.getByTestId("help-select");

userEvent.tab();
expect(screen.getByText("help")).toBeInTheDocument();
expect(select).toHaveDescription("help");
await act(async () => {});
});

Expand Down
15 changes: 14 additions & 1 deletion packages/orbit-components/src/Select/index.tsx
Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -42,6 +43,8 @@ interface StyledSelectType extends Partial<Props>, DataAttrs {
children: React.ReactNode;
error?: React.ReactNode;
filled: boolean;
ariaDescribedby?: string;
ariaInvalid?: boolean;
}

const StyledSelect = styled(
Expand All @@ -62,6 +65,8 @@ const StyledSelect = styled(
id,
dataAttrs,
readOnly,
ariaDescribedby,
ariaInvalid,
},
ref,
) => (
Expand All @@ -80,6 +85,8 @@ const StyledSelect = styled(
name={name}
tabIndex={tabIndex ? Number(tabIndex) : undefined}
ref={ref}
aria-describedby={ariaDescribedby}
aria-invalid={ariaInvalid}
{...dataAttrs}
>
{children}
Expand Down Expand Up @@ -287,6 +294,9 @@ const Select = React.forwardRef<HTMLSelectElement, Props>((props, ref) => {
} = props;
const filled = !(value == null || value === "");

const forID = useRandomId();
const selectId = id || forID;

const {
tooltipShown,
tooltipShownHover,
Expand Down Expand Up @@ -339,11 +349,13 @@ const Select = React.forwardRef<HTMLSelectElement, Props>((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 && (
<option label={placeholder.toString()} value="">
Expand All @@ -366,6 +378,7 @@ const Select = React.forwardRef<HTMLSelectElement, Props>((props, ref) => {
</SelectContainer>
{!insideInputGroup && (
<ErrorFormTooltip
id={`${selectId}-feedback`}
help={help}
error={error}
inputSize={size}
Expand Down
Expand Up @@ -44,6 +44,7 @@ describe("Textarea", () => {
expect(textarea).toHaveAttribute("maxlength", maxLength.toString());
expect(textarea).toHaveAttribute("rows", "4");
expect(textarea).toHaveAttribute("name", name);
expect(textarea).not.toBeInvalid();
expect(textarea.parentElement).toHaveStyle({ marginBottom: "12px" });
expect(textarea).toHaveStyle({ padding: "12px" });

Expand All @@ -64,9 +65,13 @@ describe("Textarea", () => {

it("should have error", async () => {
render(<Textarea error="error" size="small" />);
const textarea = screen.getByRole("textbox");

userEvent.tab();
expect(screen.getByText("error")).toBeInTheDocument();
expect(screen.getByRole("textbox")).toHaveStyle({ padding: "8px 12px" });
expect(textarea).toHaveStyle({ padding: "8px 12px" });
expect(textarea).toBeInvalid();
expect(textarea).toHaveDescription("error");
// Needs to flush async `floating-ui` hooks
// https://github.com/floating-ui/floating-ui/issues/1520
await act(async () => {});
Expand Down
9 changes: 8 additions & 1 deletion packages/orbit-components/src/Textarea/index.tsx
Expand Up @@ -13,6 +13,7 @@ import useErrorTooltip from "../ErrorFormTooltip/hooks/useErrorTooltip";
import getFieldDataState from "../common/getFieldDataState";
import mq from "../utils/mediaQuery";
import type { Props } from "./types";
import useRandomId from "../hooks/useRandomId";

const Field = styled.label<{ fullHeight?: boolean; spaceAfter?: Common.SpaceAfterSizes }>`
font-family: ${({ theme }) => theme.orbit.fontFamily};
Expand Down Expand Up @@ -145,6 +146,9 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, Props>((props, ref) => {
required,
}: Props = props;

const forID = useRandomId();
const inputId = id || forID;

const {
tooltipShown,
tooltipShownHover,
Expand Down Expand Up @@ -178,7 +182,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, Props>((props, ref) => {
data-state={getFieldDataState(!!error)}
data-test={dataTest}
aria-required={!!required}
id={id}
id={inputId}
name={name}
value={value}
size={size}
Expand All @@ -195,8 +199,11 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, Props>((props, ref) => {
tabIndex={tabIndex ? Number(tabIndex) : undefined}
readOnly={readOnly}
ref={ref}
aria-describedby={shown ? `${inputId}-feedback` : undefined}
aria-invalid={error ? true : undefined}
/>
<ErrorFormTooltip
id={`${inputId}-feedback`}
help={help}
inputSize={size}
helpClosable={helpClosable}
Expand Down

0 comments on commit 8a29fe6

Please sign in to comment.