From 4862db115cc27e0d55f0fe5e600395121d7a6741 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 23 Apr 2025 12:09:07 +0100 Subject: [PATCH 01/12] Introduce new mixin for consistency across focus outlines --- src/scss/common/accessibility.scss | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/scss/common/accessibility.scss b/src/scss/common/accessibility.scss index 159b9a0f7e..9f8e8fff21 100644 --- a/src/scss/common/accessibility.scss +++ b/src/scss/common/accessibility.scss @@ -32,7 +32,16 @@ @include reduced-motion-compliant-animation; } +@mixin focus-outline($color: var(--focus-outline-color), $width: var(--focus-outline-width)) { + // see https://stackoverflow.com/a/52616313 + box-shadow: 0px 0px 0px $width $color; + + outline-color: transparent; + outline-width: $width; +} + #main { // default values for all accessibility variables - --focus-outline: 2px solid #000; // TODO use this + --focus-outline-color: #000; + --focus-outline-width: 0.2rem; } From be0bead177983afadec70cd31c8dc12489d0b5c7 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 23 Apr 2025 12:09:29 +0100 Subject: [PATCH 02/12] Improve keyboard nav for collapsible lists --- src/app/components/elements/CollapsibleList.tsx | 3 ++- src/scss/common/button.scss | 2 +- src/scss/common/elements.scss | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/components/elements/CollapsibleList.tsx b/src/app/components/elements/CollapsibleList.tsx index efb5d7a6e4..fda6cb24ec 100644 --- a/src/app/components/elements/CollapsibleList.tsx +++ b/src/app/components/elements/CollapsibleList.tsx @@ -59,7 +59,8 @@ export const CollapsibleList = (props: CollapsibleListProps) => { className={`collapsible-body overflow-hidden ${expanded ? "open" : "closed"}`} style={{height: expanded ? expandedHeight : 0, maxHeight: expanded ? expandedHeight : 0, marginBottom: expanded ? (props.additionalOffset ?? 0) : 0}} > -
+
+ {/* when react is updated to v19, switch inert definition to regular prop */} {props.children}
diff --git a/src/scss/common/button.scss b/src/scss/common/button.scss index 84db5db186..ece51f3968 100644 --- a/src/scss/common/button.scss +++ b/src/scss/common/button.scss @@ -43,7 +43,7 @@ button { -webkit-appearance: none; border: none; - outline: none !important; +// outline: none !important; } .btn.btn-primary:focus-visible, .btn.btn-outline-primary:focus-visible { diff --git a/src/scss/common/elements.scss b/src/scss/common/elements.scss index 82a8d8d61f..e1815f81d2 100644 --- a/src/scss/common/elements.scss +++ b/src/scss/common/elements.scss @@ -289,6 +289,10 @@ iframe.email-html { border-width: 1px; border-color: $gray-107; + > button:focus-visible { + @include focus-outline; + } + img { transition: transform 0.1s ease; } From 2291a7f27940ed847a6ab40e78370882389583a5 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 23 Apr 2025 16:01:20 +0100 Subject: [PATCH 03/12] Use landmark elements in sidebars for improved SR nav --- .../elements/layout/SidebarLayout.tsx | 612 +++++++++--------- 1 file changed, 318 insertions(+), 294 deletions(-) diff --git a/src/app/components/elements/layout/SidebarLayout.tsx b/src/app/components/elements/layout/SidebarLayout.tsx index b2230a1db4..c67a1a403f 100644 --- a/src/app/components/elements/layout/SidebarLayout.tsx +++ b/src/app/components/elements/layout/SidebarLayout.tsx @@ -5,7 +5,7 @@ import classNames from "classnames"; import { AssignmentDTO, ContentSummaryDTO, GameboardDTO, GameboardItem, IsaacBookIndexPageDTO, IsaacConceptPageDTO, QuestionDTO, QuizAssignmentDTO, QuizAttemptDTO, RegisteredUserDTO, Stage } from "../../../../IsaacApiTypes"; import { above, ACCOUNT_TAB, ACCOUNT_TABS, AUDIENCE_DISPLAY_FIELDS, below, BOARD_ORDER_NAMES, BoardCompletions, BoardCreators, BoardLimit, BoardSubjects, BoardViews, confirmThen, determineAudienceViews, EventStageMap, EventStatusFilter, EventTypeFilter, filterAssignmentsByStatus, filterAudienceViewsByProperties, getDistinctAssignmentGroups, getDistinctAssignmentSetters, getHumanContext, getThemeFromContextAndTags, HUMAN_STAGES, ifKeyIsEnter, isAda, isDefined, PHY_NAV_SUBJECTS, isTeacherOrAbove, QuizStatus, siteSpecific, TAG_ID, tags, STAGE, useDeviceSize, LearningStage, HUMAN_SUBJECTS, ArrayElement, isFullyDefinedContext, isSingleStageContext, Item, stageLabelMap, extractTeacherName, determineGameboardSubjects, PATHS, getQuestionPlaceholder } from "../../../services"; import { StageAndDifficultySummaryIcons } from "../StageAndDifficultySummaryIcons"; -import { selectors, useAppSelector, useGetQuizAssignmentsAssignedToMeQuery } from "../../../state"; +import { mainContentIdSlice, selectors, useAppDispatch, useAppSelector, useGetQuizAssignmentsAssignedToMeQuery } from "../../../state"; import { Link, useHistory, useLocation } from "react-router-dom"; import { AppGroup, AssignmentBoardOrder, PageContextState, MyAssignmentsOrder, Tag } from "../../../../IsaacAppTypes"; import { AffixButton } from "../AffixButton"; @@ -34,7 +34,11 @@ export const SidebarLayout = (props: RowProps) => { export const MainContent = (props: ColProps) => { const { className, ...rest } = props; - return siteSpecific(, props.children); + + const dispatch = useAppDispatch(); + dispatch(mainContentIdSlice.actions.set("page-content")); + + return siteSpecific(, props.children); }; interface QuestionLinkProps { @@ -79,7 +83,7 @@ const NavigationSidebar = (props: SidebarProps) => { if (isAda) return <>; const { className, ...rest } = props; - return ; + return ; }; interface ContentSidebarProps extends SidebarProps { @@ -100,7 +104,7 @@ const ContentSidebar = (props: ContentSidebarProps) => { const { className, buttonTitle, ...rest } = props; return <> {above['lg'](deviceSize) - ? + ? : <>
-
Search concepts
- ) => setSearchText(e.target.value)} - /> + +
Search concepts
+ ) => setSearchText(e.target.value)} + /> -
+
-
-
Filter by topic
- !isDefined(tagCounts) || tagCounts[tag.id] > 0).length === 0} - /> -
- {applicableTags - .filter(tag => !isDefined(tagCounts) || tagCounts[tag.id] > 0) - .map(tag => - - ) - } -
+
+
Filter by topic
+ !isDefined(tagCounts) || tagCounts[tag.id] > 0).length === 0} + /> +
+ {applicableTags + .filter(tag => !isDefined(tagCounts) || tagCounts[tag.id] > 0) + .map(tag => + + ) + } +
+
@@ -433,43 +439,45 @@ export const GenericConceptsSidebar = (props: ConceptListSidebarProps) => { return
-
Search concepts
- ) => setSearchText(e.target.value)} - /> + +
Search concepts
+ ) => setSearchText(e.target.value)} + /> -
- -
-
Filter by subject
- {Object.keys(PHY_NAV_SUBJECTS).map((subject, i) => { - const subjectTag = tags.getById(subject as TAG_ID); - const descendentTags = tags.getDirectDescendents(subjectTag.id); - const isSelected = conceptFilters.includes(subjectTag) || descendentTags.some(tag => conceptFilters.includes(tag)); - const isPartial = descendentTags.some(tag => conceptFilters.includes(tag)) && descendentTags.some(tag => !conceptFilters.includes(tag)); - return
- conceptFilters.includes(tag))} // not quite isPartial; this is also true if all descendents selected - className={classNames({"icon-checkbox-off": !isSelected, "icon icon-checkbox-partial-alt": isSelected && isPartial, "icon-checkbox-selected": isSelected && !isPartial})} - /> - {isSelected &&
- {descendentTags - .filter(tag => !isDefined(tagCounts) || tagCounts[tag.id] > 0) - // .sort((a, b) => tagCounts ? tagCounts[b.id] - tagCounts[a.id] : 0) - .map((tag, j) => ) - } -
} -
; - })} -
+
+ +
+
Filter by subject
+ {Object.keys(PHY_NAV_SUBJECTS).map((subject, i) => { + const subjectTag = tags.getById(subject as TAG_ID); + const descendentTags = tags.getDirectDescendents(subjectTag.id); + const isSelected = conceptFilters.includes(subjectTag) || descendentTags.some(tag => conceptFilters.includes(tag)); + const isPartial = descendentTags.some(tag => conceptFilters.includes(tag)) && descendentTags.some(tag => !conceptFilters.includes(tag)); + return
+ conceptFilters.includes(tag))} // not quite isPartial; this is also true if all descendents selected + className={classNames({"icon-checkbox-off": !isSelected, "icon icon-checkbox-partial-alt": isSelected && isPartial, "icon-checkbox-selected": isSelected && !isPartial})} + /> + {isSelected &&
+ {descendentTags + .filter(tag => !isDefined(tagCounts) || tagCounts[tag.id] > 0) + // .sort((a, b) => tagCounts ? tagCounts[b.id] - tagCounts[a.id] : 0) + .map((tag, j) => ) + } +
} +
; + })} +
+
@@ -508,18 +516,20 @@ export const QuestionFinderSidebar = (props: QuestionFinderSidebarProps) => { return
-
Search Questions
- ) => { - setInternalSearchText(e.target.value); - setSearchText(e.target.value); - }} - /> + +
Search Questions
+ ) => { + setInternalSearchText(e.target.value); + setSearchText(e.target.value); + }} + /> - + +
{pageContext?.subject && pageContext?.stage && <>
@@ -632,34 +642,36 @@ export const MyAssignmentsSidebar = (props: MyAssignmentsSidebarProps) => { const assignmentCountByStatus = myAssignments && Object.fromEntries(Object.entries(myAssignments).map(([key, value]) => [key, value.length])); return <>
-
Search assignments
- ) => setTitleFilter(e.target.value)} - /> -
-
Sort
- setSortOrder(e.target.value as MyAssignmentsOrder)}> - {Object.values(MyAssignmentsOrder).map(order => )} - -
-
Filter by status
- -
- {Object.values(AssignmentState).filter(s => s !== AssignmentState.ALL).map(state => )} -
Filter by group
- setGroupFilter(e.target.value)}> - {["All", ...getDistinctAssignmentGroups(assignments)].map(group => )} - -
Filter by assigner
- setSetByFilter(e.target.value)}> - {["All", ...getDistinctAssignmentSetters(assignments)].map(setter => )} - + +
Search assignments
+ ) => setTitleFilter(e.target.value)} + /> +
+
Sort
+ setSortOrder(e.target.value as MyAssignmentsOrder)}> + {Object.values(MyAssignmentsOrder).map(order => )} + +
+
Filter by status
+ +
+ {Object.values(AssignmentState).filter(s => s !== AssignmentState.ALL).map(state => )} +
Filter by group
+ setGroupFilter(e.target.value)}> + {["All", ...getDistinctAssignmentGroups(assignments)].map(group => )} + +
Filter by assigner
+ setSetByFilter(e.target.value)}> + {["All", ...getDistinctAssignmentSetters(assignments)].map(setter => )} + + ; }}/> ; @@ -685,34 +697,36 @@ export const MyGameboardsSidebar = (props: MyGameboardsSidebarProps) => { return {above["lg"](deviceSize) &&
} -
Search question decks
- ) => setBoardTitleFilter(e.target.value)} - /> -
-
Filter by creator
- setBoardCreatorFilter(e.target.value as BoardCreators)}> - {Object.values(BoardCreators).map(creator => )} - -
Filter by completion
- setBoardCompletionFilter(e.target.value as BoardCompletions)}> - {Object.values(BoardCompletions).map(completion => )} - -
-
Display
-
- setDisplayMode(e.target.value as BoardViews)}> - {Object.values(BoardViews).map(view => )} + +
Search question decks
+ ) => setBoardTitleFilter(e.target.value)} + /> +
+
Filter by creator
+ setBoardCreatorFilter(e.target.value as BoardCreators)}> + {Object.values(BoardCreators).map(creator => )} - {deviceSize === "xl" ?
: } -
Limit:
- setDisplayLimit(e.target.value as BoardLimit)}> - {Object.values(BoardLimit).map(limit => )} +
Filter by completion
+ setBoardCompletionFilter(e.target.value as BoardCompletions)}> + {Object.values(BoardCompletions).map(completion => )} -
+
+
Display
+
+ setDisplayMode(e.target.value as BoardViews)}> + {Object.values(BoardViews).map(view => )} + + {deviceSize === "xl" ?
: } +
Limit:
+ setDisplayLimit(e.target.value as BoardLimit)}> + {Object.values(BoardLimit).map(limit => )} + +
+ ; }; interface SetAssignmentsSidebarProps extends SidebarProps { @@ -737,43 +751,45 @@ export const SetAssignmentsSidebar = (props: SetAssignmentsSidebarProps) => { return {above["lg"](deviceSize) &&
} -
Search question decks
- ) => setBoardTitleFilter(e.target.value)} - /> -
-
Filter by subject
- setBoardSubject(e.target.value as BoardSubjects)}> - {Object.values(BoardSubjects).map(subject => )} - -
Filter by creator
- setBoardCreator(e.target.value as BoardCreators)}> - {Object.values(BoardCreators).map(creator => )} - -
-
Display
-
- setDisplayMode(e.target.value as BoardViews)}> - {Object.values(BoardViews).map(view => )} + +
Search question decks
+ ) => setBoardTitleFilter(e.target.value)} + /> +
+
Filter by subject
+ setBoardSubject(e.target.value as BoardSubjects)}> + {Object.values(BoardSubjects).map(subject => )} - -
Limit:
- setDisplayLimit(e.target.value as BoardLimit)}> - {Object.values(BoardLimit).map(limit => )} +
Filter by creator
+ setBoardCreator(e.target.value as BoardCreators)}> + {Object.values(BoardCreators).map(creator => )} -
-
Sort by
- setSortOrder(e.target.value as AssignmentBoardOrder)} disabled={sortDisabled}> - {Object.values(AssignmentBoardOrder).filter( - order => !['attempted', '-attempted', 'correct', '-correct'].includes(order) - ).map(order => )} - - {sortDisabled &&
- Sorting is disabled if some question decks are hidden. Increase the display limit to show all question decks. -
} +
+
Display
+
+ setDisplayMode(e.target.value as BoardViews)}> + {Object.values(BoardViews).map(view => )} + + +
Limit:
+ setDisplayLimit(e.target.value as BoardLimit)}> + {Object.values(BoardLimit).map(limit => )} + +
+
Sort by
+ setSortOrder(e.target.value as AssignmentBoardOrder)} disabled={sortDisabled}> + {Object.values(AssignmentBoardOrder).filter( + order => !['attempted', '-attempted', 'correct', '-correct'].includes(order) + ).map(order => )} + + {sortDisabled &&
+ Sorting is disabled if some question decks are hidden. Increase the display limit to show all question decks. +
} + ; }; @@ -1004,40 +1020,42 @@ export const ManageQuizzesSidebar = (props: ManageQuizzesSidebarProps) => { return {above["lg"](deviceSize) &&
} -
Search tests
- setManageQuizzesTitleFilter(event.target.value)} - className="search--filter-input mt-3 mb-4" - placeholder="e.g. Forces" aria-label="Search by title" - /> -
Search by group
- setManageQuizzesGroupNameFilter(event.target.value)} - className="search--filter-input my-3" - placeholder="Group name" aria-label="Search by group" - /> -
-
Filter by date
-
- Starting - {dateFilterTypeSelector(quizSetDateFilterType, setQuizSetDateFilterType)} -
- setQuizStartDate(new Date(event.target.value))} - placeholder="Filter by set date" aria-label="Filter by set date" - /> -
- Due - {dateFilterTypeSelector(quizDueDateFilterType, setQuizDueDateFilterType)} -
- setQuizDueDate(new Date(event.target.value))} - placeholder="Filter by due date" aria-label="Filter by due date" - /> + +
Search tests
+ setManageQuizzesTitleFilter(event.target.value)} + className="search--filter-input mt-3 mb-4" + placeholder="e.g. Forces" aria-label="Search by title" + /> +
Search by group
+ setManageQuizzesGroupNameFilter(event.target.value)} + className="search--filter-input my-3" + placeholder="Group name" aria-label="Search by group" + /> +
+
Filter by date
+
+ Starting + {dateFilterTypeSelector(quizSetDateFilterType, setQuizSetDateFilterType)} +
+ setQuizStartDate(new Date(event.target.value))} + placeholder="Filter by set date" aria-label="Filter by set date" + /> +
+ Due + {dateFilterTypeSelector(quizDueDateFilterType, setQuizDueDateFilterType)} +
+ setQuizDueDate(new Date(event.target.value))} + placeholder="Filter by due date" aria-label="Filter by due date" + /> + ; }; @@ -1047,7 +1065,7 @@ export const EventsSidebar = (props: SidebarProps) => { const user = useAppSelector(selectors.user.orNull); return -
+
Event type
    {Object.entries(EventStatusFilter) @@ -1197,26 +1215,28 @@ export const MyQuizzesSidebar = (props: MyQuizzesSidebarProps) => { { return <> {above["lg"](deviceSize) &&
    } -
    Search tests
    - setQuizTitleFilter(e.target.value)} - placeholder="e.g. Forces" aria-label="Search by title"/> -
    -
    Filter by status
    - -
    - {statusOptions.map(state => )} -
    Filter by assigner
    - setQuizCreatorFilter(e.target.value)}> - {["All", ...getDistinctAssignmentSetters(quizzes)].map(setter => )} - -
    -
    Display
    - setDisplayMode(d => d === "table" ? "cards" : "table")}> - - - + +
    Search tests
    + setQuizTitleFilter(e.target.value)} + placeholder="e.g. Forces" aria-label="Search by title"/> +
    +
    Filter by status
    + +
    + {statusOptions.map(state => )} +
    Filter by assigner
    + setQuizCreatorFilter(e.target.value)}> + {["All", ...getDistinctAssignmentSetters(quizzes)].map(setter => )} + +
    +
    Display
    + setDisplayMode(d => d === "table" ? "cards" : "table")}> + + + + ; }}/> ; @@ -1234,34 +1254,36 @@ export const QuestionDecksSidebar = (props: QuestionDecksSidebarProps) => { return
    -
    Decks by stage
    -
      - {validStageSubjectPairs[context.subject].map((stage, index) => -
    • - history.push(`/${context.subject}/${stage}/question_decks`)} - /> -
    • - )} -
    -
    -
    Decks by subject
    -
      - {Object.entries(validStageSubjectPairs) - .filter(([_subject, stages]) => (stages as LearningStage[]).includes(context.stage[0])) - .map(([subject, _stages], index) => + +
      Decks by stage
      +
        + {validStageSubjectPairs[context.subject].map((stage, index) =>
      • history.push(`/${subject}/${context.stage}/question_decks`)} + checkboxTitle={HUMAN_STAGES[stage]} + checked={context.stage.includes(stage)} + onClick={() => history.push(`/${context.subject}/${stage}/question_decks`)} />
      • - ) - } -
      + )} +
    +
    +
    Decks by subject
    +
      + {Object.entries(validStageSubjectPairs) + .filter(([_subject, stages]) => (stages as LearningStage[]).includes(context.stage[0])) + .map(([subject, _stages], index) => +
    • + history.push(`/${subject}/${context.stage}/question_decks`)} + /> +
    • + ) + } +
    + ; }; @@ -1285,54 +1307,56 @@ export const GlossarySidebar = (props: GlossarySidebarProps) => { return
    -
    Search glossary
    - ) => setSearchText(e.target.value)} - /> -
    - - {!pageContext?.subject && <> -
    Select subject
    - - ({ value: s.id, label: s.title}))} - value={filterSubject ? ({value: filterSubject.id, label: filterSubject.title}) : undefined} - name="subject-select" - placeholder="Select a subject" - onChange={e => setFilterSubject(subjects.find(v => v.id === (e as Item | undefined)?.value)) } - isClearable + +
    Search glossary
    + ) => setSearchText(e.target.value)} /> - } +
    - {!pageContext?.stage?.length && <> -
    Select stage
    - - ({ value: s, label: stageLabelMap[s]})) } - value={filterStage ? ({value: filterStage, label: stageLabelMap[filterStage]}) : undefined} - name="stage-select" - placeholder="Select a stage" - onChange={e => setFilterStage(stages.find(s => s === e?.value))} - isClearable - /> - } + {!pageContext?.subject && <> +
    Select subject
    + + ({ value: s.id, label: s.title}))} + value={filterSubject ? ({value: filterSubject.id, label: filterSubject.title}) : undefined} + name="subject-select" + placeholder="Select a subject" + onChange={e => setFilterSubject(subjects.find(v => v.id === (e as Item | undefined)?.value)) } + isClearable + /> + } - {isFullyDefinedContext(pageContext) && isSingleStageContext(pageContext) && <> -
    Switch learning stage
    -
      - {PHY_NAV_SUBJECTS[pageContext.subject].map((stage, index) => -
    • - history.replace(`/${pageContext.subject}/${stage}/glossary`)} - /> -
    • - )} -
    - } + {!pageContext?.stage?.length && <> +
    Select stage
    + + ({ value: s, label: stageLabelMap[s]})) } + value={filterStage ? ({value: filterStage, label: stageLabelMap[filterStage]}) : undefined} + name="stage-select" + placeholder="Select a stage" + onChange={e => setFilterStage(stages.find(s => s === e?.value))} + isClearable + /> + } + + {isFullyDefinedContext(pageContext) && isSingleStageContext(pageContext) && <> +
    Switch learning stage
    +
      + {PHY_NAV_SUBJECTS[pageContext.subject].map((stage, index) => +
    • + history.replace(`/${pageContext.subject}/${stage}/glossary`)} + /> +
    • + )} +
    + } + ; }; From 8e1307171300bf873a962cba129ab267c5b1dc3d Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Wed, 23 Apr 2025 16:01:41 +0100 Subject: [PATCH 04/12] Use h element for homepage header --- src/app/components/elements/StudentDashboard.tsx | 2 +- src/app/components/elements/TeacherDashboard.tsx | 2 +- src/scss/phy/homepage.scss | 6 ------ 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/app/components/elements/StudentDashboard.tsx b/src/app/components/elements/StudentDashboard.tsx index 9a91be5a5c..79e9a9aad8 100644 --- a/src/app/components/elements/StudentDashboard.tsx +++ b/src/app/components/elements/StudentDashboard.tsx @@ -218,7 +218,7 @@ export const StudentDashboard = ({assignments, quizAssignments, streakRecord, gr const {assignmentsCount, quizzesCount} = getActiveWorkCount(assignments, quizAssignments); return
    - {nameToDisplay && Welcome back, {nameToDisplay}!} + {nameToDisplay &&

    Welcome back, {nameToDisplay}!

    } {deviceSize === "lg" ? <> diff --git a/src/app/components/elements/TeacherDashboard.tsx b/src/app/components/elements/TeacherDashboard.tsx index 69472cc6d0..b748cb4c6f 100644 --- a/src/app/components/elements/TeacherDashboard.tsx +++ b/src/app/components/elements/TeacherDashboard.tsx @@ -175,7 +175,7 @@ export const TeacherDashboard = ({ assignmentsSetByMe, quizzesSetByMe, myAssignm return
    - {nameToDisplay && Welcome back, {nameToDisplay}!} + {nameToDisplay &&

    Welcome back, {nameToDisplay}!

    }
    Dashboard view
    Date: Thu, 24 Apr 2025 11:17:33 +0100 Subject: [PATCH 05/12] Add aria-labels to homepage tags --- .../list-groups/AbstractListViewItem.tsx | 27 ++++++++++--------- src/app/components/site/phy/HomepagePhy.tsx | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/app/components/elements/list-groups/AbstractListViewItem.tsx b/src/app/components/elements/list-groups/AbstractListViewItem.tsx index 6fe51a1b53..b74d8cbfb8 100644 --- a/src/app/components/elements/list-groups/AbstractListViewItem.tsx +++ b/src/app/components/elements/list-groups/AbstractListViewItem.tsx @@ -1,5 +1,5 @@ -import { Link } from "react-router-dom"; -import React from "react"; +import { Link, LinkProps } from "react-router-dom"; +import React, { HTMLAttributes } from "react"; import { StageAndDifficultySummaryIcons } from "../StageAndDifficultySummaryIcons"; import { ViewingContext} from "../../../../IsaacAppTypes"; import classNames from "classnames"; @@ -35,12 +35,19 @@ const StatusDisplay = (props: React.HTMLAttributes & {status: C } }; -const LinkTags = ({linkTags}: {linkTags: {tag: string, url?: string}[];}) => { +export interface ListViewTagProps extends HTMLAttributes { + tag: string; + url?: string; +} + +const LinkTags = ({linkTags}: {linkTags: ListViewTagProps[];}) => { return <> - {linkTags.map(t => t.url ? - {t.tag} : -
    {t.tag}
    - )} + {linkTags.map(t => { + const {url, tag, ...rest} = t; + return url ? + {tag} : +
    {tag}
    ; + })} ; }; @@ -55,12 +62,6 @@ const QuizLinks = (props: React.HTMLAttributes & {previewQuizUr {quizButton}
    ; }; - -export interface ListViewTagProps { - tag: string; - url?: string; -} - export interface AbstractListViewItemProps extends ListGroupItemProps { title: string; icon?: TitleIconProps; diff --git a/src/app/components/site/phy/HomepagePhy.tsx b/src/app/components/site/phy/HomepagePhy.tsx index 9ef41d2d0e..8146a28fd5 100644 --- a/src/app/components/site/phy/HomepagePhy.tsx +++ b/src/app/components/site/phy/HomepagePhy.tsx @@ -105,7 +105,7 @@ const getListViewSubjectCard = (sc: subjectCategory) => { item: item, icon: {type: "img", icon: `/assets/phy/icons/redesign/subject-${sc.subject}.svg`}, subject: sc.subject as Subject, - linkTags: sc.subcategories.map((subcat) => ({tag: subcat.humanStage, url: subcat.href})), + linkTags: sc.subcategories.map((subcat) => ({tag: subcat.humanStage, url: subcat.href, "aria-label": `Explore ${subcat.humanStage} ${sc.humanSubject}`})), }; return listViewSubjectCard; From 0b029d867830359e2cd80bc7c0d57c3240823e4a Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 24 Apr 2025 17:14:42 +0100 Subject: [PATCH 06/12] Prevent keyboard outline overflow being cut off on dashboard --- src/app/components/elements/TeacherDashboard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/elements/TeacherDashboard.tsx b/src/app/components/elements/TeacherDashboard.tsx index b748cb4c6f..df7d50bdf3 100644 --- a/src/app/components/elements/TeacherDashboard.tsx +++ b/src/app/components/elements/TeacherDashboard.tsx @@ -23,7 +23,7 @@ const GroupsPanel = ({ groups }: GroupsPanelProps) => {

    Manage group progress

    {sortedGroups.length ? <> -
    +
    {sortedGroups.map(group => {group.groupName})}
    From 35d4d9d12b05e2d7f8f66d43a53cdf46fbf43656 Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 24 Apr 2025 17:29:19 +0100 Subject: [PATCH 07/12] Replace `:focus` styling intended for keyboards with `:focus-visible` --- src/scss/common/assignments.scss | 3 -- src/scss/common/button.scss | 5 -- src/scss/common/elements.scss | 7 +-- src/scss/common/focus.scss | 84 ++------------------------------ src/scss/common/scroll.scss | 6 --- src/scss/cs/boards.scss | 1 - src/scss/cs/button.scss | 21 -------- src/scss/cs/header.scss | 6 +-- src/scss/cs/tabs.scss | 2 +- src/scss/phy/button.scss | 80 +++--------------------------- src/scss/phy/footer.scss | 8 +-- src/scss/phy/list-groups.scss | 4 ++ 12 files changed, 24 insertions(+), 203 deletions(-) diff --git a/src/scss/common/assignments.scss b/src/scss/common/assignments.scss index e933432565..85f5cf38d4 100644 --- a/src/scss/common/assignments.scss +++ b/src/scss/common/assignments.scss @@ -114,8 +114,5 @@ &:focus { outline: none; } - &:focus-visible { - outline: 3px solid black; - } } } diff --git a/src/scss/common/button.scss b/src/scss/common/button.scss index ece51f3968..fc04bf19cd 100644 --- a/src/scss/common/button.scss +++ b/src/scss/common/button.scss @@ -43,11 +43,6 @@ button { -webkit-appearance: none; border: none; -// outline: none !important; -} - -.btn.btn-primary:focus-visible, .btn.btn-outline-primary:focus-visible { - background-color: unset !important; } .btn-close { diff --git a/src/scss/common/elements.scss b/src/scss/common/elements.scss index e1815f81d2..baaf60fd3d 100644 --- a/src/scss/common/elements.scss +++ b/src/scss/common/elements.scss @@ -289,10 +289,6 @@ iframe.email-html { border-width: 1px; border-color: $gray-107; - > button:focus-visible { - @include focus-outline; - } - img { transition: transform 0.1s ease; } @@ -404,8 +400,7 @@ iframe.email-html { } &:has(> input:focus-visible) { - // TODO: find-replace all instances of this with some SCSS vars defined in isaac.scss - box-shadow: 0 0 0 2pt black !important; + @include focus-outline; } &:focus:not(:focus-visible) { diff --git a/src/scss/common/focus.scss b/src/scss/common/focus.scss index f2a22e7151..5bab9bb259 100644 --- a/src/scss/common/focus.scss +++ b/src/scss/common/focus.scss @@ -4,78 +4,6 @@ border-color: $gray-160; } -/** Focus styles **/ - -.form-control:focus, .form-select:focus { - box-shadow: none; - outline: 0.2rem solid $focus-blue; -} - -.form-control-file:focus { - outline: none; - border: 0.2rem solid $focus-blue; - border-radius: 5px; -} - -.form-check-input:focus { - outline: 0.2rem solid $focus-blue; -} - -.page-link:focus { - box-shadow: none; - border: 0.2rem solid $focus-blue; - border-radius: 3px; -} - -.navbar-toggler:focus { - box-shadow: none; - border: 0.2rem solid $focus-blue; - border-radius: 3px; -} - -.link-list.a:focus { - box-shadow: none !important; - outline: 0.2rem solid $focus-blue !important; -} - -.navbar-toggler:focus { - box-shadow: none; - outline: 0.2rem solid $black; -} - -.nav-link.active:focus { - box-shadow: none; - outline: 0.2rem solid $secondary; -} - -.book-chapter:focus { - box-shadow: 0 0 0 2pt black !important; - border-radius: 1px; - outline: none !important; -} - -.nav-link:focus { - box-shadow: 0 0 0 2pt black !important; - border-radius: 1px; - outline: none !important; -} - -.book-link:focus { - box-shadow: 0 0 0 2pt black !important; - border-radius: 0; - outline: none !important; -} - -.login-b:focus { - box-shadow: none; - border: 0.2rem solid $focus-blue; -} - -// Close modal button -.close:not(:disabled):not(.disabled):focus { - box-shadow: none; - outline: 0.2rem solid $focus-blue; -} ::placeholder { // Firefox, Chrome, Opera color: $gray-160 !important; @@ -89,13 +17,11 @@ color: $gray-160 !important; } -:focus { - outline: 0.2rem solid $black; +:focus-visible:not([tabindex="-1"]):not(.select__input) { + @include focus-outline; } -// TODO: Review blanket suppression of focus indicator on keyboard-unfocusable elements. -[tabindex="-1"]:focus { - outline: 0 !important; +:focus-visible[tabindex="-1"] { + // remove UA styling too + outline: none; } - -/** End of focus styles **/ diff --git a/src/scss/common/scroll.scss b/src/scss/common/scroll.scss index 3bf4c7e9be..899cfda65b 100644 --- a/src/scss/common/scroll.scss +++ b/src/scss/common/scroll.scss @@ -122,9 +122,3 @@ } } } - -.expand-button > button:focus-visible { - div { - outline: 0.1rem solid #000 !important; - } -} \ No newline at end of file diff --git a/src/scss/cs/boards.scss b/src/scss/cs/boards.scss index b1f88fc804..6024c51b3b 100644 --- a/src/scss/cs/boards.scss +++ b/src/scss/cs/boards.scss @@ -37,7 +37,6 @@ outline: none !important; border: none !important; &:focus-visible { - outline: 2px solid $focus-blue !important; border-radius: 50px !important; } } diff --git a/src/scss/cs/button.scss b/src/scss/cs/button.scss index 398891db33..49bd3c8ed2 100644 --- a/src/scss/cs/button.scss +++ b/src/scss/cs/button.scss @@ -19,12 +19,6 @@ background-color: $active-color !important; box-shadow: none !important; } - &:focus:not(.disabled) { - box-shadow: none; - } - &:focus-visible { - box-shadow: 0 0 0 3px $focus-blue !important; - } &.disabled, &:disabled { color: $cs-jet; background-color: $cs-silver; @@ -32,12 +26,6 @@ } } -.btn.btn-link:focus { - box-shadow: 0 0 0 0.2rem $focus-blue !important; - outline: none !important; - border-radius: 3px; -} - @mixin ada-button-outline($color, $hover-color, $active-color) { background-color: transparent; border: 3px solid $color; @@ -56,12 +44,6 @@ background-color: transparent !important; box-shadow: none !important; } - &:focus:not(.disabled) { - box-shadow: none; - } - &:focus-visible { - box-shadow: 0 0 0 3px $focus-blue !important; - } &.disabled, &:disabled { color: $cs-jet; border-color: $cs-silver; @@ -132,7 +114,6 @@ background: none !important; border: none !important; border-radius: 0 !important; - box-shadow: none !important; } } @@ -147,7 +128,6 @@ color: $light-active-dark-pink; } &:focus-visible { - outline: 2px solid $focus-blue !important; border-radius: 0 !important; z-index: 10; } @@ -303,7 +283,6 @@ a:not(.btn):not(.nav-link) { outline: none; } &:focus-visible { - outline: 2px solid $focus-blue; border-radius: 0 !important; z-index: 10; } diff --git a/src/scss/cs/header.scss b/src/scss/cs/header.scss index 372b2ad602..35e20d8150 100644 --- a/src/scss/cs/header.scss +++ b/src/scss/cs/header.scss @@ -17,7 +17,6 @@ div.header-progress { .navbar-toggler { padding: 0.25rem; &:focus-visible { - border-color: $cyan-300; border-radius: 0; } &:focus:not(:focus-visible) { @@ -54,7 +53,6 @@ div.header-progress { background: none; &:focus-visible { box-sizing: content-box; - border: 0.2rem solid $cyan-300; border-radius: 0; } &:focus:not(:focus-visible) { @@ -75,6 +73,8 @@ header { position: relative; z-index: 20; + --focus-outline-color: #{$cyan-300}; + a:not(.navbar-brand), .nav-link { text-decoration: none !important; color: $cs-white; @@ -99,7 +99,7 @@ header { top: 0; left: 0; height: 100%; - border: $cyan-300 solid 2px; + @include focus-outline; } } diff --git a/src/scss/cs/tabs.scss b/src/scss/cs/tabs.scss index 10f5a45abe..f2d4914820 100644 --- a/src/scss/cs/tabs.scss +++ b/src/scss/cs/tabs.scss @@ -72,7 +72,7 @@ $link-text-bottom-padding: 4px; left: 0; width: 100%; height: 100%; - outline: $focus-blue solid 2px !important; + @include focus-outline; } } } diff --git a/src/scss/phy/button.scss b/src/scss/phy/button.scss index 7debb41a0d..38e34beee3 100644 --- a/src/scss/phy/button.scss +++ b/src/scss/phy/button.scss @@ -11,7 +11,7 @@ line-height: normal; opacity: 100%; - &:focus { + &:focus:not(:focus-visible) { outline: none !important; box-shadow: 0 0 0 3px var(--buttons-light), 0 0 0 4.5px var(--buttons-focus-ring) !important; } @@ -49,6 +49,10 @@ color: var(--buttons-dark-text); } + &:focus-visible { + --focus-outline-color: #{$a11y_blue}; + } + i.icon:not(:has(::after)) { background-color: var(--buttons-light-icon); } @@ -119,7 +123,7 @@ text-decoration: none; } - &:focus { + &:focus:not(:focus-visible) { // TODO: depending on use case (colour of background this is used on), we may need to change how this offset border is generated box-shadow: 0 0 0 3px var(--buttons-dark), 0 0 0 4.5px var(--buttons-focus-ring) !important; } @@ -129,6 +133,8 @@ } } +// TODO: plan removing all below button styling? + .btn, input.btn { border-radius: 22.5rem; padding: 0.625rem 1.25rem; @@ -183,12 +189,6 @@ border-color: $gray-118; } - &.focus, - &:focus, - &:not(:disabled):not(.disabled):active:focus { - box-shadow: 0 0 0 0.2rem $shadow-3; - } - &.disabled, &:disabled { color: $gray-136; @@ -225,12 +225,6 @@ border-color: $gray-118; } - &.focus, - &:focus, - &:not(:disabled):not(.disabled):active:focus { - box-shadow: 0 0 0 0.2rem $shadow-3; - } - &.disabled, &:disabled { border-color: $gray-136; @@ -295,12 +289,6 @@ a.btn { border-color: $gray-136; } - &.focus, - &:focus, - &:not(:disabled):not(.disabled):active:focus { - box-shadow: 0 0 0 0.2rem $shadow-08; - } - &.disabled, &:disabled { border-color: $gray-107; @@ -336,12 +324,6 @@ a.btn { border-color: $gray-118; } - &.focus, - &:focus, - &:not(:disabled):not(.disabled):active:focus { - box-shadow: 0 0 0 0.2rem $shadow-3; - } - &.disabled, &:disabled { border-color: $gray-160; @@ -433,52 +415,6 @@ button.close { // default - +
    {siteSpecific(
    ,
    )} }