fix: a11y issues (#47)

This commit is contained in:
Ben Warzeski
2022-10-19 15:01:50 -04:00
committed by GitHub
parent 9abcf35100
commit 4b38aaa199
24 changed files with 284 additions and 138 deletions

40
package-lock.json generated
View File

@@ -40,6 +40,7 @@
"query-string": "7.0.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-helmet": "^6.1.0",
"react-intl": "^5.20.9",
"react-pdf": "^5.5.0",
"react-redux": "^7.2.4",
@@ -24531,6 +24532,20 @@
}
}
},
"node_modules/react-helmet": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
"integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
"dependencies": {
"object-assign": "^4.1.1",
"prop-types": "^15.7.2",
"react-fast-compare": "^3.1.1",
"react-side-effect": "^2.1.0"
},
"peerDependencies": {
"react": ">=16.3.0"
}
},
"node_modules/react-intl": {
"version": "5.25.1",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-5.25.1.tgz",
@@ -24870,6 +24885,14 @@
"isarray": "0.0.1"
}
},
"node_modules/react-side-effect": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz",
"integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==",
"peerDependencies": {
"react": "^16.3.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
@@ -48625,6 +48648,17 @@
"use-sidecar": "^1.1.2"
}
},
"react-helmet": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
"integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
"requires": {
"object-assign": "^4.1.1",
"prop-types": "^15.7.2",
"react-fast-compare": "^3.1.1",
"react-side-effect": "^2.1.0"
}
},
"react-intl": {
"version": "5.25.1",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-5.25.1.tgz",
@@ -48888,6 +48922,12 @@
}
}
},
"react-side-effect": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz",
"integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==",
"requires": {}
},
"react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",

View File

@@ -58,6 +58,7 @@
"query-string": "7.0.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-helmet": "^6.1.0",
"react-intl": "^5.20.9",
"react-pdf": "^5.5.0",
"react-redux": "^7.2.4",

View File

@@ -1,7 +1,9 @@
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { Helmet } from 'react-helmet';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import Footer from '@edx/frontend-component-footer';
@@ -9,6 +11,7 @@ import { thunkActions } from 'data/redux';
import fakeData from 'data/services/lms/fakeData/courses';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import Dashboard from 'containers/Dashboard';
import messages from './messages';
import './App.scss';
@@ -16,6 +19,7 @@ export const App = () => {
const dispatch = useDispatch();
// TODO: made development-only
const { authenticatedUser } = React.useContext(AppContext);
const { formatMessage } = useIntl();
React.useEffect(() => {
if (authenticatedUser?.administrator || process.env.NODE_ENV === 'development') {
window.loadEmptyData = () => {
@@ -34,6 +38,9 @@ export const App = () => {
});
return (
<Router>
<Helmet>
<title>{formatMessage(messages.pageTitle)}</title>
</Helmet>
<div>
<LearnerDashboardHeader />
<main>

View File

@@ -2,6 +2,14 @@
exports[`App router component snapshot: enabled 1`] = `
<BrowserRouter>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
>
<title>
Learner Home
</title>
</HelmetWrapper>
<div>
<LearnerDashboardHeader />
<main>

View File

@@ -30,9 +30,11 @@ export const CourseCardContent = ({ cardId, orientation }) => {
<Card.Body>
<Card.Header
title={(
<a href={homeUrl} data-testid="CourseCardTitle">
{courseName}
</a>
<h3>
<a href={homeUrl} data-testid="CourseCardTitle">
{courseName}
</a>
</h3>
)}
actions={<CourseCardMenu cardId={cardId} />}
/>

View File

@@ -7,7 +7,7 @@ exports[`CourseCardMenu snapshot 1`] = `
alt="Actions dropdown"
as="IconButton"
iconAs="Icon"
id="dropdown-toggle-with-iconbutton"
id="course-actions-dropdown-test-card-id"
src={[MockFunction icons.MoreVert]}
variant="primary"
/>

View File

@@ -19,7 +19,7 @@ export const CourseCardMenu = ({ cardId }) => {
<>
<Dropdown>
<Dropdown.Toggle
id="dropdown-toggle-with-iconbutton"
id={`course-actions-dropdown-${cardId}`}
as={IconButton}
src={MoreVert}
iconAs={Icon}

View File

@@ -20,12 +20,14 @@ exports[`CourseCardContent snapshot orientation horizontal 1`] = `
/>
}
title={
<a
data-testid="CourseCardTitle"
href="test-home-url"
>
test-course-name
</a>
<h3>
<a
data-testid="CourseCardTitle"
href="test-home-url"
>
test-course-name
</a>
</h3>
}
/>
<Card.Section
@@ -71,12 +73,14 @@ exports[`CourseCardContent snapshot orientation vertical 1`] = `
/>
}
title={
<a
data-testid="CourseCardTitle"
href="test-home-url"
>
test-course-name
</a>
<h3>
<a
data-testid="CourseCardTitle"
href="test-home-url"
>
test-course-name
</a>
</h3>
}
/>
<Card.Section

View File

@@ -1,17 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseList snapshots renders loading 1`] = `
<div
className="course-list-loading"
>
<Spinner
animation="border"
className="mie-3"
screenReaderText="loading"
/>
</div>
`;
exports[`CourseList snapshots with filters 1`] = `
<div
className="course-list-container"

View File

@@ -6,7 +6,6 @@ import { useCheckboxSetValues } from '@edx/paragon';
import { StrictDict } from 'utils';
import { actions, hooks as appHooks } from 'data/redux';
import { ListPageSize, SortKeys } from 'data/constants/app';
import { RequestKeys } from 'data/constants/requests';
import * as module from './hooks';
@@ -27,7 +26,6 @@ export const useCourseListData = () => {
});
const handleRemoveFilter = (filter) => () => setFilters.remove(filter);
const setPageNumber = (value) => dispatch(actions.app.setPageNumber(value));
const initIsPending = appHooks.useIsPendingRequest(RequestKeys.initialize);
return {
pageNumber,
@@ -42,7 +40,6 @@ export const useCourseListData = () => {
handleRemoveFilter,
},
showFilters: filters.length > 0,
initIsPending,
};
};

View File

@@ -80,12 +80,6 @@ describe('CourseList hooks', () => {
// don't show filter when list is empty.
expect(out.showFilters).toEqual(false);
});
test('initIsPending loads from useIsPendingRequest', () => {
expect(out.initIsPending).toEqual(false);
appHooks.useIsPendingRequest.mockReturnValueOnce(true);
out = hooks.useCourseListData();
expect(out.initIsPending).toEqual(true);
});
describe('filterOptions', () => {
test('sortBy and setSortBy are connected to the state value', () => {
expect(out.filterOptions.sortBy).toEqual(testSortBy);

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Pagination, Spinner } from '@edx/paragon';
import { Pagination } from '@edx/paragon';
import {
ActiveCourseFilters,
@@ -23,13 +23,8 @@ export const CourseList = () => {
numPages,
showFilters,
visibleList,
initIsPending,
} = useCourseListData();
return initIsPending ? (
<div className="course-list-loading">
<Spinner animation="border" className="mie-3" screenReaderText="loading" />
</div>
) : (
return (
<div className="course-list-container">
<div id="course-list-heading-container">
<h2 className="my-3">{formatMessage(messages.myCourses)}</h2>

View File

@@ -20,7 +20,6 @@ describe('CourseList', () => {
setPageNumber: jest.fn().mockName('setPageNumber'),
showFilters: false,
visibleList: [],
initIsPending: false,
};
const createWrapper = (courseListData) => {
useCourseListData.mockReturnValueOnce({
@@ -31,10 +30,6 @@ describe('CourseList', () => {
};
describe('snapshots', () => {
it('renders loading', () => {
const wrapper = createWrapper({ initIsPending: true });
expect(wrapper).toMatchSnapshot();
});
test('with no filters', () => {
const wrapper = createWrapper();
expect(wrapper).toMatchSnapshot();

View File

@@ -1,20 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Dashboard snapshots there are available dashboards 1`] = `
<div
className="d-flex flex-column p-2"
id="dashboard-container"
>
<EnterpriseDashboardModal />
<EmptyCourse />
</div>
`;
exports[`Dashboard snapshots there are courses, or they are still loading 1`] = `
exports[`Dashboard snapshots courses loaded 1`] = `
<div
className="d-flex flex-column p-2"
id="dashboard-container"
>
<h1
className="sr-only"
>
Learner Home
</h1>
<Container
fluid={true}
size="xl"
@@ -67,11 +62,53 @@ exports[`Dashboard snapshots there are courses, or they are still loading 1`] =
</div>
`;
exports[`Dashboard snapshots courses still loading 1`] = `
<div
className="d-flex flex-column p-2"
id="dashboard-container"
>
<h1
className="sr-only"
>
Learner Home
</h1>
<div
className="course-list-loading"
>
<Spinner
animation="border"
className="mie-3"
screenReaderText="Loading..."
/>
</div>
</div>
`;
exports[`Dashboard snapshots there are available dashboards 1`] = `
<div
className="d-flex flex-column p-2"
id="dashboard-container"
>
<h1
className="sr-only"
>
Learner Home
</h1>
<EnterpriseDashboardModal />
<EmptyCourse />
</div>
`;
exports[`Dashboard snapshots there are no courses 1`] = `
<div
className="d-flex flex-column p-2"
id="dashboard-container"
>
<h1
className="sr-only"
>
Learner Home
</h1>
<EmptyCourse />
</div>
`;
@@ -81,6 +118,11 @@ exports[`Dashboard snapshots there is a select session modal 1`] = `
className="d-flex flex-column p-2"
id="dashboard-container"
>
<h1
className="sr-only"
>
Learner Home
</h1>
<EmptyCourse />
</div>
`;

View File

@@ -1,6 +1,12 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { Container, Col, Row } from '@edx/paragon';
import {
Container,
Col,
Row,
Spinner,
} from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
thunkActions,
@@ -14,6 +20,8 @@ import EmptyCourse from 'containers/EmptyCourse';
import SelectSessionModal from 'containers/SelectSessionModal';
import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
import appMessages from 'messages';
import './index.scss';
export const Dashboard = () => {
@@ -22,6 +30,7 @@ export const Dashboard = () => {
() => { dispatch(thunkActions.app.initialize()); },
[dispatch],
);
const { formatMessage } = useIntl();
const hasCourses = appHooks.useHasCourses();
const hasAvailableDashboards = appHooks.useHasAvailableDashboards();
@@ -30,8 +39,18 @@ export const Dashboard = () => {
return (
<div id="dashboard-container" className="d-flex flex-column p-2">
<h1 className="sr-only">{formatMessage(appMessages.pageTitle)}</h1>
{hasAvailableDashboards && <EnterpriseDashboardModal />}
{initIsPending || (!initIsPending && hasCourses) ? (
{initIsPending && (
<div className="course-list-loading">
<Spinner
animation="border"
className="mie-3"
screenReaderText={formatMessage(appMessages.loadingSR)}
/>
</div>
)}
{(!initIsPending && hasCourses) && (
<Container fluid size="xl">
<Row>
<Col
@@ -50,7 +69,8 @@ export const Dashboard = () => {
</Col>
</Row>
</Container>
) : (<EmptyCourse />)}
)}
{(!initIsPending && !hasCourses) && (<EmptyCourse />)}
</div>
);
};

View File

@@ -45,22 +45,23 @@ describe('Dashboard', () => {
};
describe('snapshots', () => {
test('there are courses, or they are still loading', () => {
const pendingNoCoursesWrapper = createWrapper({
test('courses still loading', () => {
const wrapper = createWrapper({
hasCourses: false,
hasAvailableDashboards: false,
showSelectSessionModal: false,
initIsPending: true,
});
expect(pendingNoCoursesWrapper).toMatchSnapshot();
const doneLoadingWithCoursesWrapper = createWrapper({
expect(wrapper).toMatchSnapshot();
});
test('courses loaded', () => {
const wrapper = createWrapper({
hasCourses: true,
hasAvailableDashboards: false,
showSelectSessionModal: false,
initIsPending: false,
});
expect(doneLoadingWithCoursesWrapper).toEqual(pendingNoCoursesWrapper);
expect(wrapper).toMatchSnapshot();
});
test('there are no courses', () => {

View File

@@ -31,20 +31,22 @@ export const GreetingBanner = ({ size }) => {
{ 'p-5': !isSmall, 'p-3.5': isSmall },
)}
>
<Image
style={{ width: isSmall ? '46px' : '148px' }}
className="d-block"
src={getConfig().LOGO_WHITE_URL}
alt={getConfig().SITE_NAME}
/>
<a href={`${getConfig().LMS_BASE_URL}/dashboard`}>
<Image
style={{ width: isSmall ? '46px' : '148px' }}
className="d-block"
src={getConfig().LOGO_WHITE_URL}
alt={getConfig().SITE_NAME}
/>
</a>
<div className={`greetings-slash-container-${size} bg-brand-500`} />
{isSmall
? (
<h5 className="text-center text-accent-b">
<h5 role="presentation" className="text-center text-accent-b">
{formatMessage(greetMessage)}
</h5>
) : (
<h1 className="text-center text-accent-b">
<h1 role="presentation" className="text-center text-accent-b">
{formatMessage(greetMessage)}
</h1>
)}

View File

@@ -4,21 +4,26 @@ exports[`GreetingBanner snapshots with size large and afternoon 1`] = `
<div
className="d-flex align-items-center justify-content-center p-5"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "148px",
<a
href="http://localhost:18000/dashboard"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "148px",
}
}
}
/>
/>
</a>
<div
className="greetings-slash-container-large bg-brand-500"
/>
<h1
className="text-center text-accent-b"
role="presentation"
>
Good Afternoon!
</h1>
@@ -29,21 +34,26 @@ exports[`GreetingBanner snapshots with size large and evening 1`] = `
<div
className="d-flex align-items-center justify-content-center p-5"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "148px",
<a
href="http://localhost:18000/dashboard"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "148px",
}
}
}
/>
/>
</a>
<div
className="greetings-slash-container-large bg-brand-500"
/>
<h1
className="text-center text-accent-b"
role="presentation"
>
Good Evening!
</h1>
@@ -54,21 +64,26 @@ exports[`GreetingBanner snapshots with size large and morning 1`] = `
<div
className="d-flex align-items-center justify-content-center p-5"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "148px",
<a
href="http://localhost:18000/dashboard"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "148px",
}
}
}
/>
/>
</a>
<div
className="greetings-slash-container-large bg-brand-500"
/>
<h1
className="text-center text-accent-b"
role="presentation"
>
Good Morning!
</h1>
@@ -79,21 +94,26 @@ exports[`GreetingBanner snapshots with size small and afternoon 1`] = `
<div
className="d-flex align-items-center justify-content-center p-3.5"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "46px",
<a
href="http://localhost:18000/dashboard"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "46px",
}
}
}
/>
/>
</a>
<div
className="greetings-slash-container-small bg-brand-500"
/>
<h5
className="text-center text-accent-b"
role="presentation"
>
Good Afternoon!
</h5>
@@ -104,21 +124,26 @@ exports[`GreetingBanner snapshots with size small and evening 1`] = `
<div
className="d-flex align-items-center justify-content-center p-3.5"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "46px",
<a
href="http://localhost:18000/dashboard"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "46px",
}
}
}
/>
/>
</a>
<div
className="greetings-slash-container-small bg-brand-500"
/>
<h5
className="text-center text-accent-b"
role="presentation"
>
Good Evening!
</h5>
@@ -129,21 +154,26 @@ exports[`GreetingBanner snapshots with size small and morning 1`] = `
<div
className="d-flex align-items-center justify-content-center p-3.5"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "46px",
<a
href="http://localhost:18000/dashboard"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "46px",
}
}
}
/>
/>
</a>
<div
className="greetings-slash-container-small bg-brand-500"
/>
<h5
className="text-center text-accent-b"
role="presentation"
>
Good Morning!
</h5>

View File

@@ -12,6 +12,7 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = `
title="Related Programs"
>
<ModalDialog.Header
aria-level={2}
as="h3"
className="programs-title m-0 p-0"
>
@@ -95,6 +96,7 @@ exports[`RelatedProgramsModal snapshot: open 1`] = `
title="Related Programs"
>
<ModalDialog.Header
aria-level={2}
as="h3"
className="programs-title m-0 p-0"
>

View File

@@ -27,13 +27,14 @@ export const ProgramCard = ({ data }) => {
style={{ width: '18rem', color: 'white' }}
as="a"
href={data.programUrl}
isClickable
>
<Card.ImageCap
className="program-card-banner"
src={data.bannerImgSrc}
srcAlt={formatMessage(messages.bannerAlt)}
logoSrc={data.logoImgSrc}
logoAlt={formatMessage(messages.logoAlt)}
logoAlt={formatMessage(messages.logoAlt, { provider: data.provider })}
/>
<Card.Header
title={whiteFontWrapper(data.title)}

View File

@@ -5,6 +5,7 @@ exports[`RelatedProgramsModal ProgramCard snapshot 1`] = `
as="a"
className="program-card mx-auto bg-primary-500 text-white mb-3.5 pb-3.5"
href="props.data.programUrl"
isClickable={true}
style={
Object {
"color": "white",
@@ -14,10 +15,10 @@ exports[`RelatedProgramsModal ProgramCard snapshot 1`] = `
>
<Card.ImageCap
className="program-card-banner"
logoAlt="Provider logo"
logoAlt="props.data.provider logo"
logoSrc="props.data.logoImgSrc"
src="props.data.bannerImgSrc"
srcAlt="Program banner"
srcAlt=""
/>
<Card.Header
subtitle={

View File

@@ -11,12 +11,12 @@ export const messages = {
},
logoAlt: {
id: 'learnerDashboard.programCard.logoAlt',
defaultMessage: 'Provider logo',
defaultMessage: '{provider} logo',
description: 'Program provider logo alt-text',
},
bannerAlt: {
id: 'learnerDashboard.programCard.bannerAlt',
defaultMessage: 'Program banner',
defaultMessage: '',
description: 'Program banner logo alt-text',
},
};

View File

@@ -31,7 +31,7 @@ export const RelatedProgramsModal = ({
className="related-programs-modal p-4"
data-testid="RelatedProgramsModal"
>
<ModalDialog.Header className="programs-title m-0 p-0" as="h3">
<ModalDialog.Header className="programs-title m-0 p-0" as="h3" aria-level={2}>
{formatMessage(messages.header)}
</ModalDialog.Header>
<ModalDialog.Header as="h4" className="programs-header p-0">

16
src/messages.js Normal file
View File

@@ -0,0 +1,16 @@
import { StrictDict } from 'utils';
export const messages = StrictDict({
loadingSR: {
id: 'learner-dash.loadingSR',
description: 'Page loading screen-reader text',
defaultMessage: 'Loading...',
},
pageTitle: {
id: 'learner-dash.title',
description: 'Page title: Learner Home',
defaultMessage: 'Learner Home',
},
});
export default messages;