From ba53e408ec010d8a088a58d3f0a8c7ad3a570b3c Mon Sep 17 00:00:00 2001 From: Marcy Sutton-Todd Date: Thu, 16 Jan 2025 14:34:36 -0800 Subject: [PATCH 01/22] Announcer: Part 1 (#2362) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The initial implementation for a live region component! I'm getting this draft PR up so I can test with the remote URL. Issue: https://khanacademy.atlassian.net/browse/WB-1768 Outstanding questions/work areas: - Testing in more ATs, particularly in webapp and on mobile devices. - More integration with React: usage for JSX, debouncing with custom duration in continuously re-rendering components like the Video Player with Clarifications, etc. Play with the Story with screen readers turned on 1. VoiceOver on OSX 2. VoiceOver on iOS 3. NVDA on Windows 4. JAWS on Windows Author: marcysutton Reviewers: jandrade, marcysutton, beaesguerra, nishasy, nedredmond, #wonder-blocks Required Reviewers: Approved By: beaesguerra Checks: ⌛ Test / Test (ubuntu-latest, 20.x, 2/2), ⌛ Lint / Lint (ubuntu-latest, 20.x), ⌛ Test / Test (ubuntu-latest, 20.x, 1/2), ⌛ Check build sizes (ubuntu-latest, 20.x), ⌛ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ⌛ Chromatic - Build on regular PRs / chromatic (ubuntu-latest, 20.x), ✅ Prime node_modules cache for primary configuration (ubuntu-latest, 20.x), ⏭️ Chromatic - Skip on Release PR (changesets), ✅ gerald, ⏭️ dependabot Pull Request URL: https://github.com/Khan/wonder-blocks/pull/2362 --- .storybook/preview.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index c8e137294..e3554e1dd 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -111,6 +111,7 @@ const withThemeSwitcher: Decorator = ( } }; }, [addBodyClass]); + if (enableRenderStateRootDecorator) { return ( From f8101cf1ad9458621fc85ea61bfd92319d2980b4 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Fri, 21 Feb 2025 12:10:40 -0800 Subject: [PATCH 02/22] [combobox-announcer] Expose string or node from opener --- .../src/components/multi-select.tsx | 36 +++++++++++++-- .../src/components/single-select.tsx | 45 ++++++++++++++++--- .../src/util/__tests__/helpers.test.tsx | 21 ++++++++- .../src/util/helpers.ts | 11 +++-- 4 files changed, 98 insertions(+), 15 deletions(-) diff --git a/packages/wonder-blocks-dropdown/src/components/multi-select.tsx b/packages/wonder-blocks-dropdown/src/components/multi-select.tsx index aa13d9327..24fb29717 100644 --- a/packages/wonder-blocks-dropdown/src/components/multi-select.tsx +++ b/packages/wonder-blocks-dropdown/src/components/multi-select.tsx @@ -7,6 +7,7 @@ import { type StyleType, } from "@khanacademy/wonder-blocks-core"; +import {announceMessage} from "@khanacademy/wonder-blocks-announcer"; import ActionItem from "./action-item"; import DropdownCore from "./dropdown-core"; import DropdownOpener from "./dropdown-opener"; @@ -25,7 +26,11 @@ import type { OptionItemComponent, OptionItemComponentArray, } from "../util/types"; -import {getLabel, getSelectOpenerLabel} from "../util/helpers"; +import { + getLabel, + getSelectOpenerLabel, + type OpenerStringOrNode, +} from "../util/helpers"; import {useSelectValidation} from "../hooks/use-select-validation"; export type LabelsValues = { @@ -369,7 +374,7 @@ const MultiSelect = (props: Props) => { const getMenuTextOrNode = ( children: OptionItemComponentArray, - ): string | JSX.Element => { + ): string | {[key: string]: string | JSX.Element} => { const {noneSelected, someSelected, allSelected} = labels; const numSelectedAll = children.filter( (option) => !option.props.disabled, @@ -404,7 +409,6 @@ const MultiSelect = (props: Props) => { return someSelected(1); } } - return noSelectionText; case numSelectedAll: return allSelected; @@ -536,6 +540,26 @@ const MultiSelect = (props: Props) => { handleOpenChanged(!open); }; + const handleAnnouncement = (message: string) => { + announceMessage({ + message, + }); + }; + + const maybeExtractStringFromNode = ( + openerContent: OpenerStringOrNode, + ): [string, string | JSX.Element] => { + // For a selected Custom Option Item with Node Label, + // we have to extract a string to announce + if (typeof openerContent === "object") { + const [label, node] = Object.entries(openerContent)[0]; + return [label, node]; + } else { + // For other cases, we can use the string content passed through + return [openerContent, openerContent]; + } + }; + const renderOpener = ( allChildren: React.ReactElement< React.ComponentProps @@ -547,7 +571,11 @@ const MultiSelect = (props: Props) => { | React.ReactElement> => { const {noneSelected} = labels; - const menuContent = getMenuTextOrNode(allChildren); + const menuTextOrNode = getMenuTextOrNode(allChildren); + const [menuStringLabel, menuContent] = + maybeExtractStringFromNode(menuTextOrNode); + + handleAnnouncement(menuStringLabel); const dropdownOpener = ( diff --git a/packages/wonder-blocks-dropdown/src/components/single-select.tsx b/packages/wonder-blocks-dropdown/src/components/single-select.tsx index e77e3ca9f..e1934db25 100644 --- a/packages/wonder-blocks-dropdown/src/components/single-select.tsx +++ b/packages/wonder-blocks-dropdown/src/components/single-select.tsx @@ -7,6 +7,7 @@ import { type StyleType, } from "@khanacademy/wonder-blocks-core"; +import {announceMessage} from "@khanacademy/wonder-blocks-announcer"; import DropdownCore from "./dropdown-core"; import DropdownOpener from "./dropdown-opener"; import SelectOpener from "./select-opener"; @@ -22,7 +23,11 @@ import type { OpenerProps, OptionItemComponentArray, } from "../util/types"; -import {getLabel, getSelectOpenerLabel} from "../util/helpers"; +import { + getLabel, + getSelectOpenerLabel, + OpenerStringOrNode, +} from "../util/helpers"; import {useSelectValidation} from "../hooks/use-select-validation"; export type SingleSelectLabelsValues = { @@ -430,6 +435,20 @@ const SingleSelect = (props: Props) => { handleOpenChanged(!open); }; + const maybeExtractStringFromNode = ( + openerContent: OpenerStringOrNode, + ): [string, string | JSX.Element] => { + // For a selected Custom Option Item with Node Label, + // we have to extract a string to announce + if (typeof openerContent === "object") { + const [label, node] = Object.entries(openerContent)[0]; + return [label, node]; + } else { + // For other cases, we can use the string content passed through + return [openerContent, openerContent]; + } + }; + const renderOpener = ( isDisabled: boolean, dropdownId: string, @@ -444,9 +463,23 @@ const SingleSelect = (props: Props) => { ); // If nothing is selected, or if the selectedValue doesn't match any // item in the menu, use the placeholder. - const menuText = selectedItem - ? getSelectOpenerLabel(showOpenerLabelAsText, selectedItem.props) - : placeholder; + + let menuContent; + + if (selectedItem) { + const menuOpenerLabel = getSelectOpenerLabel( + showOpenerLabelAsText, + selectedItem.props, + ); + // For Custom Option Items with Node Labels, we have to extract + // strings to announce + const [label, node] = maybeExtractStringFromNode(menuOpenerLabel); + menuContent = node; + + announceMessage({message: label}); + } else { + menuContent = placeholder; + } const dropdownOpener = ( @@ -461,7 +494,7 @@ const SingleSelect = (props: Props) => { disabled={isDisabled} ref={handleOpenerRef} role="combobox" - text={menuText} + text={menuContent} opened={open} error={hasError} onBlur={onOpenerBlurValidation} @@ -483,7 +516,7 @@ const SingleSelect = (props: Props) => { testId={testId} onBlur={onOpenerBlurValidation} > - {menuText} + {menuContent} ); }} diff --git a/packages/wonder-blocks-dropdown/src/util/__tests__/helpers.test.tsx b/packages/wonder-blocks-dropdown/src/util/__tests__/helpers.test.tsx index 3ac0ffd26..70c6cfade 100644 --- a/packages/wonder-blocks-dropdown/src/util/__tests__/helpers.test.tsx +++ b/packages/wonder-blocks-dropdown/src/util/__tests__/helpers.test.tsx @@ -126,7 +126,7 @@ describe("getLabel", () => { }); describe("getSelectOpenerLabel", () => { - it("should return the label if the label is a Node and showOpenerLabelAsText is true", () => { + it("should return an object if the label is a Node and showOpenerLabelAsText is true", () => { // Arrange const props: PropsFor = { label:
a custom node
, @@ -135,12 +135,29 @@ describe("getSelectOpenerLabel", () => { }; // Act - const label = getSelectOpenerLabel(false, props); + const labelObj = getSelectOpenerLabel(false, props); + const label = Object.values(labelObj)[0]; // Assert expect(label).toStrictEqual(
a custom node
); }); + it("should return a string as an object key if label is a Node and labelAsText is populated", () => { + // Arrange + const props: PropsFor = { + label:
a custom node
, + labelAsText: "plain text", + value: "foo", + }; + + // Act + const labelObj = getSelectOpenerLabel(false, props); + const label = Object.keys(labelObj)[0]; + + // Assert + expect(label).toStrictEqual("plain text"); + }); + it("should return a string if the label is a Node and showOpenerLabelAsText is false", () => { // Arrange const props: PropsFor = { diff --git a/packages/wonder-blocks-dropdown/src/util/helpers.ts b/packages/wonder-blocks-dropdown/src/util/helpers.ts index e6f0f7232..884b7e476 100644 --- a/packages/wonder-blocks-dropdown/src/util/helpers.ts +++ b/packages/wonder-blocks-dropdown/src/util/helpers.ts @@ -71,6 +71,8 @@ export function getLabel(props: OptionItemProps): string { return ""; } +export type OpenerStringOrNode = string | {[key: string]: string | JSX.Element}; + /** * Returns the label for the SelectOpener in the SingleSelect and MultiSelect. * If the label is a Node, and `labelAsText` is undefined, returns the label. @@ -78,9 +80,12 @@ export function getLabel(props: OptionItemProps): string { export function getSelectOpenerLabel( showOpenerLabelAsText: boolean, props: OptionItemProps, -): string | JSX.Element { +): OpenerStringOrNode { + const stringLabel = getLabel(props); if (showOpenerLabelAsText) { - return getLabel(props); + return stringLabel; } - return props.label; + return { + [stringLabel]: props.label, + }; } From 48bd83e27e86bdebcd65fe73f5ceb45265f63f95 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Fri, 21 Feb 2025 12:25:09 -0800 Subject: [PATCH 03/22] [combobox-announcer] Extract function into helper file --- .../src/components/multi-select.tsx | 16 +--------- .../src/components/single-select.tsx | 16 +--------- .../src/util/__tests__/helpers.test.tsx | 30 +++++++++++++++++++ .../src/util/helpers.ts | 20 ++++++++++++- 4 files changed, 51 insertions(+), 31 deletions(-) diff --git a/packages/wonder-blocks-dropdown/src/components/multi-select.tsx b/packages/wonder-blocks-dropdown/src/components/multi-select.tsx index 24fb29717..4909f16b4 100644 --- a/packages/wonder-blocks-dropdown/src/components/multi-select.tsx +++ b/packages/wonder-blocks-dropdown/src/components/multi-select.tsx @@ -29,7 +29,7 @@ import type { import { getLabel, getSelectOpenerLabel, - type OpenerStringOrNode, + maybeExtractStringFromNode, } from "../util/helpers"; import {useSelectValidation} from "../hooks/use-select-validation"; @@ -546,20 +546,6 @@ const MultiSelect = (props: Props) => { }); }; - const maybeExtractStringFromNode = ( - openerContent: OpenerStringOrNode, - ): [string, string | JSX.Element] => { - // For a selected Custom Option Item with Node Label, - // we have to extract a string to announce - if (typeof openerContent === "object") { - const [label, node] = Object.entries(openerContent)[0]; - return [label, node]; - } else { - // For other cases, we can use the string content passed through - return [openerContent, openerContent]; - } - }; - const renderOpener = ( allChildren: React.ReactElement< React.ComponentProps diff --git a/packages/wonder-blocks-dropdown/src/components/single-select.tsx b/packages/wonder-blocks-dropdown/src/components/single-select.tsx index e1934db25..33315d043 100644 --- a/packages/wonder-blocks-dropdown/src/components/single-select.tsx +++ b/packages/wonder-blocks-dropdown/src/components/single-select.tsx @@ -26,7 +26,7 @@ import type { import { getLabel, getSelectOpenerLabel, - OpenerStringOrNode, + maybeExtractStringFromNode, } from "../util/helpers"; import {useSelectValidation} from "../hooks/use-select-validation"; @@ -435,20 +435,6 @@ const SingleSelect = (props: Props) => { handleOpenChanged(!open); }; - const maybeExtractStringFromNode = ( - openerContent: OpenerStringOrNode, - ): [string, string | JSX.Element] => { - // For a selected Custom Option Item with Node Label, - // we have to extract a string to announce - if (typeof openerContent === "object") { - const [label, node] = Object.entries(openerContent)[0]; - return [label, node]; - } else { - // For other cases, we can use the string content passed through - return [openerContent, openerContent]; - } - }; - const renderOpener = ( isDisabled: boolean, dropdownId: string, diff --git a/packages/wonder-blocks-dropdown/src/util/__tests__/helpers.test.tsx b/packages/wonder-blocks-dropdown/src/util/__tests__/helpers.test.tsx index 70c6cfade..001a0cce5 100644 --- a/packages/wonder-blocks-dropdown/src/util/__tests__/helpers.test.tsx +++ b/packages/wonder-blocks-dropdown/src/util/__tests__/helpers.test.tsx @@ -6,6 +6,7 @@ import { getLabel, getSelectOpenerLabel, getStringForKey, + maybeExtractStringFromNode, } from "../helpers"; describe("getStringForKey", () => { @@ -173,3 +174,32 @@ describe("getSelectOpenerLabel", () => { expect(label).toBe("plain text"); }); }); + +describe("maybeExtractStringFromNode", () => { + it("should return an array with two strings if opener content is a string", () => { + // Arrange + const input = "a string"; + + // Act + const [definitelyALabel, theSameLabel] = + maybeExtractStringFromNode(input); + + // Assert + expect(definitelyALabel).toStrictEqual("a string"); + expect(theSameLabel).toStrictEqual("a string"); + }); + + it("should return an array with a string and node if opener content is a node", () => { + // Arrange + const input = { + "a string":
a custom node
, + }; + + // Act + const [label, node] = maybeExtractStringFromNode(input); + + // Assert + expect(label).toStrictEqual("a string"); + expect(node).toStrictEqual(
a custom node
); + }); +}); diff --git a/packages/wonder-blocks-dropdown/src/util/helpers.ts b/packages/wonder-blocks-dropdown/src/util/helpers.ts index 884b7e476..90fdd7eab 100644 --- a/packages/wonder-blocks-dropdown/src/util/helpers.ts +++ b/packages/wonder-blocks-dropdown/src/util/helpers.ts @@ -71,7 +71,7 @@ export function getLabel(props: OptionItemProps): string { return ""; } -export type OpenerStringOrNode = string | {[key: string]: string | JSX.Element}; +type OpenerStringOrNode = string | {[key: string]: string | JSX.Element}; /** * Returns the label for the SelectOpener in the SingleSelect and MultiSelect. @@ -89,3 +89,21 @@ export function getSelectOpenerLabel( [stringLabel]: props.label, }; } + +/** + * Returns a normalized structure for Opener content when Options can be either + * strings OR nodes with various label props + */ +export const maybeExtractStringFromNode = ( + openerContent: OpenerStringOrNode, +): [string, string | JSX.Element] => { + // For a selected Custom Option Item with Node Label, + // we have to extract a string to announce + if (typeof openerContent === "object") { + const [label, node] = Object.entries(openerContent)[0]; + return [label, node]; + } else { + // For other cases, we can use the string content passed through + return [openerContent, openerContent]; + } +}; From 7ab82b87799089bd9146ef21a990aced9b809cc6 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Fri, 21 Feb 2025 12:45:17 -0800 Subject: [PATCH 04/22] [combobox-announcer] Move announcer types into src --- .../wonder-blocks-announcer/src/__tests__/util/dom.test.ts | 2 +- .../src/__tests__/util/test-utilities.ts | 2 +- packages/wonder-blocks-announcer/src/announce-message.ts | 2 +- packages/wonder-blocks-announcer/src/announcer.ts | 2 +- .../{ => src}/types/announcer.types.ts | 0 packages/wonder-blocks-announcer/src/util/dom.ts | 5 +---- 6 files changed, 5 insertions(+), 8 deletions(-) rename packages/wonder-blocks-announcer/{ => src}/types/announcer.types.ts (100%) diff --git a/packages/wonder-blocks-announcer/src/__tests__/util/dom.test.ts b/packages/wonder-blocks-announcer/src/__tests__/util/dom.test.ts index cfed9fe46..bd9a7806e 100644 --- a/packages/wonder-blocks-announcer/src/__tests__/util/dom.test.ts +++ b/packages/wonder-blocks-announcer/src/__tests__/util/dom.test.ts @@ -5,7 +5,7 @@ import { createRegion, removeMessage, } from "../../util/dom"; -import {PolitenessLevel} from "../../../types/announcer.types"; +import {PolitenessLevel} from "../../types/announcer.types"; jest.useFakeTimers(); jest.spyOn(global, "setTimeout"); diff --git a/packages/wonder-blocks-announcer/src/__tests__/util/test-utilities.ts b/packages/wonder-blocks-announcer/src/__tests__/util/test-utilities.ts index 1f361ac2b..2dc99c85a 100644 --- a/packages/wonder-blocks-announcer/src/__tests__/util/test-utilities.ts +++ b/packages/wonder-blocks-announcer/src/__tests__/util/test-utilities.ts @@ -1,4 +1,4 @@ -import type {RegionDef, PolitenessLevel} from "../../../types/announcer.types"; +import type {RegionDef, PolitenessLevel} from "../../types/announcer.types"; export function createTestRegionList( level: PolitenessLevel, diff --git a/packages/wonder-blocks-announcer/src/announce-message.ts b/packages/wonder-blocks-announcer/src/announce-message.ts index 8bec5624e..596012a15 100644 --- a/packages/wonder-blocks-announcer/src/announce-message.ts +++ b/packages/wonder-blocks-announcer/src/announce-message.ts @@ -1,4 +1,4 @@ -import type {PolitenessLevel} from "../types/announcer.types"; +import type {PolitenessLevel} from "./types/announcer.types"; import Announcer from "./announcer"; export type AnnounceMessageProps = { diff --git a/packages/wonder-blocks-announcer/src/announcer.ts b/packages/wonder-blocks-announcer/src/announcer.ts index 2739c03b8..a3829dde9 100644 --- a/packages/wonder-blocks-announcer/src/announcer.ts +++ b/packages/wonder-blocks-announcer/src/announcer.ts @@ -3,7 +3,7 @@ import { RegionFactory, RegionDictionary, RegionDef, -} from "../types/announcer.types"; +} from "./types/announcer.types"; import { createRegionWrapper, diff --git a/packages/wonder-blocks-announcer/types/announcer.types.ts b/packages/wonder-blocks-announcer/src/types/announcer.types.ts similarity index 100% rename from packages/wonder-blocks-announcer/types/announcer.types.ts rename to packages/wonder-blocks-announcer/src/types/announcer.types.ts diff --git a/packages/wonder-blocks-announcer/src/util/dom.ts b/packages/wonder-blocks-announcer/src/util/dom.ts index c89e5c8cc..893c53c0b 100644 --- a/packages/wonder-blocks-announcer/src/util/dom.ts +++ b/packages/wonder-blocks-announcer/src/util/dom.ts @@ -1,7 +1,4 @@ -import { - type PolitenessLevel, - RegionDictionary, -} from "../../types/announcer.types"; +import {type PolitenessLevel, RegionDictionary} from "../types/announcer.types"; /** * Create a wrapper element to group regions for a given level From 2487ddce9a2c60714412765d0005b44e03ff345d Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Mon, 24 Feb 2025 08:37:02 -0800 Subject: [PATCH 05/22] [combobox-announcer] docs(changeset): Introducing WB Announcer API for ARIA Live Regions --- .changeset/clean-peas-prove.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/clean-peas-prove.md diff --git a/.changeset/clean-peas-prove.md b/.changeset/clean-peas-prove.md new file mode 100644 index 000000000..447312521 --- /dev/null +++ b/.changeset/clean-peas-prove.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/wonder-blocks-announcer": major +--- + +Introducing WB Announcer API for ARIA Live Regions From 8a02ca8749061ce891c00a343b63e388d40e9f43 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Mon, 24 Feb 2025 09:15:01 -0800 Subject: [PATCH 06/22] [combobox-announcer] Move types around for package --- .../wonder-blocks-announcer/src/__tests__/util/dom.test.ts | 2 +- .../src/__tests__/util/test-utilities.ts | 2 +- packages/wonder-blocks-announcer/src/announce-message.ts | 2 +- packages/wonder-blocks-announcer/src/announcer.ts | 4 ++-- packages/wonder-blocks-announcer/src/index.ts | 6 ++++-- .../src/{types => util}/announcer.types.ts | 0 packages/wonder-blocks-announcer/src/util/dom.ts | 2 +- 7 files changed, 10 insertions(+), 8 deletions(-) rename packages/wonder-blocks-announcer/src/{types => util}/announcer.types.ts (100%) diff --git a/packages/wonder-blocks-announcer/src/__tests__/util/dom.test.ts b/packages/wonder-blocks-announcer/src/__tests__/util/dom.test.ts index bd9a7806e..42cd069ce 100644 --- a/packages/wonder-blocks-announcer/src/__tests__/util/dom.test.ts +++ b/packages/wonder-blocks-announcer/src/__tests__/util/dom.test.ts @@ -5,7 +5,7 @@ import { createRegion, removeMessage, } from "../../util/dom"; -import {PolitenessLevel} from "../../types/announcer.types"; +import {type PolitenessLevel} from "../../util/announcer.types"; jest.useFakeTimers(); jest.spyOn(global, "setTimeout"); diff --git a/packages/wonder-blocks-announcer/src/__tests__/util/test-utilities.ts b/packages/wonder-blocks-announcer/src/__tests__/util/test-utilities.ts index 2dc99c85a..20ed2fb98 100644 --- a/packages/wonder-blocks-announcer/src/__tests__/util/test-utilities.ts +++ b/packages/wonder-blocks-announcer/src/__tests__/util/test-utilities.ts @@ -1,4 +1,4 @@ -import type {RegionDef, PolitenessLevel} from "../../types/announcer.types"; +import type {RegionDef, PolitenessLevel} from "../../util/announcer.types"; export function createTestRegionList( level: PolitenessLevel, diff --git a/packages/wonder-blocks-announcer/src/announce-message.ts b/packages/wonder-blocks-announcer/src/announce-message.ts index 596012a15..f6bee4259 100644 --- a/packages/wonder-blocks-announcer/src/announce-message.ts +++ b/packages/wonder-blocks-announcer/src/announce-message.ts @@ -1,4 +1,4 @@ -import type {PolitenessLevel} from "./types/announcer.types"; +import type {PolitenessLevel} from "./util/announcer.types"; import Announcer from "./announcer"; export type AnnounceMessageProps = { diff --git a/packages/wonder-blocks-announcer/src/announcer.ts b/packages/wonder-blocks-announcer/src/announcer.ts index a3829dde9..df796fd4a 100644 --- a/packages/wonder-blocks-announcer/src/announcer.ts +++ b/packages/wonder-blocks-announcer/src/announcer.ts @@ -1,9 +1,9 @@ -import { +import type { PolitenessLevel, RegionFactory, RegionDictionary, RegionDef, -} from "./types/announcer.types"; +} from "./util/announcer.types"; import { createRegionWrapper, diff --git a/packages/wonder-blocks-announcer/src/index.ts b/packages/wonder-blocks-announcer/src/index.ts index c87dd6045..ecf2fd0c8 100644 --- a/packages/wonder-blocks-announcer/src/index.ts +++ b/packages/wonder-blocks-announcer/src/index.ts @@ -1,4 +1,6 @@ -import {announceMessage, type AnnounceMessageProps} from "./announce-message"; +import {announceMessage} from "./announce-message"; import {clearMessages} from "./clear-messages"; +import type {AnnounceMessageProps} from "./announce-message"; -export {announceMessage, type AnnounceMessageProps, clearMessages}; +export {announceMessage, clearMessages}; +export {type AnnounceMessageProps}; diff --git a/packages/wonder-blocks-announcer/src/types/announcer.types.ts b/packages/wonder-blocks-announcer/src/util/announcer.types.ts similarity index 100% rename from packages/wonder-blocks-announcer/src/types/announcer.types.ts rename to packages/wonder-blocks-announcer/src/util/announcer.types.ts diff --git a/packages/wonder-blocks-announcer/src/util/dom.ts b/packages/wonder-blocks-announcer/src/util/dom.ts index 893c53c0b..4abb7dfb9 100644 --- a/packages/wonder-blocks-announcer/src/util/dom.ts +++ b/packages/wonder-blocks-announcer/src/util/dom.ts @@ -1,4 +1,4 @@ -import {type PolitenessLevel, RegionDictionary} from "../types/announcer.types"; +import type {PolitenessLevel, RegionDictionary} from "./announcer.types"; /** * Create a wrapper element to group regions for a given level From 59fab1c4bbac009d3cf9faa2ba8fbbc4170fafb0 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Mon, 24 Feb 2025 10:00:07 -0800 Subject: [PATCH 07/22] [combobox-announcer] Add announcer to dropdown dependencies --- packages/wonder-blocks-dropdown/package.json | 1 + packages/wonder-blocks-dropdown/tsconfig-build.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/wonder-blocks-dropdown/package.json b/packages/wonder-blocks-dropdown/package.json index 6e7e003a0..97f4ba7ee 100644 --- a/packages/wonder-blocks-dropdown/package.json +++ b/packages/wonder-blocks-dropdown/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@babel/runtime": "^7.24.5", + "@khanacademy/wonder-blocks-announcer": "workspace:*", "@khanacademy/wonder-blocks-cell": "workspace:*", "@khanacademy/wonder-blocks-clickable": "workspace:*", "@khanacademy/wonder-blocks-core": "workspace:*", diff --git a/packages/wonder-blocks-dropdown/tsconfig-build.json b/packages/wonder-blocks-dropdown/tsconfig-build.json index 883d3dc0c..6489ae14a 100644 --- a/packages/wonder-blocks-dropdown/tsconfig-build.json +++ b/packages/wonder-blocks-dropdown/tsconfig-build.json @@ -6,6 +6,7 @@ "rootDir": "src", }, "references": [ + {"path": "../wonder-blocks-announcer/tsconfig-build.json"}, {"path": "../wonder-blocks-cell/tsconfig-build.json"}, {"path": "../wonder-blocks-clickable/tsconfig-build.json"}, {"path": "../wonder-blocks-core/tsconfig-build.json"}, From d03eb982855a2ba7443d5b627b0f8f1a5f2c674b Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Mon, 24 Feb 2025 10:00:43 -0800 Subject: [PATCH 08/22] [combobox-announcer] docs(changeset): Integrates Announcer for value announcements in SingleSelect and MultiSelect --- .changeset/plenty-crews-search.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/plenty-crews-search.md diff --git a/.changeset/plenty-crews-search.md b/.changeset/plenty-crews-search.md new file mode 100644 index 000000000..bd920cf94 --- /dev/null +++ b/.changeset/plenty-crews-search.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/wonder-blocks-dropdown": minor +--- + +Integrates Announcer for value announcements in SingleSelect and MultiSelect From fe1732f50d39968f7878a1462546fa8ec9605469 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Mon, 24 Feb 2025 10:25:46 -0800 Subject: [PATCH 09/22] [combobox-announcer] Update pnpm lock --- pnpm-lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 087f3be0e..47af820b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -635,6 +635,9 @@ importers: '@babel/runtime': specifier: ^7.24.5 version: 7.26.7 + '@khanacademy/wonder-blocks-announcer': + specifier: workspace:* + version: link:../wonder-blocks-announcer '@khanacademy/wonder-blocks-cell': specifier: workspace:* version: link:../wonder-blocks-cell From ffa975ec6832a428cb42dbc4520124b3cd0a03c7 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Mon, 24 Feb 2025 11:52:05 -0800 Subject: [PATCH 10/22] [combobox-announcer] Update BirthdayPicker tests --- .../__tests__/birthday-picker.test.tsx | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/wonder-blocks-birthday-picker/src/components/__tests__/birthday-picker.test.tsx b/packages/wonder-blocks-birthday-picker/src/components/__tests__/birthday-picker.test.tsx index 458182ac8..63e8b5be4 100644 --- a/packages/wonder-blocks-birthday-picker/src/components/__tests__/birthday-picker.test.tsx +++ b/packages/wonder-blocks-birthday-picker/src/components/__tests__/birthday-picker.test.tsx @@ -255,7 +255,9 @@ describe("BirthdayPicker", () => { await userEvent.click( await screen.findByTestId("birthday-picker-month"), ); - const monthOption = await screen.findByText("Jul"); + const monthOption = await screen.findByRole("combobox", { + name: "Month", + }); await userEvent.click(monthOption, { pointerEventsCheck: PointerEventsCheckLevel.Never, }); @@ -263,7 +265,9 @@ describe("BirthdayPicker", () => { await userEvent.click( await screen.findByTestId("birthday-picker-day"), ); - const dayOption = await screen.findByText("5"); + const dayOption = await screen.findByRole("combobox", { + name: "Day", + }); await userEvent.click(dayOption, { pointerEventsCheck: PointerEventsCheckLevel.Never, }); @@ -271,7 +275,9 @@ describe("BirthdayPicker", () => { await userEvent.click( await screen.findByTestId("birthday-picker-year"), ); - const yearOption = await screen.findByText("2021"); + const yearOption = await screen.findByRole("combobox", { + name: "Year", + }); await userEvent.click(yearOption, { pointerEventsCheck: PointerEventsCheckLevel.Never, }); @@ -288,7 +294,9 @@ describe("BirthdayPicker", () => { await userEvent.click( await screen.findByTestId("birthday-picker-month"), ); - const monthOption = await screen.findByText("Jul"); + const monthOption = await screen.findByRole("combobox", { + name: "Month", + }); await userEvent.click(monthOption, { pointerEventsCheck: PointerEventsCheckLevel.Never, }); @@ -296,7 +304,9 @@ describe("BirthdayPicker", () => { await userEvent.click( await screen.findByTestId("birthday-picker-day"), ); - const dayOption = await screen.findByText("5"); + const dayOption = await screen.findByRole("combobox", { + name: "Day", + }); await userEvent.click(dayOption, { pointerEventsCheck: PointerEventsCheckLevel.Never, @@ -305,7 +315,9 @@ describe("BirthdayPicker", () => { await userEvent.click( await screen.findByTestId("birthday-picker-year"), ); - const yearOption = await screen.findByText("2021"); + const yearOption = await screen.findByRole("combobox", { + name: "Year", + }); await userEvent.click(yearOption, { pointerEventsCheck: PointerEventsCheckLevel.Never, }); From 3a6fc7d34a4ddb325ffd10bc4834f45b97c6a364 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Mon, 24 Feb 2025 13:11:20 -0800 Subject: [PATCH 11/22] [combobox-announcer] Update BirthdayPicker tests --- .../__tests__/birthday-picker.test.tsx | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/packages/wonder-blocks-birthday-picker/src/components/__tests__/birthday-picker.test.tsx b/packages/wonder-blocks-birthday-picker/src/components/__tests__/birthday-picker.test.tsx index 63e8b5be4..7fa0b7779 100644 --- a/packages/wonder-blocks-birthday-picker/src/components/__tests__/birthday-picker.test.tsx +++ b/packages/wonder-blocks-birthday-picker/src/components/__tests__/birthday-picker.test.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import moment from "moment"; -import {render, screen, waitFor} from "@testing-library/react"; +import {render, act, screen, waitFor} from "@testing-library/react"; import * as DateMock from "jest-date-mock"; import {userEvent, PointerEventsCheckLevel} from "@testing-library/user-event"; @@ -8,6 +8,19 @@ import BirthdayPicker, {defaultLabels} from "../birthday-picker"; import type {Labels} from "../birthday-picker"; +jest.mock("react-popper", () => ({ + ...jest.requireActual("react-popper"), + Popper: jest.fn().mockImplementation(({children}) => { + // Mock `isReferenceHidden` to always return false (or true for testing visibility) + return children({ + ref: jest.fn(), + style: {}, + placement: "bottom", + isReferenceHidden: false, // Mocking isReferenceHidden + }); + }), +})); + describe("BirthdayPicker", () => { const today = new Date("2021-07-19T09:30:00Z"); @@ -251,12 +264,14 @@ describe("BirthdayPicker", () => { render(); - // Act - await userEvent.click( - await screen.findByTestId("birthday-picker-month"), + const monthDropdown = await screen.findByTestId( + "birthday-picker-month", ); - const monthOption = await screen.findByRole("combobox", { - name: "Month", + // Act + await userEvent.click(monthDropdown); + + const monthOption = await screen.findByRole("option", { + name: "Jul", }); await userEvent.click(monthOption, { pointerEventsCheck: PointerEventsCheckLevel.Never, @@ -265,8 +280,8 @@ describe("BirthdayPicker", () => { await userEvent.click( await screen.findByTestId("birthday-picker-day"), ); - const dayOption = await screen.findByRole("combobox", { - name: "Day", + const dayOption = await screen.findByRole("option", { + name: "5", }); await userEvent.click(dayOption, { pointerEventsCheck: PointerEventsCheckLevel.Never, @@ -275,8 +290,8 @@ describe("BirthdayPicker", () => { await userEvent.click( await screen.findByTestId("birthday-picker-year"), ); - const yearOption = await screen.findByRole("combobox", { - name: "Year", + const yearOption = await screen.findByRole("option", { + name: "2021", }); await userEvent.click(yearOption, { pointerEventsCheck: PointerEventsCheckLevel.Never, @@ -294,8 +309,8 @@ describe("BirthdayPicker", () => { await userEvent.click( await screen.findByTestId("birthday-picker-month"), ); - const monthOption = await screen.findByRole("combobox", { - name: "Month", + const monthOption = await screen.findByRole("option", { + name: "Jul", }); await userEvent.click(monthOption, { pointerEventsCheck: PointerEventsCheckLevel.Never, @@ -304,8 +319,8 @@ describe("BirthdayPicker", () => { await userEvent.click( await screen.findByTestId("birthday-picker-day"), ); - const dayOption = await screen.findByRole("combobox", { - name: "Day", + const dayOption = await screen.findByRole("option", { + name: "5", }); await userEvent.click(dayOption, { @@ -315,8 +330,8 @@ describe("BirthdayPicker", () => { await userEvent.click( await screen.findByTestId("birthday-picker-year"), ); - const yearOption = await screen.findByRole("combobox", { - name: "Year", + const yearOption = await screen.findByRole("option", { + name: "2021", }); await userEvent.click(yearOption, { pointerEventsCheck: PointerEventsCheckLevel.Never, @@ -327,7 +342,9 @@ describe("BirthdayPicker", () => { await userEvent.click( await screen.findByTestId("birthday-picker-month"), ); - const monthOptionNew = await screen.findByText("Aug"); + const monthOptionNew = await screen.findByRole("option", { + name: "Aug", + }); await userEvent.click(monthOptionNew, { pointerEventsCheck: PointerEventsCheckLevel.Never, }); @@ -335,7 +352,7 @@ describe("BirthdayPicker", () => { await userEvent.click( await screen.findByTestId("birthday-picker-day"), ); - const dayOptionNew = await screen.findByText("9"); + const dayOptionNew = await screen.findByRole("option", {name: "9"}); await userEvent.click(dayOptionNew, { pointerEventsCheck: PointerEventsCheckLevel.Never, }); @@ -343,7 +360,9 @@ describe("BirthdayPicker", () => { await userEvent.click( await screen.findByTestId("birthday-picker-year"), ); - const yearOptionNew = await screen.findByText("2020"); + const yearOptionNew = await screen.findByRole("option", { + name: "2020", + }); await userEvent.click(yearOptionNew, { pointerEventsCheck: PointerEventsCheckLevel.Never, }); @@ -415,9 +434,9 @@ describe("BirthdayPicker", () => { // This test was written by calling methods on the instance because // react-window (used by SingleSelect) doesn't show all of the items // in the dropdown. - instance.handleMonthChange("1"); - instance.handleDayChange("31"); - instance.handleYearChange("2021"); + await act(() => instance.handleMonthChange("1")); + await act(() => instance.handleDayChange("31")); + await act(() => instance.handleYearChange("2021")); // Assert await waitFor(() => expect(onChange).toHaveBeenCalledWith(null)); From c459f5d75e17ac48bdb6bbbedbfb0d954f79f68f Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Mon, 24 Feb 2025 13:12:28 -0800 Subject: [PATCH 12/22] [combobox-announcer] Remove dropdown-core live region --- .../__tests__/dropdown-core.test.tsx | 24 ------- .../__tests__/multi-select.test.tsx | 65 ------------------- .../src/components/dropdown-core.tsx | 23 +------ 3 files changed, 1 insertion(+), 111 deletions(-) diff --git a/packages/wonder-blocks-dropdown/src/components/__tests__/dropdown-core.test.tsx b/packages/wonder-blocks-dropdown/src/components/__tests__/dropdown-core.test.tsx index e54972b67..ff2976a6f 100644 --- a/packages/wonder-blocks-dropdown/src/components/__tests__/dropdown-core.test.tsx +++ b/packages/wonder-blocks-dropdown/src/components/__tests__/dropdown-core.test.tsx @@ -733,30 +733,6 @@ describe("DropdownCore", () => { }); }); - describe("a11y > Live region", () => { - it("should render a live region announcing the number of options", async () => { - // Arrange - - // Act - const {container} = render( - } - onOpenChanged={jest.fn()} - />, - ); - - // Assert - expect(container).toHaveTextContent("3 items"); - }); - }); - describe("onOpenChanged", () => { it("Should be triggered when the down key is pressed and the menu is closed", async () => { // Arrange diff --git a/packages/wonder-blocks-dropdown/src/components/__tests__/multi-select.test.tsx b/packages/wonder-blocks-dropdown/src/components/__tests__/multi-select.test.tsx index 35d1bc398..7e9ceb507 100644 --- a/packages/wonder-blocks-dropdown/src/components/__tests__/multi-select.test.tsx +++ b/packages/wonder-blocks-dropdown/src/components/__tests__/multi-select.test.tsx @@ -1666,71 +1666,6 @@ describe("MultiSelect", () => { }); }); - describe("a11y > Live region", () => { - it("should announce the number of options when the listbox is open", async () => { - // Arrange - const labels: LabelsValues = { - ...builtinLabels, - someSelected: (numOptions: number): string => - numOptions <= 1 - ? `${numOptions} school` - : `${numOptions} schools`, - }; - - // Act - const {container} = doRender( - - - - - , - ); - - // Assert - expect(container).toHaveTextContent("3 schools"); - }); - - it("should change the number of options after using the search filter", async () => { - // Arrange - const labels: LabelsValues = { - ...builtinLabels, - someSelected: (numOptions: number): string => - numOptions <= 1 - ? `${numOptions} planet` - : `${numOptions} planets`, - }; - - const {container, userEvent} = doRender( - - - - - , - ); - - // Act - const textbox = await screen.findByRole("textbox"); - await userEvent.click(textbox); - await userEvent.paste("ear"); - - // Assert - await waitFor(() => { - expect(container).toHaveTextContent("1 planet"); - }); - }); - }); - describe("a11y > Focusable", () => { it("should be focusable", () => { // Arrange diff --git a/packages/wonder-blocks-dropdown/src/components/dropdown-core.tsx b/packages/wonder-blocks-dropdown/src/components/dropdown-core.tsx index c2f86d448..8498c91e0 100644 --- a/packages/wonder-blocks-dropdown/src/components/dropdown-core.tsx +++ b/packages/wonder-blocks-dropdown/src/components/dropdown-core.tsx @@ -14,7 +14,7 @@ import { border, } from "@khanacademy/wonder-blocks-tokens"; -import {addStyle, PropsFor, View, keys} from "@khanacademy/wonder-blocks-core"; +import {PropsFor, View, keys} from "@khanacademy/wonder-blocks-core"; import SearchField from "@khanacademy/wonder-blocks-search-field"; import {LabelMedium} from "@khanacademy/wonder-blocks-typography"; import {withActionScheduler} from "@khanacademy/wonder-blocks-timing"; @@ -41,8 +41,6 @@ import OptionItem from "./option-item"; */ const VIRTUALIZE_THRESHOLD = 125; -const StyledSpan = addStyle("span"); - type LabelsValues = { /** * Label for describing the dismiss icon on the search filter. @@ -1056,24 +1054,6 @@ class DropdownCore extends React.Component { ); } - renderLiveRegion(): React.ReactNode { - const {items, open} = this.props; - const {labels} = this.state; - const totalItems = items.length; - - return ( - - {open && labels.someResults(totalItems)} - - ); - } - render(): React.ReactNode { const {open, opener, style, className, disabled} = this.props; @@ -1084,7 +1064,6 @@ class DropdownCore extends React.Component { style={[styles.menuWrapper, style]} className={className} > - {this.renderLiveRegion()} {opener} {open && this.renderDropdown()} From 14458ab892187df52799093294bab207e258b523 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Mon, 24 Feb 2025 13:12:53 -0800 Subject: [PATCH 13/22] [combobox-announcer] Try restoring wb-dev-build-settings --- packages/wonder-blocks-announcer/package.json | 1 + pnpm-lock.yaml | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/packages/wonder-blocks-announcer/package.json b/packages/wonder-blocks-announcer/package.json index 0f2f53a02..209cec145 100644 --- a/packages/wonder-blocks-announcer/package.json +++ b/packages/wonder-blocks-announcer/package.json @@ -23,5 +23,6 @@ "react": "18.2.0" }, "devDependencies": { + "@khanacademy/wb-dev-build-settings": "workspace:*" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47af820b3..d145be101 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -369,6 +369,10 @@ importers: react: specifier: 18.2.0 version: 18.2.0 + devDependencies: + '@khanacademy/wb-dev-build-settings': + specifier: workspace:* + version: link:../../build-settings packages/wonder-blocks-banner: dependencies: From 51b2e431101b1bf45fecb2f14c3c142018c09d80 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Mon, 24 Feb 2025 13:20:34 -0800 Subject: [PATCH 14/22] [combobox-announcer] Add system types to announcer --- packages/wonder-blocks-announcer/types | 1 + 1 file changed, 1 insertion(+) create mode 120000 packages/wonder-blocks-announcer/types diff --git a/packages/wonder-blocks-announcer/types b/packages/wonder-blocks-announcer/types new file mode 120000 index 000000000..8788aa284 --- /dev/null +++ b/packages/wonder-blocks-announcer/types @@ -0,0 +1 @@ +../../types \ No newline at end of file From cbd1aa5a52d58204d19b9a0b81a4655d678c36d8 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Wed, 26 Feb 2025 14:12:28 -0800 Subject: [PATCH 15/22] [combobox-announcer] Add API to init Announcer on load --- .../src/__tests__/init-announcer.test.tsx | 30 +++++++++++++++++++ packages/wonder-blocks-announcer/src/index.ts | 3 +- .../src/init-announcer.ts | 10 +++++++ .../src/components/multi-select.tsx | 9 +++++- .../src/components/single-select.tsx | 9 +++++- 5 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 packages/wonder-blocks-announcer/src/__tests__/init-announcer.test.tsx create mode 100644 packages/wonder-blocks-announcer/src/init-announcer.ts diff --git a/packages/wonder-blocks-announcer/src/__tests__/init-announcer.test.tsx b/packages/wonder-blocks-announcer/src/__tests__/init-announcer.test.tsx new file mode 100644 index 000000000..d64e0911c --- /dev/null +++ b/packages/wonder-blocks-announcer/src/__tests__/init-announcer.test.tsx @@ -0,0 +1,30 @@ +import {screen} from "@testing-library/react"; +import Announcer from "../announcer"; +import {initAnnouncer} from "../init-announcer"; +import {announceMessage} from "../announce-message"; + +describe("Announcer.initAnnouncer", () => { + let announcer: Announcer; + afterEach(() => { + announcer.reset(); + }); + + it("injects the Announcer when called", () => { + // Arrange + announcer = initAnnouncer(); + // Act + const regionWrapper = screen.getByTestId("wbAnnounce"); + // Assert + expect(regionWrapper).toBeInTheDocument(); + }); + + it("only injects one Announcer", () => { + // Arrange + announcer = initAnnouncer(); + announceMessage({message: "A thing"}); + // Act + const regionWrapper = screen.getAllByTestId("wbAnnounce"); + // Assert + expect(regionWrapper.length).toEqual(1); + }); +}); diff --git a/packages/wonder-blocks-announcer/src/index.ts b/packages/wonder-blocks-announcer/src/index.ts index ecf2fd0c8..be5cfe5d9 100644 --- a/packages/wonder-blocks-announcer/src/index.ts +++ b/packages/wonder-blocks-announcer/src/index.ts @@ -1,6 +1,7 @@ +import {initAnnouncer} from "./init-announcer"; import {announceMessage} from "./announce-message"; import {clearMessages} from "./clear-messages"; import type {AnnounceMessageProps} from "./announce-message"; -export {announceMessage, clearMessages}; +export {initAnnouncer, announceMessage, clearMessages}; export {type AnnounceMessageProps}; diff --git a/packages/wonder-blocks-announcer/src/init-announcer.ts b/packages/wonder-blocks-announcer/src/init-announcer.ts new file mode 100644 index 000000000..45a0c2b6d --- /dev/null +++ b/packages/wonder-blocks-announcer/src/init-announcer.ts @@ -0,0 +1,10 @@ +import Announcer from "./announcer"; + +/** + * Utility to inject Announcer on page load. + * It can be called from useEffect or elsewhere to improve ARIA Live Region performance on the first announcement. + * @returns {Announcer} The Announcer instance created. + */ +export function initAnnouncer(): Announcer { + return Announcer.getInstance(); +} diff --git a/packages/wonder-blocks-dropdown/src/components/multi-select.tsx b/packages/wonder-blocks-dropdown/src/components/multi-select.tsx index 4909f16b4..ac9efdf40 100644 --- a/packages/wonder-blocks-dropdown/src/components/multi-select.tsx +++ b/packages/wonder-blocks-dropdown/src/components/multi-select.tsx @@ -7,7 +7,10 @@ import { type StyleType, } from "@khanacademy/wonder-blocks-core"; -import {announceMessage} from "@khanacademy/wonder-blocks-announcer"; +import { + announceMessage, + initAnnouncer, +} from "@khanacademy/wonder-blocks-announcer"; import ActionItem from "./action-item"; import DropdownCore from "./dropdown-core"; import DropdownOpener from "./dropdown-opener"; @@ -308,6 +311,10 @@ const MultiSelect = (props: Props) => { const hasError = error || !!errorMessage; + React.useEffect(() => { + initAnnouncer(); + }, []); + React.useEffect(() => { // Used to sync the `opened` state when this component acts as a controlled component if (disabled) { diff --git a/packages/wonder-blocks-dropdown/src/components/single-select.tsx b/packages/wonder-blocks-dropdown/src/components/single-select.tsx index 33315d043..4bd528840 100644 --- a/packages/wonder-blocks-dropdown/src/components/single-select.tsx +++ b/packages/wonder-blocks-dropdown/src/components/single-select.tsx @@ -7,7 +7,10 @@ import { type StyleType, } from "@khanacademy/wonder-blocks-core"; -import {announceMessage} from "@khanacademy/wonder-blocks-announcer"; +import { + initAnnouncer, + announceMessage, +} from "@khanacademy/wonder-blocks-announcer"; import DropdownCore from "./dropdown-core"; import DropdownOpener from "./dropdown-opener"; import SelectOpener from "./select-opener"; @@ -321,6 +324,10 @@ const SingleSelect = (props: Props) => { }); const hasError = error || !!errorMessage; + React.useEffect(() => { + initAnnouncer(); + }, []); + React.useEffect(() => { // Used to sync the `opened` state when this component acts as a controlled if (disabled) { From f56178b3c82416ffcc0f77314bc9f397d9c2d18f Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Fri, 28 Feb 2025 15:32:19 -0800 Subject: [PATCH 16/22] [combobox-announcer] Hoist uniqueID to top of Announcer --- packages/wonder-blocks-announcer/src/announcer.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/wonder-blocks-announcer/src/announcer.ts b/packages/wonder-blocks-announcer/src/announcer.ts index df796fd4a..80cbc6f25 100644 --- a/packages/wonder-blocks-announcer/src/announcer.ts +++ b/packages/wonder-blocks-announcer/src/announcer.ts @@ -20,6 +20,7 @@ export const DEFAULT_WAIT_THRESHOLD = 250; */ class Announcer { private static _instance: Announcer | null; + topLevelId: string = `wbAnnounce`; node: HTMLElement | null = null; regionFactory: RegionFactory = { count: 2, @@ -36,13 +37,12 @@ class Announcer { private constructor() { if (typeof document !== "undefined") { - const topLevelId = `wbAnnounce`; // Check if our top level element already exists - const announcerCheck = document.getElementById(topLevelId); + const announcerCheck = document.getElementById(this.topLevelId); // Init new structure if the coast is clear if (announcerCheck === null) { - this.init(topLevelId); + this.init(this.topLevelId); } // The structure exists but references are lost, so help HMR recover else { @@ -109,7 +109,7 @@ class Announcer { * Announcer exists, but it loses the connection to DOM element Refs */ reattachNodes() { - const announcerCheck = document.getElementById(`wbAnnounce`); + const announcerCheck = document.getElementById(this.topLevelId); if (announcerCheck !== null) { this.node = announcerCheck; const regions = Array.from( From b17979b41a8f88c0d9db7b6c59918d18dc12affd Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Fri, 28 Feb 2025 15:32:57 -0800 Subject: [PATCH 17/22] [combobox-announcer] Add debounce wait prop to initializer --- .../wonder-blocks-announcer/src/init-announcer.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/wonder-blocks-announcer/src/init-announcer.ts b/packages/wonder-blocks-announcer/src/init-announcer.ts index 45a0c2b6d..e65e68f8b 100644 --- a/packages/wonder-blocks-announcer/src/init-announcer.ts +++ b/packages/wonder-blocks-announcer/src/init-announcer.ts @@ -1,10 +1,18 @@ import Announcer from "./announcer"; +type InitAnnouncerProps = { + debounceThreshold?: number; +}; + /** * Utility to inject Announcer on page load. * It can be called from useEffect or elsewhere to improve ARIA Live Region performance on the first announcement. * @returns {Announcer} The Announcer instance created. */ -export function initAnnouncer(): Announcer { - return Announcer.getInstance(); +export function initAnnouncer(props?: InitAnnouncerProps): Announcer { + const announcer = Announcer.getInstance(); + if (props?.debounceThreshold !== undefined) { + announcer.updateWaitThreshold(props?.debounceThreshold); + } + return announcer; } From ce65679fca66bbc7ae9c6ff05b69c5738246203f Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Fri, 28 Feb 2025 15:34:04 -0800 Subject: [PATCH 18/22] [combobox-announcer] WIP: rework debounce for combobox --- .../src/__tests__/announce-message.test.tsx | 6 +- .../src/__tests__/announcer.test.ts | 7 +- .../src/__tests__/clear-messages.test.tsx | 32 +++--- .../src/__tests__/util/util.test.ts | 10 +- .../wonder-blocks-announcer/src/util/util.ts | 22 ++--- .../__tests__/multi-select.test.tsx | 98 ++++++++++++++++++- .../src/components/multi-select.tsx | 17 +++- 7 files changed, 142 insertions(+), 50 deletions(-) diff --git a/packages/wonder-blocks-announcer/src/__tests__/announce-message.test.tsx b/packages/wonder-blocks-announcer/src/__tests__/announce-message.test.tsx index f17ed1ea0..87286c386 100644 --- a/packages/wonder-blocks-announcer/src/__tests__/announce-message.test.tsx +++ b/packages/wonder-blocks-announcer/src/__tests__/announce-message.test.tsx @@ -19,7 +19,7 @@ describe("Announcer.announceMessage", () => { const message1 = "One Fish Two Fish"; // ACT - const announcement1Id = await announceMessage({ + const announcement1Id = announceMessage({ message: message1, initialTimeout: 0, debounceThreshold: 0, @@ -27,7 +27,7 @@ describe("Announcer.announceMessage", () => { jest.advanceTimersByTime(500); // ASSERT - expect(announcement1Id).toBe("wbARegion-polite1"); + await expect(announcement1Id).resolves.toBe("wbARegion-polite1"); }); test("creates the live region elements when called", () => { @@ -159,7 +159,7 @@ describe("Announcer.announceMessage", () => { expect(message1Region).toHaveTextContent(message1); expect(setTimeout).toHaveBeenNthCalledWith( - 1, + 2, expect.any(Function), 5250, ); diff --git a/packages/wonder-blocks-announcer/src/__tests__/announcer.test.ts b/packages/wonder-blocks-announcer/src/__tests__/announcer.test.ts index 4be252e19..62b2d5956 100644 --- a/packages/wonder-blocks-announcer/src/__tests__/announcer.test.ts +++ b/packages/wonder-blocks-announcer/src/__tests__/announcer.test.ts @@ -161,6 +161,7 @@ describe("Announcer class", () => { const waitThreshold = 1000; // Act + // The second call will win out in the trailing edge implementation announcer.announce("a thing", "polite", waitThreshold); announcer.announce("two things", "polite", waitThreshold); @@ -173,7 +174,7 @@ describe("Announcer class", () => { announcer.dictionary.get(`wbARegion-polite0`)?.element; // ASSERT - await expect(targetElement?.textContent).toBe("a thing"); + await expect(targetElement?.textContent).toBe("two things"); await expect(targetElement2?.textContent).toBe(""); }); }); @@ -206,16 +207,12 @@ describe("Announcer class", () => { // Act announcer.announce("One Fish", "polite", 0); jest.advanceTimersByTime(5); - announcer.announce("Loud Fish", "assertive", 0); - expect(screen.getByText("One Fish")).toBeInTheDocument(); - expect(screen.getByText("Loud Fish")).toBeInTheDocument(); announcer.clear(); // Assert expect(screen.queryByText("One Fish")).not.toBeInTheDocument(); - expect(screen.queryByText("Loud Fish")).not.toBeInTheDocument(); }); test("handling calls when nothing has been announced", () => { diff --git a/packages/wonder-blocks-announcer/src/__tests__/clear-messages.test.tsx b/packages/wonder-blocks-announcer/src/__tests__/clear-messages.test.tsx index 3387b4708..d5acf570d 100644 --- a/packages/wonder-blocks-announcer/src/__tests__/clear-messages.test.tsx +++ b/packages/wonder-blocks-announcer/src/__tests__/clear-messages.test.tsx @@ -19,28 +19,28 @@ describe("Announcer.clearMessages", () => { const region1 = screen.getByTestId("wbARegion-polite1"); - jest.advanceTimersByTime(250); + // jest.advanceTimersByTime(250); - await waitFor(() => { - expect(region1).toHaveTextContent(message1); - }); + // await waitFor(() => { + // expect(region1).toHaveTextContent(message1); + // clearMessages(announcement1Id); + // }); - await announceMessage({ - message: message2, - initialTimeout: 0, - debounceThreshold: 0, - }); + // announceMessage({ + // message: message2, + // initialTimeout: 0, + // debounceThreshold: 0, + // }); - const region2 = screen.getByTestId("wbARegion-polite0"); + // const region2 = screen.getByTestId("wbARegion-polite0"); - jest.advanceTimersByTime(250); - clearMessages(announcement1Id); + // jest.advanceTimersByTime(250); // ASSERT await waitFor(() => { expect(region1).toBeEmptyDOMElement(); }); - expect(region2).toHaveTextContent(message2); + // expect(region2).toHaveTextContent(message2); }); test("empties all live region elements by default", async () => { @@ -49,7 +49,7 @@ describe("Announcer.clearMessages", () => { const message2 = "Red fish blue fish"; // ACT - await announceMessage({ + announceMessage({ message: message1, initialTimeout: 0, debounceThreshold: 0, @@ -60,7 +60,7 @@ describe("Announcer.clearMessages", () => { const region1 = screen.queryByTestId("wbARegion-polite1"); expect(region1).toHaveTextContent(message1); - await announceMessage({ + announceMessage({ message: message2, initialTimeout: 0, debounceThreshold: 0, @@ -69,7 +69,7 @@ describe("Announcer.clearMessages", () => { const region2 = screen.getByTestId("wbARegion-polite0"); expect(region2).toHaveTextContent(message2); - await announceMessage({ + announceMessage({ message: message1, level: "assertive", initialTimeout: 0, diff --git a/packages/wonder-blocks-announcer/src/__tests__/util/util.test.ts b/packages/wonder-blocks-announcer/src/__tests__/util/util.test.ts index 09d3a0ed5..8824a153b 100644 --- a/packages/wonder-blocks-announcer/src/__tests__/util/util.test.ts +++ b/packages/wonder-blocks-announcer/src/__tests__/util/util.test.ts @@ -8,17 +8,17 @@ describe("Debouncing messages", () => { // ARRANGE const announcer = Announcer.getInstance(); const callback = jest.fn((message: string) => message); - const debounced = createDebounceFunction(announcer, callback, 100); + const debounced = createDebounceFunction(announcer, callback, 10); // ACT - const result = await debounced("Hello, World!"); + const result = debounced("Hello, World!"); jest.advanceTimersByTime(100); // ASSERT - expect(result).toBe("Hello, World!"); + await expect(result).resolves.toBe("Hello, World!"); }); - test("resolving with the first argument passed if debounced multiple times", async () => { + test("resolving with the last argument passed if debounced multiple times", async () => { // ARRANGE const announcer = Announcer.getInstance(); const callback = jest.fn((message: string) => message); @@ -34,6 +34,6 @@ describe("Debouncing messages", () => { expect(callback).toHaveBeenCalledTimes(1); // ASSERT - expect(callback).toHaveBeenCalledWith("First message"); + expect(callback).toHaveBeenCalledWith("Third message"); }); }); diff --git a/packages/wonder-blocks-announcer/src/util/util.ts b/packages/wonder-blocks-announcer/src/util/util.ts index 2270044f9..685f6189a 100644 --- a/packages/wonder-blocks-announcer/src/util/util.ts +++ b/packages/wonder-blocks-announcer/src/util/util.ts @@ -28,22 +28,18 @@ export function createDebounceFunction( updateWaitTime: (time: number) => void; } { let timeoutId: ReturnType | null = null; - let executed = false; - let lastExecutionTime = 0; const debouncedFn = (...args: []) => { return new Promise((resolve) => { - const now = Date.now(); - const timeSinceLastExecution = now - lastExecutionTime; - if (timeSinceLastExecution >= debounceThreshold) { - lastExecutionTime = now; - // Leading edge: Execute the callback immediately - if (!executed) { - executed = true; - const result = callback.apply(context, args); - resolve(result); + console.log("debounceFn", args); + const later = () => { + const result = callback.apply(context, args); + if (timeoutId) { + clearTimeout(timeoutId); } - } + console.log("later", args); + return resolve(result); + }; // If the timeout exists, clear it if (timeoutId !== null) { @@ -52,7 +48,7 @@ export function createDebounceFunction( // Trailing edge: Set the timeout for the next allowed execution timeoutId = setTimeout(() => { - executed = false; + later(); }, debounceThreshold); }); }; diff --git a/packages/wonder-blocks-dropdown/src/components/__tests__/multi-select.test.tsx b/packages/wonder-blocks-dropdown/src/components/__tests__/multi-select.test.tsx index 7e9ceb507..26ae8002f 100644 --- a/packages/wonder-blocks-dropdown/src/components/__tests__/multi-select.test.tsx +++ b/packages/wonder-blocks-dropdown/src/components/__tests__/multi-select.test.tsx @@ -15,6 +15,7 @@ import { } from "@testing-library/user-event"; import {PropsFor} from "@khanacademy/wonder-blocks-core"; +import {initAnnouncer} from "@khanacademy/wonder-blocks-announcer"; import OptionItem from "../option-item"; import MultiSelect from "../multi-select"; import {defaultLabels as builtinLabels} from "../../util/constants"; @@ -40,6 +41,8 @@ const defaultLabels: LabelsValues = { allSelected: "All students", }; +jest.useFakeTimers(); + describe("MultiSelect", () => { beforeEach(() => { window.scrollTo = jest.fn(); @@ -965,9 +968,7 @@ describe("MultiSelect", () => { expect(filteredOption).toBeInTheDocument(); }); - // NOTE(john) FEI-5533: After upgrading to user-event v14, this test is failing. - // The Venus option is still in the document. - it.skip("should filter out an option if it's not part of the results", async () => { + it("should filter out an option if it's not part of the results", async () => { // Arrange const labels: LabelsValues = { ...builtinLabels, @@ -1666,6 +1667,97 @@ describe("MultiSelect", () => { }); }); + describe("a11y > Live region", () => { + beforeEach(() => { + initAnnouncer({debounceThreshold: 0}); + }); + + it("should announce the number of options when the listbox is open", async () => { + // const mockDateNow = jest.spyOn(Date, "now"); + // mockDateNow.mockImplementation(() => 0); + + // Arrange + const labels: LabelsValues = { + ...builtinLabels, + someSelected: (numOptions: number): string => + numOptions <= 1 + ? `${numOptions} school` + : `${numOptions} schools`, + }; + + const {userEvent} = doRender( + + + + + , + ); + const opener = await screen.findByRole("combobox"); + + jest.advanceTimersByTime(10); + // mockDateNow.mockImplementation(() => 150); + + // Act + await userEvent.click(opener); + + const announcer = screen.getByTestId("wbAnnounce"); + const announcementText = + await within(announcer).findByText("3 schools"); + + // Assert + expect(announcementText).toBeInTheDocument(); + + // mockDateNow.mockRestore(); + }); + + it("should change the number of options after using the search filter", async () => { + // Arrange + const labels: LabelsValues = { + ...builtinLabels, + noneSelected: "0 planets", + someSelected: (numOptions: number): string => + numOptions <= 1 + ? `${numOptions} planet` + : `${numOptions} planets`, + }; + + const {userEvent} = doRender( + + + + + , + ); + + // Act + const textbox = await screen.findByRole("textbox"); + await userEvent.click(textbox); + await userEvent.paste("ear"); + + // wait to avoid getting caught in the Announcer debounce + jest.advanceTimersByTime(250); + + const announcer = await screen.findByTestId("wbAnnounce"); + const announcementText = + await within(announcer).findByText("1 planet"); + // Assert + await waitFor(() => { + expect(announcementText).toBeInTheDocument(); + }); + }); + }); + describe("a11y > Focusable", () => { it("should be focusable", () => { // Arrange diff --git a/packages/wonder-blocks-dropdown/src/components/multi-select.tsx b/packages/wonder-blocks-dropdown/src/components/multi-select.tsx index ac9efdf40..69f6c938c 100644 --- a/packages/wonder-blocks-dropdown/src/components/multi-select.tsx +++ b/packages/wonder-blocks-dropdown/src/components/multi-select.tsx @@ -565,10 +565,13 @@ const MultiSelect = (props: Props) => { const {noneSelected} = labels; const menuTextOrNode = getMenuTextOrNode(allChildren); - const [menuStringLabel, menuContent] = + const [openerStringValue, openerContent] = maybeExtractStringFromNode(menuTextOrNode); - handleAnnouncement(menuStringLabel); + if (openerStringValue) { + // opener value changed, so let's announce it + handleAnnouncement(openerStringValue); + } const dropdownOpener = ( @@ -585,7 +588,7 @@ const MultiSelect = (props: Props) => { disabled={isDisabled} ref={handleOpenerRef} role="combobox" - text={menuContent} + text={openerContent} opened={open} > {opener} @@ -598,14 +601,14 @@ const MultiSelect = (props: Props) => { id={uniqueOpenerId} aria-label={ariaLabel} aria-controls={dropdownId} - isPlaceholder={menuContent === noneSelected} + isPlaceholder={openerContent === noneSelected} onOpenChanged={handleOpenChanged} onBlur={onOpenerBlurValidation} open={open} ref={handleOpenerRef} testId={testId} > - {menuContent} + {openerContent} ); }} @@ -628,6 +631,10 @@ const MultiSelect = (props: Props) => { const filteredItems = getMenuItems(allChildren); const isDisabled = numEnabledOptions === 0 || disabled; + if (open && isFilterable) { + handleAnnouncement(labels.someSelected(filteredItems.length)); + } + return ( {(uniqueDropdownId) => ( From 3677b3932431add4b215e1dc6d80e4bb0c85322a Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Fri, 28 Feb 2025 15:34:20 -0800 Subject: [PATCH 19/22] [combobox-announcer] Show Announcer in Storybook/MultiSelect --- __docs__/wonder-blocks-dropdown/multi-select.stories.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/__docs__/wonder-blocks-dropdown/multi-select.stories.tsx b/__docs__/wonder-blocks-dropdown/multi-select.stories.tsx index e4a5dfafa..f537dc0de 100644 --- a/__docs__/wonder-blocks-dropdown/multi-select.stories.tsx +++ b/__docs__/wonder-blocks-dropdown/multi-select.stories.tsx @@ -74,6 +74,7 @@ export default { "aria-label": "Planets", }, parameters: { + addBodyClass: "showAnnouncer", componentSubtitle: ( >( [], ); - const [opened, setOpened] = React.useState(props.opened || false); + const [opened, setOpened] = React.useState(false); return ( From f15ea8686ffa4d3833dd5d1a7b3c751f76a261bc Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Fri, 28 Feb 2025 15:35:36 -0800 Subject: [PATCH 20/22] [combobox-announcer] Clean up WIP code --- .../src/__tests__/clear-messages.test.tsx | 12 ++++++------ packages/wonder-blocks-announcer/src/util/util.ts | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/wonder-blocks-announcer/src/__tests__/clear-messages.test.tsx b/packages/wonder-blocks-announcer/src/__tests__/clear-messages.test.tsx index d5acf570d..0a16e76b6 100644 --- a/packages/wonder-blocks-announcer/src/__tests__/clear-messages.test.tsx +++ b/packages/wonder-blocks-announcer/src/__tests__/clear-messages.test.tsx @@ -8,7 +8,7 @@ describe("Announcer.clearMessages", () => { test("empties a targeted live region element by IDREF", async () => { // ARRANGE const message1 = "Shine a million stars"; - const message2 = "Dull no stars"; + // const message2 = "Dull no stars"; // ACT const announcement1Id = await announceMessage({ @@ -19,12 +19,12 @@ describe("Announcer.clearMessages", () => { const region1 = screen.getByTestId("wbARegion-polite1"); - // jest.advanceTimersByTime(250); + jest.advanceTimersByTime(250); - // await waitFor(() => { - // expect(region1).toHaveTextContent(message1); - // clearMessages(announcement1Id); - // }); + await waitFor(() => { + expect(region1).toHaveTextContent(message1); + clearMessages(announcement1Id); + }); // announceMessage({ // message: message2, diff --git a/packages/wonder-blocks-announcer/src/util/util.ts b/packages/wonder-blocks-announcer/src/util/util.ts index 685f6189a..386993f3c 100644 --- a/packages/wonder-blocks-announcer/src/util/util.ts +++ b/packages/wonder-blocks-announcer/src/util/util.ts @@ -31,13 +31,13 @@ export function createDebounceFunction( const debouncedFn = (...args: []) => { return new Promise((resolve) => { - console.log("debounceFn", args); + // console.log("debounceFn", args); const later = () => { const result = callback.apply(context, args); if (timeoutId) { clearTimeout(timeoutId); } - console.log("later", args); + // console.log("later", args); return resolve(result); }; From ed991abef8706c7b065d767bdbdb4f2ed77ada97 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Mon, 3 Mar 2025 15:27:03 -0800 Subject: [PATCH 21/22] [combobox-announcer] Fix clearMessage test w/ debounce --- .../src/__tests__/clear-messages.test.tsx | 41 +++++++++++-------- .../src/announce-message.ts | 9 ++-- .../wonder-blocks-announcer/src/util/util.ts | 2 - 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/wonder-blocks-announcer/src/__tests__/clear-messages.test.tsx b/packages/wonder-blocks-announcer/src/__tests__/clear-messages.test.tsx index 0a16e76b6..334160a40 100644 --- a/packages/wonder-blocks-announcer/src/__tests__/clear-messages.test.tsx +++ b/packages/wonder-blocks-announcer/src/__tests__/clear-messages.test.tsx @@ -2,45 +2,52 @@ import {screen, waitFor} from "@testing-library/react"; import {announceMessage} from "../announce-message"; import {clearMessages} from "../clear-messages"; -jest.useFakeTimers(); - describe("Announcer.clearMessages", () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); + }); test("empties a targeted live region element by IDREF", async () => { // ARRANGE const message1 = "Shine a million stars"; - // const message2 = "Dull no stars"; + const message2 = "Dull no stars"; // ACT - const announcement1Id = await announceMessage({ + const announcement1IdPromise = announceMessage({ message: message1, initialTimeout: 0, debounceThreshold: 0, }); + jest.advanceTimersByTime(0); + await Promise.resolve(); const region1 = screen.getByTestId("wbARegion-polite1"); - jest.advanceTimersByTime(250); + const announcement1Id = await announcement1IdPromise; - await waitFor(() => { - expect(region1).toHaveTextContent(message1); - clearMessages(announcement1Id); - }); + jest.advanceTimersByTime(0); + await Promise.resolve(); - // announceMessage({ - // message: message2, - // initialTimeout: 0, - // debounceThreshold: 0, - // }); + expect(region1).toHaveTextContent(message1); + clearMessages(announcement1Id); - // const region2 = screen.getByTestId("wbARegion-polite0"); + announceMessage({ + message: message2, + initialTimeout: 0, + debounceThreshold: 0, + }); - // jest.advanceTimersByTime(250); + const region2 = screen.getByTestId("wbARegion-polite0"); + + jest.advanceTimersByTime(0); // ASSERT await waitFor(() => { expect(region1).toBeEmptyDOMElement(); }); - // expect(region2).toHaveTextContent(message2); + expect(region2).toHaveTextContent(message2); }); test("empties all live region elements by default", async () => { diff --git a/packages/wonder-blocks-announcer/src/announce-message.ts b/packages/wonder-blocks-announcer/src/announce-message.ts index f6bee4259..2a7618be3 100644 --- a/packages/wonder-blocks-announcer/src/announce-message.ts +++ b/packages/wonder-blocks-announcer/src/announce-message.ts @@ -25,8 +25,8 @@ export function announceMessage({ const announcer = Announcer.getInstance(); if (initialTimeout > 0) { return new Promise((resolve) => { - setTimeout(async () => { - const result = await announcer.announce( + return setTimeout(async () => { + const result = announcer.announce( message, level, debounceThreshold, @@ -35,9 +35,6 @@ export function announceMessage({ }, initialTimeout); }); } else { - const result = announcer.announce(message, level, debounceThreshold); - return new Promise((resolve) => { - resolve(result); - }); + return announcer.announce(message, level, debounceThreshold); } } diff --git a/packages/wonder-blocks-announcer/src/util/util.ts b/packages/wonder-blocks-announcer/src/util/util.ts index 386993f3c..99e1516e0 100644 --- a/packages/wonder-blocks-announcer/src/util/util.ts +++ b/packages/wonder-blocks-announcer/src/util/util.ts @@ -31,13 +31,11 @@ export function createDebounceFunction( const debouncedFn = (...args: []) => { return new Promise((resolve) => { - // console.log("debounceFn", args); const later = () => { const result = callback.apply(context, args); if (timeoutId) { clearTimeout(timeoutId); } - // console.log("later", args); return resolve(result); }; From ff24d0cfc6cc7a465ca9b45178e7b970439b35ab Mon Sep 17 00:00:00 2001 From: Marcy Sutton Todd Date: Mon, 3 Mar 2025 15:27:14 -0800 Subject: [PATCH 22/22] [combobox-announcer] Clean up unused storybook styles --- __docs__/wonder-blocks-announcer/announcer.stories.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/__docs__/wonder-blocks-announcer/announcer.stories.tsx b/__docs__/wonder-blocks-announcer/announcer.stories.tsx index 6763cfa19..10d9d1808 100644 --- a/__docs__/wonder-blocks-announcer/announcer.stories.tsx +++ b/__docs__/wonder-blocks-announcer/announcer.stories.tsx @@ -114,14 +114,4 @@ const styles = StyleSheet.create({ alignItems: "center", justifyContent: "center", }, - container: { - width: "100%", - }, - narrowBanner: { - maxWidth: 400, - }, - rightToLeft: { - width: "100%", - direction: "rtl", - }, });