+
} />
diff --git a/teachertool/src/components/ScreenReaderAnnouncer.tsx b/teachertool/src/components/ScreenReaderAnnouncer.tsx
new file mode 100644
index 000000000000..2ac53aba18ba
--- /dev/null
+++ b/teachertool/src/components/ScreenReaderAnnouncer.tsx
@@ -0,0 +1,17 @@
+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/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/CatalogModal.module.scss b/teachertool/src/components/styling/CatalogModal.module.scss
deleted file mode 100644
index 929ac1138c79..000000000000
--- a/teachertool/src/components/styling/CatalogModal.module.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-.catalog-modal {
- div[class*="common-modal-footer"] {
- display: flex;
- justify-content: flex-end;
- }
-
- .catalog-done {
- align-self: flex-end;
- }
-
- .catalog-item {
- padding: 0.5rem;
- }
-}
diff --git a/teachertool/src/components/styling/CatalogOverlay.module.scss b/teachertool/src/components/styling/CatalogOverlay.module.scss
new file mode 100644
index 000000000000..94c1db07038b
--- /dev/null
+++ b/teachertool/src/components/styling/CatalogOverlay.module.scss
@@ -0,0 +1,103 @@
+.catalog-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 49; // Above everything except toasts
+ background-color: rgba(0, 0, 0, 0.5);
+
+ color: var(--pxt-page-foreground);
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .catalog-content-container {
+ max-width: 95%;
+ max-height: 95%;
+ background-color: var(--pxt-page-background);
+ border-radius: .285rem; // Match modal
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+
+ .header {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ border-bottom: 1px solid var(--pxt-content-accent);
+ height: 4rem;
+
+ .title {
+ font-size: 1.2rem;
+ font-weight: 800;
+ padding: 1rem;
+ }
+
+ .close-button {
+ height: 100%;
+ padding: 0 1rem;
+
+ i {
+ font-size: 1.3rem;
+ transition: scale 0.1s ease-in-out;
+ }
+
+ &:hover i {
+ scale: 1.1;
+ }
+ }
+ }
+
+ .catalog-list {
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+
+ .catalog-item {
+ width: 100%;
+ padding: 1.25rem 1.5rem 1.25rem 1rem;
+ border-bottom: 1px solid var(--pxt-content-accent);
+ flex-shrink: 0;
+ margin: 0;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .catalog-item-label {
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+
+ .action-indicators {
+ position: relative;
+ padding: 0 1rem 0 0;
+ width: 3rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+
+ i {
+ position: absolute;
+ transition: opacity 0.1s ease-in-out;
+ }
+
+ .hide-indicator {
+ opacity: 0;
+ }
+
+ .recently-added-indicator {
+ color: var(--pxt-success-accent);
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/teachertool/src/components/styling/ProjectWorkspace.module.scss b/teachertool/src/components/styling/ProjectWorkspace.module.scss
index 1392fa500b85..620eafb83724 100644
--- a/teachertool/src/components/styling/ProjectWorkspace.module.scss
+++ b/teachertool/src/components/styling/ProjectWorkspace.module.scss
@@ -4,6 +4,7 @@
display: flex;
flex-direction: column;
background-color: var(--pxt-headerbar-accent-smoke);
+ position: relative;
.projectName {
height: 100%;
diff --git a/teachertool/src/components/styling/ReadonlyCriteriaDisplay.module.scss b/teachertool/src/components/styling/ReadonlyCriteriaDisplay.module.scss
index 43f727415a11..6cef3907b25c 100644
--- a/teachertool/src/components/styling/ReadonlyCriteriaDisplay.module.scss
+++ b/teachertool/src/components/styling/ReadonlyCriteriaDisplay.module.scss
@@ -4,11 +4,15 @@
align-items: flex-start;
.criteria-template {
+ margin-bottom: 0.4rem;
+
.plain-text-segment {
+ display: inline-block;
margin-right: 0.3rem;
}
.param-segment {
+ display: inline-block;
border-radius: 0.5rem;
border: 1px solid var(--pxt-page-foreground);
padding: 0.2rem 0.5rem;
diff --git a/teachertool/src/constants.ts b/teachertool/src/constants.ts
index 484d31f41527..4cf131add02f 100644
--- a/teachertool/src/constants.ts
+++ b/teachertool/src/constants.ts
@@ -26,6 +26,10 @@ export namespace Strings {
export const AddSelected = lf("Add Selected");
export const Continue = lf("Continue");
export const Loading = lf("Loading...");
+ export const Close = lf("Close");
+ export const Max = lf("Max");
+ export const AddToChecklist = lf("Add to Checklist");
+ export const SelectCriteriaDescription = lf("Select the criteria you'd like to include");
}
export namespace Ticks {
diff --git a/teachertool/src/services/loggingService.ts b/teachertool/src/services/loggingService.ts
index 4a969408b661..91540961d4f1 100644
--- a/teachertool/src/services/loggingService.ts
+++ b/teachertool/src/services/loggingService.ts
@@ -37,4 +37,3 @@ export const logDebug = (message: any, data?: any) => {
console.log(timestamp(), message, data);
}
};
-
diff --git a/teachertool/src/state/actions.ts b/teachertool/src/state/actions.ts
index 8e0be1f69ddb..cd3cc472f9e1 100644
--- a/teachertool/src/state/actions.ts
+++ b/teachertool/src/state/actions.ts
@@ -51,6 +51,11 @@ type SetCatalog = ActionBase & {
catalog: CatalogCriteria[] | undefined;
};
+type SetCatalogOpen = ActionBase & {
+ type: "SET_CATALOG_OPEN";
+ open: boolean;
+};
+
type SetRubric = ActionBase & {
type: "SET_RUBRIC";
rubric: Rubric;
@@ -100,6 +105,11 @@ type SetBlockImageUri = ActionBase & {
imageUri: string;
};
+type SetScreenReaderAnnouncement = ActionBase & {
+ type: "SET_SCREEN_READER_ANNOUNCEMENT";
+ announcement: string;
+};
+
/**
* Union of all actions
*/
@@ -115,6 +125,7 @@ export type Action =
| SetEvalResultsBatch
| SetTargetConfig
| SetCatalog
+ | SetCatalogOpen
| SetRubric
| ShowModal
| HideModal
@@ -122,7 +133,8 @@ export type Action =
| SetActiveTab
| SetAutorun
| SetToolboxCategories
- | SetBlockImageUri;
+ | SetBlockImageUri
+ | SetScreenReaderAnnouncement;
/**
* Action creators
@@ -167,6 +179,11 @@ const setCatalog = (catalog: CatalogCriteria[] | undefined): SetCatalog => ({
catalog,
});
+const setCatalogOpen = (open: boolean): SetCatalogOpen => ({
+ type: "SET_CATALOG_OPEN",
+ open,
+});
+
const setRubric = (rubric: Rubric): SetRubric => ({
type: "SET_RUBRIC",
rubric,
@@ -216,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,
@@ -227,6 +249,7 @@ export {
setEvalResultsBatch,
setTargetConfig,
setCatalog,
+ setCatalogOpen,
setRubric,
showModal,
hideModal,
@@ -235,4 +258,5 @@ export {
setAutorun,
setToolboxCategories,
setBlockImageUri,
+ setScreenReaderAnnouncement,
};
diff --git a/teachertool/src/state/helpers.ts b/teachertool/src/state/helpers.ts
index 8988cded86b5..04d96f8e5766 100644
--- a/teachertool/src/state/helpers.ts
+++ b/teachertool/src/state/helpers.ts
@@ -75,17 +75,15 @@ export function getSafeRubricName(state: AppState): string | undefined {
return state.rubric.name || Strings.UntitledRubric;
}
-export function getSelectableCatalogCriteria(state: AppState): CatalogCriteria[] {
- const usedCatalogCriteria = state.rubric.criteria.map(c => c.catalogCriteriaId) ?? [];
+export function getCatalogCriteria(state: AppState): CatalogCriteria[] {
+ return state.catalog?.filter(c => !c.hideInCatalog) ?? [];
+}
+export function criteriaIsSelectable(state: AppState, catalogCriteria: CatalogCriteria): boolean {
// Return a criteria as selectable if it has parameters (so it can be used multiple times in a rubric)
// or if it has not yet been used in the active rubric.
return (
- state.catalog?.filter(
- catalogCriteria =>
- ((catalogCriteria.params && catalogCriteria.params.length > 0) ||
- !usedCatalogCriteria.includes(catalogCriteria.id)) &&
- !catalogCriteria.hideInCatalog
- ) ?? []
+ (catalogCriteria.params && catalogCriteria.params.length > 0) ||
+ !state.rubric.criteria.some(c => c.catalogCriteriaId === catalogCriteria.id)
);
}
diff --git a/teachertool/src/state/reducer.ts b/teachertool/src/state/reducer.ts
index a1df05ebb446..d260aa14a8ab 100644
--- a/teachertool/src/state/reducer.ts
+++ b/teachertool/src/state/reducer.ts
@@ -76,6 +76,12 @@ export default function reducer(state: AppState, action: Action): AppState {
catalog: action.catalog,
};
}
+ case "SET_CATALOG_OPEN": {
+ return {
+ ...state,
+ catalogOpen: action.open,
+ };
+ }
case "SET_RUBRIC": {
/*await*/ updateStoredRubricAsync(state.rubric, action.rubric); // fire and forget, we don't need to wait for this to finish.
return {
@@ -133,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 8f2484cb24f4..4a74375f8634 100644
--- a/teachertool/src/state/state.ts
+++ b/teachertool/src/state/state.ts
@@ -18,6 +18,8 @@ export type AppState = {
toolboxCategories?: pxt.Map;
blockImageCache: pxt.Map; // block id -> image uri
copilotEndpointOverride?: string; // TODO: remove once copilot is available in prod.
+ catalogOpen: boolean;
+ screenReaderAnnouncement?: string;
flags: {
testCatalog: boolean;
};
@@ -36,6 +38,8 @@ export const initialAppState: AppState = {
toolboxCategories: undefined,
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));
+}
diff --git a/teachertool/src/transforms/getRubricFromFileAsync.ts b/teachertool/src/transforms/getRubricFromFileAsync.ts
index e516d520d23d..d833e4c16968 100644
--- a/teachertool/src/transforms/getRubricFromFileAsync.ts
+++ b/teachertool/src/transforms/getRubricFromFileAsync.ts
@@ -7,7 +7,7 @@ export async function getRubricFromFileAsync(file: File, allowPartial: boolean):
let rubric = await loadRubricFromFileAsync(file);
if (rubric) {
- logDebug("Loading rubric from file...", {file, rubric});
+ logDebug("Loading rubric from file...", { file, rubric });
const rubricVerificationResult = verifyRubricIntegrity(rubric);
diff --git a/teachertool/src/transforms/runEvaluateAsync.ts b/teachertool/src/transforms/runEvaluateAsync.ts
index 602428044db8..7988fe223214 100644
--- a/teachertool/src/transforms/runEvaluateAsync.ts
+++ b/teachertool/src/transforms/runEvaluateAsync.ts
@@ -112,7 +112,7 @@ export async function runEvaluateAsync(fromUserInteraction: boolean) {
const plan = generateValidatorPlan(criteriaInstance, fromUserInteraction);
- logDebug(`${criteriaInstance.instanceId}: Generated Plan`, plan)
+ logDebug(`${criteriaInstance.instanceId}: Generated Plan`, plan);
if (!plan) {
dispatch(Actions.clearEvalResult(criteriaInstance.instanceId));
diff --git a/teachertool/src/transforms/setCatalogOpen.ts b/teachertool/src/transforms/setCatalogOpen.ts
new file mode 100644
index 000000000000..299e235a6102
--- /dev/null
+++ b/teachertool/src/transforms/setCatalogOpen.ts
@@ -0,0 +1,10 @@
+import { stateAndDispatch } from "../state";
+import * as Actions from "../state/actions";
+
+export function setCatalogOpen(open: boolean) {
+ const { state: teacherTool, dispatch } = stateAndDispatch();
+
+ if (teacherTool.catalogOpen != open) {
+ dispatch(Actions.setCatalogOpen(open));
+ }
+}
diff --git a/teachertool/src/types/modalOptions.ts b/teachertool/src/types/modalOptions.ts
index f6c5f645a1ce..a33331ef1047 100644
--- a/teachertool/src/types/modalOptions.ts
+++ b/teachertool/src/types/modalOptions.ts
@@ -12,12 +12,8 @@ export type BlockPickerOptions = {
paramName: string;
};
-export type CatalogDisplayOptions = {
- modal: "catalog-display";
-};
-
export type ImportRubricOptions = {
modal: "import-rubric";
};
-export type ModalOptions = CatalogDisplayOptions | ImportRubricOptions | ConfirmationModalOptions | BlockPickerOptions;
+export type ModalOptions = ImportRubricOptions | ConfirmationModalOptions | BlockPickerOptions;