Skip to content

Commit 9dc45e1

Browse files
authored
fix: accessibility issues on outline and unit pages (#1580)
This PR fixes the following accessibility issues: 1. Header used for screenreader only text 2. Element focus when expanding and dismissing welcome message 3. Bookmark button using wrong ARIA attributing while processing bookmark status
1 parent bd9c97c commit 9dc45e1

File tree

11 files changed

+81
-73
lines changed

11 files changed

+81
-73
lines changed

src/course-home/outline-tab/OutlineTab.jsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import React, { useEffect, useState } from 'react';
1+
import { useEffect, useRef, useState } from 'react';
22
import { useLocation, useNavigate } from 'react-router-dom';
33
import { useSelector } from 'react-redux';
44
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
55
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
6-
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
6+
import { useIntl } from '@edx/frontend-platform/i18n';
77
import { Button } from '@openedx/paragon';
88
import { PluginSlot } from '@openedx/frontend-plugin-framework';
99
import { AlertList } from '../../generic/user-messages';
@@ -29,7 +29,8 @@ import WelcomeMessage from './widgets/WelcomeMessage';
2929
import ProctoringInfoPanel from './widgets/ProctoringInfoPanel';
3030
import AccountActivationAlert from '../../alerts/logistration-alert/AccountActivationAlert';
3131

32-
const OutlineTab = ({ intl }) => {
32+
const OutlineTab = () => {
33+
const intl = useIntl();
3334
const {
3435
courseId,
3536
proctoringPanelStatus,
@@ -42,6 +43,8 @@ const OutlineTab = ({ intl }) => {
4243
userTimezone,
4344
} = useModel('courseHomeMeta', courseId);
4445

46+
const expandButtonRef = useRef();
47+
4548
const {
4649
accessExpiration,
4750
courseBlocks: {
@@ -159,12 +162,12 @@ const OutlineTab = ({ intl }) => {
159162
</>
160163
)}
161164
<StartOrResumeCourseCard />
162-
<WelcomeMessage courseId={courseId} />
165+
<WelcomeMessage courseId={courseId} nextElementRef={expandButtonRef} />
163166
{rootCourseId && (
164167
<>
165168
<div className="row w-100 m-0 mb-3 justify-content-end">
166169
<div className="col-12 col-md-auto p-0">
167-
<Button variant="outline-primary" block onClick={() => { setExpandAll(!expandAll); }}>
170+
<Button ref={expandButtonRef} variant="outline-primary" block onClick={() => { setExpandAll(!expandAll); }}>
168171
{expandAll ? intl.formatMessage(messages.collapseAll) : intl.formatMessage(messages.expandAll)}
169172
</Button>
170173
</div>
@@ -225,8 +228,4 @@ const OutlineTab = ({ intl }) => {
225228
);
226229
};
227230

228-
OutlineTab.propTypes = {
229-
intl: intlShape.isRequired,
230-
};
231-
232-
export default injectIntl(OutlineTab);
231+
export default OutlineTab;

src/course-home/outline-tab/OutlineTab.test.jsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,18 @@ describe('Outline Tab', () => {
292292
showMoreButton = screen.getByRole('button', { name: 'Show More' });
293293
expect(showMoreButton).toBeInTheDocument();
294294
});
295+
296+
fit('dismisses message', async () => {
297+
expect(screen.getByTestId('alert-container-welcome')).toBeInTheDocument();
298+
const dismissButton = screen.queryByRole('button', { name: 'Dismiss' });
299+
const expandButton = screen.queryByRole('button', { name: 'Expand all' });
300+
301+
fireEvent.click(dismissButton);
302+
303+
expect(expandButton).toHaveFocus();
304+
305+
expect(screen.queryByText('Welcome Message')).toBeNull();
306+
});
295307
});
296308

297309
it('ignores comments and misformatted HTML', async () => {

src/course-home/outline-tab/widgets/WelcomeMessage.jsx

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import React, { useState, useMemo } from 'react';
1+
import { useState, useMemo, useRef } from 'react';
22
import PropTypes from 'prop-types';
33

4-
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
4+
import { useIntl } from '@edx/frontend-platform/i18n';
55
import { Alert, Button, TransitionReplace } from '@openedx/paragon';
66
import truncate from 'truncate-html';
77

@@ -11,11 +11,13 @@ import messages from '../messages';
1111
import { useModel } from '../../../generic/model-store';
1212
import { dismissWelcomeMessage } from '../../data/thunks';
1313

14-
const WelcomeMessage = ({ courseId, intl }) => {
14+
const WelcomeMessage = ({ courseId, nextElementRef }) => {
15+
const intl = useIntl();
1516
const {
1617
welcomeMessageHtml,
1718
} = useModel('outline', courseId);
1819

20+
const messageBodyRef = useRef();
1921
const [display, setDisplay] = useState(true);
2022

2123
// welcomeMessageHtml can contain comments or malformatted HTML which can impact the length that determines
@@ -49,46 +51,55 @@ const WelcomeMessage = ({ courseId, intl }) => {
4951
dismissible
5052
show={display}
5153
onClose={() => {
54+
nextElementRef.current?.focus();
5255
setDisplay(false);
5356
dispatch(dismissWelcomeMessage(courseId));
5457
}}
5558
className="raised-card"
5659
actions={messageCanBeShortened ? [
5760
<Button
58-
onClick={() => setShowShortMessage(!showShortMessage)}
61+
onClick={() => {
62+
if (showShortMessage) {
63+
messageBodyRef.current?.focus();
64+
}
65+
66+
setShowShortMessage(!showShortMessage);
67+
}}
5968
variant="outline-primary"
6069
>
6170
{showShortMessage ? intl.formatMessage(messages.welcomeMessageShowMoreButton)
6271
: intl.formatMessage(messages.welcomeMessageShowLessButton)}
6372
</Button>,
6473
] : []}
6574
>
66-
<TransitionReplace className="mb-3" enterDuration={400} exitDuration={200}>
67-
{showShortMessage ? (
68-
<LmsHtmlFragment
69-
className="inline-link"
70-
data-testid="short-welcome-message-iframe"
71-
key="short-html"
72-
html={shortWelcomeMessageHtml}
73-
title={intl.formatMessage(messages.welcomeMessage)}
74-
/>
75-
) : (
76-
<LmsHtmlFragment
77-
className="inline-link"
78-
data-testid="long-welcome-message-iframe"
79-
key="full-html"
80-
html={cleanedWelcomeMessageHtml}
81-
title={intl.formatMessage(messages.welcomeMessage)}
82-
/>
83-
)}
84-
</TransitionReplace>
75+
<div ref={messageBodyRef} tabIndex="-1">
76+
<TransitionReplace className="mb-3" enterDuration={400} exitDuration={200}>
77+
{showShortMessage ? (
78+
<LmsHtmlFragment
79+
className="inline-link"
80+
data-testid="short-welcome-message-iframe"
81+
key="short-html"
82+
html={shortWelcomeMessageHtml}
83+
title={intl.formatMessage(messages.welcomeMessage)}
84+
/>
85+
) : (
86+
<LmsHtmlFragment
87+
className="inline-link"
88+
data-testid="long-welcome-message-iframe"
89+
key="full-html"
90+
html={cleanedWelcomeMessageHtml}
91+
title={intl.formatMessage(messages.welcomeMessage)}
92+
/>
93+
)}
94+
</TransitionReplace>
95+
</div>
8596
</Alert>
8697
);
8798
};
8899

89100
WelcomeMessage.propTypes = {
90101
courseId: PropTypes.string.isRequired,
91-
intl: intlShape.isRequired,
102+
nextElementRef: PropTypes.shape({ current: PropTypes.instanceOf(HTMLInputElement) }),
92103
};
93104

94-
export default injectIntl(WelcomeMessage);
105+
export default WelcomeMessage;

src/courseware/course/bookmark/BookmarkButton.jsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import React, { useCallback } from 'react';
1+
import { useCallback } from 'react';
22
import PropTypes from 'prop-types';
3-
import { StatefulButton } from '@openedx/paragon';
3+
import { Icon, StatefulButton } from '@openedx/paragon';
44
import { FormattedMessage } from '@edx/frontend-platform/i18n';
55
import { useDispatch } from 'react-redux';
6-
import BookmarkOutlineIcon from './BookmarkOutlineIcon';
7-
import BookmarkFilledIcon from './BookmarkFilledIcon';
6+
import { Bookmark, BookmarkBorder } from '@openedx/paragon/icons';
87
import { removeBookmark, addBookmark } from './data/thunks';
98

109
const addBookmarkLabel = (
@@ -42,21 +41,22 @@ const BookmarkButton = ({
4241
return (
4342
<StatefulButton
4443
variant="link"
45-
className="px-1 ml-n1 btn-sm text-primary-500"
44+
className={`px-1 ml-n1 btn-sm text-primary-500 ${isProcessing && 'disabled'}`}
4645
onClick={toggleBookmark}
4746
state={state}
48-
disabledStates={['defaultProcessing', 'bookmarkedProcessing']}
47+
aria-busy={isProcessing}
48+
disabled={isProcessing}
4949
labels={{
5050
default: addBookmarkLabel,
5151
defaultProcessing: addBookmarkLabel,
5252
bookmarked: hasBookmarkLabel,
5353
bookmarkedProcessing: hasBookmarkLabel,
5454
}}
5555
icons={{
56-
default: <BookmarkOutlineIcon className="text-primary" />,
57-
defaultProcessing: <BookmarkOutlineIcon className="text-primary" />,
58-
bookmarked: <BookmarkFilledIcon className="text-primary" />,
59-
bookmarkedProcessing: <BookmarkFilledIcon className="text-primary" />,
56+
default: <Icon src={BookmarkBorder} className="text-primary" />,
57+
defaultProcessing: <Icon src={BookmarkBorder} className="text-primary" />,
58+
bookmarked: <Icon src={Bookmark} className="text-primary" />,
59+
bookmarkedProcessing: <Icon src={Bookmark} className="text-primary" />,
6060
}}
6161
/>
6262
);

src/courseware/course/bookmark/BookmarkFilledIcon.jsx

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/courseware/course/bookmark/BookmarkOutlineIcon.jsx

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
11
export { default as BookmarkButton } from './BookmarkButton';
2-
export { default as BookmarkFilledIcon } from './BookmarkFilledIcon';
3-
export { default as BookmarkOutlineIcon } from './BookmarkFilledIcon';

src/courseware/course/sequence/Unit/__snapshots__/index.test.jsx.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ exports[`Unit component output snapshot: not bookmarked, do not show content 1`]
3434
unitTitle="unit-title"
3535
/>
3636
</div>
37-
<h2
37+
<p
3838
className="sr-only"
3939
>
4040
Level 2 headings may be created by course providers in the future.
41-
</h2>
41+
</p>
4242
<BookmarkButton
4343
isBookmarked={false}
4444
isProcessing={false}

src/courseware/course/sequence/Unit/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const Unit = ({
5252
<h3 className="h3">{unit.title}</h3>
5353
<UnitTitleSlot courseId={courseId} unitId={id} unitTitle={unit.title} />
5454
</div>
55-
<h2 className="sr-only">{formatMessage(messages.headerPlaceholder)}</h2>
55+
<p className="sr-only">{formatMessage(messages.headerPlaceholder)}</p>
5656
<BookmarkButton
5757
unitId={unit.id}
5858
isBookmarked={unit.bookmarked}

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import React, { useCallback } from 'react';
1+
import { useCallback } from 'react';
22
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 } from '@openedx/paragon';
6+
import { Button, Icon } from '@openedx/paragon';
7+
import { Bookmark } from '@openedx/paragon/icons';
78

89
import UnitIcon from './UnitIcon';
910
import CompleteIcon from './CompleteIcon';
10-
import BookmarkFilledIcon from '../../bookmark/BookmarkFilledIcon';
1111

1212
const UnitButton = ({
1313
onClick,
@@ -46,7 +46,9 @@ const UnitButton = ({
4646
{showTitle && <span className="unit-title">{title}</span>}
4747
{showCompletion && complete ? <CompleteIcon size="sm" className="text-success ml-2" /> : null}
4848
{bookmarked ? (
49-
<BookmarkFilledIcon
49+
<Icon
50+
data-testid="bookmark-icon"
51+
src={Bookmark}
5052
className="text-primary small position-absolute"
5153
style={{ top: '-3px', right: '5px' }}
5254
/>

0 commit comments

Comments
 (0)