Skip to content

Commit e7897de

Browse files
huyenltnguyenmoT01bbsmooth
authored
fix(a11y): fill in the blank challenges (freeCodeCamp#56775)
Co-authored-by: Tom <20648924+moT01@users.noreply.github.com> Co-authored-by: Bruce Blaser <bbsmooth@gmail.com>
1 parent 1c8217a commit e7897de

4 files changed

Lines changed: 105 additions & 34 deletions

File tree

client/i18n/locales/english/translations.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,8 +527,10 @@
527527
"building-a-university": "We're Building a Free Computer Science University Degree Program 🎉",
528528
"if-help-university": "We've already made a ton of progress. Donate now to help our charity with the road ahead.",
529529
"preview-external-window": "Preview currently showing in external window.",
530-
"fill-in-the-blank": "Fill in the blank",
531-
"blank": "blank",
530+
"fill-in-the-blank": {
531+
"heading": "Fill in the blank",
532+
"blank": "blank"
533+
},
532534
"quiz": {
533535
"correct-answer": "Correct!",
534536
"incorrect-answer": "Incorrect.",

client/src/templates/Challenges/components/fill-in-the-blanks.tsx

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { useTranslation } from 'react-i18next';
3-
43
import { Spacer } from '@freecodecamp/ui';
4+
55
import { parseBlanks } from '../fill-in-the-blank/parse-blanks';
66
import PrismFormatted from '../components/prism-formatted';
77
import { FillInTheBlank } from '../../../redux/prop-types';
@@ -26,18 +26,22 @@ function FillInTheBlanks({
2626
}: FillInTheBlankProps): JSX.Element {
2727
const { t } = useTranslation();
2828

29-
const addInputClass = (index: number): string => {
30-
if (answersCorrect[index] === true) return 'green-underline';
31-
if (answersCorrect[index] === false) return 'red-underline';
32-
return '';
29+
const getInputClass = (index: number): string => {
30+
let cls = 'fill-in-the-blank-input';
31+
32+
if (answersCorrect[index] === false) {
33+
cls += ' incorrect-blank-answer';
34+
}
35+
36+
return cls;
3337
};
3438

3539
const paragraphs = parseBlanks(sentence);
3640
const blankAnswers = blanks.map(b => b.answer);
3741

3842
return (
3943
<>
40-
<ChallengeHeading heading={t('learn.fill-in-the-blank')} />
44+
<ChallengeHeading heading={t('learn.fill-in-the-blank.heading')} />
4145
<Spacer size='xs' />
4246
<div className='fill-in-the-blank-wrap'>
4347
{paragraphs.map((p, i) => {
@@ -47,36 +51,49 @@ function FillInTheBlanks({
4751
<p key={i}>
4852
{p.map((node, j) => {
4953
const { type, value } = node;
50-
if (type === 'text') return value;
51-
if (type === 'blank')
54+
if (type === 'text') {
55+
return value;
56+
}
57+
58+
// If a blank is answered correctly, render the answer as part of the sentence.
59+
if (type === 'blank' && answersCorrect[value] === true) {
5260
return (
53-
<input
54-
key={j}
55-
type='text'
56-
maxLength={blankAnswers[value].length + 3}
57-
className={`fill-in-the-blank-input ${addInputClass(
58-
value
59-
)}`}
60-
onChange={handleInputChange}
61-
data-index={node.value}
62-
size={blankAnswers[value].length}
63-
aria-label={t('learn.blank')}
64-
/>
61+
<span key={j} className='correct-blank-answer'>
62+
{blankAnswers[value]}
63+
</span>
6564
);
65+
}
66+
67+
return (
68+
<input
69+
key={j}
70+
type='text'
71+
maxLength={blankAnswers[value].length + 3}
72+
className={getInputClass(value)}
73+
onChange={handleInputChange}
74+
data-index={node.value}
75+
size={blankAnswers[value].length}
76+
autoComplete='off'
77+
aria-label={t('learn.fill-in-the-blank.blank')}
78+
{...(answersCorrect[value] === false
79+
? { 'aria-invalid': 'true' }
80+
: {})}
81+
/>
82+
);
6683
})}
6784
</p>
6885
);
6986
})}
7087
</div>
7188
<Spacer size='m' />
72-
{showFeedback && feedback && (
73-
<>
74-
<PrismFormatted text={feedback} />
75-
<Spacer size='m' />
76-
</>
77-
)}
78-
<div className='text-center'>
79-
{showWrong && <span>{t('learn.wrong-answer')}</span>}
89+
<div aria-live='polite'>
90+
{showWrong && (
91+
<div className='text-center'>
92+
<span>{t('learn.wrong-answer')}</span>
93+
<Spacer size='m' />
94+
</div>
95+
)}
96+
{showFeedback && feedback && <PrismFormatted text={feedback} />}
8097
</div>
8198
</>
8299
);

client/src/templates/Challenges/fill-in-the-blank/show.css

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@
3636
z-index: 2;
3737
}
3838

39-
.green-underline {
40-
border-bottom-color: var(--success-background) !important;
39+
.correct-blank-answer {
40+
color: var(--background-success) !important;
41+
font-weight: bold;
4142
}
4243

43-
.red-underline {
44-
border-bottom-color: var(--danger-background) !important;
44+
.incorrect-blank-answer {
45+
border-bottom-color: var(--background-danger) !important;
4546
}

e2e/fill-in-the-blanks.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Fill in the blanks challenge', () => {
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto(
6+
'/learn/a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/task-46'
7+
);
8+
});
9+
10+
test('should display feedback if there is an incorrect answer', async ({
11+
page
12+
}) => {
13+
const blanks = page.getByRole('textbox', { name: 'blank' });
14+
15+
await blanks.first().fill('this'); // Answer the first blank correctly
16+
await blanks.last().fill('bar'); // Answer the second blank incorrectly
17+
await page.getByRole('button', { name: 'Check your answer' }).click();
18+
19+
await expect(
20+
page.getByText("Sorry, that's not the right answer. Give it another try?")
21+
).toBeVisible();
22+
23+
// Once a blank is answered correctly, it is no longer rendered as an input field
24+
await expect(blanks).toHaveCount(1);
25+
26+
await expect(blanks.last()).toHaveAttribute('aria-invalid', 'true');
27+
});
28+
29+
test('should not display feedback if all blanks are answered correctly', async ({
30+
page
31+
}) => {
32+
const blanks = page.getByRole('textbox', { name: 'blank' });
33+
34+
await blanks.first().fill('this');
35+
await blanks.last().fill('those');
36+
await page.getByRole('button', { name: 'Check your answer' }).click();
37+
38+
// Close the completion modal
39+
await page
40+
.getByRole('dialog')
41+
.getByRole('button', { name: 'Close' })
42+
.click();
43+
44+
await expect(
45+
page.getByText("Sorry, that's not the right answer. Give it another try?")
46+
).toBeHidden();
47+
48+
// There aren't any blanks as all the inputs are rendered as text
49+
await expect(blanks).toBeHidden();
50+
});
51+
});

0 commit comments

Comments
 (0)