Skip to content

Commit

Permalink
♿️(course glimpse) add alt text on meaningful icons
Browse files Browse the repository at this point in the history
organization, code and date icons let users understand that what is
written next to them are organization name, course code and course date.

So, they convey meaning but they are hidden from screen readers.

Add the alt text so that screen reader users get the meaning from the
icons.
  • Loading branch information
manuhabitela committed Mar 24, 2022
1 parent 917c9fb commit 32006c4
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 62 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -26,6 +26,7 @@ Versioning](https://semver.org/spec/v2.0.0.html).
- Add the website's name by default in every page title, that can be changed
or disabled by overriding the new `site_title` and `site_title_separator`
blocks
- Add alternative text on course glimpse icons for screen reader users

### Changed

Expand Down
12 changes: 10 additions & 2 deletions src/frontend/js/components/CourseGlimpse/CourseGlimpseFooter.tsx
@@ -1,9 +1,17 @@
import { useIntl } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';

import { Icon } from 'components/Icon';
import { CommonDataProps } from 'types/commonDataProps';
import { Course } from 'types/Course';

const messages = defineMessages({
dateIconAlt: {
defaultMessage: 'Course date',
description: 'Course date logo alternative text for screen reader users',
id: 'components.CourseGlimpseFooter.dateIconAlt',
},
});

/**
* <CourseGlimpseFooter />.
* This is spun off from <CourseGlimpse /> to allow easier override through webpack.
Expand All @@ -13,7 +21,7 @@ export const CourseGlimpseFooter: React.FC<{ course: Course } & CommonDataProps>
return (
<div className="course-glimpse-footer">
<div className="course-glimpse-footer__date">
<Icon name="icon-calendar" />
<Icon name="icon-calendar" title={intl.formatMessage(messages.dateIconAlt)} />
{course.state.text.charAt(0).toUpperCase() +
course.state.text.substr(1) +
(course.state.datetime
Expand Down
3 changes: 3 additions & 0 deletions src/frontend/js/components/CourseGlimpse/index.spec.tsx
Expand Up @@ -55,9 +55,12 @@ describe('components/CourseGlimpse', () => {
expect(screen.getByRole('link')).not.toHaveAttribute('title');
// The course glimpse shows the relevant information
screen.getByRole('heading', { name: 'Course 42', level: 3 });
screen.getByLabelText('Course code');
screen.getByText('123abc');
screen.getByLabelText('Organization');
screen.getByText('Some Organization');
// Matches on 'Starts on Mar 14, 2019', date is wrapped with intl <span>
screen.getByLabelText('Course date');
screen.getByText('Starts on Mar 14, 2019');

// Check course logo
Expand Down
125 changes: 69 additions & 56 deletions src/frontend/js/components/CourseGlimpse/index.tsx
@@ -1,5 +1,5 @@
import { memo } from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';

import { CommonDataProps } from 'types/commonDataProps';
import { Course } from 'types/Course';
Expand All @@ -16,67 +16,80 @@ const messages = defineMessages({
description: 'Placeholder text when the course we are glimpsing at is missing a cover image',
id: 'components.CourseGlimpse.cover',
},
organizationIconAlt: {
defaultMessage: 'Organization',
description: 'Organization logo alternative text for screen reader users',
id: 'components.CourseGlimpse.organizationIconAlt',
},
codeIconAlt: {
defaultMessage: 'Course code',
description: 'Course code logo alternative text for screen reader users',
id: 'components.CourseGlimpse.codeIconAlt',
},
});

const CourseGlimpseBase = ({ context, course }: CourseGlimpseProps & CommonDataProps) => (
<a className="course-glimpse course-glimpse--link" href={course.absolute_url}>
<div className="course-glimpse__media">
{/* alt forced to empty string because it's a decorative image */}
{course.cover_image ? (
<img
alt=""
sizes={course.cover_image.sizes}
src={course.cover_image.src}
srcSet={course.cover_image.srcset}
/>
) : (
<div className="course-glimpse__media__empty">
<FormattedMessage {...messages.cover} />
</div>
)}
</div>
<div className="course-glimpse__content">
{course.icon ? (
<div className="course-glimpse__icon">
<span className="category-badge">
{/* alt forced to empty string because it's a decorative image */}
<img
alt=""
className="category-badge__icon"
sizes={course.icon.sizes}
src={course.icon.src}
srcSet={course.icon.srcset}
/>
<span className="category-badge__title">{course.icon.title}</span>
</span>
</div>
) : null}
<div className="course-glimpse__wrapper">
<h3 className="course-glimpse__title">{course.title}</h3>
{course.organization_highlighted_cover_image ? (
<div className="course-glimpse__organization-logo">
{/* alt forced to empty string because it's a decorative image */}
<img
alt=""
sizes={course.organization_highlighted_cover_image.sizes}
src={course.organization_highlighted_cover_image.src}
srcSet={course.organization_highlighted_cover_image.srcset}
/>
const CourseGlimpseBase = ({ context, course }: CourseGlimpseProps & CommonDataProps) => {
const intl = useIntl();
return (
<a className="course-glimpse course-glimpse--link" href={course.absolute_url}>
<div className="course-glimpse__media">
{/* alt forced to empty string because it's a decorative image */}
{course.cover_image ? (
<img
alt=""
sizes={course.cover_image.sizes}
src={course.cover_image.src}
srcSet={course.cover_image.srcset}
/>
) : (
<div className="course-glimpse__media__empty">
<FormattedMessage {...messages.cover} />
</div>
)}
</div>
<div className="course-glimpse__content">
{course.icon ? (
<div className="course-glimpse__icon">
<span className="category-badge">
{/* alt forced to empty string because it's a decorative image */}
<img
alt=""
className="category-badge__icon"
sizes={course.icon.sizes}
src={course.icon.src}
srcSet={course.icon.srcset}
/>
<span className="category-badge__title">{course.icon.title}</span>
</span>
</div>
) : null}
<div className="course-glimpse__metadata course-glimpse__metadata--organization">
<Icon name="icon-org" />
<span className="title">{course.organization_highlighted}</span>
</div>
<div className="course-glimpse__metadata course-glimpse__metadata--code">
<Icon name="icon-barcode" />
<span>{course.code || '-'}</span>
<div className="course-glimpse__wrapper">
<h3 className="course-glimpse__title">{course.title}</h3>
{course.organization_highlighted_cover_image ? (
<div className="course-glimpse__organization-logo">
{/* alt forced to empty string because the organization name is rendered after */}
<img
alt=""
sizes={course.organization_highlighted_cover_image.sizes}
src={course.organization_highlighted_cover_image.src}
srcSet={course.organization_highlighted_cover_image.srcset}
/>
</div>
) : null}
<div className="course-glimpse__metadata course-glimpse__metadata--organization">
<Icon name="icon-org" title={intl.formatMessage(messages.organizationIconAlt)} />
<span className="title">{course.organization_highlighted}</span>
</div>
<div className="course-glimpse__metadata course-glimpse__metadata--code">
<Icon name="icon-barcode" title={intl.formatMessage(messages.codeIconAlt)} />
<span>{course.code || '-'}</span>
</div>
</div>
<CourseGlimpseFooter context={context} course={course} />
</div>
<CourseGlimpseFooter context={context} course={course} />
</div>
</a>
);
</a>
);
};

const areEqual: (
prevProps: Readonly<CourseGlimpseProps & CommonDataProps>,
Expand Down
Expand Up @@ -53,14 +53,16 @@
{% endif %}
{% if main_organization_title %}
<div class="course-{{ course_variant }}__metadata course-{{ course_variant }}__metadata--organization">
<svg role="img" aria-hidden="true" class="icon">
<svg role="img" aria-label="{% trans "Organization" %}" class="icon">
<title>{% trans "Organization" %}</title>
<use href="#icon-org" />
</svg>
<span class="title">{{ main_organization_title }}</span>
</div>
{% endif %}
<div class="course-{{ course_variant }}__metadata course-{{ course_variant }}__metadata--code">
<svg role="img" aria-hidden="true" class="icon">
<svg role="img" aria-label="{% trans "Course code" %}" class="icon">
<title>{% trans "Course code" %}</title>
<use href="#icon-barcode" />
</svg>
<span>{% if course.code %}{{ course.code }}{% else %}-{% endif %}</span>
Expand All @@ -70,7 +72,8 @@
{% block course_glimpse_footer %}
<div class="course-{{ course_variant }}-footer">
<div class="course-{{ course_variant }}-footer__date">
<svg role="img" aria-hidden="true" class="icon">
<svg role="img" aria-label="{% trans "Course date" %}" class="icon">
<title>{% trans "Course date" %}</title>
<use href="#icon-calendar" />
</svg>
{{ course_state.text|capfirst }}
Expand Down
3 changes: 2 additions & 1 deletion tests/apps/courses/test_cms_plugins_course.py
Expand Up @@ -431,7 +431,8 @@ def test_cms_plugins_course_glimpse_organization_acronym(self):
# pylint: disable=consider-using-f-string
'<div class="course-glimpse__metadata '
'course-glimpse__metadata--organization">'
'<svg role="img" aria-hidden="true" class="icon">'
'<svg role="img" aria-label="Organization" class="icon">'
"<title>Organization</title>"
'<use href="#icon-org"></use></svg>'
'<span class="title">{0:s}</span>'
).format(menu_title),
Expand Down

0 comments on commit 32006c4

Please sign in to comment.