Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding aria-live region on error #66

Merged
merged 5 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
"prettier",
"./configuration/eslint-rules/best-practices.cjs",
"./configuration/eslint-rules/possible-errors.cjs",
"./configuration/eslint-rules/variables.cjs",
],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
plugins: ["react-refresh", "simple-import-sort"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
},
};
4 changes: 3 additions & 1 deletion bundlemon.config.cjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable no-undef */

module.exports = {
reportOutput: ["github"],
baseDir: "./packages/bundlesize/dist",
Expand All @@ -9,7 +11,7 @@ module.exports = {
},
{
path: "assets/index.js",
maxSize: "60kb",
maxSize: "10kb",
},
{
path: "assets/style.css",
Expand Down
2 changes: 2 additions & 0 deletions configuration/eslint-rules/best-practices.cjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable no-undef */

module.exports = {
rules: {
// enforce return statements in callbacks of array methods
Expand Down
2 changes: 2 additions & 0 deletions configuration/eslint-rules/possible-errors.cjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable no-undef */

module.exports = {
rules: {
// disallow assignment operators in conditional expressions
Expand Down
2 changes: 2 additions & 0 deletions configuration/eslint-rules/variables.cjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable no-undef */

module.exports = {
rules: {
// disallow catch clause parameters from shadowing variables in the outer scope
Expand Down
2 changes: 2 additions & 0 deletions configuration/lint-staged.config.cjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable no-undef */

module.exports = {
"*.{ts,js,tsx,jsx}": [
"eslint --ext ts,tsx --report-unused-disable-directives --fix",
Expand Down
9 changes: 9 additions & 0 deletions configuration/vite.common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const externalDependencies = [
"@floating-ui/react",
"@tailwindcss/typography",
"react",
"react/jsx-runtime",
"react-dom",
"react-dom/server",
"tailwindcss",
];
11 changes: 3 additions & 8 deletions packages/bundlesize/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import fs from "fs-extra";
import { defineConfig } from "vite";

import { externalDependencies } from "../../configuration/vite.common";

const packageJson = fs.readJSONSync("package.json");

const buildTime = new Date()
Expand All @@ -23,14 +25,7 @@ export default defineConfig({
},
build: {
rollupOptions: {
external: [
"@floating-ui/react",
"@tailwindcss/typography",
"react",
"react/jsx-runtime",
"react-dom",
"tailwindcss",
],
external: externalDependencies,
output: {
assetFileNames: "assets/style[extname]",
entryFileNames: "assets/[name].js",
Expand Down
1 change: 0 additions & 1 deletion packages/ui-components/src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ export const TEXT_INPUT_CLASSNAME = "av-text-input";
export const TEXT_INPUT_WRAPPER_CLASSNAME = "av-text-input-wrapper";
export const TEXT_INPUT_HELPER_TEXT_CLASSNAME = "av-text-input-helper-text";

export const RAW_CLASSNAME = "av-raw";
export const VISUALLY_HIDDEN_CLASSNAME = "av-visually-hidden";
8 changes: 8 additions & 0 deletions packages/ui-components/src/components/TextInput/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import useUniqueId from "../../common/hooks/useUniqueId";
import { LiveRegion } from "../private/LiveRegion/LiveRegion";
import type { TextInputProps } from "./TextInputTypes";
import { getTextInputClasses } from "./utilities";

Expand All @@ -24,6 +25,7 @@ export const TextInput = ({
...extraProps
}: TextInputProps) => {
const inputId = useUniqueId({ id, prefix: "av-text-input-" });
const liveErrorMessage = `${name} error, ${helperText}`;
const textInputClassName = getTextInputClasses({
className,
error,
Expand Down Expand Up @@ -53,6 +55,7 @@ export const TextInput = ({
placeholder={!raw ? " " : undefined}
disabled={disabled}
{...(helperText && { "aria-describedby": `${inputId}-helper` })}
{...(error && { "aria-invalid": "true" })}
className={textInputClassName.input}
/>
{!raw && (
Expand All @@ -70,6 +73,11 @@ export const TextInput = ({
{helperText}
</div>
)}
{error && helperText && (
<LiveRegion politeness="polite" clearAnnouncementDelay={500}>
{liveErrorMessage}
</LiveRegion>
)}
</span>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe("TextInput modifiers", () => {
it("should render a raw text input with no styling", async () => {
render(<TextInput label="toto" name="toto" raw data-testid="txtnpt-1" />);
const input = await screen.findByTestId("txtnpt-1");
expect(input.className).toBe("av-raw");
expect(input.className).toBe("");
});
});

Expand Down Expand Up @@ -74,3 +74,42 @@ describe("TextInput methods", () => {
expect(spyOnChange).toHaveBeenCalledTimes(2);
});
});

describe("TextInput accessibility", () => {
it("should render a text input with an error message", async () => {
render(
<TextInput
error
helperText="error message"
label="hello world"
name="toto"
/>,
);
const errorMessage = await screen.findByText("error message");
expect(errorMessage.className).toContain("text-copy-error-dark");

const input = await screen.findByLabelText("hello world");
expect(input.getAttribute("aria-invalid")).toBe("true");
expect(input.getAttribute("aria-describedby")).toContain("av-text-input-");
expect(input.getAttribute("aria-describedby")).toContain("-helper");
});

it("should render a text input with a live region update", () => {
vi.useFakeTimers();
const clearTimeout = 500;

render(
<TextInput
error
helperText="error message"
label="hello world"
name="toto"
/>,
);
const liveRegion = screen.getByText("toto error, error message");
expect(liveRegion.getAttribute("aria-live")).toBe("polite");
expect(liveRegion.textContent).toBe("toto error, error message");
vi.advanceTimersByTime(clearTimeout);
expect(liveRegion.textContent).toBe("");
});
});
21 changes: 11 additions & 10 deletions packages/ui-components/src/components/TextInput/utilities.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import clsx from "clsx";

import {
RAW_CLASSNAME,
TEXT_INPUT_CLASSNAME,
TEXT_INPUT_HELPER_TEXT_CLASSNAME,
TEXT_INPUT_WRAPPER_CLASSNAME,
Expand Down Expand Up @@ -67,11 +66,13 @@ const getTextInputLabelClasses = (
});
};

const getTextInputHelperTextClasses = (error: boolean) => {
return clsx(TEXT_INPUT_HELPER_TEXT_CLASSNAME, "text-xs", {
"text-copy-error-dark": error,
"text-copy-medium": !error,
});
const getTextInputHelperTextClasses = (error: boolean, raw: boolean) => {
return raw
? undefined
: clsx(TEXT_INPUT_HELPER_TEXT_CLASSNAME, "text-xs", {
"text-copy-error-dark": error,
"text-copy-medium": !error,
});
};

export const getTextInputClasses = ({
Expand All @@ -86,13 +87,13 @@ export const getTextInputClasses = ({
error,
}: getTextInputClassesProps) => {
const wrapper = raw
? clsx(RAW_CLASSNAME)
? undefined
: clsx(TEXT_INPUT_WRAPPER_CLASSNAME, {
"w-full": fullWidth,
});

const input = raw
? clsx(RAW_CLASSNAME, className)
? className
: clsx(
TEXT_INPUT_CLASSNAME,
className,
Expand All @@ -106,11 +107,11 @@ export const getTextInputClasses = ({
},
);

const topLabel = raw ? "" : VISUALLY_HIDDEN_CLASSNAME;
const topLabel = raw ? undefined : VISUALLY_HIDDEN_CLASSNAME;

const bottomLabel = getTextInputLabelClasses(kind, disabled, raw);

const helperText = getTextInputHelperTextClasses(error);
const helperText = getTextInputHelperTextClasses(error, raw);

return {
wrapper,
Expand Down
10 changes: 3 additions & 7 deletions packages/ui-components/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { resolve } from "node:path";
import fs from "fs-extra";
import { defineConfig } from "vite";

import { externalDependencies } from "../../configuration/vite.common";

const packageJson = fs.readJSONSync("package.json");

const buildTime = new Date()
Expand All @@ -27,13 +29,7 @@ export default defineConfig({
fileName: (format) => `index.${format}.js`,
},
rollupOptions: {
external: [
"@floating-ui/react",
"@tailwindcss/typography",
"react",
"react/jsx-runtime",
"tailwindcss",
],
external: externalDependencies,
output: {
assetFileNames: "style[extname]",
entryFileNames: "[name].js",
Expand Down