Skip to content

Commit

Permalink
Merge pull request #71 from aversini/fix-refactoring-LiveRegion-to-dr…
Browse files Browse the repository at this point in the history
…op-react-dom-server-usage

fix: refactoring LiveRegion to drop react dom server usage
  • Loading branch information
aversini authored Nov 23, 2023
2 parents f2b0101 + 8bbb6d3 commit db942a5
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 48 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */

import { render, screen } from "@testing-library/react";
import { act, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import { expectToHaveClasses } from "../../../common/__tests__/helpers";
Expand Down Expand Up @@ -109,7 +109,9 @@ describe("TextInput accessibility", () => {
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);
act(() => {
vi.advanceTimersByTime(clearTimeout);
});
expect(liveRegion.textContent).toBe("");
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import clsx from "clsx";
import { useEffect, useRef } from "react";
import { useEffect, useReducer, useRef } from "react";

import { VISUALLY_HIDDEN_CLASSNAME } from "../../../common/constants";
import { DEFAULT_POLITENESS_BY_ROLE } from "./constants";
import type { LiveRegionProps } from "./LiveRegionTypes";
import { reducer } from "./reducer";
import { conditionallyDelayAnnouncement } from "./utilities";

/**
Expand All @@ -24,10 +25,13 @@ export function LiveRegion({

...otherProps
}: LiveRegionProps) {
const liveRegionRef = useRef<HTMLDivElement>(null);
const announcementTimeoutRef = useRef();
const clearAnnouncementTimeoutRef = useRef();

const [state, dispatch] = useReducer(reducer, {
announcement: null,
});

let politeness = politenessProp;
/**
* We check `undefined` since it is our default,
Expand All @@ -42,10 +46,10 @@ export function LiveRegion({
announcementTimeoutRef,
announcementDelay,
children,
liveRegionRef,
clearAnnouncementDelay,
clearAnnouncementTimeoutRef,
onAnnouncementClear,
dispatch,
});
}, [
children,
Expand All @@ -60,11 +64,12 @@ export function LiveRegion({

return (
<div
ref={liveRegionRef}
aria-live={politeness as "polite" | "assertive" | "off" | undefined}
{...(role && { role: role })}
className={generatedClassName}
{...otherProps}
/>
>
{state.announcement}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
import { ROLES } from "./constants";
import {
ACTION_CLEAR_ANNOUNCEMENT,
ACTION_SET_ANNOUNCEMENT,
ROLES,
} from "./constants";

export type ActionProps =
| Record<string, never>
| {
type: typeof ACTION_SET_ANNOUNCEMENT | typeof ACTION_CLEAR_ANNOUNCEMENT;
payload?: string | React.ReactNode;
};

export type StateProps = {
announcement: string | React.ReactNode;
};

export type PolitenessByRole = {
[key: string]: any;
};

export type ClearAnnouncementProps = {
liveRegionRef: React.RefObject<HTMLElement | undefined>;
onAnnouncementClear?: () => void;
dispatch: React.Dispatch<ActionProps>;
};

export type announceProps = {
children: React.ReactNode;
liveRegionRef: React.RefObject<HTMLElement | undefined>;
clearAnnouncementDelay?: number;
clearAnnouncementTimeoutRef: React.MutableRefObject<
NodeJS.Timeout | number | null | undefined
>;
onAnnouncementClear?: () => void;
dispatch: React.Dispatch<ActionProps>;
};

export type conditionallyDelayAnnouncementProps = {
children: React.ReactNode;
liveRegionRef: React.RefObject<HTMLElement | undefined>;
announcementTimeoutRef: React.MutableRefObject<
NodeJS.Timeout | null | undefined
>;
Expand All @@ -31,6 +45,7 @@ export type conditionallyDelayAnnouncementProps = {
NodeJS.Timeout | number | null | undefined
>;
onAnnouncementClear?: () => void;
dispatch: React.Dispatch<ActionProps>;
};

export type LiveRegionProps = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { fireEvent, render } from "@testing-library/react";
import { act, fireEvent, render } from "@testing-library/react";
import React from "react";

import { VISUALLY_HIDDEN_CLASSNAME } from "../../../../common/constants";
import { ROLES } from "../constants";
import {
ACTION_CLEAR_ANNOUNCEMENT,
ACTION_SET_ANNOUNCEMENT,
ROLES,
} from "../constants";
import { LiveRegion } from "../LiveRegion";
import { reducer } from "../reducer";

const getContent = (renderResult: any) =>
renderResult.container.firstChild.textContent;
Expand All @@ -13,7 +18,7 @@ const getRole = (renderResult: any) =>
renderResult.container.firstChild.getAttribute("role");

const content = "Foo Bar Baz";
const content2 = "Biz buzz";
const content2 = "Biz Buzz";

/**
* This is a helper component that renders a LiveRegion and a button. It accepts all of LiveRegion's
Expand Down Expand Up @@ -90,6 +95,46 @@ class LiveRegionPropChanger extends React.PureComponent<LiveRegionProps> {
}

describe(`The LiveRegion Component`, () => {
describe("reducer tests", () => {
it("should return the initial state", () => {
const state = {
announcement: "foo",
};
expect(
reducer(state, {
type: "UNKNOWN_ACTION" as any,
}),
).toEqual(state);
});

it("should update the data state on action", () => {
const previousState = {
announcement: "foo",
};
const actionPayload = <p>hello world</p>;
expect(
reducer(previousState, {
type: ACTION_SET_ANNOUNCEMENT,
payload: actionPayload,
}),
).toEqual({
announcement: actionPayload,
});
});

it("should clear the data state on action", () => {
const previousState = {
announcement: "foo",
};
expect(
reducer(previousState, {
type: ACTION_CLEAR_ANNOUNCEMENT,
}),
).toEqual({
announcement: null,
});
});
});
describe(`When it renders without any props`, () => {
let renderResult: any;
beforeEach(() => {
Expand Down Expand Up @@ -117,8 +162,10 @@ describe(`The LiveRegion Component`, () => {
<LiveRegion announcementDelay={timeout}>{content}</LiveRegion>,
);
expect(getContent(renderResult)).toEqual("");
vi.advanceTimersByTime(timeout + 1000);
expect(getContent(renderResult)).toEqual("Foo Bar Baz");
act(() => {
vi.advanceTimersByTime(timeout);
});
expect(getContent(renderResult)).toEqual(content);
});

it(`and with children as React Component Then it should render with some delay`, () => {
Expand All @@ -129,53 +176,57 @@ describe(`The LiveRegion Component`, () => {
<LiveRegion announcementDelay={timeout}>{children}</LiveRegion>,
);
expect(renderResult.queryByText(content)).toBeNull();
vi.advanceTimersByTime(timeout);
act(() => {
vi.advanceTimersByTime(timeout);
});
expect(renderResult.getByText(content)).toBeInTheDocument();
});
});
describe(`When clearAnnouncementDelay is supplied`, () => {
it(`Then it should clear the content with some delay`, () => {
vi.useFakeTimers();
const clearTimeout = 3000;
const timeout = 3000;
const renderResult = render(
<LiveRegion clearAnnouncementDelay={clearTimeout}>
{content}
</LiveRegion>,
<LiveRegion clearAnnouncementDelay={timeout}>{content}</LiveRegion>,
);
expect(getContent(renderResult)).toEqual("Foo Bar Baz");
vi.advanceTimersByTime(clearTimeout);
expect(getContent(renderResult)).toEqual("");
expect(getContent(renderResult)).toEqual(content);
act(() => {
vi.advanceTimersByTime(timeout);
});
expect(getContent(renderResult)).toBe("");
});

it(`and with children as React Component Then it should render with some delay`, () => {
vi.useFakeTimers();
const clearTimeout = 3000;
const timeout = 3000;
const children = <p>{content}</p>;
const renderResult = render(
<LiveRegion clearAnnouncementDelay={clearTimeout}>
{children}
</LiveRegion>,
<LiveRegion clearAnnouncementDelay={timeout}>{children}</LiveRegion>,
);
expect(renderResult.getByText(content)).toBeInTheDocument();
vi.advanceTimersByTime(clearTimeout);
act(() => {
vi.advanceTimersByTime(timeout);
});
expect(renderResult.queryByText(content)).toBeNull();
});
});
describe(`When onAnnouncementClear is supplied`, () => {
it(`Then it should call the callback function after clearing the timeout`, () => {
vi.useFakeTimers();
const clearTimeout = 3000;
const timeout = 3000;
const onAnnouncementClearMock = vi.fn();
render(
<LiveRegion
clearAnnouncementDelay={clearTimeout}
clearAnnouncementDelay={timeout}
onAnnouncementClear={onAnnouncementClearMock}
>
{content}
</LiveRegion>,
);
expect(onAnnouncementClearMock).toHaveBeenCalledTimes(0);
vi.advanceTimersByTime(clearTimeout);
act(() => {
vi.advanceTimersByTime(timeout);
});
expect(onAnnouncementClearMock).toHaveBeenCalledTimes(1);
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { PolitenessByRole } from "./LiveRegionTypes";

export const ACTION_SET_ANNOUNCEMENT = "SET_ANNOUNCEMENT";
export const ACTION_CLEAR_ANNOUNCEMENT = "CLEAR_ANNOUNCEMENT";

/**
* The default politeness value for each role.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {
ACTION_CLEAR_ANNOUNCEMENT,
ACTION_SET_ANNOUNCEMENT,
} from "./constants";
import type { ActionProps, StateProps } from "./LiveRegionTypes";

export const reducer = (state: StateProps, action: ActionProps) => {
switch (action?.type) {
case ACTION_SET_ANNOUNCEMENT:
return {
...state,
announcement: action.payload,
};
case ACTION_CLEAR_ANNOUNCEMENT:
return {
...state,
announcement: null,
};
default:
return state;
}
};
Loading

0 comments on commit db942a5

Please sign in to comment.