fix(a11y): improve RWD challege grid accessibility (#45957)

* fix: improve RWD challege grid accessibility

* fix: change wording from challenge to step

* fix: improve keyboard focus indicator on step links

* fix: style jump link as button

* fix: accessible background color for completed steps

* refactor: make steps a nav region

* refactor: remove duplicate selector

* fix: enhance focus indicator on jump link

* fix: TS type

* refactor: remove unneeded CSS

* chore: resolve conflict

* chore: translate accessible name on nav

* fix: remove h4 from button

* fix: add course name to toggle button

* fix: improve accessibility of course stats on toggle button

* fix: prefix step links with sr-only "Step"

* fix: add start project button

* refactor: remove unnecessary spaces

* fix: clean up styles

* Update client/src/templates/Introduction/intro.css

Co-authored-by: ahmad abdolsaheb <ahmad.abdolsaheb@gmail.com>
This commit is contained in:
Bruce Blaser
2022-06-17 05:45:40 -07:00
committed by GitHub
parent c72aa66056
commit bcb048a93b
4 changed files with 146 additions and 51 deletions

View File

@@ -73,7 +73,9 @@
"click-start-course": "Start the course",
"click-start-project": "Start the project",
"change-language": "Change Language",
"cancel-change": "Cancel Change"
"cancel-change": "Cancel Change",
"resume-project": "Resume project",
"start-project": "Start project"
},
"landing": {
"big-heading-1": "Learn to code — for free.",
@@ -321,7 +323,8 @@
"sorry-keep-trying":"Sorry, your code does not pass. Keep trying.",
"sorry-getting-there":"Sorry, your code does not pass. You're getting there.",
"sorry-hang-in-there":"Sorry, your code does not pass. Hang in there.",
"sorry-dont-giveup":"Sorry, your code does not pass. Don't give up."
"sorry-dont-giveup":"Sorry, your code does not pass. Don't give up.",
"challenges-completed": "{{completedCount}} of {{totalChallenges}} challenges completed"
},
"donate": {
"title": "Support our nonprofit",
@@ -482,7 +485,10 @@
"primary-nav": "primary",
"breadcrumb-nav": "breadcrumb",
"submit": "Use Ctrl + Enter to submit.",
"running-tests": "Running tests"
"running-tests": "Running tests",
"step": "Step",
"steps": "Steps",
"steps-for": "Steps for {{blockTitle}}"
},
"flash": {
"honest-first": "To claim a certification, you must first accept our academic honesty policy",

View File

@@ -193,12 +193,23 @@ export class Block extends Component<BlockProps> {
}}
>
<Caret />
<h4 className='course-title'>
{`${isExpanded ? collapseText : expandText}`}
</h4>
<div className='course-title'>
{`${isExpanded ? collapseText : expandText}`}{' '}
<span className='sr-only'>{blockTitle}</span>
</div>
<div className='map-title-completed course-title'>
{this.renderCheckMark(isBlockCompleted)}
<span className='map-completed-count'>{`${completedCount}/${challengesWithCompleted.length}`}</span>
<span
aria-hidden='true'
className='map-completed-count'
>{`${completedCount}/${challengesWithCompleted.length}`}</span>
<span className='sr-only'>
,{' '}
{t('learn.challenges-completed', {
completedCount,
totalChallenges: challengesWithCompleted.length
})}
</span>
</div>
</button>
{isExpanded && (
@@ -268,7 +279,7 @@ export class Block extends Component<BlockProps> {
<span className='block-header-button-text map-title'>
{this.renderCheckMark(isBlockCompleted)}
<span>
{blockTitle}{' '}
{blockTitle}
<span className='sr-only'>
, {courseCompletionStatus()}
</span>
@@ -293,13 +304,12 @@ export class Block extends Component<BlockProps> {
</div>
{isExpanded && this.renderBlockIntros(blockIntroArr)}
{isExpanded && (
<>
<Challenges
challengesWithCompleted={challengesWithCompleted}
isProjectBlock={isProjectBlock}
superBlock={superBlock}
/>
</>
<Challenges
challengesWithCompleted={challengesWithCompleted}
isProjectBlock={isProjectBlock}
superBlock={superBlock}
blockTitle={blockTitle}
/>
)}
</div>
</ScrollableAnchor>

View File

@@ -1,6 +1,6 @@
import { Link } from 'gatsby';
import React from 'react';
import { withTranslation } from 'react-i18next';
import { withTranslation, useTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux';
@@ -21,6 +21,7 @@ interface Challenges {
executeGA: (payload: ExecuteGaArg) => void;
isProjectBlock: boolean;
superBlock: SuperBlocks;
blockTitle?: string | null;
}
const mapIconStyle = { height: '15px', marginRight: '10px', width: '15px' };
@@ -29,8 +30,10 @@ function Challenges({
challengesWithCompleted,
executeGA,
isProjectBlock,
superBlock
superBlock,
blockTitle
}: Challenges): JSX.Element {
const { t } = useTranslation();
const handleChallengeClick = (slug: string) =>
executeGA({
type: 'event',
@@ -49,40 +52,78 @@ function Challenges({
const isGridMap = isNewRespCert(superBlock) || isNewJsCert(superBlock);
const firstIncompleteChallenge = challengesWithCompleted.find(
challenge => !challenge.isCompleted
);
const isChallengeStarted = !!challengesWithCompleted.find(
challenge => challenge.isCompleted
);
return isGridMap ? (
<ul className={`map-challenges-ul map-challenges-grid `}>
{challengesWithCompleted.map((challenge, i) => (
<li
className={`map-challenge-title map-challenge-title-grid ${
isProjectBlock ? 'map-project-wrap' : 'map-challenge-wrap'
}`}
id={challenge.dashedName}
key={`map-challenge ${challenge.fields.slug}`}
>
{!isProjectBlock ? (
<Link
onClick={() => handleChallengeClick(challenge.fields.slug)}
to={challenge.fields.slug}
className={`map-grid-item ${
challenge.isCompleted ? 'challenge-completed' : ''
<>
{firstIncompleteChallenge && (
<div className='challenge-jump-link'>
<Link
className='btn btn-primary'
onClick={() =>
handleChallengeClick(firstIncompleteChallenge.fields.slug)
}
to={firstIncompleteChallenge.fields.slug}
>
{!isChallengeStarted
? t('buttons.start-project')
: t('buttons.resume-project')}{' '}
{blockTitle && <span className='sr-only'>{blockTitle}</span>}
</Link>
</div>
)}
<nav
aria-label={
blockTitle ? t('aria.steps-for', { blockTitle }) : t('aria.steps')
}
>
<ul className={`map-challenges-ul map-challenges-grid `}>
{challengesWithCompleted.map((challenge, i) => (
<li
className={`map-challenge-title map-challenge-title-grid ${
isProjectBlock ? 'map-project-wrap' : 'map-challenge-wrap'
}`}
id={challenge.dashedName}
key={`map-challenge ${challenge.fields.slug}`}
>
{i + 1}
</Link>
) : (
<Link
onClick={() => handleChallengeClick(challenge.fields.slug)}
to={challenge.fields.slug}
>
{challenge.title}
<span className=' badge map-badge map-project-checkmark'>
{renderCheckMark(challenge.isCompleted)}
</span>
</Link>
)}
</li>
))}
</ul>
{!isProjectBlock ? (
<Link
onClick={() => handleChallengeClick(challenge.fields.slug)}
to={challenge.fields.slug}
className={`map-grid-item ${
+challenge.isCompleted ? 'challenge-completed' : ''
}`}
>
<span className='sr-only'>{t('aria.step')}</span>
<span>{i + 1}</span>
<span className='sr-only'>
{challenge.isCompleted
? t('icons.passed')
: t('icons.not-passed')}
</span>
</Link>
) : (
<Link
onClick={() => handleChallengeClick(challenge.fields.slug)}
to={challenge.fields.slug}
>
{challenge.title}
<span className=' badge map-badge map-project-checkmark'>
{renderCheckMark(challenge.isCompleted)}
</span>
</Link>
)}
</li>
))}
</ul>
</nav>
</>
) : (
<ul className={`map-challenges-ul`}>
{challengesWithCompleted.map(challenge => (

View File

@@ -44,11 +44,23 @@ a.cert-tag:active {
}
.block-grid-title {
font-size: 1.2rem;
margin: 0;
overflow-wrap: break-word;
}
.block-grid .challenge-jump-link {
margin: 0 25px 25px;
}
.block-grid .challenge-jump-link a:focus {
outline: 3px solid var(--blue-mid);
outline-offset: 0;
}
.block-grid .challenge-jump-link a:focus:not(:focus-visible) {
outline: none;
}
.block-title-translation-cta {
white-space: nowrap;
padding: 0.2em 0.5em;
@@ -234,11 +246,30 @@ button.map-title {
.map-challenge-title a {
width: 100%;
padding: 10px 15px;
align-items: center;
}
.map-challenges-grid .map-challenge-title a {
width: 3.4rem;
height: 2.75rem;
min-width: 24px;
min-height: 24px;
padding: 0;
}
.map-challenges-grid .map-challenge-title a:focus {
outline: 3px solid var(--secondary-color);
outline-offset: -3px;
}
.map-challenges-grid .map-challenge-title a:focus:not(:focus-visible) {
outline: none;
}
.map-challenges-grid {
display: flex;
flex-wrap: wrap;
margin-top: 1rem;
}
.map-challenge-title-grid {
flex: 0 1 60px;
@@ -261,6 +292,13 @@ button.map-title {
.challenge-completed {
background: var(--highlight-background);
position: relative;
}
.challenge-completed span {
font-weight: 700;
font-size: 1.3rem;
margin-top: -0.05rem;
}
@media screen and (max-width: 500px) {
@@ -346,7 +384,7 @@ button.map-title {
}
.block-grid {
border-bottom: 3px solid var(--secondary-background);
border-bottom: 4px solid var(--secondary-background);
}
.block-grid .block-header {
@@ -366,7 +404,7 @@ button.map-title {
}
.block-grid .block-header[aria-expanded='true'] {
border-bottom: 3px solid var(--secondary-background);
border-bottom: 2px solid var(--secondary-background);
}
.block-header-button-text {