Skip to content

Commit

Permalink
fix(#2028): input formitem add accessibility features
Browse files Browse the repository at this point in the history
  • Loading branch information
syedszeeshan committed Aug 19, 2024
1 parent 783dc30 commit 171c970
Show file tree
Hide file tree
Showing 9 changed files with 372 additions and 55 deletions.
79 changes: 51 additions & 28 deletions libs/web-components/src/components/checkbox/Checkbox.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,44 @@
import { render, fireEvent } from '@testing-library/svelte';
import GoACheckbox from './Checkbox.svelte'
import { render, fireEvent, waitFor } from "@testing-library/svelte";
import GoACheckbox from "./Checkbox.svelte";
import { it, describe } from "vitest";

const testid = "checkbox-test";

async function createElement(props = {}) {
return render(GoACheckbox, { testid: testid, name: "checkbox-test-name", ...props });
return render(GoACheckbox, {
testid: testid,
name: "checkbox-test-name",
...props,
});
}

describe('GoACheckbox Component', () => {

describe("GoACheckbox Component", () => {
it("should render", async () => {
const el = await createElement();
const checkbox = await el.findByTestId(testid);
expect(checkbox).toBeTruthy();
});

it("should dispatch input:mounted event on mount", async () => {
const onInputMounted = vi.fn();
const testid = "checkbox-test";

const { getByTestId } = render(GoACheckbox, {
testid: testid,
name: "checkbox-test-name",
value: "foobar",
});

const rootEl = getByTestId(testid).closest(".root");
expect(rootEl).not.toBeNull();

rootEl?.addEventListener("input:mounted", onInputMounted);

await waitFor(() => {
expect(onInputMounted).toHaveBeenCalledTimes(1);
});
});

describe("properties", () => {
it("can set value", async () => {
const el = await createElement({ value: "foobar" });
Expand All @@ -32,28 +55,28 @@ describe('GoACheckbox Component', () => {

it("can set text", async () => {
const el = await createElement({ text: "foobar" });
const div = await el.findByTestId('text');
const div = await el.findByTestId("text");
expect(div).toHaveTextContent("foobar");
});

it("can set max width", async () => {
const el = await createElement({ text: "foobar", maxwidth: "480px" });
const root = await el.container.querySelector(".root");
expect(root?.getAttribute("style")).toContain("max-width: 480px;")
expect(root?.getAttribute("style")).toContain("max-width: 480px;");
});

it("can set description", async () => {
const el = await createElement({ description: "foobar" });
const div = await el.findByTestId('description');
const div = await el.findByTestId("description");
expect(div).toHaveTextContent("foobar");
});

it("can be checked", async () => {
const el = await createElement({ checked: "true" });
const root = el.container.querySelector('.selected');
const root = el.container.querySelector(".selected");
expect(root).toBeTruthy();

const svg = await el.findByTestId('checkmark');
const svg = await el.findByTestId("checkmark");
expect(svg).toBeTruthy();

const checkbox = el.container.querySelector("input");
Expand All @@ -62,34 +85,34 @@ describe('GoACheckbox Component', () => {

it("can be disabled", async () => {
const el = await createElement({ disabled: "true" });
const root = el.container.querySelector('.disabled');
const root = el.container.querySelector(".disabled");
expect(root).toBeTruthy();
const checkbox = el.container.querySelector("input");
expect((checkbox as HTMLInputElement).disabled).toBeTruthy();
});

it("can set error state", async () => {
const el = await createElement({ error: "true" });
const root = el.container.querySelector('.error');
const root = el.container.querySelector(".error");
expect(root).toBeTruthy();
});
});

describe("events", () => {
it("handles change event that results in checked state with value initialized", async () => {
const el = await createElement({ value: 'foobar' });
const el = await createElement({ value: "foobar" });
const checkbox = el.container.querySelector("input");
const change = vi.fn();

checkbox?.addEventListener('_change', (e: Event) => {
checkbox?.addEventListener("_change", (e: Event) => {
const detail = (e as CustomEvent).detail;
expect(detail.name).toBe('checkbox-test-name');
expect(detail.value).toBe('foobar');
expect(detail.name).toBe("checkbox-test-name");
expect(detail.value).toBe("foobar");
expect(detail.checked).toBeTruthy();
change();
})
});

checkbox && await fireEvent.click(checkbox);
checkbox && (await fireEvent.click(checkbox));
expect(change).toBeCalledTimes(1);
});

Expand All @@ -98,15 +121,15 @@ describe('GoACheckbox Component', () => {
const checkbox = el.container.querySelector("input");
const change = vi.fn();

checkbox?.addEventListener('_change', (e: Event) => {
checkbox?.addEventListener("_change", (e: Event) => {
const detail = (e as CustomEvent).detail;
expect(detail.name).toBe('checkbox-test-name');
expect(detail.value).toBe('checked');
expect(detail.name).toBe("checkbox-test-name");
expect(detail.value).toBe("checked");
expect(detail.checked).toBeTruthy();
change();
})
});

checkbox && await fireEvent.click(checkbox);
checkbox && (await fireEvent.click(checkbox));
expect(change).toBeCalledTimes(1);
});

Expand All @@ -115,15 +138,15 @@ describe('GoACheckbox Component', () => {
const checkbox = el.container.querySelector("input");
const change = vi.fn();

checkbox?.addEventListener('_change', (e: Event) => {
checkbox?.addEventListener("_change", (e: Event) => {
const detail = (e as CustomEvent).detail;
expect(detail.name).toBe('checkbox-test-name');
expect(detail.value).toBe('');
expect(detail.name).toBe("checkbox-test-name");
expect(detail.value).toBe("");
expect(detail.checked).toBeFalsy();
change();
})
});

checkbox && await fireEvent.click(checkbox);
checkbox && (await fireEvent.click(checkbox));
expect(change).toBeCalledTimes(1);
});
});
Expand Down
56 changes: 42 additions & 14 deletions libs/web-components/src/components/checkbox/Checkbox.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

<!-- Script -->
<script lang="ts">
import { onMount } from "svelte";
import { onMount, tick } from "svelte";
import type { Spacing } from "../../common/styling";
import { calculateMargin } from "../../common/styling";
import { fromBoolean, toBoolean } from "../../common/utils";
import { FormItemChannelProps } from "../form-item/FormItem.svelte";
// Required
export let name: string;
Expand All @@ -29,24 +30,43 @@
// Private
let _value: string;
let _checkboxRef: HTMLElement;
let _checkboxEl: HTMLElement;
let _descriptionId: string;
let _rootEl: HTMLElement;
let isError = toBoolean(error);
let prevError = isError;
// Binding
$: isDisabled = toBoolean(disabled);
$: isError = toBoolean(error);
$: {
isError = toBoolean(error);
if (isError !== prevError) {
dispatch("errorChange", { isError });
prevError = isError;
}
}
$: isChecked = toBoolean(checked);
$: isIndeterminate = false; // Design review. To be built with TreeView Later
onMount(() => {
onMount(async () => {
await tick();
_rootEl?.dispatchEvent(
new CustomEvent<FormItemChannelProps>("input:mounted", {
composed: true,
bubbles: true,
detail: { el: _checkboxEl },
}),
);
// hold on to the initial value to prevent losing it on check changes
_value = value;
_descriptionId = `description_${name}`;
});
function onChange(e: Event) {
// Manually set the focus back to the checkbox after the state change
_checkboxRef.focus();
_checkboxEl.focus();
// An empty string is required as setting the second value to `null` caused the data to get
// out of sync with the events.
const newCheckStatus = !isChecked;
Expand All @@ -62,11 +82,22 @@
}),
);
}
function dispatch(name: string, detail: any) {
_rootEl?.dispatchEvent(
new CustomEvent(name, {
bubbles: true,
composed: true,
detail,
}),
);
}
</script>

<!-- View -->

<div
bind:this={_rootEl}
class="root"
style={`
${calculateMargin(mt, mr, mb, ml)}
Expand All @@ -79,12 +110,9 @@
class:disabled={isDisabled}
class:error={isError}
>
<div
class="container"
class:selected={isChecked}
>
<div class="container" class:selected={isChecked}>
<input
bind:this={_checkboxRef}
bind:this={_checkboxEl}
id={name}
{name}
checked={isChecked}
Expand All @@ -93,6 +121,7 @@
value={`${value}`}
aria-label={arialabel || text || name}
aria-describedby={description ? _descriptionId : null}
aria-invalid={isError ? "true" : "false"}
on:change={onChange}
/>
{#if isIndeterminate}
Expand Down Expand Up @@ -123,7 +152,7 @@
</label>
{#if $$slots.description || description}
<div class="description" id={_descriptionId} data-testid="description">
<slot name="description"/>
<slot name="description" />
{description}
</div>
{/if}
Expand Down Expand Up @@ -172,7 +201,6 @@
margin-top: var(--goa-space-2xs);
}
/* Container */
.container {
box-sizing: border-box;
Expand All @@ -189,7 +217,8 @@
flex: 0 0 auto;
}
.container:hover {
box-shadow: 0 0 0 var(--goa-border-width-m) var(--goa-color-interactive-hover);
box-shadow: 0 0 0 var(--goa-border-width-m)
var(--goa-color-interactive-hover);
border: var(--goa-border-width-s) solid var(--goa-color-greyscale-700);
}
.container:focus-visible,
Expand All @@ -212,7 +241,6 @@
background-color: var(--goa-color-interactive-hover);
}
/* Error Container */
.error .container,
.error .container:hover,
Expand Down
Loading

0 comments on commit 171c970

Please sign in to comment.