Skip to content

Commit 36723d8

Browse files
DawoudSherazarbiralihinakhadim
authored andcommitted
feat: merge a11y changes from sumac into teak branch (#32)
* fix: Add visible titles to image-based links * feat: enhance accessibility by adding descriptive label to theme toggle (#29) * feat: Header - Focus order meaning - Open Edx Demo Course (#30) * feat: Header - Focus order meaning - Open Edx Demo Course * refactor: unify responsive rendering using the existing Responsive component Co-authored-by: Rahat Ali <rahat.ali@arbisoft.com> Co-authored-by: hinakhadim <hina.khadim@arbisoft.com>
1 parent ee264ca commit 36723d8

File tree

7 files changed

+109
-11
lines changed

7 files changed

+109
-11
lines changed

src/Header.messages.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ const messages = defineMessages({
2121
defaultMessage: 'Schools & Partners',
2222
description: 'Link to the schools and partners landing page',
2323
},
24+
'header.user.theme': {
25+
id: 'header.user.theme',
26+
defaultMessage: 'Toggle Theme',
27+
description: 'Toggle between light and dark theme',
28+
},
2429
'header.user.menu.dashboard': {
2530
id: 'header.user.menu.dashboard',
2631
defaultMessage: 'Dashboard',

src/Logo.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const Logo = ({
77
alt,
88
...attributes
99
}) => (
10-
<a href={href} className="logo" {...attributes}>
10+
<a href={href} className="logo" title={alt} {...attributes}>
1111
<img className="d-block" src={src} alt={alt} />
1212
</a>
1313
);

src/ThemeToggleButton.jsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import { getConfig } from '@edx/frontend-platform';
33
import Cookies from 'universal-cookie';
44
import { Icon } from '@openedx/paragon';
55
import { WbSunny, Nightlight } from '@openedx/paragon/icons';
6+
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
7+
import messages from './Header.messages';
68

79
const themeCookie = 'indigo-toggle-dark';
810
const themeCookieExpiry = 90; // days
911

10-
const ThemeToggleButton = () => {
12+
const ThemeToggleButton = ({ intl }) => {
13+
const [isDarkThemeEnabled, setIsDarkThemeEnabled] = useState(false);
1114
const cookies = new Cookies();
1215
const isThemeToggleEnabled = getConfig().INDIGO_ENABLE_DARK_TOGGLE;
1316

@@ -61,10 +64,12 @@ const ThemeToggleButton = () => {
6164
if (cookies.get(themeCookie) === 'dark') {
6265
document.body.classList.remove('indigo-dark-theme');
6366
removeDarkThemeFromiframes();
67+
setIsDarkThemeEnabled(false);
6468
theme = 'light';
6569
} else {
6670
document.body.classList.add('indigo-dark-theme');
6771
addDarkThemeToIframes();
72+
setIsDarkThemeEnabled(true);
6873
theme = 'dark';
6974
}
7075
cookies.set(themeCookie, theme, getCookieOptions(serverURL));
@@ -75,6 +80,12 @@ const ThemeToggleButton = () => {
7580
}
7681
};
7782

83+
const hanldeKeyUp = (event) => {
84+
if (event.key === 'Enter') {
85+
onToggleTheme();
86+
}
87+
};
88+
7889
if (!isThemeToggleEnabled) {
7990
return <div />;
8091
}
@@ -84,13 +95,19 @@ const ThemeToggleButton = () => {
8495
<div className="light-theme-icon"><Icon src={WbSunny} /></div>
8596
<div className="toggle-switch">
8697
<label htmlFor="theme-toggle-checkbox" className="switch">
87-
<input id="theme-toggle-checkbox" defaultChecked={cookies.get(themeCookie) === 'dark'} onChange={onToggleTheme} type="checkbox" />
98+
<input id="theme-toggle-checkbox" defaultChecked={cookies.get(themeCookie) === 'dark'} onChange={onToggleTheme} onKeyUp={hanldeKeyUp} type="checkbox" title={intl.formatMessage(messages['header.user.theme'])} />
8899
<span className="slider round" />
100+
<span id="theme-label" className="sr-only">{`Switch to ${isDarkThemeEnabled ? 'Light' : 'Dark'} Mode`}</span>
89101
</label>
90102
</div>
91103
<div className="dark-theme-icon"><Icon src={Nightlight} /></div>
92104
</div>
93105
);
94106
};
95107

96-
export default ThemeToggleButton;
108+
ThemeToggleButton.propTypes = {
109+
// i18n
110+
intl: intlShape.isRequired,
111+
};
112+
113+
export default injectIntl(ThemeToggleButton);

src/ThemeToggleButton.test.jsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/* eslint-disable react/prop-types */
2+
import React from 'react';
3+
import { IntlProvider } from '@edx/frontend-platform/i18n';
4+
import { render, fireEvent } from '@testing-library/react';
5+
import { getConfig } from '@edx/frontend-platform';
6+
import ThemeToggleButton from './ThemeToggleButton';
7+
8+
jest.mock('@edx/frontend-platform', () => ({
9+
getConfig: jest.fn(),
10+
}));
11+
12+
const mockCookiesGet = jest.fn();
13+
const mockCookiesSet = jest.fn();
14+
jest.mock('universal-cookie', () => jest.fn().mockImplementation(() => ({
15+
get: mockCookiesGet, // Simulate initial light mode
16+
set: mockCookiesSet,
17+
})));
18+
19+
describe('ThemeToggleButton', () => {
20+
beforeEach(() => {
21+
getConfig.mockReturnValue({
22+
LMS_BASE_URL: 'https://fake.url',
23+
INDIGO_ENABLE_DARK_TOGGLE: true,
24+
});
25+
26+
// Reset body class
27+
document.body.classList.remove('indigo-dark-theme');
28+
});
29+
30+
it('calls onToggleTheme when Enter key is pressed', () => {
31+
mockCookiesGet.mockReturnValue('light');
32+
33+
const { container } = render(
34+
<IntlProvider locale="en" messages={{}}>
35+
<ThemeToggleButton />
36+
</IntlProvider>,
37+
);
38+
const checkbox = container.querySelector('#theme-toggle-checkbox');
39+
checkbox.focus();
40+
fireEvent.keyUp(checkbox, { key: 'Enter' });
41+
42+
expect(mockCookiesSet).toHaveBeenCalledWith(
43+
'indigo-toggle-dark',
44+
'dark',
45+
expect.objectContaining({
46+
domain: 'fake.url',
47+
path: '/',
48+
expires: expect.any(Date),
49+
}),
50+
);
51+
});
52+
});

src/__snapshots__/Header.test.jsx.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ exports[`<Header /> renders correctly for anonymous desktop 1`] = `
1919
<a
2020
className="logo"
2121
href="http://localhost:18000/dashboard"
22+
title="edX"
2223
>
2324
<img
2425
alt="edX"
@@ -140,6 +141,7 @@ exports[`<Header /> renders correctly for anonymous mobile 1`] = `
140141
className="logo"
141142
href="http://localhost:18000/dashboard"
142143
itemType="http://schema.org/Organization"
144+
title="edX"
143145
>
144146
<img
145147
alt="edX"
@@ -208,6 +210,7 @@ exports[`<Header /> renders correctly for authenticated desktop 1`] = `
208210
<a
209211
className="logo"
210212
href="http://localhost:18000/dashboard"
213+
title="edX"
211214
>
212215
<img
213216
alt="edX"
@@ -379,6 +382,7 @@ exports[`<Header /> renders correctly for authenticated mobile 1`] = `
379382
className="logo"
380383
href="http://localhost:18000/dashboard"
381384
itemType="http://schema.org/Organization"
385+
title="edX"
382386
>
383387
<img
384388
alt="edX"

src/learning-header/LearningHeader.jsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useContext } from 'react';
2+
import Responsive from 'react-responsive';
23
import PropTypes from 'prop-types';
34
import { getConfig } from '@edx/frontend-platform';
45
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
@@ -30,6 +31,13 @@ const LearningHeader = ({
3031
<header className="learning-header customise indigo-header-version">
3132
<a className="sr-only sr-only-focusable" href="#main-content">{intl.formatMessage(messages.skipNavLink)}</a>
3233
<div className="container-xl py-2 d-flex align-items-center">
34+
{showUserDropdown && authenticatedUser && (
35+
<Responsive maxWidth={991}>
36+
<AuthenticatedUserDropdown
37+
username={authenticatedUser.username}
38+
/>
39+
</Responsive>
40+
)}
3341
{headerLogo}
3442
<div className="flex-grow-1 course-title-lockup d-flex" style={{ lineHeight: 1 }}>
3543
<CourseInfoSlot courseOrg={courseOrg} courseNumber={courseNumber} courseTitle={courseTitle} />
@@ -46,12 +54,14 @@ const LearningHeader = ({
4654
</div>
4755
<ThemeToggleButton />
4856
{showUserDropdown && authenticatedUser && (
49-
<>
50-
<LearningHelpSlot />
51-
<AuthenticatedUserDropdown
52-
username={authenticatedUser.username}
53-
/>
54-
</>
57+
<>
58+
<LearningHelpSlot />
59+
<Responsive minWidth={992}>
60+
<AuthenticatedUserDropdown
61+
username={authenticatedUser.username}
62+
/>
63+
</Responsive>
64+
</>
5565
)}
5666
{showUserDropdown && !authenticatedUser && (
5767
<AnonymousUserMenu />

src/learning-header/LearningHeader.test.jsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ import {
44
} from '../setupTest';
55
import { LearningHeader as Header } from '../index';
66

7+
jest.mock('react-responsive', () => ({
8+
__esModule: true,
9+
default: ({ minWidth, maxWidth, children }) => {
10+
const screenWidth = 1200; // Simulate desktop
11+
const matchesMin = minWidth !== undefined ? screenWidth >= minWidth : true;
12+
const matchesMax = maxWidth !== undefined ? screenWidth <= maxWidth : true;
13+
return matchesMin && matchesMax ? children : null;
14+
},
15+
}));
16+
717
describe('Header', () => {
818
beforeAll(async () => {
919
// We need to mock AuthService to implicitly use `getAuthenticatedUser` within `AppContext.Provider`.

0 commit comments

Comments
 (0)