Skip to content

Commit

Permalink
Add screen reader announcer
Browse files Browse the repository at this point in the history
  • Loading branch information
thsparks committed Apr 22, 2024
1 parent ade227b commit d3ab60f
Show file tree
Hide file tree
Showing 10 changed files with 63 additions and 5 deletions.
2 changes: 2 additions & 0 deletions teachertool/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -60,6 +61,7 @@ export const App = () => {
<ConfirmationModal />
<BlockPickerModal />
<Toasts />
<ScreenReaderAnnouncer />
</>
);
};
5 changes: 4 additions & 1 deletion teachertool/src/components/CatalogOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 (
Expand Down
13 changes: 13 additions & 0 deletions teachertool/src/components/ScreenReaderAnnouncer.tsx
Original file line number Diff line number Diff line change
@@ -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<ScreenReaderAnnouncerProps> = () => {
const { state: teacherTool } = useContext(AppStateContext);
return <>
{teacherTool.screenReaderAnnouncement && (
<div className={css["sr-only"]} aria-live="polite"> {teacherTool.screenReaderAnnouncement} </div>
)}
</>
}
4 changes: 2 additions & 2 deletions teachertool/src/components/Toasts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ const ToastNotification: React.FC<IToastNotificationProps> = ({ toast }) => {
}, [sliderActive]);

return (
<div className={classList(css["toast"], css[toast.type], toast.className)}>
<div className={classList(css["toast"], css[toast.type], toast.className)} aria-live="assertive">
<div className={css["toast-content"]}>
{!toast.hideIcon && (
<div className={classList(css["icon-container"], css[toast.type])}>
{toast.icon ?? icons[toast.type]}
</div>
)}
<div className={css["text-container"]}>
{toast.text && <div className={classList(css["text"], "tt-toast-text")}>{toast.text}</div>}
{toast.text && <div className={classList(css["text"], "tt-toast-text")} >{toast.text}</div>}
{toast.detail && <div className={css["detail"]}>{toast.detail}</div>}
{toast.jsx && <div>{toast.jsx}</div>}
</div>
Expand Down
13 changes: 13 additions & 0 deletions teachertool/src/components/styling/ActionAnnouncer.module.scss
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 13 additions & 1 deletion teachertool/src/state/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ type SetBlockImageUri = ActionBase & {
imageUri: string;
};

type SetScreenReaderAnnouncement = ActionBase & {
type: "SET_SCREEN_READER_ANNOUNCEMENT";
announcement: string;
};

/**
* Union of all actions
*/
Expand All @@ -128,7 +133,8 @@ export type Action =
| SetActiveTab
| SetAutorun
| SetToolboxCategories
| SetBlockImageUri;
| SetBlockImageUri
| SetScreenReaderAnnouncement;

/**
* Action creators
Expand Down Expand Up @@ -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,
Expand All @@ -247,4 +258,5 @@ export {
setAutorun,
setToolboxCategories,
setBlockImageUri,
setScreenReaderAnnouncement,
};
6 changes: 6 additions & 0 deletions teachertool/src/state/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
}
}
2 changes: 2 additions & 0 deletions teachertool/src/state/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type AppState = {
blockImageCache: pxt.Map<string>; // block id -> image uri
copilotEndpointOverride?: string; // TODO: remove once copilot is available in prod.
catalogOpen: boolean;
screenReaderAnnouncement?: string;
flags: {
testCatalog: boolean;
};
Expand All @@ -38,6 +39,7 @@ export const initialAppState: AppState = {
blockImageCache: {},
copilotEndpointOverride: undefined,
catalogOpen: false,
screenReaderAnnouncement: undefined,
flags: {
testCatalog: false,
},
Expand Down
7 changes: 7 additions & 0 deletions teachertool/src/transforms/announceToScreenReader.ts
Original file line number Diff line number Diff line change
@@ -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));
}

0 comments on commit d3ab60f

Please sign in to comment.