Skip to content

Commit ee6d0b8

Browse files
fix: a11y roles and arias
1 parent 02da445 commit ee6d0b8

File tree

12 files changed

+136
-41
lines changed

12 files changed

+136
-41
lines changed

index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!DOCTYPE html>
1+
<!doctype html>
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
@@ -7,6 +7,7 @@
77
<title>freeCodeCamp: Exam Environment</title>
88
</head>
99
<body class="light-palette">
10+
<a href="#main-content" class="skip-link">Skip to main content</a>
1011
<div id="root"></div>
1112
<script type="module" src="/src/main.tsx"></script>
1213
</body>

src/components/audio-player.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,19 +139,22 @@ export function AudioPlayer({ fullQuestion }: AudioPlayerProps) {
139139
}
140140

141141
return (
142-
<Box>
142+
<Box role="region" aria-label="Audio player">
143143
<Flex direction="column">
144144
<Flex alignItems="center">
145-
<Button onClick={togglePlay}>
145+
<Button
146+
onClick={togglePlay}
147+
aria-label={isPlaying ? "Pause audio" : "Play audio"}
148+
>
146149
{isPlaying ? "| |" : <TriangleUpIcon style={{ rotate: "90deg" }} />}
147150
</Button>
148-
<Text ml={2}>
151+
<Text ml={2} aria-live="polite">
149152
{formatTime(audioRef.current.currentTime)} / {formatTime(duration)}
150153
</Text>
151154
</Flex>
152155

153156
<Slider
154-
aria-label="slider-ex-1"
157+
aria-label="Audio progress and seek"
155158
min={0}
156159
max={duration}
157160
value={progress}
@@ -160,7 +163,7 @@ export function AudioPlayer({ fullQuestion }: AudioPlayerProps) {
160163
<SliderTrack>
161164
<SliderFilledTrack />
162165
</SliderTrack>
163-
<SliderThumb />
166+
<SliderThumb aria-label="Seek position" />
164167
</Slider>
165168
</Flex>
166169
</Box>

src/components/button-loading.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ export function ButtonLoading({
2020
alignItems={"center"}
2121
{...rest}
2222
type="button"
23+
aria-busy={isPending}
2324
>
24-
{isPending && <Spinner marginInlineEnd="0.5rem" />}
25+
{isPending && <Spinner marginInlineEnd="0.5rem" aria-hidden="true" />}
2526
{children}
2627
</Button>
2728
);

src/components/exam-card.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,20 @@ export function ExamCard({ exam }: ExamCardProps) {
5353
return (
5454
<li style={{ listStyle: "none", marginBottom: "1rem" }}>
5555
<Card
56+
as="article"
5657
borderWidth={examStatus.status === "InProgress" ? "3px" : "1px"}
5758
borderColor={
5859
examStatus.status === "InProgress" ? "orange.400" : "gray.200"
5960
}
6061
boxShadow={examStatus.status === "InProgress" ? "lg" : "sm"}
6162
_hover={{ boxShadow: "md" }}
6263
transition="all 0.2s"
64+
aria-label={`${exam.config.name} exam card`}
6365
>
6466
<CardBody>
6567
<Flex justifyContent="space-between" alignItems="flex-start" mb={2}>
6668
<Box flex={1}>
67-
<Heading size="md" mb={2}>
69+
<Heading as="h2" size="md" mb={2}>
6870
{exam.config.name}
6971
</Heading>
7072
<Flex justifyContent={"space-between"}>
@@ -92,7 +94,13 @@ export function ExamCard({ exam }: ExamCardProps) {
9294
</Flex>
9395

9496
{attemptsQuery.isPending ? (
95-
<Flex alignItems="center" gap={2} mt={3}>
97+
<Flex
98+
alignItems="center"
99+
gap={2}
100+
mt={3}
101+
aria-live="polite"
102+
aria-busy="true"
103+
>
96104
<Spinner size="sm" />
97105
<Text fontSize="sm" color="gray.500">
98106
Getting attempt status...

src/components/exam-submission-modal.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ export function ExamSubmissionModal({
3333
return (
3434
<Modal isOpen={hasFinishedExam} onClose={onClose}>
3535
<ModalOverlay />
36-
<ModalContent>
37-
<ModalHeader>{maxTimeReached ? "Time's up!" : "Exam End"}</ModalHeader>
36+
<ModalContent role="dialog" aria-labelledby="exam-submission-title">
37+
<ModalHeader id="exam-submission-title">
38+
{maxTimeReached ? "Time's up!" : "Exam End"}
39+
</ModalHeader>
3840
<ModalBody>
3941
<Text>Thank you for taking the exam.</Text>
4042
<Text>
@@ -53,6 +55,7 @@ export function ExamSubmissionModal({
5355
textDecoration: "underline",
5456
textDecorationColor: "blue.300",
5557
}}
58+
aria-label="Open freeCodeCamp page in browser"
5659
>
5760
https://freecodecamp.org
5861
</Button>{" "}

src/components/header.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export function Header() {
5252
>
5353
<Img
5454
src=" https://cdn.freecodecamp.org/platform/universal/fcc_puck_500.jpg"
55+
alt="freeCodeCamp logo"
5556
width="32px"
5657
height="32px"
5758
mr="12px"
@@ -62,7 +63,12 @@ export function Header() {
6263
</Flex>
6364
</Box>
6465
<Flex alignSelf={"center"} width={"auto"} pr={"1em"}>
65-
<IconButton ref={btnRef} aria-label="Menu" onClick={onOpen}>
66+
<IconButton
67+
ref={btnRef}
68+
aria-label="Open navigation menu"
69+
aria-expanded={isOpen}
70+
onClick={onOpen}
71+
>
6672
<HamburgerIcon />
6773
</IconButton>
6874
</Flex>
@@ -74,8 +80,8 @@ export function Header() {
7480
finalFocusRef={btnRef}
7581
>
7682
<DrawerOverlay />
77-
<DrawerContent>
78-
<DrawerCloseButton />
83+
<DrawerContent aria-label="Navigation menu">
84+
<DrawerCloseButton aria-label="Close navigation menu" />
7985
<DrawerHeader>Navigate</DrawerHeader>
8086
<DrawerBody>
8187
<Button

src/components/question-submission-error-modal.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,21 @@ export function QuestionSubmissionErrorModal({
6060
closeOnOverlayClick={false}
6161
>
6262
<ModalOverlay />
63-
<ModalContent>
64-
<ModalHeader backgroundColor={"#edb1af"} borderRadius={"md"}>
63+
<ModalContent
64+
role="alertdialog"
65+
aria-labelledby="error-modal-title"
66+
aria-describedby="error-modal-description"
67+
>
68+
<ModalHeader
69+
id="error-modal-title"
70+
backgroundColor={"#edb1af"}
71+
borderRadius={"md"}
72+
>
6573
Question Submission Error
6674
</ModalHeader>
67-
<ModalBody>{error}</ModalBody>
75+
<ModalBody id="error-modal-description" role="alert">
76+
{error}
77+
</ModalBody>
6878
<ModalFooter justifyContent={"center"}>
6979
<ButtonLoading
7080
onClick={onClick}

src/index.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ body {
156156
line-height: 2;
157157
}
158158

159+
.bottom-bubble-nav:focus {
160+
outline: 3px solid var(--blue-mid);
161+
outline-offset: 2px;
162+
}
163+
159164
.bubble-active {
160165
border: 3px solid rgb(48, 48, 204);
161166
background-color: rgb(48, 48, 204);
@@ -167,6 +172,28 @@ body {
167172
color: white;
168173
}
169174

175+
/* Skip to main content link */
176+
.skip-link {
177+
position: absolute;
178+
left: -9999px;
179+
z-index: 999;
180+
padding: 1em;
181+
background-color: #000;
182+
color: #fff;
183+
text-decoration: none;
184+
}
185+
186+
.skip-link:focus {
187+
left: 0;
188+
top: 0;
189+
}
190+
191+
/* Ensure focus is visible across the application */
192+
*:focus-visible {
193+
outline: 3px solid var(--blue-mid);
194+
outline-offset: 2px;
195+
}
196+
170197
audio {
171198
width: 100%;
172199
}

src/pages/exam-landing.tsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,13 @@ export function ExamLanding() {
9696
return (
9797
<>
9898
<Header />
99-
<Box width={"full"}>
99+
<Box width={"full"} as="main" id="main-content">
100100
<Center height={"100%"}>
101101
<Flex direction={"column"} maxWidth={"70%"}>
102102
<Spacer size="m" />
103-
<Heading>Instructions</Heading>
103+
<Heading as="h1">Instructions</Heading>
104104
<Spacer size="s" />
105-
<Text color={"red"}>
105+
<Text color={"red"} role="alert">
106106
Please note that any attempt to cheat will result in immediate
107107
disqualification from the exam and you will need to retake the
108108
exam to qualify for the certification.
@@ -117,11 +117,16 @@ export function ExamLanding() {
117117
</Text>
118118
{!!note && (
119119
<>
120-
<Heading size={"md"}>Exam Note</Heading>
120+
<Heading as="h2" size={"md"}>
121+
Exam Note
122+
</Heading>
121123
<Text>{note}</Text>
122124
</>
123125
)}
124-
<Checkbox onChange={(e) => setHasAgreed(e.target.checked)}>
126+
<Checkbox
127+
id="terms-agreement"
128+
onChange={(e) => setHasAgreed(e.target.checked)}
129+
>
125130
I agree to the terms and conditions
126131
</Checkbox>
127132
<Spacer size="s" />
@@ -163,7 +168,7 @@ export function ExamLanding() {
163168
)}
164169
<Spacer size="m" />
165170
{updateMutation.isError && (
166-
<Box maxWidth="100%">
171+
<Box maxWidth="100%" role="alert">
167172
<Text>Error checking for updates:</Text>
168173
<Code
169174
p={2}
@@ -183,16 +188,20 @@ export function ExamLanding() {
183188
</Box>
184189
)}
185190
{downloadAndInstallMutation.isPending && !!updateMutation.data && (
186-
<Box>
191+
<Box role="status" aria-live="polite">
187192
<Text>
188193
Downloading update (version {updateMutation.data.version}).
189194
App will restart when finished.
190195
</Text>
191-
<Progress hasStripe value={progress} />
196+
<Progress
197+
hasStripe
198+
value={progress}
199+
aria-label="Download progress"
200+
/>
192201
</Box>
193202
)}
194203
{downloadAndInstallMutation.isError && (
195-
<Box maxWidth="100%">
204+
<Box maxWidth="100%" role="alert">
196205
<Text>Error downloading and installing update:</Text>
197206
<Code
198207
p={2}

src/pages/exam.tsx

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ export function Exam() {
395395
fullQuestion={fullQuestion}
396396
newSelectedAnswers={newSelectedAnswers}
397397
/>
398-
<Box overflowY="hidden">
398+
<Box overflowY="hidden" as="main" id="main-content">
399399
<Box width={"full"} mt="2em">
400400
<Center height={"100%"} display={"flex"} flexDirection={"column"}>
401401
<Center width="full" borderBottom={"2px"} borderColor={"gray.300"}>
@@ -576,7 +576,9 @@ function Timer({
576576
}, [secondsLeft]);
577577

578578
return (
579-
<Text fontWeight={"bold"}>Time: {secondsToHHMMSS(availableTime)}</Text>
579+
<Text fontWeight={"bold"} aria-live="polite" aria-atomic="true">
580+
Time: {secondsToHHMMSS(availableTime)}
581+
</Text>
580582
);
581583
}
582584

@@ -622,9 +624,14 @@ function NavigationBubbles({
622624
const bubblesArr = getCurrentBubbleIndex(wantedIndex);
623625

624626
return (
625-
<Flex width="80%" justifyContent={"center"}>
627+
<Flex
628+
width="80%"
629+
justifyContent={"center"}
630+
as="nav"
631+
aria-label="Question navigation"
632+
>
626633
<IconButton
627-
aria-label="previous question"
634+
aria-label="Go to previous question"
628635
icon={<ChevronLeftIcon />}
629636
m={"0.3em"}
630637
isDisabled={currentQuestionNumber === 1}
@@ -633,7 +640,7 @@ function NavigationBubbles({
633640
}}
634641
/>
635642
<IconButton
636-
aria-label="previous set of questions"
643+
aria-label="Go to previous set of questions"
637644
icon={<ArrowLeftIcon />}
638645
m={"0.3em"}
639646
isDisabled={wantedIndex === 0}
@@ -655,12 +662,24 @@ function NavigationBubbles({
655662
className={`bottom-bubble-nav ${
656663
currentQuestionNumber === question_num ? "bubble-active" : ""
657664
} ${isAnswered(question_num) ? "bubble-answered" : ""}`}
665+
role="button"
666+
tabIndex={0}
667+
aria-label={`Go to question ${question_num}${isAnswered(question_num) ? " (answered)" : ""}`}
668+
aria-current={
669+
currentQuestionNumber === question_num ? "page" : undefined
670+
}
671+
onKeyDown={(e) => {
672+
if (e.key === "Enter" || e.key === " ") {
673+
e.preventDefault();
674+
specificQuestion(question_num);
675+
}
676+
}}
658677
>
659678
<Text>{question_num.toString()}</Text>
660679
</Box>
661680
))}
662681
<IconButton
663-
aria-label="next"
682+
aria-label="Go to next set of questions"
664683
icon={<ArrowRightIcon />}
665684
m={"0.3em"}
666685
isDisabled={maxIndex == wantedIndex}
@@ -669,7 +688,7 @@ function NavigationBubbles({
669688
}}
670689
/>
671690
<IconButton
672-
aria-label="next question"
691+
aria-label="Go to next question"
673692
icon={<ChevronRightIcon />}
674693
m={"0.3em"}
675694
isDisabled={currentQuestionNumber === questions.length}

0 commit comments

Comments
 (0)