Skip to content

Commit 1850731

Browse files
Onboarding Accessibility Improvements (#1960)
* screenreader improvements * reorganize tests * change the h3s to h2s
1 parent c044de9 commit 1850731

File tree

2 files changed

+97
-174
lines changed

2 files changed

+97
-174
lines changed

frontends/main/src/app-pages/OnboardingPage/OnboardingPage.test.tsx

Lines changed: 59 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ const STEP_TITLES = [
5252
"Are you seeking a certificate?",
5353
"What is your current level of education?",
5454
"What course format are you interested in?",
55-
]
55+
].map((title, index) => ({
56+
title,
57+
step: index,
58+
}))
5659

5760
const PROFILES_FOR_STEPS = times(STEPS_DATA.length, profileForStep)
5861

@@ -87,166 +90,66 @@ const queryBackButton = () => screen.queryByRole("button", { name: "Back" })
8790
const queryFinishButton = () => screen.queryByRole("button", { name: "Finish" })
8891

8992
describe("OnboardingPage", () => {
90-
describe("Topic Interests step", () => {
91-
const STEP = 0
92-
const TITLE = STEP_TITLES[STEP]
93-
94-
beforeEach(async () => {
95-
await setupAndProgressToStep(STEP)
96-
})
97-
98-
test(`Title should be '${TITLE}'`, async () => {
99-
expect(await screen.findByText(TITLE, { exact: false })).not.toBeNil()
100-
})
101-
102-
test("Has 'Next' but not 'Back' or 'Finish' buttons", async () => {
103-
const backButton = queryBackButton()
104-
const nextButton = await findNextButton()
105-
const finishButton = queryFinishButton()
106-
107-
expect(backButton).toBeNil()
108-
expect(nextButton).not.toBeNil()
109-
expect(finishButton).toBeNil()
110-
})
111-
})
112-
113-
describe("Goals step", () => {
114-
const STEP = 1
115-
const TITLE = STEP_TITLES[STEP]
116-
117-
beforeEach(async () => {
118-
await setupAndProgressToStep(STEP)
119-
})
120-
121-
test(`Title should be '${TITLE}'`, async () => {
122-
expect(await screen.findByText(TITLE, { exact: false })).not.toBeNil()
123-
})
124-
125-
test("Has 'Next' and 'Back' buttons", async () => {
126-
const backButton = await findBackButton()
127-
const nextButton = await findNextButton()
128-
const finishButton = queryFinishButton()
129-
130-
expect(backButton).not.toBeNil()
131-
expect(nextButton).not.toBeNil()
132-
expect(finishButton).toBeNil()
133-
})
134-
135-
test("Back button should go to previous step", async () => {
136-
const backButton = await findBackButton()
137-
138-
await user.click(backButton)
139-
140-
await waitFor(async () => {
141-
expect(
142-
await screen.findByText(STEP_TITLES[STEP - 1], { exact: false }),
143-
).not.toBeNil()
144-
})
145-
})
146-
})
147-
148-
describe("Certificate step", () => {
149-
const STEP = 2
150-
const TITLE = STEP_TITLES[STEP]
151-
152-
beforeEach(async () => {
153-
await setupAndProgressToStep(STEP)
154-
})
155-
156-
test(`Title should be '${TITLE}'`, async () => {
157-
expect(await screen.findByText(TITLE, { exact: false })).not.toBeNil()
158-
})
159-
160-
test("Has 'Next' and 'Back' buttons", async () => {
161-
const backButton = await findBackButton()
162-
const nextButton = await findNextButton()
163-
const finishButton = queryFinishButton()
164-
165-
expect(backButton).not.toBeNil()
166-
expect(nextButton).not.toBeNil()
167-
expect(finishButton).toBeNil()
168-
})
169-
170-
test("Back button should go to previous step", async () => {
171-
const backButton = await findBackButton()
172-
173-
await user.click(backButton)
174-
175-
await waitFor(async () => {
176-
expect(
177-
await screen.findByText(STEP_TITLES[STEP - 1], { exact: false }),
178-
).not.toBeNil()
93+
test.each(STEP_TITLES)(
94+
"Has expected title (step: $step)",
95+
async ({ step, title }) => {
96+
await setupAndProgressToStep(step)
97+
const heading = await screen.findByRole("heading", {
98+
name: new RegExp(title),
17999
})
180-
})
181-
})
182-
183-
describe("Current education step", () => {
184-
const STEP = 3
185-
const TITLE = STEP_TITLES[STEP]
100+
expect(heading).toBeInTheDocument()
101+
},
102+
)
103+
104+
test.each(STEP_TITLES)(
105+
"Navigation to next step (start: $step)",
106+
async ({ step }) => {
107+
const nextStep = step + 1
108+
await setupAndProgressToStep(step)
109+
if (step === STEP_TITLES.length - 1) {
110+
await findFinishButton()
111+
expect(queryBackButton()).not.toBeNil()
112+
return
113+
}
186114

187-
beforeEach(async () => {
188-
await setupAndProgressToStep(STEP)
189-
})
190-
191-
test(`Title should be '${TITLE}'`, async () => {
192-
expect(await screen.findByText(TITLE, { exact: false })).not.toBeNil()
193-
})
194-
195-
test("Has 'Next' and 'Back' buttons", async () => {
196-
const backButton = await findBackButton()
197115
const nextButton = await findNextButton()
198-
const finishButton = queryFinishButton()
116+
expect(!!queryBackButton()).toBe(step !== 0)
117+
expect(queryFinishButton()).toBeNil()
118+
119+
await user.click(nextButton)
120+
121+
// "Next" button should focus the form so its title is read
122+
const form = screen.getByRole("form")
123+
await waitFor(() => expect(form).toHaveFocus())
124+
expect(form).toHaveAccessibleName(
125+
expect.stringContaining(STEP_TITLES[nextStep].title),
126+
)
127+
},
128+
)
129+
130+
test.each(STEP_TITLES)(
131+
"Navigation to prev step (start: $step)",
132+
async ({ step }) => {
133+
const prevStep = step - 1
134+
await setupAndProgressToStep(step)
135+
if (step === 0) {
136+
await findNextButton()
137+
expect(queryBackButton()).toBeNil()
138+
expect(queryFinishButton()).toBeNil()
139+
return
140+
}
199141

200-
expect(backButton).not.toBeNil()
201-
expect(nextButton).not.toBeNil()
202-
expect(finishButton).toBeNil()
203-
})
204-
205-
test("Back button should go to previous step", async () => {
206142
const backButton = await findBackButton()
207-
143+
expect(!!queryNextButton()).toBe(step !== STEPS_DATA.length - 1)
144+
expect(!!queryFinishButton()).toBe(step === STEPS_DATA.length - 1)
208145
await user.click(backButton)
209146

210-
await waitFor(async () => {
211-
expect(
212-
await screen.findByText(STEP_TITLES[STEP - 1], { exact: false }),
213-
).not.toBeNil()
214-
})
215-
})
216-
})
217-
218-
describe("Learning format step", () => {
219-
const STEP = 4
220-
const TITLE = STEP_TITLES[STEP]
221-
222-
beforeEach(async () => {
223-
await setupAndProgressToStep(STEP)
224-
})
225-
226-
test(`Title should be '${TITLE}'`, async () => {
227-
expect(await screen.findByText(TITLE, { exact: false })).not.toBeNil()
228-
})
229-
230-
test("Has 'Next' and 'Finish' buttons", async () => {
231-
const backButton = await findBackButton()
232-
const nextButton = queryNextButton()
233-
const finishButton = await findFinishButton()
234-
235-
expect(backButton).not.toBeNil()
236-
expect(nextButton).toBeNil()
237-
expect(finishButton).not.toBeNil()
238-
})
239-
240-
test("Back button should go to previous step", async () => {
241-
const backButton = await findBackButton()
242-
243-
await user.click(backButton)
244-
245-
await waitFor(async () => {
246-
expect(
247-
await screen.findByText(STEP_TITLES[STEP - 1], { exact: false }),
248-
).not.toBeNil()
249-
})
250-
})
251-
})
147+
// "Prev" button should focus the form so its title is read
148+
const form = screen.getByRole("form")
149+
await waitFor(() => expect(form).toHaveFocus())
150+
expect(form).toHaveAccessibleName(
151+
expect.stringContaining(STEP_TITLES[prevStep].title),
152+
)
153+
},
154+
)
252155
})

frontends/main/src/app-pages/OnboardingPage/OnboardingPage.tsx

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
"use client"
22

3-
import React, { useId, useMemo } from "react"
3+
import React, { useEffect, useId, useMemo } from "react"
44
import { useRouter } from "next-nprogress-bar"
5-
import range from "lodash/range"
65
import {
76
styled,
87
Step,
@@ -18,6 +17,7 @@ import {
1817
RadioChoiceBoxField,
1918
SimpleSelectField,
2019
Skeleton,
20+
VisuallyHidden,
2121
} from "ol-components"
2222

2323
import { RiArrowRightLine, RiArrowLeftLine } from "@remixicon/react"
@@ -189,10 +189,16 @@ const OnboardingPage: React.FC = () => {
189189
value: topic.id.toString(),
190190
})) ?? []
191191

192+
useEffect(() => {
193+
document.getElementById(formId)?.focus()
194+
}, [activeStep, formId])
195+
192196
if (!profile) {
193197
return null
194198
}
195199

200+
const formLabelId = `${formId}-label`
201+
196202
const pages = [
197203
<Container key="topic_interests" maxWidth="lg">
198204
<CheckboxChoiceBoxField
@@ -204,7 +210,7 @@ const OnboardingPage: React.FC = () => {
204210
{userLoading ? (
205211
<Skeleton variant="text" width="100%" height={40} />
206212
) : (
207-
<Title variant="h4">
213+
<Title component="h2" variant="h4" id={formLabelId}>
208214
Welcome{user?.first_name ? `, ${user.first_name}` : ""}! What
209215
are you interested in learning about?
210216
</Title>
@@ -223,7 +229,7 @@ const OnboardingPage: React.FC = () => {
223229
{...GridStyle()}
224230
label={
225231
<Label>
226-
<Title component="h3" variant="h6">
232+
<Title component="h2" variant="h6" id={formLabelId}>
227233
What are your learning goals?
228234
</Title>
229235
<Prompt component="p">Select all that apply:</Prompt>
@@ -242,7 +248,7 @@ const OnboardingPage: React.FC = () => {
242248
{...GridStyle()}
243249
label={
244250
<Label>
245-
<Title component="h3" variant="h6">
251+
<Title component="h2" variant="h6" id={formLabelId}>
246252
Are you seeking a certificate?
247253
</Title>
248254
</Label>
@@ -258,7 +264,7 @@ const OnboardingPage: React.FC = () => {
258264
fullWidth
259265
label={
260266
<Label>
261-
<Title component="h3" variant="h6">
267+
<Title component="h2" variant="h6" id={formLabelId}>
262268
What is your current level of education?
263269
</Title>
264270
</Label>
@@ -274,7 +280,7 @@ const OnboardingPage: React.FC = () => {
274280
{...GridStyle()}
275281
label={
276282
<Label>
277-
<Title component="h3" variant="h6">
283+
<Title component="h2" variant="h6" id={formLabelId}>
278284
What course format are you interested in?
279285
</Title>
280286
<Prompt>Select all that apply:</Prompt>
@@ -291,24 +297,38 @@ const OnboardingPage: React.FC = () => {
291297
<StepContainer>
292298
<div />
293299
<Stepper connector={null}>
294-
{range(NUM_STEPS).map((index) => (
295-
<Step
296-
key={index}
297-
completed={activeStep > index}
298-
active={activeStep === index}
299-
>
300-
<StepLabel StepIconComponent={StepIcon} />
301-
</Step>
302-
))}
300+
{Array<null>(NUM_STEPS)
301+
.fill(null)
302+
.map((_null, index) => (
303+
<Step
304+
key={index}
305+
completed={activeStep > index}
306+
active={activeStep === index}
307+
>
308+
<StepLabel
309+
slots={{
310+
stepIcon: StepIcon,
311+
}}
312+
/>
313+
</Step>
314+
))}
303315
</Stepper>
304-
<StepNumbers>
316+
<StepNumbers aria-hidden="true">
305317
<span className="current-step">{activeStep + 1}</span>/{NUM_STEPS}
306318
</StepNumbers>
319+
<VisuallyHidden aria-live="polite" aria-atomic="true">
320+
Step {activeStep + 1} of {NUM_STEPS}
321+
</VisuallyHidden>
307322
</StepContainer>
308323
{isLoadingProfile ? (
309324
<LoadingSpinner loading={true} />
310325
) : (
311-
<Form id={formId} onSubmit={formik.handleSubmit}>
326+
<Form
327+
id={formId}
328+
aria-labelledby={formLabelId}
329+
tabIndex={-1}
330+
onSubmit={formik.handleSubmit}
331+
>
312332
{pages[activeStep]}
313333
</Form>
314334
)}

0 commit comments

Comments
 (0)