Skip to content

Commit 8f8069d

Browse files
feat: improved accessibility skipping to main content
1 parent f43ac7b commit 8f8069d

File tree

16 files changed

+333
-20
lines changed

16 files changed

+333
-20
lines changed

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,5 @@ export const LOADED = 'loaded';
7272
export const FAILED = 'failed';
7373
export const DENIED = 'denied';
7474
export type StatusValue = typeof LOADING | typeof LOADED | typeof FAILED | typeof DENIED;
75+
76+
export const MAIN_CONTENT_ID = 'main-content-heading';

src/course-home/dates-tab/DatesTab.jsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useSelector } from 'react-redux';
33
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
44
import { useIntl } from '@edx/frontend-platform/i18n';
55

6+
import { MAIN_CONTENT_ID } from '@src/constants';
67
import messages from './messages';
78
import Timeline from './timeline/Timeline';
89

@@ -13,13 +14,16 @@ import SuggestedScheduleHeader from '../suggested-schedule-messaging/SuggestedSc
1314
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
1415
import UpgradeToCompleteAlert from '../suggested-schedule-messaging/UpgradeToCompleteAlert';
1516
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
17+
import { useScrollToContent } from '../../generic/hooks';
1618

1719
const DatesTab = () => {
1820
const intl = useIntl();
1921
const {
2022
courseId,
2123
} = useSelector(state => state.courseHome);
2224

25+
useScrollToContent(MAIN_CONTENT_ID);
26+
2327
const {
2428
isSelfPaced,
2529
org,
@@ -44,7 +48,13 @@ const DatesTab = () => {
4448

4549
return (
4650
<>
47-
<div role="heading" aria-level="1" className="h2 my-3">
51+
<div
52+
id={MAIN_CONTENT_ID}
53+
tabIndex="-1"
54+
role="heading"
55+
aria-level="1"
56+
className="h2 my-3"
57+
>
4858
{intl.formatMessage(messages.title)}
4959
</div>
5060
{isSelfPaced && hasDeadlines && (

src/course-home/progress-tab/ProgressHeader.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { useIntl } from '@edx/frontend-platform/i18n';
33
import { Button } from '@openedx/paragon';
44
import { useSelector } from 'react-redux';
55

6+
import { useScrollToContent } from '@src/generic/hooks';
7+
import { MAIN_CONTENT_ID } from '@src/constants';
68
import { useModel } from '../../generic/model-store';
79

810
import messages from './messages';
@@ -18,6 +20,8 @@ const ProgressHeader = () => {
1820

1921
const { studioUrl, username } = useModel('progress', courseId);
2022

23+
useScrollToContent(MAIN_CONTENT_ID);
24+
2125
const viewingOtherStudentsProgressPage = (targetUserId && targetUserId !== userId);
2226

2327
const pageTitle = viewingOtherStudentsProgressPage
@@ -26,7 +30,7 @@ const ProgressHeader = () => {
2630

2731
return (
2832
<div className="row w-100 m-0 mt-3 mb-4 justify-content-between">
29-
<h1>{pageTitle}</h1>
33+
<h1 id={MAIN_CONTENT_ID} tabIndex="-1">{pageTitle}</h1>
3034
{administrator && studioUrl && (
3135
<Button variant="outline-primary" size="sm" className="align-self-center" href={studioUrl}>
3236
{intl.formatMessage(messages.studioLink)}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Icon } from '@openedx/paragon';
2+
import { Bookmark } from '@openedx/paragon/icons';
3+
4+
const BookmarkFilledIcon = (props) => <Icon src={Bookmark} screenReaderText="Bookmark" {...props} />;
5+
6+
export default BookmarkFilledIcon;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { default as BookmarkButton } from './BookmarkButton';
2+
export { default as BookmarkFilledIcon } from './BookmarkFilledIcon';

src/courseware/course/sequence/sequence-navigation/SequenceNavigation.test.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ describe('Sequence Navigation', () => {
7676
const onNavigate = jest.fn();
7777
render(<SequenceNavigation {...mockData} {...{ onNavigate }} />, { wrapWithRouter: true });
7878

79-
const unitButtons = screen.getAllByRole('link', { name: /\d+/ });
79+
const unitButtons = screen.getAllByRole('tabpanel', { name: /\d+/ });
8080
expect(unitButtons).toHaveLength(unitButtons.length);
8181
unitButtons.forEach(button => fireEvent.click(button));
8282
expect(onNavigate).toHaveBeenCalledTimes(unitButtons.length);

src/courseware/course/sequence/sequence-navigation/SequenceNavigationDropdown.test.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('Sequence Navigation Dropdown', () => {
5050
});
5151
const dropdownMenu = container.querySelector('.dropdown-menu');
5252
// Only the current unit should be marked as active.
53-
getAllByRole(dropdownMenu, 'link', { hidden: true }).forEach(button => {
53+
getAllByRole(dropdownMenu, 'tabpanel', { hidden: true }).forEach(button => {
5454
if (button.textContent === unit.display_name) {
5555
expect(button).toHaveClass('active');
5656
} else {
@@ -72,7 +72,7 @@ describe('Sequence Navigation Dropdown', () => {
7272
fireEvent.click(dropdownToggle);
7373
});
7474
const dropdownMenu = container.querySelector('.dropdown-menu');
75-
getAllByRole(dropdownMenu, 'link', { hidden: true }).forEach(button => fireEvent.click(button));
75+
getAllByRole(dropdownMenu, 'tabpanel', { hidden: true }).forEach(button => fireEvent.click(button));
7676
expect(onNavigate).toHaveBeenCalledTimes(unitBlocks.length);
7777
unitBlocks.forEach((unit, index) => {
7878
expect(onNavigate).toHaveBeenNthCalledWith(index + 1, unit.id);

src/courseware/course/sequence/sequence-navigation/SequenceNavigationTabs.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,14 @@ const SequenceNavigationTabs = ({
4040
style={shouldDisplayDropdown ? invisibleStyle : null}
4141
ref={containerRef}
4242
>
43-
{unitIds.map(buttonUnitId => (
43+
{unitIds.map((buttonUnitId, idx) => (
4444
<UnitButton
4545
key={buttonUnitId}
4646
unitId={buttonUnitId}
4747
isActive={unitId === buttonUnitId}
4848
showCompletion={showCompletion}
4949
onClick={onNavigate}
50+
unitIdx={idx}
5051
/>
5152
))}
5253
</div>

src/courseware/course/sequence/sequence-navigation/SequenceNavigationTabs.test.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('Sequence Navigation Tabs', () => {
4444
useIndexOfLastVisibleChild.mockReturnValue([0, null, null]);
4545
render(<SequenceNavigationTabs {...mockData} />, { wrapWithRouter: true });
4646

47-
expect(screen.getAllByRole('link')).toHaveLength(unitBlocks.length);
47+
expect(screen.getAllByRole('tabpanel')).toHaveLength(unitBlocks.length);
4848
});
4949

5050
it('renders unit buttons and dropdown button', async () => {
@@ -54,15 +54,15 @@ describe('Sequence Navigation Tabs', () => {
5454
const booyah = render(<SequenceNavigationTabs {...mockData} />, { wrapWithRouter: true });
5555

5656
// wait for links to appear so we aren't testing an empty div
57-
await screen.findAllByRole('link');
57+
await screen.findAllByRole('tabpanel');
5858

5959
container = booyah.container;
6060

6161
const dropdownToggle = container.querySelector('.dropdown-toggle');
6262
await userEvent.click(dropdownToggle);
6363

6464
const dropdownMenu = container.querySelector('.dropdown');
65-
const dropdownButtons = getAllByRole(dropdownMenu, 'link');
65+
const dropdownButtons = getAllByRole(dropdownMenu, 'tabpanel');
6666
expect(dropdownButtons).toHaveLength(unitBlocks.length);
6767
expect(screen.getByRole('button', { name: `${activeBlockNumber} of ${unitBlocks.length}` }))
6868
.toHaveClass('dropdown-toggle');

src/courseware/course/sequence/sequence-navigation/UnitButton.jsx

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { Link, useLocation } from 'react-router-dom';
33
import PropTypes from 'prop-types';
44
import { connect, useSelector } from 'react-redux';
55
import classNames from 'classnames';
6-
import { Button, Icon } from '@openedx/paragon';
7-
import { Bookmark } from '@openedx/paragon/icons';
6+
import { Button } from '@openedx/paragon';
87

8+
import { BookmarkFilledIcon } from '@src/courseware/course/bookmark';
9+
import { useScrollToContent } from '@src/generic/hooks';
910
import UnitIcon from './UnitIcon';
1011
import CompleteIcon from './CompleteIcon';
1112

@@ -20,16 +21,36 @@ const UnitButton = ({
2021
unitId,
2122
className,
2223
showTitle,
24+
unitIdx,
2325
}) => {
2426
const { courseId, sequenceId } = useSelector(state => state.courseware);
2527
const { pathname } = useLocation();
2628
const basePath = `/course/${courseId}/${sequenceId}/${unitId}`;
2729
const unitPath = pathname.startsWith('/preview') ? `/preview${basePath}` : basePath;
2830

31+
useScrollToContent(isActive ? `${title}-${unitIdx}` : null);
32+
2933
const handleClick = useCallback(() => {
3034
onClick(unitId);
3135
}, [onClick, unitId]);
3236

37+
const handleKeyDown = (event) => {
38+
if (event.key === 'Enter' || event.key === ' ') {
39+
onClick(unitId);
40+
41+
const performFocus = () => {
42+
const targetElement = document.getElementById('bookmark-button');
43+
if (targetElement) {
44+
targetElement.focus();
45+
}
46+
};
47+
48+
requestAnimationFrame(() => {
49+
requestAnimationFrame(performFocus);
50+
});
51+
}
52+
};
53+
3354
return (
3455
<Button
3556
className={classNames({
@@ -41,16 +62,20 @@ const UnitButton = ({
4162
title={title}
4263
as={Link}
4364
to={unitPath}
65+
role="tabpanel"
66+
tabIndex={isActive ? 0 : -1}
67+
aria-controls={title}
68+
id={`${title}-${unitIdx}`}
69+
aria-labelledby={title}
70+
onKeyDown={handleKeyDown}
4471
>
4572
<UnitIcon type={contentType} />
4673
{showTitle && <span className="unit-title">{title}</span>}
4774
{showCompletion && complete ? <CompleteIcon size="sm" className="text-success ml-2" /> : null}
4875
{bookmarked ? (
49-
<Icon
76+
<BookmarkFilledIcon
77+
className="unit-filled-bookmark text-primary small position-absolute"
5078
data-testid="bookmark-icon"
51-
src={Bookmark}
52-
className="text-primary small position-absolute"
53-
style={{ top: '-3px', right: '5px' }}
5479
/>
5580
) : null}
5681
</Button>
@@ -68,6 +93,7 @@ UnitButton.propTypes = {
6893
showTitle: PropTypes.bool,
6994
title: PropTypes.string.isRequired,
7095
unitId: PropTypes.string.isRequired,
96+
unitIdx: PropTypes.number.isRequired,
7197
};
7298

7399
UnitButton.defaultProps = {

0 commit comments

Comments
 (0)