Skip to content

Commit

Permalink
Teacher Tool: Make Catalog Non-Modal (#9983)
Browse files Browse the repository at this point in the history
This change makes it so you can keep the catalog open, add multiple criteria, and interact with other elements on the page without having to close the catalog.
  • Loading branch information
thsparks authored Apr 24, 2024
1 parent 8c30855 commit 5a3bb20
Show file tree
Hide file tree
Showing 22 changed files with 384 additions and 130 deletions.
4 changes: 2 additions & 2 deletions teachertool/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import { logDebug } from "./services/loggingService";
import { HeaderBar } from "./components/HeaderBar";
import { MainPanel } from "./components/MainPanel";
import { Toasts } from "./components/Toasts";
import { CatalogModal } from "./components/CatalogModal";
import { showToast } from "./transforms/showToast";
import { loadCatalogAsync } from "./transforms/loadCatalogAsync";
import { loadValidatorPlansAsync } from "./transforms/loadValidatorPlansAsync";
import { tryLoadLastActiveRubricAsync } from "./transforms/tryLoadLastActiveRubricAsync";
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 @@ -57,11 +57,11 @@ export const App = () => {
<>
<HeaderBar />
<MainPanel />
<CatalogModal />
<ImportRubricModal />
<ConfirmationModal />
<BlockPickerModal />
<Toasts />
<ScreenReaderAnnouncer />
</>
);
};
17 changes: 5 additions & 12 deletions teachertool/src/components/AddCriteriaButton.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
import { getSelectableCatalogCriteria } from "../state/helpers";
import { Button } from "react-common/components/controls/Button";
import { showModal } from "../transforms/showModal";
import { AppStateContext } from "../state/appStateContext";
import { useContext, useMemo } from "react";
import { useContext } from "react";
import { classList } from "react-common/components/util";
import { Strings } from "../constants";
import { CatalogDisplayOptions } from "../types/modalOptions";
import { setCatalogOpen } from "../transforms/setCatalogOpen";

interface IProps {}

export const AddCriteriaButton: React.FC<IProps> = ({}) => {
const { state: teacherTool } = useContext(AppStateContext);

const hasAvailableCriteria = useMemo<boolean>(
() => getSelectableCatalogCriteria(teacherTool).length > 0,
[teacherTool.catalog, teacherTool.rubric]
);
return (
return !teacherTool.catalogOpen ? (
<Button
className={classList("inline", "outline-button")}
label={Strings.AddCriteria}
onClick={() => showModal({ modal: "catalog-display" } as CatalogDisplayOptions)}
onClick={() => setCatalogOpen(true)}
title={Strings.AddCriteria}
leftIcon="fas fa-plus-circle"
disabled={!hasAvailableCriteria}
/>
);
) : null;
};
85 changes: 0 additions & 85 deletions teachertool/src/components/CatalogModal.tsx

This file was deleted.

166 changes: 166 additions & 0 deletions teachertool/src/components/CatalogOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { useContext, useMemo, useState } from "react";
import { AppStateContext } from "../state/appStateContext";
import { addCriteriaToRubric } from "../transforms/addCriteriaToRubric";
import { CatalogCriteria } from "../types/criteria";
import { getCatalogCriteria } from "../state/helpers";
import { ReadOnlyCriteriaDisplay } from "./ReadonlyCriteriaDisplay";
import { Strings } from "../constants";
import { Button } from "react-common/components/controls/Button";
import { getReadableCriteriaTemplate, makeToast } from "../utils";
import { setCatalogOpen } from "../transforms/setCatalogOpen";
import { classList } from "react-common/components/util";
import { announceToScreenReader } from "../transforms/announceToScreenReader";
import { FocusTrap } from "react-common/components/controls/FocusTrap";
import css from "./styling/CatalogOverlay.module.scss";

interface CatalogHeaderProps {
onClose: () => void;
}
const CatalogHeader: React.FC<CatalogHeaderProps> = ({ onClose }) => {
return (
<div className={css["header"]}>
<span className={css["title"]}>{Strings.SelectCriteriaDescription}</span>
<Button
className={css["close-button"]}
rightIcon="fas fa-times-circle"
onClick={onClose}
title={Strings.Close}
/>
</div>
);
};

interface CatalogItemLabelProps {
catalogCriteria: CatalogCriteria;
allowsMultiple: boolean;
existingInstanceCount: number;
recentlyAdded: boolean;
}
const CatalogItemLabel: React.FC<CatalogItemLabelProps> = ({
catalogCriteria,
allowsMultiple,
existingInstanceCount,
recentlyAdded,
}) => {
const canAddMore = allowsMultiple || existingInstanceCount === 0;
const showRecentlyAddedIndicator = recentlyAdded && canAddMore;
return (
<div className={css["catalog-item-label"]}>
<div className={css["action-indicators"]}>
{canAddMore ? (
<>
<i
className={classList(
"fas fa-check",
css["recently-added-indicator"],
showRecentlyAddedIndicator ? undefined : css["hide-indicator"]
)}
title={lf("Added!")}
/>
<i
className={classList(
"fas fa-plus",
showRecentlyAddedIndicator ? css["hide-indicator"] : undefined
)}
title={Strings.AddToChecklist}
/>
</>
) : (
<span className={css["max-label"]}>{Strings.Max}</span>
)}
</div>
<ReadOnlyCriteriaDisplay catalogCriteria={catalogCriteria} showDescription={true} />
</div>
);
};

const CatalogList: React.FC = () => {
const { state: teacherTool } = useContext(AppStateContext);

const recentlyAddedWindowMs = 500;
const [recentlyAddedIds, setRecentlyAddedIds] = useState<pxsim.Map<NodeJS.Timeout>>({});

const criteria = useMemo<CatalogCriteria[]>(
() => getCatalogCriteria(teacherTool),
[teacherTool.catalog, teacherTool.rubric]
);

function updateRecentlyAddedValue(id: string, value: NodeJS.Timeout | undefined) {
setRecentlyAddedIds(prevState => {
const newState = { ...prevState };
if (value) {
newState[id] = value;
} else {
delete newState[id];
}
return newState;
});
}

function onItemClicked(c: CatalogCriteria) {
addCriteriaToRubric([c.id]);

// Set a timeout to remove the recently added indicator
// and save it in the state.
if (recentlyAddedIds[c.id]) {
clearTimeout(recentlyAddedIds[c.id]);
}
const timeoutId = setTimeout(() => {
updateRecentlyAddedValue(c.id, undefined);
}, recentlyAddedWindowMs);
updateRecentlyAddedValue(c.id, timeoutId);

announceToScreenReader(lf("Added '{0}' to checklist.", getReadableCriteriaTemplate(c)));
}

return (
<div className={css["catalog-list"]}>
{criteria.map(c => {
const allowsMultiple = c.params !== undefined && c.params.length !== 0; // TODO add a json flag for this (MaxCount or AllowMultiple)
const existingInstanceCount = teacherTool.rubric.criteria.filter(
i => i.catalogCriteriaId === c.id
).length;
return (
c.template && (
<Button
id={`criteria_${c.id}`}
title={getReadableCriteriaTemplate(c)}
key={c.id}
className={css["catalog-item"]}
label={
<CatalogItemLabel
catalogCriteria={c}
allowsMultiple={allowsMultiple}
existingInstanceCount={existingInstanceCount}
recentlyAdded={recentlyAddedIds[c.id] !== undefined}
/>
}
onClick={() => onItemClicked(c)}
disabled={!allowsMultiple && existingInstanceCount > 0}
/>
)
);
})}
</div>
);
};

interface CatalogOverlayProps {}
export const CatalogOverlay: React.FC<CatalogOverlayProps> = ({}) => {
const { state: teacherTool } = useContext(AppStateContext);

function closeOverlay() {
setCatalogOpen(false);
}

return teacherTool.catalogOpen ? (
<FocusTrap onEscape={() => {}}>
<div className={css["catalog-overlay"]}>
<div className={css["catalog-content-container"]}>
<CatalogHeader onClose={closeOverlay} />
<CatalogList />
</div>
</div>
</FocusTrap>
) : null;
};
2 changes: 2 additions & 0 deletions teachertool/src/components/ProjectWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getSafeProjectName } from "../state/helpers";
import { Toolbar } from "./Toolbar";
import { ShareLinkInput } from "./ShareLinkInput";
import { MakeCodeFrame } from "./MakecodeFrame";
import { CatalogOverlay } from "./CatalogOverlay";

const ProjectName: React.FC = () => {
const { state: teacherTool } = useContext(AppStateContext);
Expand All @@ -16,6 +17,7 @@ const ProjectName: React.FC = () => {
export const ProjectWorkspace: React.FC = () => {
return (
<div className={css.panel}>
<CatalogOverlay />
<Toolbar center={<ProjectName />} />
<ShareLinkInput />
<MakeCodeFrame />
Expand Down
17 changes: 17 additions & 0 deletions teachertool/src/components/ScreenReaderAnnouncer.tsx
Original file line number Diff line number Diff line change
@@ -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<ScreenReaderAnnouncerProps> = () => {
const { state: teacherTool } = useContext(AppStateContext);
return (
<>
{teacherTool.screenReaderAnnouncement && (
<div className={css["sr-only"]} aria-live="polite">
{teacherTool.screenReaderAnnouncement}
</div>
)}
</>
);
};
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;
}
14 changes: 0 additions & 14 deletions teachertool/src/components/styling/CatalogModal.module.scss

This file was deleted.

Loading

0 comments on commit 5a3bb20

Please sign in to comment.