= ({
const page = +(searchParams.get("page") ?? "1")
return (
-
+
+ Search within {channelTitle}
= ({
= ({
SHOW_PROFESSIONAL_TOGGLE_BY_CHANNEL_TYPE[channelType]
}
/>
-
+
)
}
diff --git a/frontends/mit-learn/src/pages/ChannelPage/UnitChannelTemplate.tsx b/frontends/mit-learn/src/pages/ChannelPage/UnitChannelTemplate.tsx
index d4884dc286..eeb699533b 100644
--- a/frontends/mit-learn/src/pages/ChannelPage/UnitChannelTemplate.tsx
+++ b/frontends/mit-learn/src/pages/ChannelPage/UnitChannelTemplate.tsx
@@ -7,6 +7,7 @@ import {
Stack,
BannerBackground,
Typography,
+ VisuallyHidden,
} from "ol-components"
import { SearchSubscriptionToggle } from "@/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle"
import { ChannelDetails } from "@/page-components/ChannelDetails/ChannelDetails"
@@ -131,7 +132,8 @@ const UnitChannelTemplate: React.FC = ({
/>
-
+
+ {channel.data?.title}
{channel.data ? (
) : null}
@@ -181,14 +183,18 @@ const UnitChannelTemplate: React.FC = ({
-
+
-
+
{children}
>
)
diff --git a/frontends/mit-learn/src/pages/DashboardPage/DashboardPage.tsx b/frontends/mit-learn/src/pages/DashboardPage/DashboardPage.tsx
index 392c8eb927..7b0073d72a 100644
--- a/frontends/mit-learn/src/pages/DashboardPage/DashboardPage.tsx
+++ b/frontends/mit-learn/src/pages/DashboardPage/DashboardPage.tsx
@@ -18,6 +18,7 @@ import {
TabPanel,
Tabs,
Typography,
+ TypographyProps,
styled,
} from "ol-components"
import { Link } from "react-router-dom"
@@ -206,14 +207,16 @@ const TabPanelStyled = styled(TabPanel)({
width: "100%",
})
-const TitleText = styled(Typography)(({ theme }) => ({
- color: theme.custom.colors.black,
- paddingBottom: "16px",
- ...theme.typography.h3,
- [theme.breakpoints.down("md")]: {
- ...theme.typography.h5,
- },
-}))
+const TitleText = styled(Typography)>(
+ ({ theme }) => ({
+ color: theme.custom.colors.black,
+ paddingBottom: "16px",
+ ...theme.typography.h3,
+ [theme.breakpoints.down("md")]: {
+ ...theme.typography.h5,
+ },
+ }),
+)
const SubTitleText = styled(Typography)(({ theme }) => ({
color: theme.custom.colors.darkGray2,
@@ -418,7 +421,7 @@ const DashboardPage: React.FC = () => {
-
+
Your MIT Learning Journey
@@ -432,6 +435,7 @@ const DashboardPage: React.FC = () => {
{
{topics?.map((topic, index) => (
{
))}
{certification === true ? (
{
/>
) : (
{
/>
)}
{
- Profile
+ Profile
{isLoadingProfile || !profile ? (
) : (
@@ -486,7 +495,7 @@ const DashboardPage: React.FC = () => {
)}
- Settings
+ Settings
{isLoadingProfile || !profile ? (
) : (
diff --git a/frontends/mit-learn/src/pages/DepartmentListingPage/DepartmentListingPage.test.tsx b/frontends/mit-learn/src/pages/DepartmentListingPage/DepartmentListingPage.test.tsx
index b65256f3c1..8138a4bb73 100644
--- a/frontends/mit-learn/src/pages/DepartmentListingPage/DepartmentListingPage.test.tsx
+++ b/frontends/mit-learn/src/pages/DepartmentListingPage/DepartmentListingPage.test.tsx
@@ -5,6 +5,7 @@ import DepartmentListingPage from "./DepartmentListingPage"
import { factories, setMockResponse, urls } from "api/test-utils"
import invariant from "tiny-invariant"
import { faker } from "@faker-js/faker/locale/en"
+import { assertHeadings } from "ol-test-utilities"
const makeSearchResponse = (
aggregations: Record,
@@ -99,12 +100,12 @@ describe("DepartmentListingPage", () => {
await screen.findByRole("heading", {
name: schools[0].name,
})
- ).closest("li")
+ ).closest("section")
const school1 = (
await screen.findByRole("heading", {
name: schools[1].name,
})
- ).closest("li")
+ ).closest("section")
invariant(school0)
invariant(school1)
@@ -157,4 +158,16 @@ describe("DepartmentListingPage", () => {
})
expect(link).toHaveAttribute("href", dept.channel_url)
})
+
+ test("headings", async () => {
+ const { schools } = setupApis()
+ renderWithProviders( )
+
+ await waitFor(() => {
+ assertHeadings([
+ { level: 1, name: "Browse by Academic Department" },
+ ...schools.map(({ name }) => ({ level: 2, name })),
+ ])
+ })
+ })
})
diff --git a/frontends/mit-learn/src/pages/DepartmentListingPage/DepartmentListingPage.tsx b/frontends/mit-learn/src/pages/DepartmentListingPage/DepartmentListingPage.tsx
index 2479707d8a..b7165eb714 100644
--- a/frontends/mit-learn/src/pages/DepartmentListingPage/DepartmentListingPage.tsx
+++ b/frontends/mit-learn/src/pages/DepartmentListingPage/DepartmentListingPage.tsx
@@ -3,7 +3,6 @@ import {
Container,
Typography,
styled,
- PlainList,
List,
ListItem,
ListItemLink,
@@ -130,10 +129,9 @@ const SchoolDepartments: React.FC = ({
courseCounts,
programCounts,
className,
- as: Component = "div",
}) => {
return (
-
+
{SCHOOL_ICONS[school.url] ?? }
@@ -172,12 +170,12 @@ const SchoolDepartments: React.FC = ({
)
})}
-
+
)
}
-const SchoolList = styled(PlainList)(({ theme }) => ({
- "> li": {
+const SchoolList = styled.div(({ theme }) => ({
+ "> section": {
marginTop: "40px",
[theme.breakpoints.down("sm")]: {
marginTop: "30px",
diff --git a/frontends/mit-learn/src/pages/HomePage/BrowseTopicsSection.tsx b/frontends/mit-learn/src/pages/HomePage/BrowseTopicsSection.tsx
index 384672465f..bad7557396 100644
--- a/frontends/mit-learn/src/pages/HomePage/BrowseTopicsSection.tsx
+++ b/frontends/mit-learn/src/pages/HomePage/BrowseTopicsSection.tsx
@@ -1,5 +1,12 @@
import React from "react"
-import { Container, styled, theme, Typography, ButtonLink } from "ol-components"
+import {
+ Container,
+ styled,
+ theme,
+ Typography,
+ ButtonLink,
+ TypographyProps,
+} from "ol-components"
import { Link } from "react-router-dom"
import { useLearningResourceTopics } from "api/hooks/learningResources"
import { RiArrowRightLine } from "@remixicon/react"
@@ -18,7 +25,7 @@ const Section = styled.section`
}
`
-const Title = styled(Typography)`
+const Title = styled(Typography)>`
text-align: center;
`
@@ -102,7 +109,9 @@ const BrowseTopicsSection: React.FC = () => {
return (
- Browse by Topic
+
+ Browse by Topic
+
{topics?.results.map(
({ id, name, channel_url: channelUrl, icon }) => {
diff --git a/frontends/mit-learn/src/pages/HomePage/HeroSearch.tsx b/frontends/mit-learn/src/pages/HomePage/HeroSearch.tsx
index fe8314cc41..08827bb975 100644
--- a/frontends/mit-learn/src/pages/HomePage/HeroSearch.tsx
+++ b/frontends/mit-learn/src/pages/HomePage/HeroSearch.tsx
@@ -185,6 +185,7 @@ const HeroSearch: React.FC = () => {
diff --git a/frontends/mit-learn/src/pages/HomePage/HomePage.test.tsx b/frontends/mit-learn/src/pages/HomePage/HomePage.test.tsx
index 1bdfcd9b06..3f1f9eb441 100644
--- a/frontends/mit-learn/src/pages/HomePage/HomePage.test.tsx
+++ b/frontends/mit-learn/src/pages/HomePage/HomePage.test.tsx
@@ -14,9 +14,9 @@ import {
within,
waitFor,
} from "../../test-utils"
-import type { FeaturedApiFeaturedListRequest as FeaturedRequest } from "api"
import invariant from "tiny-invariant"
import * as routes from "@/common/urls"
+import { assertHeadings } from "ol-test-utilities"
const assertLinksTo = (
el: HTMLElement,
@@ -46,27 +46,8 @@ const setupAPIs = () => {
resources,
)
- setMockResponse.get(urls.learningResources.featured({ limit: 12 }), resources)
setMockResponse.get(
- urls.learningResources.featured({
- free: true,
- limit: 12,
- }),
- resources,
- )
- setMockResponse.get(
- urls.learningResources.featured({
- certification: true,
- professional: false,
- limit: 12,
- }),
- resources,
- )
- setMockResponse.get(
- urls.learningResources.featured({
- professional: true,
- limit: 12,
- }),
+ expect.stringContaining(urls.learningResources.featured()),
resources,
)
@@ -315,55 +296,12 @@ describe("Home Page Testimonials", () => {
})
describe("Home Page Carousel", () => {
- test.each<{ tab: string; params: FeaturedRequest }>([
- {
- tab: "All",
- params: { limit: 12, resource_type: ["course"] },
- },
- {
- tab: "Free",
- params: { limit: 12, resource_type: ["course"], free: true },
- },
- {
- tab: "With Certificate",
- params: {
- resource_type: ["course"],
- limit: 12,
- certification: true,
- professional: false,
- },
- },
- {
- tab: "Professional & Executive Learning",
- params: { resource_type: ["course"], limit: 12, professional: true },
- },
- ])("Featured Courses Carousel Tabs", async ({ tab, params }) => {
- const resources = learningResources.resources({ count: 12 })
- setupAPIs()
-
- // The tab buttons eager-load the resources so we need to set them all up.
-
- // This is for the clicked tab (which might be "All")
- // We will check that its response is visible as cards.
- setMockResponse.get(
- urls.learningResources.featured({ ...params }),
- resources,
- )
-
- renderWithProviders( )
- screen.findByRole("tab", { name: tab }).then(async (featuredTab) => {
- await user.click(within(featuredTab).getByRole("tab", { name: tab }))
- const [featuredPanel] = screen.getAllByRole("tabpanel")
- await within(featuredPanel).findByText(resources.results[0].title)
- })
- })
-
test("Tabbed Carousel sanity check", async () => {
setupAPIs()
renderWithProviders( )
- screen.findAllByRole("tablist").then(([featured, media]) => {
+ await screen.findAllByRole("tablist").then(([featured, media]) => {
within(featured).getByRole("tab", { name: "All" })
within(featured).getByRole("tab", { name: "Free" })
within(featured).getByRole("tab", { name: "With Certificate" })
@@ -376,3 +314,22 @@ describe("Home Page Carousel", () => {
})
})
})
+
+test("Headings", async () => {
+ setupAPIs()
+
+ renderWithProviders( )
+ await waitFor(() => {
+ assertHeadings([
+ { level: 1, name: "Learn with MIT" },
+ { level: 2, name: "Featured Courses" },
+ { level: 2, name: "Continue Your Journey" },
+ { level: 2, name: "Media" },
+ { level: 2, name: "Browse by Topic" },
+ { level: 2, name: "From Our Community" },
+ { level: 2, name: "MIT Stories & Events" },
+ { level: 3, name: "Stories" },
+ { level: 3, name: "Events" },
+ ])
+ })
+})
diff --git a/frontends/mit-learn/src/pages/HomePage/HomePage.tsx b/frontends/mit-learn/src/pages/HomePage/HomePage.tsx
index ba9ec13d23..d55d25e523 100644
--- a/frontends/mit-learn/src/pages/HomePage/HomePage.tsx
+++ b/frontends/mit-learn/src/pages/HomePage/HomePage.tsx
@@ -43,15 +43,22 @@ const HomePage: React.FC = () => {
-
+
-
-
+
+
diff --git a/frontends/mit-learn/src/pages/HomePage/NewsEventsSection.tsx b/frontends/mit-learn/src/pages/HomePage/NewsEventsSection.tsx
index cf7ae54175..125ae6aec9 100644
--- a/frontends/mit-learn/src/pages/HomePage/NewsEventsSection.tsx
+++ b/frontends/mit-learn/src/pages/HomePage/NewsEventsSection.tsx
@@ -7,6 +7,7 @@ import {
Grid,
useMuiBreakpointAtLeast,
Card,
+ TypographyProps,
} from "ol-components"
import {
useNewsEventsList,
@@ -24,7 +25,7 @@ const Section = styled.section`
}
`
-const Title = styled(Typography)`
+const Title = styled(Typography)>`
text-align: center;
margin-bottom: 8px;
`
@@ -231,7 +232,9 @@ const NewsEventsSection: React.FC = () => {
return (
- MIT Stories & Events
+
+ MIT Stories & Events
+
See what's happening in the world of learning with the latest news,
insights, and upcoming events at MIT.
@@ -239,7 +242,9 @@ const NewsEventsSection: React.FC = () => {
{isMobile ? (
- Stories
+
+ Stories
+
{stories.map((item) => (
{
- Events
+
+ Events
+
{EventCards}
@@ -259,7 +266,9 @@ const NewsEventsSection: React.FC = () => {
- Stories
+
+ Stories
+
{stories.map((item) => (
@@ -269,7 +278,9 @@ const NewsEventsSection: React.FC = () => {
- Events
+
+ Events
+
{EventCards}
diff --git a/frontends/mit-learn/src/pages/HomePage/TestimonialsSection.tsx b/frontends/mit-learn/src/pages/HomePage/TestimonialsSection.tsx
index 3606d50314..f9b58f5d90 100644
--- a/frontends/mit-learn/src/pages/HomePage/TestimonialsSection.tsx
+++ b/frontends/mit-learn/src/pages/HomePage/TestimonialsSection.tsx
@@ -297,7 +297,9 @@ const TestimonialsSection: React.FC = () => {
return (
- From Our Community
+
+ From Our Community
+
Millions of learners are reaching their goals with MIT's non-degree
learning resources. Here's what they're saying.
diff --git a/frontends/mit-learn/src/pages/HomePage/UpcomingCoursesSection.tsx b/frontends/mit-learn/src/pages/HomePage/UpcomingCoursesSection.tsx
index 5f4b922c76..10b6092146 100644
--- a/frontends/mit-learn/src/pages/HomePage/UpcomingCoursesSection.tsx
+++ b/frontends/mit-learn/src/pages/HomePage/UpcomingCoursesSection.tsx
@@ -37,6 +37,7 @@ const UpcomingCoursesSection: React.FC = () => {
return (
diff --git a/frontends/mit-learn/src/pages/LearningPathListingPage/LearningPathListingPage.test.tsx b/frontends/mit-learn/src/pages/LearningPathListingPage/LearningPathListingPage.test.tsx
index cf0a72994d..6ce6a169da 100644
--- a/frontends/mit-learn/src/pages/LearningPathListingPage/LearningPathListingPage.test.tsx
+++ b/frontends/mit-learn/src/pages/LearningPathListingPage/LearningPathListingPage.test.tsx
@@ -45,13 +45,10 @@ describe("LearningPathListingPage", () => {
it("Renders a card for each learning path", async () => {
const { paths } = setup()
const titles = paths.results.map((resource) => resource.title)
- const headings = await screen.findAllByRole("heading", {
- name: (value) => titles.includes(value),
+ const cards = await screen.findAllByRole("link", {
+ name: (name) => titles.some((title) => name.includes(title)),
})
-
- // for sanity
- expect(headings.length).toBeGreaterThan(0)
- expect(titles.length).toBe(headings.length)
+ expect(cards.length).toBeGreaterThan(0) // sanity
})
it.each([
@@ -67,8 +64,8 @@ describe("LearningPathListingPage", () => {
// Ensure the lists have loaded
const path = paths.results[0]
- await screen.findAllByRole("heading", {
- name: path.title,
+ await screen.findAllByRole("link", {
+ name: new RegExp(path.title),
})
const menuButton = screen.queryByRole("button", {
name: `Edit list ${path.title}`,
@@ -136,7 +133,9 @@ describe("LearningPathListingPage", () => {
test("Clicking on list title navigates to list page", async () => {
const { location, paths } = setup()
const path = faker.helpers.arrayElement(paths.results)
- const listTitle = await screen.findByRole("heading", { name: path.title })
+ const listTitle = await screen.findByRole("link", {
+ name: new RegExp(path.title),
+ })
await user.click(listTitle)
expect(location.current).toEqual(
expect.objectContaining({
diff --git a/frontends/mit-learn/src/pages/PrivacyPage/PrivacyPage.tsx b/frontends/mit-learn/src/pages/PrivacyPage/PrivacyPage.tsx
index 8e71dd999e..2528683377 100644
--- a/frontends/mit-learn/src/pages/PrivacyPage/PrivacyPage.tsx
+++ b/frontends/mit-learn/src/pages/PrivacyPage/PrivacyPage.tsx
@@ -1,4 +1,10 @@
-import { Breadcrumbs, Container, Typography, styled } from "ol-components"
+import {
+ Breadcrumbs,
+ Container,
+ Typography,
+ TypographyProps,
+ styled,
+} from "ol-components"
import MetaTags from "@/page-components/MetaTags/MetaTags"
import * as urls from "@/common/urls"
import React from "react"
@@ -29,10 +35,12 @@ const BannerContainerInner = styled.div({
justifyContent: "center",
})
-const Header = styled(Typography)(({ theme }) => ({
- alignSelf: "stretch",
- color: theme.custom.colors.black,
-}))
+const Header = styled(Typography)>(
+ ({ theme }) => ({
+ alignSelf: "stretch",
+ color: theme.custom.colors.black,
+ }),
+)
const BodyContainer = styled.div({
display: "flex",
@@ -42,10 +50,12 @@ const BodyContainer = styled.div({
gap: "20px",
})
-const BodyText = styled(Typography)(({ theme }) => ({
- alignSelf: "stretch",
- color: theme.custom.colors.black,
-}))
+const BodyText = styled(Typography)>(
+ ({ theme }) => ({
+ alignSelf: "stretch",
+ color: theme.custom.colors.black,
+ }),
+)
const UnorderedList = styled.ul(({ theme }) => ({
width: "100%",
@@ -66,18 +76,24 @@ const PrivacyPage: React.FC = () => {
ancestors={[{ href: urls.HOME, label: "Home" }]}
current="Privacy Policy"
/>
-
+
- Introduction
+
+ Introduction
+
{SITE_NAME} provides information about MIT courses, programs, and
learning materials to learners from across the world. This Privacy
Statement explains how {SITE_NAME} collects, uses, and processes
personal information about our learners.
- What personal information we collect
+
+ What personal information we collect
+
We may collect, use, store, and transfer different kinds of personal
information about you, which we have grouped together as follows:
@@ -94,7 +110,7 @@ const PrivacyPage: React.FC = () => {
IP addresses
Course progress and performance
-
+
How we collect personal information about you
@@ -130,7 +146,9 @@ const PrivacyPage: React.FC = () => {
"Help" section of the toolbar. If you reject our cookies, many
functions and conveniences of this Site may not work properly.
- How we use your personal information
+
+ How we use your personal information
+
We collect, use, and process your personal information (1) to
process transactions requested by you and meet our contractual
@@ -168,7 +186,7 @@ const PrivacyPage: React.FC = () => {
will always respect a request by you to stop processing your
personal information (subject to our legal obligations).
-
+
When we share your personal information
@@ -189,7 +207,7 @@ const PrivacyPage: React.FC = () => {
or otherwise address fraud, security or technical issues; or to
protect the rights, property or safety of us, our users or others.
-
+
How your information is stored and secured
@@ -213,7 +231,7 @@ const PrivacyPage: React.FC = () => {
based on verified business use cases and subject to auditing to
verify appropriate applications.
-
+
How long we keep your personal information
@@ -226,7 +244,7 @@ const PrivacyPage: React.FC = () => {
archival, scientific and historical research and for the defense of
potential legal claims.
-
+
Rights for Individuals in the European Economic Area (EEA) or United
Kingdom (UK)
@@ -289,7 +307,9 @@ const PrivacyPage: React.FC = () => {
Address: 71 Queen Victoria Street, London, EC4V 4BE, United Kingdom
- Additional Information
+
+ Additional Information
+
We may change this Privacy Statement from time to time. If we make
any significant changes in the way we treat your personal
diff --git a/frontends/mit-learn/src/pages/SearchPage/SearchPage.test.tsx b/frontends/mit-learn/src/pages/SearchPage/SearchPage.test.tsx
index cb169cdd6d..bac2bf3cb1 100644
--- a/frontends/mit-learn/src/pages/SearchPage/SearchPage.test.tsx
+++ b/frontends/mit-learn/src/pages/SearchPage/SearchPage.test.tsx
@@ -14,6 +14,7 @@ import type {
} from "api"
import invariant from "tiny-invariant"
import { Permissions } from "@/common/permissions"
+import { assertHeadings } from "ol-test-utilities"
const setMockApiResponses = ({
search,
@@ -21,7 +22,7 @@ const setMockApiResponses = ({
}: {
search?: Partial
offerors?: PaginatedLearningResourceOfferorDetailList
-}) => {
+} = {}) => {
setMockResponse.get(urls.userMe.get(), {
[Permissions.Authenticated]: false,
})
@@ -662,4 +663,15 @@ describe("Search Page pagination controls", () => {
expect(items.at(-2)?.textContent).toBe("7") // "Last page"
expect(items.at(-1)?.textContent).toBe("") // "Next" button
})
+
+ test("headings", () => {
+ setMockApiResponses()
+ renderWithProviders( )
+
+ assertHeadings([
+ { level: 1, name: "Search" },
+ { level: 2, name: "Filter" },
+ { level: 2, name: "Search Results" },
+ ])
+ })
})
diff --git a/frontends/mit-learn/src/pages/SearchPage/SearchPage.tsx b/frontends/mit-learn/src/pages/SearchPage/SearchPage.tsx
index a539e17dc7..a3d6d08318 100644
--- a/frontends/mit-learn/src/pages/SearchPage/SearchPage.tsx
+++ b/frontends/mit-learn/src/pages/SearchPage/SearchPage.tsx
@@ -13,7 +13,7 @@ import { SearchInput } from "@/page-components/SearchDisplay/SearchInput"
import { GridColumn, GridContainer } from "@/components/GridLayout/GridLayout"
import type { LearningResourceOfferor } from "api"
import { useOfferorsList } from "api/hooks/learningResources"
-import { styled, Container, Grid, theme } from "ol-components"
+import { styled, Container, Grid, theme, VisuallyHidden } from "ol-components"
import { capitalize } from "ol-utilities"
import MetaTags from "@/page-components/MetaTags/MetaTags"
@@ -212,6 +212,9 @@ const SearchPage: React.FC = () => {
return (
+
+ Search
+
@@ -233,6 +236,8 @@ const SearchPage: React.FC = () => {
({
- alignSelf: "stretch",
- color: theme.custom.colors.black,
-}))
+const Header = styled(Typography)>(
+ ({ theme }) => ({
+ alignSelf: "stretch",
+ color: theme.custom.colors.black,
+ }),
+)
const BodyContainer = styled.div({
display: "flex",
@@ -67,7 +76,9 @@ const TermsPage: React.FC = () => {
ancestors={[{ href: urls.HOME, label: "Home" }]}
current="Terms of Service"
/>
-
+
diff --git a/frontends/mit-learn/src/pages/TopicListingPage/TopicsListingPage.test.tsx b/frontends/mit-learn/src/pages/TopicListingPage/TopicsListingPage.test.tsx
index 9eaa60ca3f..91802d56df 100644
--- a/frontends/mit-learn/src/pages/TopicListingPage/TopicsListingPage.test.tsx
+++ b/frontends/mit-learn/src/pages/TopicListingPage/TopicsListingPage.test.tsx
@@ -4,6 +4,7 @@ import type { LearningResourcesSearchResponse } from "api"
import TopicsListingPage from "./TopicsListingPage"
import { factories, setMockResponse, urls } from "api/test-utils"
import invariant from "tiny-invariant"
+import { assertHeadings } from "ol-test-utilities"
const makeSearchResponse = (
aggregations: Record,
@@ -25,7 +26,10 @@ const makeSearchResponse = (
}
}
-describe("DepartmentListingPage", () => {
+const sorter = (a: { name: string }, b: { name: string }) =>
+ a.name.localeCompare(b.name)
+
+describe("TopicsListingPage", () => {
const setupApis = () => {
const make = factories.learningResources
const t1 = make.topic({ parent: null })
@@ -68,8 +72,6 @@ describe("DepartmentListingPage", () => {
makeSearchResponse(programCounts),
)
- const sorter = (a: { name: string }, b: { name: string }) =>
- a.name.localeCompare(b.name)
const sortedSubtopics1 = [topics.t1a, topics.t1b, topics.t1c].sort(sorter)
const sortedSubtopics2 = [topics.t2a, topics.t2b].sort(sorter)
@@ -149,4 +151,16 @@ describe("DepartmentListingPage", () => {
expect(topic2).toHaveTextContent("Courses: 200")
expect(topic2).toHaveTextContent("Programs: 20")
})
+
+ test("headings", async () => {
+ const { topics } = setupApis()
+ const sorted = [topics.t1, topics.t2].sort(sorter)
+ renderWithProviders( )
+ await waitFor(() => {
+ assertHeadings([
+ { level: 1, name: "Browse by Topic" },
+ ...sorted.map((t) => ({ level: 2, name: t.name })),
+ ])
+ })
+ })
})
diff --git a/frontends/mit-learn/src/pages/TopicListingPage/TopicsListingPage.tsx b/frontends/mit-learn/src/pages/TopicListingPage/TopicsListingPage.tsx
index d2296ddffa..9e535856b2 100644
--- a/frontends/mit-learn/src/pages/TopicListingPage/TopicsListingPage.tsx
+++ b/frontends/mit-learn/src/pages/TopicListingPage/TopicsListingPage.tsx
@@ -42,7 +42,7 @@ type TopicBoxHeaderProps = {
const TopicBoxHeader = styled(
({ title, icon, href, className }: TopicBoxHeaderProps) => {
return (
-
+
diff --git a/frontends/mit-learn/src/pages/UnitsListingPage/UnitsListingPage.test.tsx b/frontends/mit-learn/src/pages/UnitsListingPage/UnitsListingPage.test.tsx
index f0ed639a13..f7ad6b95e3 100644
--- a/frontends/mit-learn/src/pages/UnitsListingPage/UnitsListingPage.test.tsx
+++ b/frontends/mit-learn/src/pages/UnitsListingPage/UnitsListingPage.test.tsx
@@ -3,6 +3,7 @@ import { renderWithProviders, screen, waitFor, within } from "@/test-utils"
import type { LearningResourcesSearchResponse } from "api"
import UnitsListingPage from "./UnitsListingPage"
import { factories, setMockResponse, urls } from "api/test-utils"
+import { assertHeadings } from "ol-test-utilities"
const makeSearchResponse = (
aggregations: Record,
@@ -175,4 +176,15 @@ describe("DepartmentListingPage", () => {
})
})
})
+
+ test("headings", async () => {
+ setupApis()
+ renderWithProviders( )
+
+ assertHeadings([
+ { level: 1, name: "Academic & Professional Learning" },
+ { level: 2, name: "Academic Units" },
+ { level: 2, name: "Professional Units" },
+ ])
+ })
})
diff --git a/frontends/mit-learn/src/pages/UnitsListingPage/UnitsListingPage.tsx b/frontends/mit-learn/src/pages/UnitsListingPage/UnitsListingPage.tsx
index f72e2906e0..69693ddd52 100644
--- a/frontends/mit-learn/src/pages/UnitsListingPage/UnitsListingPage.tsx
+++ b/frontends/mit-learn/src/pages/UnitsListingPage/UnitsListingPage.tsx
@@ -98,7 +98,7 @@ const PageHeaderText = styled(Typography)(({ theme }) => ({
...theme.typography.subtitle1,
}))
-const UnitContainer = styled.div(({ theme }) => ({
+const UnitContainer = styled.section(({ theme }) => ({
display: "flex",
flexDirection: "column",
alignItems: "center",
@@ -118,7 +118,8 @@ const UnitTitleContainer = styled.div({
paddingBottom: "16px",
})
-const UnitTitle = styled(Typography)(({ theme }) => ({
+const UnitTitle = styled.h2(({ theme }) => ({
+ margin: 0,
color: theme.custom.colors.darkGray2,
...theme.typography.h4,
}))
diff --git a/frontends/mit-learn/src/pages/UserListListingComponent/UserListListingComponent.test.tsx b/frontends/mit-learn/src/pages/UserListListingComponent/UserListListingComponent.test.tsx
index c1aa9dfaf2..8157aa1027 100644
--- a/frontends/mit-learn/src/pages/UserListListingComponent/UserListListingComponent.test.tsx
+++ b/frontends/mit-learn/src/pages/UserListListingComponent/UserListListingComponent.test.tsx
@@ -82,8 +82,8 @@ describe("UserListListingComponent", () => {
it("Renders a card for each user list", async () => {
const { paths } = setup()
const titles = paths.results.map((userList) => userList.title)
- const headings = await screen.findAllByRole("heading", {
- name: (value) => titles.includes(value),
+ const headings = await screen.findAllByRole("link", {
+ name: (name) => titles.some((title) => name.includes(title)),
})
// for sanity
diff --git a/frontends/mit-learn/src/pages/UserListListingComponent/UserListListingComponent.tsx b/frontends/mit-learn/src/pages/UserListListingComponent/UserListListingComponent.tsx
index 93733666d2..6ed4f8c84f 100644
--- a/frontends/mit-learn/src/pages/UserListListingComponent/UserListListingComponent.tsx
+++ b/frontends/mit-learn/src/pages/UserListListingComponent/UserListListingComponent.tsx
@@ -7,6 +7,7 @@ import {
PlainList,
Card,
theme,
+ TypographyProps,
} from "ol-components"
import { RiListCheck3 } from "@remixicon/react"
import { useUserListList } from "api/hooks/learningResources"
@@ -15,7 +16,7 @@ import { manageListDialogs } from "@/page-components/ManageListDialogs/ManageLis
import { userListView } from "@/common/urls"
import UserListCardCondensed from "@/page-components/UserListCard/UserListCardCondensed"
-const Header = styled(Typography)({
+const Header = styled(Typography)>({
marginBottom: "16px",
})
@@ -71,7 +72,9 @@ const UserListListingComponent: React.FC = (
return (
-
+
{!data?.results.length && !isLoading ? (
diff --git a/frontends/mit-learn/src/routes.tsx b/frontends/mit-learn/src/routes.tsx
index f1328693b3..fb5b338066 100644
--- a/frontends/mit-learn/src/routes.tsx
+++ b/frontends/mit-learn/src/routes.tsx
@@ -35,7 +35,7 @@ const PageWrapper = styled.div({
flexDirection: "column",
})
-const PageWrapperInner = styled.div({
+const PageWrapperInner = styled.main({
flex: "1",
})
diff --git a/frontends/ol-components/src/components/Banner/Banner.tsx b/frontends/ol-components/src/components/Banner/Banner.tsx
index 8babefdad7..2c1b209766 100644
--- a/frontends/ol-components/src/components/Banner/Banner.tsx
+++ b/frontends/ol-components/src/components/Banner/Banner.tsx
@@ -114,6 +114,7 @@ const Banner = ({
{avatar ? {avatar}
: null}
`
margin-bottom: ${({ size }) => (size === "small" ? 4 : 8)}px;
`
-const Title = styled.h3<{ lines?: number; size?: Size }>`
+const Title = styled.span<{ lines?: number; size?: Size }>`
text-overflow: ellipsis;
height: ${({ lines, size }) => {
const lineHeightPx = size === "small" ? 18 : 20
diff --git a/frontends/ol-components/src/components/Card/ListCard.tsx b/frontends/ol-components/src/components/Card/ListCard.tsx
index b32c073690..6307acb230 100644
--- a/frontends/ol-components/src/components/Card/ListCard.tsx
+++ b/frontends/ol-components/src/components/Card/ListCard.tsx
@@ -107,7 +107,7 @@ export const Info = styled.div`
align-items: center;
`
-export const Title = styled.h3`
+export const Title = styled.span`
flex-grow: 1;
color: ${theme.custom.colors.darkGray2};
text-overflow: ellipsis;
diff --git a/frontends/ol-components/src/components/Dialog/Dialog.tsx b/frontends/ol-components/src/components/Dialog/Dialog.tsx
index 176ae5a5e6..a7a43c6afc 100644
--- a/frontends/ol-components/src/components/Dialog/Dialog.tsx
+++ b/frontends/ol-components/src/components/Dialog/Dialog.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useState } from "react"
+import React, { useCallback, useId, useState } from "react"
import styled from "@emotion/styled"
import { theme } from "../ThemeProvider/ThemeProvider"
import { default as MuiDialog } from "@mui/material/Dialog"
@@ -93,6 +93,7 @@ const Dialog: React.FC = ({
disableEnforceFocus,
}) => {
const [confirming, setConfirming] = useState(isSubmitting)
+ const titleId = useId()
const handleConfirm = useCallback(async () => {
try {
@@ -115,6 +116,7 @@ const Dialog: React.FC = ({
disableEnforceFocus={disableEnforceFocus}
PaperProps={PaperProps}
TransitionComponent={Transition}
+ aria-labelledby={titleId}
>
= ({
{title && (
)}
diff --git a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.test.tsx b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.test.tsx
index 8660f1807a..a059281b20 100644
--- a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.test.tsx
+++ b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.test.tsx
@@ -1,6 +1,6 @@
import React from "react"
import { BrowserRouter } from "react-router-dom"
-import { screen, render, act } from "@testing-library/react"
+import { screen, render } from "@testing-library/react"
import { LearningResourceCard } from "./LearningResourceCard"
import type { LearningResourceCardProps } from "./LearningResourceCard"
import { DEFAULT_RESOURCE_IMG, embedlyCroppedImage } from "ol-utilities"
@@ -11,10 +11,7 @@ import { ThemeProvider } from "../ThemeProvider/ThemeProvider"
const setup = (props: LearningResourceCardProps) => {
return render(
-
+
,
{ wrapper: ThemeProvider },
)
@@ -30,7 +27,7 @@ describe("Learning Resource Card", () => {
setup({ resource })
screen.getByText("Course")
- screen.getByRole("heading", { name: resource.title })
+ screen.getByText(resource.title)
screen.getByText("Starts:")
screen.getByText("January 01, 2026")
})
@@ -94,20 +91,18 @@ describe("Learning Resource Card", () => {
},
)
- test("Click to navigate", async () => {
+ test("Links to specified href", async () => {
const resource = factories.learningResources.resource({
resource_type: ResourceTypeEnum.Course,
platform: { code: PlatformEnum.Ocw },
})
- setup({ resource })
+ setup({ resource, href: "/path/to/thing" })
- const heading = screen.getByRole("heading", { name: resource.title })
- await act(async () => {
- await heading.click()
+ const link = screen.getByRole("link", {
+ name: new RegExp(resource.title),
})
-
- expect(window.location.search).toBe(`?resource=${resource.id}`)
+ expect(new URL(link.href).pathname).toBe("/path/to/thing")
})
test("Click action buttons", async () => {
diff --git a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.test.tsx b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.test.tsx
index df930f2f53..9189cb40d1 100644
--- a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.test.tsx
+++ b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.test.tsx
@@ -1,6 +1,6 @@
import React from "react"
import { BrowserRouter } from "react-router-dom"
-import { screen, render, act } from "@testing-library/react"
+import { screen, render } from "@testing-library/react"
import { LearningResourceListCard } from "./LearningResourceListCard"
import type { LearningResourceListCardProps } from "./LearningResourceListCard"
import { DEFAULT_RESOURCE_IMG, embedlyCroppedImage } from "ol-utilities"
@@ -11,10 +11,7 @@ import { ThemeProvider } from "../ThemeProvider/ThemeProvider"
const setup = (props: LearningResourceListCardProps) => {
return render(
-
+
,
{ wrapper: ThemeProvider },
)
@@ -30,7 +27,7 @@ describe("Learning Resource List Card", () => {
setup({ resource })
screen.getByText("Course")
- screen.getByRole("heading", { name: resource.title })
+ screen.getByText(resource.title)
screen.getByText("Starts:")
screen.getByText("January 01, 2026")
})
@@ -92,14 +89,13 @@ describe("Learning Resource List Card", () => {
platform: { code: PlatformEnum.Ocw },
})
- setup({ resource })
+ setup({ resource, href: "/path/to/thing" })
- const heading = screen.getByRole("heading", { name: resource.title })
- await act(async () => {
- await heading.click()
+ const card = screen.getByRole("link", {
+ name: new RegExp(resource.title),
})
- expect(window.location.search).toBe(`?resource=${resource.id}`)
+ expect(card).toHaveAttribute("href", "/path/to/thing")
})
test("Click action buttons", async () => {
diff --git a/frontends/ol-components/src/components/ThemeProvider/typography.ts b/frontends/ol-components/src/components/ThemeProvider/typography.ts
index 22c6f575d0..c358c86857 100644
--- a/frontends/ol-components/src/components/ThemeProvider/typography.ts
+++ b/frontends/ol-components/src/components/ThemeProvider/typography.ts
@@ -147,6 +147,11 @@ const globalSettings: ThemeOptions["typography"] = {
const component: NonNullable["MuiTypography"] = {
defaultProps: {
variantMapping: {
+ h1: "span",
+ h2: "span",
+ h3: "span",
+ h4: "span",
+ h5: "span",
body1: "p",
body2: "p",
body3: "p",
diff --git a/frontends/ol-components/src/components/VisuallyHidden/VisuallyHidden.tsx b/frontends/ol-components/src/components/VisuallyHidden/VisuallyHidden.tsx
new file mode 100644
index 0000000000..c23456ea7f
--- /dev/null
+++ b/frontends/ol-components/src/components/VisuallyHidden/VisuallyHidden.tsx
@@ -0,0 +1,30 @@
+import styled from "@emotion/styled"
+
+/**
+ * VisuallyHidden is a utility component that hides its children from sighted
+ * users, but keeps them accessible to screen readers.
+ *
+ * Often, screenreader-only content can be handled with an `aria-label`. However,
+ * occasionally we need actual elements.
+ *
+ * Example:
+ * - a visually hidden Heading for a section whose purpose is clear for sighted users
+ * - a visually hidden description used for aria-describeddby
+ * - There is an aria-descriptionby attribute that can be used to provide a
+ * without an actual element on the page. However, it is introduced in
+ * ARIA 1.3 (working draft), not compatible with some screen readers, and
+ * flagged problematic by our linting.
+ *
+ * The CSS here is based on https://inclusive-components.design/tooltips-toggletips/
+ */
+const VisuallyHidden = styled.span({
+ clipPath: "inset(100%)",
+ clip: "rect(1px, 1px, 1px, 1px)",
+ height: "1px",
+ overflow: "hidden",
+ position: "absolute",
+ whiteSpace: "nowrap",
+ width: "1px",
+})
+
+export { VisuallyHidden }
diff --git a/frontends/ol-components/src/index.ts b/frontends/ol-components/src/index.ts
index 7ee530994a..c8416defa9 100644
--- a/frontends/ol-components/src/index.ts
+++ b/frontends/ol-components/src/index.ts
@@ -193,6 +193,7 @@ export * from "./components/ThemeProvider/ThemeProvider"
export * from "./components/TruncateText/TruncateText"
export * from "./components/Radio/Radio"
export * from "./components/RadioChoiceField/RadioChoiceField"
+export * from "./components/VisuallyHidden/VisuallyHidden"
export * from "./constants/imgConfigs"
diff --git a/frontends/ol-test-utilities/package.json b/frontends/ol-test-utilities/package.json
index d1fbac79e1..912aa20650 100644
--- a/frontends/ol-test-utilities/package.json
+++ b/frontends/ol-test-utilities/package.json
@@ -10,6 +10,7 @@
"@faker-js/faker": "^8.0.0",
"@testing-library/react": "16.0.0",
"css-mediaquery": "^0.1.2",
+ "dom-accessibility-api": "^0.7.0",
"tiny-invariant": "^1.3.1"
}
}
diff --git a/frontends/ol-test-utilities/src/assertions.ts b/frontends/ol-test-utilities/src/assertions.ts
new file mode 100644
index 0000000000..7128ec0895
--- /dev/null
+++ b/frontends/ol-test-utilities/src/assertions.ts
@@ -0,0 +1,26 @@
+import { screen } from "@testing-library/react"
+/**
+ * This is the library that @testing-library uses to compute accessible names.
+ */
+import { computeAccessibleName } from "dom-accessibility-api"
+
+type HeadingSpec = {
+ level: number
+ /**
+ * The accessible name of the heading.
+ * Can be a matcher like `expect.stringContaining("foo")`.
+ */
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ name: any
+}
+const assertHeadings = (expected: HeadingSpec[]) => {
+ const headings = screen.getAllByRole("heading")
+ const actual = headings.map((heading) => {
+ const level = parseInt(heading.tagName[1], 10)
+ const name = computeAccessibleName(heading)
+ return { level, name }
+ })
+ expect(actual).toEqual(expected)
+}
+
+export { assertHeadings }
diff --git a/frontends/ol-test-utilities/src/index.ts b/frontends/ol-test-utilities/src/index.ts
index 5bd11487e9..ae10ad5a02 100644
--- a/frontends/ol-test-utilities/src/index.ts
+++ b/frontends/ol-test-utilities/src/index.ts
@@ -2,3 +2,4 @@ export { default as ControlledPromise } from "./ControlledPromise/ControlledProm
export * from "./factories"
export * from "./domQueries"
export * from "./mocks/mocks"
+export * from "./assertions"
diff --git a/yarn.lock b/yarn.lock
index 13500e370a..d531e73db2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8782,6 +8782,13 @@ __metadata:
languageName: node
linkType: hard
+"dom-accessibility-api@npm:^0.7.0":
+ version: 0.7.0
+ resolution: "dom-accessibility-api@npm:0.7.0"
+ checksum: 10/ac69d27099bc0650633545c461347a355c2794b330f69b6b67fd98df91be9a48547d85176758747841e10ee041905143b39b6094e72c704c265b0f4f9bb7ab28
+ languageName: node
+ linkType: hard
+
"dom-converter@npm:^0.2.0":
version: 0.2.0
resolution: "dom-converter@npm:0.2.0"
@@ -15413,6 +15420,7 @@ __metadata:
"@faker-js/faker": "npm:^8.0.0"
"@testing-library/react": "npm:16.0.0"
css-mediaquery: "npm:^0.1.2"
+ dom-accessibility-api: "npm:^0.7.0"
tiny-invariant: "npm:^1.3.1"
languageName: unknown
linkType: soft