fix(a11y): make breadcrumb nav more accessible (#45021)

* fix: make breadcrumb nav more accessible

* refactor: cypress test and jest snapshot

* slight adjustment to left breadcrumb focus outline

* feat: translate aria for nav

* chore(tools): update snapshot

Co-authored-by: ahmad abdolsaheb <ahmad.abdolsaheb@gmail.com>
Co-authored-by: Naomi Carrigan <nhcarrigan@gmail.com>
This commit is contained in:
Bruce B
2022-02-15 10:44:18 -08:00
committed by GitHub
parent 1f8df61b1e
commit 7abb89326b
6 changed files with 117 additions and 78 deletions

View File

@ -466,7 +466,9 @@
"first-page": "Go to first page", "first-page": "Go to first page",
"previous-page": "Go to previous page", "previous-page": "Go to previous page",
"next-page": "Go to next page", "next-page": "Go to next page",
"last-page": "Go to last page" "last-page": "Go to last page",
"primary-nav": "primary",
"breadcrumb-nav": "breadcrumb"
}, },
"flash": { "flash": {
"honest-first": "To claim a certification, you must first accept our academic honesty policy", "honest-first": "To claim a certification, you must first accept our academic honesty policy",

View File

@ -5,6 +5,7 @@
// @ts-nocheck // @ts-nocheck
import Loadable from '@loadable/component'; import Loadable from '@loadable/component';
import React, { Ref } from 'react'; import React, { Ref } from 'react';
import { useTranslation } from 'react-i18next';
import { isLanding } from '../../../utils/path-parsers'; import { isLanding } from '../../../utils/path-parsers';
import { Link, SkeletonSprite } from '../../helpers'; import { Link, SkeletonSprite } from '../../helpers';
import MenuButton from './menu-button'; import MenuButton from './menu-button';
@ -34,6 +35,7 @@ export const UniversalNav = ({
fetchState fetchState
}: UniversalNavProps): JSX.Element => { }: UniversalNavProps): JSX.Element => {
const { pending } = fetchState; const { pending } = fetchState;
const { t } = useTranslation();
const search = const search =
typeof window !== `undefined` && isLanding(window.location.pathname) ? ( typeof window !== `undefined` && isLanding(window.location.pathname) ? (
@ -44,6 +46,7 @@ export const UniversalNav = ({
return ( return (
<nav <nav
aria-label={t('aria.primary-nav')}
className={'universal-nav' + (displayMenu ? ' expand-nav' : '')} className={'universal-nav' + (displayMenu ? ' expand-nav' : '')}
id='universal-nav' id='universal-nav'
> >

View File

@ -4,11 +4,15 @@ exports[`<ChallengeTitle/> renders correctly 1`] = `
<div <div
className="challenge-title-wrap" className="challenge-title-wrap"
> >
<div <nav
aria-label="aria.breadcrumb-nav"
className="challenge-title-breadcrumbs" className="challenge-title-breadcrumbs"
> >
<a <ol>
<li
className="breadcrumb-left" className="breadcrumb-left"
>
<a
href="/learn/fake-superblock" href="/learn/fake-superblock"
state={ state={
Object { Object {
@ -20,11 +24,11 @@ exports[`<ChallengeTitle/> renders correctly 1`] = `
className="ellipsis" className="ellipsis"
/> />
</a> </a>
<div </li>
className="breadcrumb-center" <li
/>
<a
className="breadcrumb-right" className="breadcrumb-right"
>
<a
href="/learn/fake-superblock/#fake-block" href="/learn/fake-superblock/#fake-block"
state={ state={
Object { Object {
@ -32,7 +36,9 @@ exports[`<ChallengeTitle/> renders correctly 1`] = `
} }
} }
/> />
</div> </li>
</ol>
</nav>
<div <div
className="challenge-title" className="challenge-title"
> >

View File

@ -1,5 +1,6 @@
import i18next from 'i18next'; import i18next from 'i18next';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from '../../../components/helpers/index'; import { Link } from '../../../components/helpers/index';
import './challenge-title.css'; import './challenge-title.css';
@ -10,10 +11,15 @@ interface BreadCrumbProps {
} }
function BreadCrumb({ block, superBlock }: BreadCrumbProps): JSX.Element { function BreadCrumb({ block, superBlock }: BreadCrumbProps): JSX.Element {
const { t } = useTranslation();
return ( return (
<div className='challenge-title-breadcrumbs'> <nav
className='challenge-title-breadcrumbs'
aria-label={t('aria.breadcrumb-nav')}
>
<ol>
<li className='breadcrumb-left'>
<Link <Link
className='breadcrumb-left'
state={{ breadcrumbBlockClick: block }} state={{ breadcrumbBlockClick: block }}
to={`/learn/${superBlock}`} to={`/learn/${superBlock}`}
> >
@ -21,15 +27,17 @@ function BreadCrumb({ block, superBlock }: BreadCrumbProps): JSX.Element {
{i18next.t(`intro:${superBlock}.title`)} {i18next.t(`intro:${superBlock}.title`)}
</span> </span>
</Link> </Link>
<div className='breadcrumb-center' /> </li>
<li className='breadcrumb-right'>
<Link <Link
className='breadcrumb-right'
state={{ breadcrumbBlockClick: block }} state={{ breadcrumbBlockClick: block }}
to={`/learn/${superBlock}/#${block}`} to={`/learn/${superBlock}/#${block}`}
> >
{i18next.t(`intro:${superBlock}.blocks.${block}.title`)} {i18next.t(`intro:${superBlock}.blocks.${block}.title`)}
</Link> </Link>
</div> </li>
</ol>
</nav>
); );
} }

View File

@ -13,39 +13,69 @@
} }
.challenge-title-breadcrumbs { .challenge-title-breadcrumbs {
display: flex;
flex-direction: row;
justify-content: space-around;
font-size: 16px; font-size: 16px;
border: 1px solid var(--quaternary-background); border: 1px solid var(--quaternary-background);
height: 25px;
text-align: center; text-align: center;
} }
.challenge-title-breadcrumbs ol {
display: flex;
justify-content: space-around;
list-style-type: none;
margin-bottom: 0;
padding-left: 0;
width: 100%;
}
.challenge-title-breadcrumbs ol a {
width: 100%;
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
display: block;
padding: 0 3px;
}
.challenge-title-breadcrumbs ol a:focus {
outline: 2px solid var(--secondary-color);
background-color: inherit;
}
.challenge-title-breadcrumbs ol a:focus:not(:focus-visible) {
outline: none;
}
.challenge-title-breadcrumbs ol a:hover {
background-color: inherit;
text-decoration: underline;
}
.breadcrumb-left { .breadcrumb-left {
text-decoration: none; text-decoration: none;
min-width: 25px; min-width: 3rem;
display: inline-block; display: inline-block;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
max-height: 25px;
white-space: nowrap; white-space: nowrap;
flex-grow: 1; flex-grow: 1;
flex-shrink: 2; flex-shrink: 2;
background-color: var(--quaternary-background); background-color: var(--quaternary-background);
padding: 0px 3px; padding: 0 0.57rem 0 0;
position: relative;
} }
.breadcrumb-center { .breadcrumb-left:after {
width: 0; background-color: var(--secondary-background);
height: 0; content: '';
border-top: calc(25px / 2) solid transparent; border-top: calc(1.375rem / 2) solid transparent;
border-bottom: calc(23px / 2) solid transparent; border-bottom: calc(1.2rem / 2) solid transparent;
border-left: calc(23px / 2) solid var(--quaternary-background); border-left: calc(1.1rem / 2) solid var(--quaternary-background);
flex-grow: 0; position: absolute;
flex-shrink: 0; top: 0;
right: 0;
height: 100%;
} }
.breadcrumb-right { .breadcrumb-right {
@ -53,29 +83,19 @@
display: inline-block; display: inline-block;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
max-height: 25px;
min-width: 50px; min-width: 50px;
flex-grow: 2; flex-grow: 2;
flex-shrink: 1; flex-shrink: 1;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
padding: 0px 3px; padding: 0;
} }
.breadcrumb-rule { .breadcrumb-rule {
margin: 5px -10px; margin: 5px -10px;
} }
.breadcrumb-left:hover {
text-decoration: underline;
background-color: var(--quaternary-background);
}
.breadcrumb-right:hover {
text-decoration: underline;
background-color: var(--secondary-background);
}
.title-text { .title-text {
text-decoration: none; text-decoration: none;
min-width: 25px; min-width: 25px;

View File

@ -8,23 +8,23 @@ describe('The breadcumbs should work corectly', () => {
it('It should have a superblock and a course', () => { it('It should have a superblock and a course', () => {
cy.visit(challengeUrl); cy.visit(challengeUrl);
cy.get('.ellipsis').contains('Responsive Web Design').and('be.visible'); cy.get('.ellipsis').contains('Responsive Web Design').and('be.visible');
cy.get('.breadcrumb-left') cy.get('.breadcrumb-left > a')
.should('have.attr', 'href') .should('have.attr', 'href')
.and('include', superBlockUrl); .and('include', superBlockUrl);
cy.get('.breadcrumb-right') cy.get('.breadcrumb-right > a')
.contains('Basic HTML and HTML5') .contains('Basic HTML and HTML5')
.and('be.visible'); .and('be.visible');
cy.get('.breadcrumb-right') cy.get('.breadcrumb-right > a')
.should('have.attr', 'href') .should('have.attr', 'href')
.and('include', courseUrl); .and('include', courseUrl);
}); });
it('Should redirect to the right url', () => { it('Should redirect to the right url', () => {
cy.visit(challengeUrl); cy.visit(challengeUrl);
cy.get('.breadcrumb-left').click(); cy.get('.breadcrumb-left > a').click();
cy.url().should('include', '/responsive-web-design'); cy.url().should('include', '/responsive-web-design');
cy.visit(challengeUrl); cy.visit(challengeUrl);
cy.get('.breadcrumb-right').click(); cy.get('.breadcrumb-right > a').click();
cy.url().should('include', '/responsive-web-design/#basic-html-and-html5'); cy.url().should('include', '/responsive-web-design/#basic-html-and-html5');
}); });
}); });