Skip to content

Commit a6306bd

Browse files
feat: improved accessibility for Show/Hide Accordions
1 parent 14c662d commit a6306bd

File tree

4 files changed

+166
-2
lines changed

4 files changed

+166
-2
lines changed

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,24 @@ describe('Outline Tab', () => {
132132
expect(expandedSectionNode).toHaveAttribute('aria-expanded', 'true');
133133
});
134134

135+
it('displays correct heading for expanded section', async () => {
136+
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
137+
setTabData({ course_blocks: { blocks: courseBlocks.blocks } });
138+
await fetchAndRender();
139+
const headingContent = screen.getByText('Title of Section');
140+
const { parentElement } = headingContent;
141+
expect(parentElement.tagName).toBe('H2');
142+
});
143+
144+
it('checks that the expanded section is within the correct list', async () => {
145+
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
146+
setTabData({ course_blocks: { blocks: courseBlocks.blocks } });
147+
await fetchAndRender();
148+
const listElement = screen.getByRole('presentation', { id: 'courseHome-outline' });
149+
expect(listElement).toBeInTheDocument();
150+
expect(listElement.tagName).toBe('OL');
151+
});
152+
135153
it('includes outline_tab_notifications_slot', async () => {
136154
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
137155
setTabData({
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import React, { useEffect, useState } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
4+
import { Collapsible, IconButton, Icon } from '@openedx/paragon';
5+
import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
6+
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
7+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
8+
9+
import { DisabledVisible } from '@openedx/paragon/icons';
10+
import SequenceLink from './SequenceLink';
11+
import { useModel } from '../../generic/model-store';
12+
13+
import genericMessages from '../../generic/messages';
14+
import messages from './messages';
15+
16+
const Section = ({
17+
courseId,
18+
defaultOpen,
19+
expand,
20+
intl,
21+
section,
22+
}) => {
23+
const {
24+
complete,
25+
sequenceIds,
26+
title,
27+
hideFromTOC,
28+
} = section;
29+
const {
30+
courseBlocks: {
31+
sequences,
32+
},
33+
} = useModel('outline', courseId);
34+
35+
const [open, setOpen] = useState(defaultOpen);
36+
37+
useEffect(() => {
38+
setOpen(expand);
39+
}, [expand]);
40+
41+
useEffect(() => {
42+
setOpen(defaultOpen);
43+
// eslint-disable-next-line react-hooks/exhaustive-deps
44+
}, []);
45+
46+
const sectionTitle = (
47+
<div className="d-flex row w-100 m-0">
48+
<div className="col-auto p-0">
49+
{complete ? (
50+
<FontAwesomeIcon
51+
icon={fasCheckCircle}
52+
fixedWidth
53+
className="float-left mt-1 text-success"
54+
aria-hidden="true"
55+
title={intl.formatMessage(messages.completedSection)}
56+
/>
57+
) : (
58+
<FontAwesomeIcon
59+
icon={farCheckCircle}
60+
fixedWidth
61+
className="float-left mt-1 text-gray-400"
62+
aria-hidden="true"
63+
title={intl.formatMessage(messages.incompleteSection)}
64+
/>
65+
)}
66+
</div>
67+
<div className="col-7 ml-3 p-0 font-weight-bold text-dark-500">
68+
<h2 className="course-outline-tab-section-title text-dark-500 mb-0">
69+
<span className="align-middle col-6">{title}</span>
70+
</h2>
71+
<span className="sr-only">
72+
, {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
73+
</span>
74+
</div>
75+
{hideFromTOC && (
76+
<div className="row">
77+
{hideFromTOC && (
78+
<span className="small d-flex align-content-end">
79+
<Icon className="mr-2" src={DisabledVisible} data-testid="hide-from-toc-section-icon" />
80+
<span data-testid="hide-from-toc-section-text">
81+
{intl.formatMessage(messages.hiddenSection)}
82+
</span>
83+
</span>
84+
)}
85+
</div>
86+
)}
87+
</div>
88+
);
89+
90+
return (
91+
<li>
92+
<Collapsible
93+
className="mb-2"
94+
styling="card-lg"
95+
title={sectionTitle}
96+
open={open}
97+
onToggle={() => { setOpen(!open); }}
98+
iconWhenClosed={(
99+
<IconButton
100+
alt={intl.formatMessage(messages.openSection)}
101+
icon={faPlus}
102+
onClick={() => { setOpen(true); }}
103+
size="sm"
104+
/>
105+
)}
106+
iconWhenOpen={(
107+
<IconButton
108+
alt={intl.formatMessage(genericMessages.close)}
109+
icon={faMinus}
110+
onClick={() => { setOpen(false); }}
111+
size="sm"
112+
/>
113+
)}
114+
>
115+
<ol className="list-unstyled">
116+
{sequenceIds.map((sequenceId, index) => (
117+
<SequenceLink
118+
key={sequenceId}
119+
id={sequenceId}
120+
courseId={courseId}
121+
sequence={sequences[sequenceId]}
122+
first={index === 0}
123+
/>
124+
))}
125+
</ol>
126+
</Collapsible>
127+
</li>
128+
);
129+
};
130+
131+
Section.propTypes = {
132+
courseId: PropTypes.string.isRequired,
133+
defaultOpen: PropTypes.bool.isRequired,
134+
expand: PropTypes.bool.isRequired,
135+
intl: intlShape.isRequired,
136+
section: PropTypes.shape().isRequired,
137+
};
138+
139+
export default injectIntl(Section);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.course-outline-tab-section-title {
2+
font-size: $font-size-base;
3+
line-height: $line-height-base;
4+
}

src/index.scss

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -446,12 +446,12 @@
446446
.course-outline-tab .pgn__card {
447447
.pgn__card-header {
448448
display: block;
449-
449+
450450
.pgn__card-header-content {
451451
margin-top: 0;
452452
}
453453
}
454-
454+
455455
.pgn__card-header-actions {
456456
margin-left: 0;
457457
}
@@ -466,6 +466,9 @@
466466
@import "courseware/course/content-tools/calculator/calculator.scss";
467467
@import "courseware/course/content-tools/contentTools.scss";
468468
@import "course-home/dates-tab/timeline/Day.scss";
469+
@import "course-home/outline-tab/Section.scss";
470+
@import "generic/upgrade-notification/UpgradeNotification.scss";
471+
@import "generic/upsell-bullets/UpsellBullets.scss";
469472
@import "course-home/outline-tab/widgets/ProctoringInfoPanel.scss";
470473
@import "course-home/outline-tab/widgets/FlagButton.scss";
471474
@import "course-home/progress-tab/course-completion/CompletionDonutChart.scss";

0 commit comments

Comments
 (0)