From b286d17222b80e805f2e840175b77c6bf5095d8b Mon Sep 17 00:00:00 2001 From: Arno V Date: Tue, 21 Nov 2023 17:32:29 -0500 Subject: [PATCH 1/4] feat: introducing LiveRegion component --- .../private/LiveRegion/LiveRegion.tsx | 72 ++++ .../private/LiveRegion/LiveRegionTypes.d.ts | 65 ++++ .../LiveRegion/__tests__/LiveRegion.test.tsx | 341 ++++++++++++++++++ .../private/LiveRegion/constants.ts | 110 ++++++ .../private/LiveRegion/utilities.ts | 88 +++++ 5 files changed, 676 insertions(+) create mode 100644 packages/ui-components/src/components/private/LiveRegion/LiveRegion.tsx create mode 100644 packages/ui-components/src/components/private/LiveRegion/LiveRegionTypes.d.ts create mode 100644 packages/ui-components/src/components/private/LiveRegion/__tests__/LiveRegion.test.tsx create mode 100644 packages/ui-components/src/components/private/LiveRegion/constants.ts create mode 100644 packages/ui-components/src/components/private/LiveRegion/utilities.ts diff --git a/packages/ui-components/src/components/private/LiveRegion/LiveRegion.tsx b/packages/ui-components/src/components/private/LiveRegion/LiveRegion.tsx new file mode 100644 index 00000000..cf0972b3 --- /dev/null +++ b/packages/ui-components/src/components/private/LiveRegion/LiveRegion.tsx @@ -0,0 +1,72 @@ +import clsx from "clsx"; +import { useEffect, useRef } from "react"; + +import { VISUALLY_HIDDEN_CLASSNAME } from "../../../common/constants"; +import { DEFAULT_POLITENESS_BY_ROLE } from "./constants"; +import type { LiveRegionProps } from "./LiveRegionTypes"; +import { conditionallyDelayAnnouncement } from "./utilities"; + +/** + * The `LiveRegion` component abstracts the logic for + * rendering live region content that consistently announces + * across assistive technologies (e.g. JAWS, VoiceOver, + * NVDA). + */ +export function LiveRegion({ + children, + className, + politeness: politenessProp, + role = null, + announcementDelay, + clearAnnouncementDelay, + onAnnouncementClear, + visible, + + ...otherProps +}: LiveRegionProps) { + const liveRegionRef = useRef(null); + const announcementTimeoutRef = useRef(); + const clearAnnouncementTimeoutRef = useRef(); + + let politeness = politenessProp; + /** + * We check `undefined` since it is our default, + * and we want to honor when the user supplies `null`. + */ + if (typeof politeness === "undefined") { + politeness = role + ? (DEFAULT_POLITENESS_BY_ROLE[role] as "polite" | "assertive") + : "assertive"; + } + + useEffect(() => { + conditionallyDelayAnnouncement({ + announcementTimeoutRef, + announcementDelay, + children, + liveRegionRef, + clearAnnouncementDelay, + clearAnnouncementTimeoutRef, + onAnnouncementClear, + }); + }, [ + children, + announcementDelay, + clearAnnouncementDelay, + onAnnouncementClear, + ]); + + const generatedClassName = clsx(className, { + [VISUALLY_HIDDEN_CLASSNAME]: !visible, + }); + + return ( +
+ ); +} diff --git a/packages/ui-components/src/components/private/LiveRegion/LiveRegionTypes.d.ts b/packages/ui-components/src/components/private/LiveRegion/LiveRegionTypes.d.ts new file mode 100644 index 00000000..e8c8d713 --- /dev/null +++ b/packages/ui-components/src/components/private/LiveRegion/LiveRegionTypes.d.ts @@ -0,0 +1,65 @@ +import { ROLES } from "./constants"; + +export type ClearAnnouncementProps = { + liveRegionRef: React.RefObject; + onAnnouncementClear?: () => void; +}; + +export type announceProps = { + children: React.ReactNode; + liveRegionRef: React.RefObject; + clearAnnouncementDelay?: number; + clearAnnouncementTimeoutRef: React.MutableRefObject< + NodeJS.Timeout | number | null | undefined + >; + onAnnouncementClear?: () => void; +}; + +export type conditionallyDelayAnnouncementProps = { + children: React.ReactNode; + liveRegionRef: React.RefObject; + announcementTimeoutRef: React.MutableRefObject< + NodeJS.Timeout | null | undefined + >; + announcementDelay?: number; + clearAnnouncementDelay?: number; + clearAnnouncementTimeoutRef: React.MutableRefObject< + NodeJS.Timeout | number | null | undefined + >; + onAnnouncementClear?: () => void; +}; + +export type LiveRegionProps = { + /** + * The content to be announced by the live region. + */ + children: React.ReactNode; + /** + * The `className` to apply to the live region. + */ + className?: string; + /** + * The `aria-live` politeness level to apply to the live region. + */ + politeness?: "polite" | "assertive" | "off" | null | undefined; + /** + * The `role` to apply to the live region. + */ + role?: typeof ROLES.ALERT | "status" | null | undefined; + /** + * Whether or not the live region should be visible. + */ + visible?: boolean; + /** + * The number of milliseconds to wait before announcing the content. + */ + announcementDelay?: number; + /** + * The number of milliseconds to wait before clearing the announcement. + */ + clearAnnouncementDelay?: number; + /** + * A callback to be invoked when the announcement is cleared. + */ + onAnnouncementClear?: () => void; +}; diff --git a/packages/ui-components/src/components/private/LiveRegion/__tests__/LiveRegion.test.tsx b/packages/ui-components/src/components/private/LiveRegion/__tests__/LiveRegion.test.tsx new file mode 100644 index 00000000..26406f36 --- /dev/null +++ b/packages/ui-components/src/components/private/LiveRegion/__tests__/LiveRegion.test.tsx @@ -0,0 +1,341 @@ +import { act, fireEvent, render } from "@testing-library/react"; +import React from "react"; + +import { VISUALLY_HIDDEN_CLASSNAME } from "../../../../common/constants"; +import { ROLES } from "../constants"; +import { LiveRegion } from "../LiveRegion"; + +const getContent = (renderResult: any) => + renderResult.container.firstChild.textContent; +const getPoliteness = (renderResult: any) => + renderResult.container.firstChild.getAttribute("aria-live"); +const getRole = (renderResult: any) => + renderResult.container.firstChild.getAttribute("role"); + +const content = "Foo Bar Baz"; +const content2 = "Biz buzz"; + +/** + * This is a helper component that renders a LiveRegion and a button. It accepts all of LiveRegion's + * props as well as a `children2` prop. On the first render, it will use the value + * of `props.children`. + * + * Clicking on the "toggle children value" button will cause it to toggle between the `children` and + * `children2` props. + * + * This allows us to simulate click events in order to cause re-renders with different prop values, + * and test the behavior of the LiveRegion as it would be used in an application. + */ +interface LiveRegionProps { + children: React.ReactNode; + children2: React.ReactNode; + politeness: "polite" | "assertive" | null | undefined; + politeness2: "polite" | "assertive" | null | undefined; + toggleChildrenButtonLabel: string; + togglePolitenessButtonLabel: string; +} +class LiveRegionPropChanger extends React.PureComponent { + static defaultProps = { + toggleChildrenButtonLabel: "toggle children value", + togglePolitenessButtonLabel: "toggle politeness value", + }; + + state = { + renderChildren2: false, + renderPoliteness2: false, + }; + + onToggleChildrenClick = () => { + this.setState((prevState: any) => ({ + renderChildren2: !prevState.renderChildren2, + })); + }; + + onTogglePolitenessClick = () => { + this.setState((prevState: any) => ({ + renderPoliteness2: !prevState.renderPoliteness2, + })); + }; + + render() { + const { + children, + children2, + politeness, + politeness2, + toggleChildrenButtonLabel, + togglePolitenessButtonLabel, + ...liveRegionProps + } = this.props; + const { renderChildren2, renderPoliteness2 } = this.state; + + return ( + + + {renderChildren2 ? children2 : children} + + + + + ); + } +} + +describe(`The LiveRegion Component`, () => { + describe(`When it renders without any props`, () => { + let renderResult: any; + beforeEach(() => { + renderResult = render(yo); + }); + + it(`Then it should render with the "assertive" politeness level`, () => { + expect(getPoliteness(renderResult)).toEqual("assertive"); + }); + + it(`Then it should not render with a role`, () => { + expect(getRole(renderResult)).toBeNull(); + }); + + it(`Then it should not render any content into the live region`, () => { + expect(renderResult.container.textContent).toEqual("yo"); + }); + }); + describe(`When announcementDelay is supplied`, () => { + it(`Then it should render with some delay`, () => { + vi.useFakeTimers(); + + const timeout = 3000; + const renderResult = render( + {content}, + ); + expect(getContent(renderResult)).toEqual(""); + vi.advanceTimersByTime(timeout + 1000); + expect(getContent(renderResult)).toEqual("Foo Bar Baz"); + }); + + it(`and with children as React Component Then it should render with some delay`, () => { + vi.useFakeTimers(); + const timeout = 3000; + const children =

{content}

; + const renderResult = render( + {children}, + ); + expect(renderResult.queryByText(content)).toBeNull(); + 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 renderResult = render( + + {content} + , + ); + expect(getContent(renderResult)).toEqual("Foo Bar Baz"); + vi.advanceTimersByTime(clearTimeout); + expect(getContent(renderResult)).toEqual(""); + }); + + it(`and with children as React Component Then it should render with some delay`, () => { + vi.useFakeTimers(); + const clearTimeout = 3000; + const children =

{content}

; + const renderResult = render( + + {children} + , + ); + expect(renderResult.getByText(content)).toBeInTheDocument(); + vi.advanceTimersByTime(clearTimeout); + 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 onAnnouncementClearMock = vi.fn(); + render( + + {content} + , + ); + expect(onAnnouncementClearMock).toHaveBeenCalledTimes(0); + vi.advanceTimersByTime(clearTimeout); + expect(onAnnouncementClearMock).toHaveBeenCalledTimes(1); + }); + }); + describe(`Given a known role with a default politeness is supplied`, () => { + const role = ROLES.ALERT; + + describe(`And the politeness prop is also supplied`, () => { + describe(`When the LiveRegion renders with a different, non-null politeness`, () => { + const politeness = "off"; + let renderResult: any; + beforeEach(() => { + renderResult = render( + + {content} + , + ); + }); + + it.skip(`Then it honors the supplied politeness`, () => { + expect(getPoliteness(renderResult)).toEqual(politeness); + }); + + it.skip(`Then it honors the supplied role`, () => { + expect(getRole(renderResult)).toEqual(role); + }); + + it.skip(`Then it renders the provided children`, () => { + expect(renderResult.getByText(content)).toBeInTheDocument(); + }); + }); + + describe.skip(`When the LiveRegion renders with a null politeness`, () => { + const politeness = null; + let renderResult; + beforeEach(() => { + renderResult = render( + + {content} + , + ); + }); + + it(`Then it should not render with the "aria-live" attribute`, () => { + expect(getPoliteness(renderResult)).toBeNull(); + }); + + it(`Then it honors the supplied role`, () => { + expect(getRole(renderResult)).toEqual(role); + }); + + it(`Then it renders the provided children`, () => { + expect(renderResult.getByText(content)).toBeInTheDocument(); + }); + }); + }); + }); + describe.skip(`When the "visible" prop is set to true`, () => { + it("Then the content should be visible in the DOM", () => { + const { container } = render(Foo); + + expect(container.firstChild).not.toHaveClass("wf-u-visually-hidden"); + }); + }); + + describe.skip.each` + type | children | children2 + ${"string"} | ${content} | ${content2} + ${"React Element"} | ${(

{content}

)} | ${(
{content2}
)} + ${"string then React Element"} | ${content} | ${(
{content2}
)} + `( + `Given that a $type is passed for "props.children"`, + ({ children, children2 }) => { + describe.each` + role | expectedPoliteness | politeness2 + ${null} | ${"assertive"} | ${"polite"} + ${ROLES.ALERT} | ${null} | ${"off"} + ${ROLES.ALERTDIALOG} | ${null} | ${"off"} + ${ROLES.LOG} | ${"polite"} | ${"assertive"} + ${ROLES.MARQUEE} | ${null} | ${"off"} + ${ROLES.PROGRESSBAR} | ${null} | ${"off"} + ${ROLES.STATUS} | ${"polite"} | ${"assertive"} + ${ROLES.TIMER} | ${"assertive"} | ${"polite"} + `( + `And the role is "$role"`, + ({ role, expectedPoliteness, politeness2 }) => { + describe(`When it renders`, () => { + let renderResult; + beforeEach(() => { + renderResult = render( + + {children} + , + ); + }); + + it(`Then it inserts the content into the DOM on first render`, () => { + expect(getContent(renderResult)).toEqual(content); + }); + + it(`Then the politeness is set to "${expectedPoliteness}"`, () => { + expect(getPoliteness(renderResult)).toEqual(expectedPoliteness); + }); + + it(`Then the role is set to "${role}"`, () => { + expect(getRole(renderResult)).toEqual(role); + }); + + it(`Then the content is visually hidden`, () => { + expect(renderResult.container.firstChild).toHaveClass( + VISUALLY_HIDDEN_CLASSNAME, + ); + }); + + describe(`And the "children" prop changes`, () => { + beforeEach(() => { + // Click the toggle children button to update `children` to `children2`. + fireEvent.click( + renderResult.getByText( + LiveRegionPropChanger.defaultProps + .toggleChildrenButtonLabel, + ), + ); + }); + + it(`Then it should replace the initial children with the new children`, () => { + expect(renderResult.queryByText(content)).toBeNull(); + expect(renderResult.getByText(content2)).toBeInTheDocument(); + }); + }); + + describe(`And the "politeness" prop changes`, () => { + beforeEach(() => { + // Click the toggle politeness button to update `politeness` to `politeness2` + fireEvent.click( + renderResult.getByText( + LiveRegionPropChanger.defaultProps + .togglePolitenessButtonLabel, + ), + ); + }); + + it(`Then it should not re-announce`, () => { + /* + * This isn't a great test, but we don't have MutationObserver, and we can't / don't want + * to access the component instance to white box test whether or not it called the method + * that would announce. + */ + expect(getContent(renderResult)).toEqual(content); + }); + + it(`Then it should replace the initial politeness with the new politeness`, () => { + expect(getPoliteness(renderResult)).toEqual(politeness2); + }); + }); + }); + }, + ); + }, + ); +}); diff --git a/packages/ui-components/src/components/private/LiveRegion/constants.ts b/packages/ui-components/src/components/private/LiveRegion/constants.ts new file mode 100644 index 00000000..995347e8 --- /dev/null +++ b/packages/ui-components/src/components/private/LiveRegion/constants.ts @@ -0,0 +1,110 @@ +/** + * The default politeness value for each role. + * + * @enum {String} + */ +export const DEFAULT_POLITENESS_BY_ROLE = { + alert: null, + alertdialog: null, + log: "polite", + marquee: null, + progressbar: null, + status: "polite", + timer: "assertive", +}; + +/** + * All supported `LiveRegion` roles. + * + * @enum {String} + * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques#Live_Region_Roles + */ +export const ROLES = { + /** + * A message with important, and usually time-sensitive + * information. + * + * Default politeness is `null` (does not add the + * `aria-live` attribute). + * + * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Alert_Role + * @see https://www.w3.org/TR/2009/WD-wai-aria-20091215/roles#alert + */ + ALERT: "alert", + + /** + * A type of dialog that contains an alert message, where + * initial focus goes to the dialog or an element + * within it. + * + * Authors SHOULD use aria-describedby on an alertdialog + * to point to the alert message element in the + * dialog. + * + * Default politeness is `null` (does not add the + * `aria-live` attribute). + * + * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_alertdialog_role + * @see https://www.w3.org/TR/2009/WD-wai-aria-20091215/roles#alertdialog + */ + ALERTDIALOG: "alertdialog", + + /** + * A type of live region where new information is added in + * meaningful order and old information may + * disappear. + * + * Default politeness is `"polite"`. + * + * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_log_role + * @see https://www.w3.org/TR/2009/WD-wai-aria-20091215/roles#log + */ + LOG: "log", + + /** + * A type of live region where non-essential information + * changes frequently. The primary difference + * between a marquee and a log is that logs usually have + * a meaningful order or sequence of important + * content changes. + * + * Default politeness is `null` (does not add the + * `aria-live` attribute). + */ + MARQUEE: "marquee", + + /** + * An element that displays the progress status for tasks + * that take a long time. + * + * Default politeness is `null` (does not add the + * `aria-live` attribute). + * + * @see https://www.w3.org/TR/2009/WD-wai-aria-20091215/roles#progressbar + */ + PROGRESSBAR: "progressbar", + + /** + * A container whose content is advisory information for + * the user but is not important enough to justify + * an alert. + * + * Default politeness is `"polite"`. + * + * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_status_role + * @see https://www.w3.org/TR/2009/WD-wai-aria-20091215/roles#status + */ + STATUS: "status", + + /** + * A numerical counter which indicates an amount of + * elapsed time from a start point, or the time remaining + * until an end point. + * + * Default politeness is `"assertive"`. + * + * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/ARIA_timer_role + * @see https://www.w3.org/TR/2009/WD-wai-aria-20091215/roles#timer + */ + TIMER: "timer", +}; diff --git a/packages/ui-components/src/components/private/LiveRegion/utilities.ts b/packages/ui-components/src/components/private/LiveRegion/utilities.ts new file mode 100644 index 00000000..85f8272f --- /dev/null +++ b/packages/ui-components/src/components/private/LiveRegion/utilities.ts @@ -0,0 +1,88 @@ +import React from "react"; +import { renderToString } from "react-dom/server"; + +import type { + announceProps, + ClearAnnouncementProps, + conditionallyDelayAnnouncementProps, +} from "./LiveRegionTypes"; + +/** + * Removes the content from the live region. + */ +const clearAnnouncement = ({ + liveRegionRef, + onAnnouncementClear, +}: ClearAnnouncementProps) => { + if (liveRegionRef.current) { + liveRegionRef.current.innerHTML = ""; + } + onAnnouncementClear && onAnnouncementClear(); +}; + +/** + * Announce the content of "children". + */ +export const announce = ({ + children, + liveRegionRef, + clearAnnouncementDelay, + clearAnnouncementTimeoutRef, + onAnnouncementClear, +}: announceProps) => { + if (clearAnnouncementTimeoutRef?.current !== null) { + clearTimeout(clearAnnouncementTimeoutRef.current as unknown as number); + } + + if (children !== null && liveRegionRef.current) { + liveRegionRef.current.innerHTML = renderToString( + children as React.ReactElement, + ); + } + + if (clearAnnouncementDelay) { + clearAnnouncementTimeoutRef.current = setTimeout( + () => + clearAnnouncement({ + liveRegionRef, + onAnnouncementClear, + }), + clearAnnouncementDelay, + ); + } +}; + +/** + * Inserts the content into the live region with some + * delay if the announcementDelay prop is supplied. + * Otherwise inserts the content immediately. + */ +export const conditionallyDelayAnnouncement = ({ + children, + liveRegionRef, + announcementTimeoutRef, + announcementDelay, + clearAnnouncementDelay, + clearAnnouncementTimeoutRef, + onAnnouncementClear, +}: conditionallyDelayAnnouncementProps) => { + clearTimeout(announcementTimeoutRef.current as unknown as number); + + if (announcementDelay) { + announcementTimeoutRef.current = setTimeout(announce, announcementDelay, { + children, + liveRegionRef, + clearAnnouncementDelay, + clearAnnouncementTimeoutRef, + onAnnouncementClear, + }); + } else { + announce({ + children, + liveRegionRef, + clearAnnouncementDelay, + clearAnnouncementTimeoutRef, + onAnnouncementClear, + }); + } +}; From 6a67216aaf938f872d7a197383c6bf83d8c1b160 Mon Sep 17 00:00:00 2001 From: Arno V Date: Tue, 21 Nov 2023 19:33:36 -0500 Subject: [PATCH 2/4] chore: refactor --- .../private/LiveRegion/LiveRegion.tsx | 4 +- .../private/LiveRegion/LiveRegionTypes.d.ts | 4 ++ .../LiveRegion/__tests__/LiveRegion.test.tsx | 38 ++++++++++--------- .../private/LiveRegion/constants.ts | 4 +- 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/packages/ui-components/src/components/private/LiveRegion/LiveRegion.tsx b/packages/ui-components/src/components/private/LiveRegion/LiveRegion.tsx index cf0972b3..de002f8b 100644 --- a/packages/ui-components/src/components/private/LiveRegion/LiveRegion.tsx +++ b/packages/ui-components/src/components/private/LiveRegion/LiveRegion.tsx @@ -34,9 +34,7 @@ export function LiveRegion({ * and we want to honor when the user supplies `null`. */ if (typeof politeness === "undefined") { - politeness = role - ? (DEFAULT_POLITENESS_BY_ROLE[role] as "polite" | "assertive") - : "assertive"; + politeness = role ? DEFAULT_POLITENESS_BY_ROLE[role] : "assertive"; } useEffect(() => { diff --git a/packages/ui-components/src/components/private/LiveRegion/LiveRegionTypes.d.ts b/packages/ui-components/src/components/private/LiveRegion/LiveRegionTypes.d.ts index e8c8d713..7f088f46 100644 --- a/packages/ui-components/src/components/private/LiveRegion/LiveRegionTypes.d.ts +++ b/packages/ui-components/src/components/private/LiveRegion/LiveRegionTypes.d.ts @@ -1,5 +1,9 @@ import { ROLES } from "./constants"; +export type PolitenessByRole = { + [key: string]: any; +}; + export type ClearAnnouncementProps = { liveRegionRef: React.RefObject; onAnnouncementClear?: () => void; diff --git a/packages/ui-components/src/components/private/LiveRegion/__tests__/LiveRegion.test.tsx b/packages/ui-components/src/components/private/LiveRegion/__tests__/LiveRegion.test.tsx index 26406f36..0cf8bae1 100644 --- a/packages/ui-components/src/components/private/LiveRegion/__tests__/LiveRegion.test.tsx +++ b/packages/ui-components/src/components/private/LiveRegion/__tests__/LiveRegion.test.tsx @@ -1,4 +1,4 @@ -import { act, fireEvent, render } from "@testing-library/react"; +import { fireEvent, render } from "@testing-library/react"; import React from "react"; import { VISUALLY_HIDDEN_CLASSNAME } from "../../../../common/constants"; @@ -27,12 +27,13 @@ const content2 = "Biz buzz"; * and test the behavior of the LiveRegion as it would be used in an application. */ interface LiveRegionProps { - children: React.ReactNode; - children2: React.ReactNode; - politeness: "polite" | "assertive" | null | undefined; - politeness2: "polite" | "assertive" | null | undefined; - toggleChildrenButtonLabel: string; - togglePolitenessButtonLabel: string; + children?: React.ReactNode; + children2?: React.ReactNode; + politeness?: "polite" | "assertive" | null | undefined; + politeness2?: "polite" | "assertive" | null | undefined; + toggleChildrenButtonLabel?: string; + togglePolitenessButtonLabel?: string; + role?: string; } class LiveRegionPropChanger extends React.PureComponent { static defaultProps = { @@ -193,22 +194,22 @@ describe(`The LiveRegion Component`, () => { ); }); - it.skip(`Then it honors the supplied politeness`, () => { + it(`Then it honors the supplied politeness`, () => { expect(getPoliteness(renderResult)).toEqual(politeness); }); - it.skip(`Then it honors the supplied role`, () => { + it(`Then it honors the supplied role`, () => { expect(getRole(renderResult)).toEqual(role); }); - it.skip(`Then it renders the provided children`, () => { + it(`Then it renders the provided children`, () => { expect(renderResult.getByText(content)).toBeInTheDocument(); }); }); - describe.skip(`When the LiveRegion renders with a null politeness`, () => { + describe(`When the LiveRegion renders with a null politeness`, () => { const politeness = null; - let renderResult; + let renderResult: any; beforeEach(() => { renderResult = render( @@ -231,15 +232,15 @@ describe(`The LiveRegion Component`, () => { }); }); }); - describe.skip(`When the "visible" prop is set to true`, () => { + describe(`When the "visible" prop is set to true`, () => { it("Then the content should be visible in the DOM", () => { const { container } = render(Foo); - expect(container.firstChild).not.toHaveClass("wf-u-visually-hidden"); + expect(container.firstChild).not.toHaveClass(VISUALLY_HIDDEN_CLASSNAME); }); }); - describe.skip.each` + describe.each` type | children | children2 ${"string"} | ${content} | ${content2} ${"React Element"} | ${(

{content}

)} | ${(
{content2}
)} @@ -261,7 +262,7 @@ describe(`The LiveRegion Component`, () => { `And the role is "$role"`, ({ role, expectedPoliteness, politeness2 }) => { describe(`When it renders`, () => { - let renderResult; + let renderResult: any; beforeEach(() => { renderResult = render( { describe(`And the "children" prop changes`, () => { beforeEach(() => { - // Click the toggle children button to update `children` to `children2`. + /** + * Click the toggle children button to update + * `children` to `children2`. + */ fireEvent.click( renderResult.getByText( LiveRegionPropChanger.defaultProps diff --git a/packages/ui-components/src/components/private/LiveRegion/constants.ts b/packages/ui-components/src/components/private/LiveRegion/constants.ts index 995347e8..ced1dcf9 100644 --- a/packages/ui-components/src/components/private/LiveRegion/constants.ts +++ b/packages/ui-components/src/components/private/LiveRegion/constants.ts @@ -1,9 +1,11 @@ +import { PolitenessByRole } from "./LiveRegionTypes"; + /** * The default politeness value for each role. * * @enum {String} */ -export const DEFAULT_POLITENESS_BY_ROLE = { +export const DEFAULT_POLITENESS_BY_ROLE: PolitenessByRole = { alert: null, alertdialog: null, log: "polite", From a75e97c7ddc8b2f99e558364a2e35492f213bdaf Mon Sep 17 00:00:00 2001 From: Arno V Date: Tue, 21 Nov 2023 19:39:07 -0500 Subject: [PATCH 3/4] Update useUniqueId.ts --- packages/ui-components/src/common/hooks/useUniqueId.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui-components/src/common/hooks/useUniqueId.ts b/packages/ui-components/src/common/hooks/useUniqueId.ts index 8ba757a4..f8729b7f 100644 --- a/packages/ui-components/src/common/hooks/useUniqueId.ts +++ b/packages/ui-components/src/common/hooks/useUniqueId.ts @@ -27,9 +27,9 @@ import { useRef } from "react"; * // -> inputId = "av-text-input-42" * * const inputHintId = useUniqueId({ - * prefix: "pnr-text-input-hint-", + * prefix: "av-text-input-hint-", * }); - * // -> inputHintId = "pnr-text-input-hint-1j3h4f5" + * // -> inputHintId = "av-text-input-hint-1j3h4f5" * */ From 79765233b25f8253ee67a8804139632ff91f08f1 Mon Sep 17 00:00:00 2001 From: Arno V Date: Tue, 21 Nov 2023 19:41:47 -0500 Subject: [PATCH 4/4] Update utilities.ts --- .../src/components/private/LiveRegion/utilities.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ui-components/src/components/private/LiveRegion/utilities.ts b/packages/ui-components/src/components/private/LiveRegion/utilities.ts index 85f8272f..e30d954f 100644 --- a/packages/ui-components/src/components/private/LiveRegion/utilities.ts +++ b/packages/ui-components/src/components/private/LiveRegion/utilities.ts @@ -17,7 +17,10 @@ const clearAnnouncement = ({ if (liveRegionRef.current) { liveRegionRef.current.innerHTML = ""; } - onAnnouncementClear && onAnnouncementClear(); + + if (typeof onAnnouncementClear === "function") { + onAnnouncementClear(); + } }; /**