From d3ab60fbd6e3cbf245a7ebb5b52bd074d11fdc62 Mon Sep 17 00:00:00 2001 From: thsparks Date: Mon, 22 Apr 2024 16:47:18 -0700 Subject: [PATCH] Add screen reader announcer --- teachertool/src/App.tsx | 2 ++ teachertool/src/components/CatalogOverlay.tsx | 5 ++++- .../src/components/ScreenReaderAnnouncer.tsx | 13 +++++++++++++ teachertool/src/components/Toasts.tsx | 4 ++-- .../components/styling/ActionAnnouncer.module.scss | 13 +++++++++++++ .../components/styling/CatalogOverlay.module.scss | 2 +- teachertool/src/state/actions.ts | 14 +++++++++++++- teachertool/src/state/reducer.ts | 6 ++++++ teachertool/src/state/state.ts | 2 ++ .../src/transforms/announceToScreenReader.ts | 7 +++++++ 10 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 teachertool/src/components/ScreenReaderAnnouncer.tsx create mode 100644 teachertool/src/components/styling/ActionAnnouncer.module.scss create mode 100644 teachertool/src/transforms/announceToScreenReader.ts diff --git a/teachertool/src/App.tsx b/teachertool/src/App.tsx index 460453a5f60a..13a9b3bf4391 100644 --- a/teachertool/src/App.tsx +++ b/teachertool/src/App.tsx @@ -15,6 +15,7 @@ import { tryLoadLastActiveRubricAsync } from "./transforms/tryLoadLastActiveRubr import { ImportRubricModal } from "./components/ImportRubricModal"; import { ConfirmationModal } from "./components/ConfirmationModal"; import { BlockPickerModal } from "./components/BlockPickerModal"; +import { ScreenReaderAnnouncer } from "./components/ScreenReaderAnnouncer"; export const App = () => { const { state, dispatch } = useContext(AppStateContext); @@ -60,6 +61,7 @@ export const App = () => { + ); }; diff --git a/teachertool/src/components/CatalogOverlay.tsx b/teachertool/src/components/CatalogOverlay.tsx index 6b82665a0c31..2b33ca096d1c 100644 --- a/teachertool/src/components/CatalogOverlay.tsx +++ b/teachertool/src/components/CatalogOverlay.tsx @@ -6,9 +6,10 @@ import { getCatalogCriteria } from "../state/helpers"; import { ReadOnlyCriteriaDisplay } from "./ReadonlyCriteriaDisplay"; import { Strings } from "../constants"; import { Button } from "react-common/components/controls/Button"; -import { getReadableCriteriaTemplate } from "../utils"; +import { getReadableCriteriaTemplate, makeToast } from "../utils"; import { setCatalogOpen } from "../transforms/setCatalogOpen"; import { classList } from "react-common/components/util"; +import { announceToScreenReader } from "../transforms/announceToScreenReader"; import css from "./styling/CatalogOverlay.module.scss"; interface CatalogHeaderProps { @@ -90,6 +91,8 @@ const CatalogList: React.FC = () => { setTimeout(() => { setRecentlyAddedIds(recentlyAddedIds.filter(id => id !== c.id)); }, recentlyAddedWindowMs); + + announceToScreenReader(lf("Added '{0}' to checklist.", getReadableCriteriaTemplate(c))); } return ( diff --git a/teachertool/src/components/ScreenReaderAnnouncer.tsx b/teachertool/src/components/ScreenReaderAnnouncer.tsx new file mode 100644 index 000000000000..f39568fd8a49 --- /dev/null +++ b/teachertool/src/components/ScreenReaderAnnouncer.tsx @@ -0,0 +1,13 @@ +import { useContext } from "react"; +import { AppStateContext } from "../state/appStateContext"; +import css from "./styling/ActionAnnouncer.module.scss"; + +export interface ScreenReaderAnnouncerProps {} +export const ScreenReaderAnnouncer: React.FC = () => { + const { state: teacherTool } = useContext(AppStateContext); + return <> + {teacherTool.screenReaderAnnouncement && ( +
{teacherTool.screenReaderAnnouncement}
+ )} + +} diff --git a/teachertool/src/components/Toasts.tsx b/teachertool/src/components/Toasts.tsx index a57d8ad5e464..5a08058b1d4c 100644 --- a/teachertool/src/components/Toasts.tsx +++ b/teachertool/src/components/Toasts.tsx @@ -44,7 +44,7 @@ const ToastNotification: React.FC = ({ toast }) => { }, [sliderActive]); return ( -
+
{!toast.hideIcon && (
@@ -52,7 +52,7 @@ const ToastNotification: React.FC = ({ toast }) => {
)}
- {toast.text &&
{toast.text}
} + {toast.text &&
{toast.text}
} {toast.detail &&
{toast.detail}
} {toast.jsx &&
{toast.jsx}
}
diff --git a/teachertool/src/components/styling/ActionAnnouncer.module.scss b/teachertool/src/components/styling/ActionAnnouncer.module.scss new file mode 100644 index 000000000000..dd16f6f581fa --- /dev/null +++ b/teachertool/src/components/styling/ActionAnnouncer.module.scss @@ -0,0 +1,13 @@ +// Hide content but keep component around for screen readers +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; + white-space: nowrap; + border-width: 0; +} diff --git a/teachertool/src/components/styling/CatalogOverlay.module.scss b/teachertool/src/components/styling/CatalogOverlay.module.scss index b9656ed64026..82011a855321 100644 --- a/teachertool/src/components/styling/CatalogOverlay.module.scss +++ b/teachertool/src/components/styling/CatalogOverlay.module.scss @@ -4,7 +4,7 @@ left: 0; width: 100%; height: 100%; - z-index: 1000; // Above everything + z-index: 49; // Above everything except toasts background-color: rgba(0, 0, 0, 0.5); color: var(--pxt-page-foreground); diff --git a/teachertool/src/state/actions.ts b/teachertool/src/state/actions.ts index 749c4f698c05..cd3cc472f9e1 100644 --- a/teachertool/src/state/actions.ts +++ b/teachertool/src/state/actions.ts @@ -105,6 +105,11 @@ type SetBlockImageUri = ActionBase & { imageUri: string; }; +type SetScreenReaderAnnouncement = ActionBase & { + type: "SET_SCREEN_READER_ANNOUNCEMENT"; + announcement: string; +}; + /** * Union of all actions */ @@ -128,7 +133,8 @@ export type Action = | SetActiveTab | SetAutorun | SetToolboxCategories - | SetBlockImageUri; + | SetBlockImageUri + | SetScreenReaderAnnouncement; /** * Action creators @@ -227,6 +233,11 @@ const setBlockImageUri = (blockId: string, imageUri: string): SetBlockImageUri = imageUri, }); +const setScreenReaderAnnouncement = (announcement: string): SetScreenReaderAnnouncement => ({ + type: "SET_SCREEN_READER_ANNOUNCEMENT", + announcement, +}); + export { showToast, dismissToast, @@ -247,4 +258,5 @@ export { setAutorun, setToolboxCategories, setBlockImageUri, + setScreenReaderAnnouncement, }; diff --git a/teachertool/src/state/reducer.ts b/teachertool/src/state/reducer.ts index 7c634efb9d34..d260aa14a8ab 100644 --- a/teachertool/src/state/reducer.ts +++ b/teachertool/src/state/reducer.ts @@ -139,5 +139,11 @@ export default function reducer(state: AppState, action: Action): AppState { blockImageCache: cache, }; } + case "SET_SCREEN_READER_ANNOUNCEMENT": { + return { + ...state, + screenReaderAnnouncement: action.announcement, + }; + } } } diff --git a/teachertool/src/state/state.ts b/teachertool/src/state/state.ts index 11134ee4ac41..4a74375f8634 100644 --- a/teachertool/src/state/state.ts +++ b/teachertool/src/state/state.ts @@ -19,6 +19,7 @@ export type AppState = { blockImageCache: pxt.Map; // block id -> image uri copilotEndpointOverride?: string; // TODO: remove once copilot is available in prod. catalogOpen: boolean; + screenReaderAnnouncement?: string; flags: { testCatalog: boolean; }; @@ -38,6 +39,7 @@ export const initialAppState: AppState = { blockImageCache: {}, copilotEndpointOverride: undefined, catalogOpen: false, + screenReaderAnnouncement: undefined, flags: { testCatalog: false, }, diff --git a/teachertool/src/transforms/announceToScreenReader.ts b/teachertool/src/transforms/announceToScreenReader.ts new file mode 100644 index 000000000000..bf76ed6ab53a --- /dev/null +++ b/teachertool/src/transforms/announceToScreenReader.ts @@ -0,0 +1,7 @@ +import { stateAndDispatch } from "../state"; +import * as Actions from "../state/actions"; + +export function announceToScreenReader(announcement: string) { + const { dispatch } = stateAndDispatch(); + dispatch(Actions.setScreenReaderAnnouncement(announcement)); +}