-
+
)}
)
}
-export { Dialog }
+export { Dialog, DialogActions }
export type { DialogProps }
diff --git a/frontends/ol-components/src/components/FormDialog/FormDialog.stories.tsx b/frontends/ol-components/src/components/FormDialog/FormDialog.stories.tsx
index fbe2a9336b..1c0f3c5ad3 100644
--- a/frontends/ol-components/src/components/FormDialog/FormDialog.stories.tsx
+++ b/frontends/ol-components/src/components/FormDialog/FormDialog.stories.tsx
@@ -72,6 +72,5 @@ export const Simple: Story = {
args: {
title: "Form Title",
fullWidth: true,
- footerContent: "Footer content",
},
}
diff --git a/frontends/ol-components/src/components/FormDialog/FormDialog.tsx b/frontends/ol-components/src/components/FormDialog/FormDialog.tsx
index 7b4ec1e616..85d7cbb281 100644
--- a/frontends/ol-components/src/components/FormDialog/FormDialog.tsx
+++ b/frontends/ol-components/src/components/FormDialog/FormDialog.tsx
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from "react"
import styled from "@emotion/styled"
import { Dialog } from "../Dialog/Dialog"
-import type { DialogProps } from "@mui/material/Dialog"
+import type { DialogProps } from "../Dialog/Dialog"
const FormContent = styled.div`
display: flex;
@@ -50,20 +50,12 @@ interface FormDialogProps {
* The form content. These will be direct children of MUI's [DialogContent](https://mui.com/material-ui/api/dialog-content/)
*/
children?: React.ReactNode
- /**
- * Extra content below the cancel/submit buttons. This is useful, e.g., for
- * displaying overall form error messages.
- */
- footerContent?: React.ReactNode
+ actions?: DialogProps["actions"]
/**
* Class applied to the `
` element.
*/
formClassName?: string
- /**
- * MUI Dialog's [TransitionProps](https://mui.com/material-ui/api/dialog/#props)
- */
- TransitionProps?: DialogProps["TransitionProps"]
/**
* If `true`, the dialog stretches to its `maxWidth`.
*
@@ -93,7 +85,7 @@ const FormDialog: React.FC
= ({
title,
noValidate,
children,
- footerContent,
+ actions,
confirmText = "Submit",
cancelText = "Cancel",
className,
@@ -139,11 +131,9 @@ const FormDialog: React.FC = ({
isSubmitting={isSubmitting}
className={className}
PaperProps={paperProps}
+ actions={actions}
>
-
- {children}
- {footerContent}
-
+ {children}
)
}
diff --git a/frontends/ol-components/src/components/FormHelpers/FormHelpers.tsx b/frontends/ol-components/src/components/FormHelpers/FormHelpers.tsx
index 65cc5f2c95..1e67cf3ccf 100644
--- a/frontends/ol-components/src/components/FormHelpers/FormHelpers.tsx
+++ b/frontends/ol-components/src/components/FormHelpers/FormHelpers.tsx
@@ -35,7 +35,7 @@ const Container = styled.div<{ fullWidth?: boolean }>(({ fullWidth }) => [
display: "inline-flex",
flexDirection: "column",
alignItems: "start",
- "> *": {
+ "> *:not(:last-child)": {
marginBottom: "4px",
},
},
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..7c04f30eec 100644
--- a/frontends/ol-components/src/index.ts
+++ b/frontends/ol-components/src/index.ts
@@ -64,11 +64,11 @@ export type { ContainerProps } from "@mui/material/Container"
export { default as MuiDialog } from "@mui/material/Dialog"
export type { DialogProps as MuiDialogProps } from "@mui/material/Dialog"
-export { default as DialogActions } from "@mui/material/DialogActions"
+export { default as MuiDialogActions } from "@mui/material/DialogActions"
export type { DialogActionsProps } from "@mui/material/DialogActions"
-export { default as DialogContent } from "@mui/material/DialogContent"
+export { default as MuiDialogContent } from "@mui/material/DialogContent"
export type { DialogContentProps } from "@mui/material/DialogContent"
-export { default as DialogTitle } from "@mui/material/DialogTitle"
+export { default as MuiDialogTitle } from "@mui/material/DialogTitle"
export type { DialogTitleProps } from "@mui/material/DialogTitle"
export { default as Divider } from "@mui/material/Divider"
@@ -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/frontends/ol-widgets/src/components/editing/ManageWidgetDialog.tsx b/frontends/ol-widgets/src/components/editing/ManageWidgetDialog.tsx
index 2240a698e0..3e1c0d97a9 100644
--- a/frontends/ol-widgets/src/components/editing/ManageWidgetDialog.tsx
+++ b/frontends/ol-widgets/src/components/editing/ManageWidgetDialog.tsx
@@ -1,9 +1,9 @@
import React, { useId, useCallback, useEffect, useMemo, useState } from "react"
import {
- Dialog,
- DialogActions,
- DialogContent,
- DialogTitle,
+ MuiDialog,
+ MuiDialogActions,
+ MuiDialogContent,
+ MuiDialogTitle,
Button,
RadioChoiceField,
} from "ol-components"
@@ -122,7 +122,7 @@ const DialogContentEditing: React.FC = ({
)
return (
<>
- {title}
+ {title}
>}
validationSchema={validationSchema}
@@ -135,7 +135,7 @@ const DialogContentEditing: React.FC = ({
const { htmlFor: labelFor, ...labelAttrs } = titleAttrs.label
return (
)
}}
@@ -275,11 +275,11 @@ const DialogContentAdding: React.FC = ({
)
return (
<>
- New widget
+ New widget
{({ handleSubmit, values }) => (
)}
@@ -345,7 +345,7 @@ const ManageWidgetDialog: React.FC = ({
[],
)
return (
-
+
)
}
diff --git a/main/settings.py b/main/settings.py
index cf48dc1a9a..8c66f7f483 100644
--- a/main/settings.py
+++ b/main/settings.py
@@ -33,7 +33,7 @@
from main.settings_pluggy import * # noqa: F403
from openapi.settings_spectacular import open_spectacular_settings
-VERSION = "0.17.12"
+VERSION = "0.17.13"
log = logging.getLogger()
diff --git a/yarn.lock b/yarn.lock
index 13500e370a..60766f3154 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4648,6 +4648,15 @@ __metadata:
languageName: node
linkType: hard
+"@swc/plugin-emotion@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "@swc/plugin-emotion@npm:4.0.0"
+ dependencies:
+ "@swc/counter": "npm:^0.1.3"
+ checksum: 10/31159f1a510732f48b9e3bdd9929f6f503bb866957d15ebcc8635f273b1aebad23f23b6798e6c0d426d5a015b15403b20fcd3102ac33c7908e614ab480bf92e3
+ languageName: node
+ linkType: hard
+
"@swc/types@npm:^0.1.12":
version: 0.1.12
resolution: "@swc/types@npm:0.1.12"
@@ -8782,6 +8791,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"
@@ -14769,6 +14785,7 @@ __metadata:
"@storybook/react-webpack5": "npm:^8.0.9"
"@storybook/test": "npm:^8.0.9"
"@swc/core": "npm:^1.4.11"
+ "@swc/plugin-emotion": "npm:^4.0.0"
"@tanstack/react-query": "npm:^4.36.1"
"@tanstack/react-query-devtools": "npm:^4.29.6"
"@testing-library/react": "npm:14.3.1"
@@ -15413,6 +15430,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