Skip to content

Commit 7269bbf

Browse files
authored
(refactor) Improve footer component and a11y (#1240)
This PR is a follow-up to #1192 that makes the following changes: - Extracts footer styles to a dedicated stylesheet - Replaces the `Button` Carbon component with a `Link` component for the "Learn more" button - Adds error handing for the logo image loading - Improves a11y with aria labels - Adds a type annotation for the `Logo`` interface
1 parent 78eae7c commit 7269bbf

File tree

6 files changed

+105
-83
lines changed

6 files changed

+105
-83
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,54 @@
1-
import React from 'react';
2-
import { useConfig, ArrowRightIcon } from '@openmrs/esm-framework';
3-
import { Tile, Button } from '@carbon/react';
1+
import React, { useCallback } from 'react';
2+
import { Link, Tile } from '@carbon/react';
43
import { useTranslation } from 'react-i18next';
4+
import { useConfig, ArrowRightIcon } from '@openmrs/esm-framework';
55
import { type ConfigSchema } from './config-schema';
6-
import styles from './login/login.scss';
6+
import styles from './footer.scss';
7+
8+
interface Logo {
9+
src: string;
10+
alt?: string;
11+
}
712

813
const Footer: React.FC = () => {
9-
const {t} = useTranslation();
14+
const { t } = useTranslation();
1015
const config = useConfig<ConfigSchema>();
11-
const logos = config.footer.additionalLogos || [];
16+
const logos: Logo[] = config.footer.additionalLogos || [];
17+
18+
const handleImageLoadError = useCallback((error: React.SyntheticEvent<HTMLImageElement, Event>) => {
19+
console.error('Failed to load image', error);
20+
}, []);
1221

1322
return (
1423
<div className={styles.footer}>
1524
<Tile className={styles.poweredByTile}>
1625
<div className={styles.poweredByContainer}>
1726
<span className={styles.poweredByText}>{t('builtWith', 'Built with')}</span>
18-
<svg role="img" className={styles.poweredByLogo}>
19-
<use href="#omrs-logo-full-color"></use>
20-
</svg>
27+
<svg aria-label={t('openmrsLogo', 'OpenMRS Logo')} className={styles.poweredByLogo} role="img">
28+
<use href="#omrs-logo-full-color"></use>
29+
</svg>
2130
<span className={styles.poweredByText}>
2231
{t('poweredBySubtext', 'An open-source medical record system and global community')}
2332
</span>
24-
<Button
25-
className={styles.learnMore}
26-
iconDescription={t('learnMore', 'Learn More')}
27-
kind="ghost"
28-
onClick={() => window.open('https://openmrs.org', '_blank')}
29-
renderIcon={(props) => <ArrowRightIcon {...props} size={20} className={styles.arrowRightIcon}/>}
33+
<Link
34+
className={styles.learnMoreButton}
35+
href="https://openmrs.org"
36+
rel="noopener noreferrer"
37+
renderIcon={() => <ArrowRightIcon size={16} aria-label="Arrow right icon" />}
38+
target="_blank"
3039
>
31-
<span>{t('learnMore', 'Learn More')}</span>
32-
</Button>
40+
{t('learnMore', 'Learn more')}
41+
</Link>
3342
</div>
3443
</Tile>
3544

3645
<div className={styles.logosContainer}>
37-
{logos.map((logo, index) => (
46+
{logos.map((logo) => (
3847
<img
39-
key={index}
4048
alt={logo.alt ? t(logo.alt) : t('footerlogo', 'Footer Logo')}
4149
className={styles.poweredByLogo}
50+
key={logo.src}
51+
onError={handleImageLoadError}
4252
src={logo.src}
4353
/>
4454
))}
@@ -47,4 +57,4 @@ const Footer: React.FC = () => {
4757
);
4858
};
4959

50-
export default Footer;
60+
export default Footer;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
@use '@carbon/colors';
2+
@use '@carbon/layout';
3+
4+
.footer {
5+
display: flex;
6+
justify-content: space-between;
7+
align-items: center;
8+
padding: layout.$spacing-05;
9+
position: absolute;
10+
bottom: 0;
11+
flex-wrap: wrap;
12+
gap: layout.$spacing-05;
13+
width: 100%;
14+
}
15+
16+
.poweredByTile {
17+
display: flex;
18+
text-align: left;
19+
max-width: fit-content;
20+
min-height: fit-content;
21+
font-size: smaller;
22+
background-color: colors.$white;
23+
padding: layout.$spacing-03 layout.$spacing-05;
24+
border: 1px solid colors.$gray-20;
25+
border-radius: layout.$spacing-04;
26+
flex-wrap: wrap;
27+
}
28+
29+
.poweredByContainer {
30+
display: flex;
31+
height: layout.$spacing-06;
32+
align-items: center;
33+
gap: layout.$spacing-03;
34+
}
35+
36+
.poweredByLogo {
37+
height: layout.$spacing-07;
38+
width: auto;
39+
max-width: layout.$spacing-12;
40+
border-collapse: collapse;
41+
padding: 0;
42+
object-fit: contain;
43+
display: block;
44+
flex-shrink: 0;
45+
}
46+
47+
.poweredByLogo + .poweredByText {
48+
margin-left: layout.$spacing-02;
49+
}
50+
51+
.learnMoreButton {
52+
display: flex;
53+
align-items: center;
54+
55+
svg {
56+
fill: colors.$blue-60;
57+
}
58+
}

packages/apps/esm-login-app/src/login/login.component.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ const Login: React.FC = () => {
5151
useEffect(() => {
5252
if (showPasswordOnSeparateScreen) {
5353
if (showPasswordField) {
54-
passwordInputRef.current?.focus();
54+
passwordInputRef.current?.focus();
5555
} else {
56-
usernameInputRef.current?.focus();
56+
usernameInputRef.current?.focus();
5757
}
5858
}
5959
}, [showPasswordField, showPasswordOnSeparateScreen]);
@@ -135,7 +135,7 @@ const Login: React.FC = () => {
135135
[username, password, navigate, showPasswordOnSeparateScreen],
136136
);
137137

138-
if (!loginProvider || loginProvider.type === 'basic'){
138+
if (!loginProvider || loginProvider.type === 'basic') {
139139
return (
140140
<div className={styles.container}>
141141
<Tile className={styles.loginCard}>
@@ -182,7 +182,7 @@ const Login: React.FC = () => {
182182
type="submit"
183183
className={styles.continueButton}
184184
renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
185-
iconDescription="Log in"
185+
iconDescription={t('loginButtonIconDescription', 'Log in button')}
186186
disabled={!isLoginEnabled || isLoggingIn}
187187
>
188188
{isLoggingIn ? (
@@ -238,7 +238,7 @@ const Login: React.FC = () => {
238238
</div>
239239
);
240240
}
241-
return null;
241+
return null;
242242
};
243243

244244
export default Login;

packages/apps/esm-login-app/src/login/login.scss

+6-53
Original file line numberDiff line numberDiff line change
@@ -50,31 +50,6 @@
5050
margin-left: 0.5rem;
5151
}
5252

53-
.footer {
54-
display: flex;
55-
justify-content: space-between;
56-
align-items: center;
57-
padding: 1rem;
58-
position: absolute;
59-
bottom: 0;
60-
flex-wrap: wrap;
61-
gap: 1rem;
62-
width: 100%;
63-
}
64-
65-
.poweredByTile {
66-
display: flex;
67-
text-align: left;
68-
max-width: fit-content;
69-
min-height: fit-content;
70-
font-size: smaller;
71-
background-color: #ffffff;
72-
padding: 0.5rem 1rem;
73-
border: 1px solid #e0e0e0;
74-
border-radius: 1rem;
75-
flex-wrap: wrap;
76-
}
77-
7853
.logosContainer {
7954
display: flex;
8055
max-height: 2rem;
@@ -85,28 +60,6 @@
8560
opacity: 80%;
8661
}
8762

88-
.poweredByContainer{
89-
display: flex;
90-
height: 1.5rem;
91-
align-items: center;
92-
gap: 0.5rem;
93-
}
94-
95-
.poweredByLogo {
96-
height: 2rem;
97-
width: auto;
98-
max-width: 6rem;
99-
border-collapse: collapse;
100-
padding: 0;
101-
object-fit: contain;
102-
display: block;
103-
flex-shrink: 0;
104-
}
105-
106-
.poweredByLogo + .poweredByText {
107-
margin-left: 0.25rem;
108-
}
109-
11063
.loginCard {
11164
border-radius: 0;
11265
border: 1px solid $ui-03;
@@ -127,7 +80,7 @@
12780
@media only screen and (max-width: 1024px) {
12881
.footer {
12982
flex-direction: row;
130-
justify-content: center;
83+
justify-content: center;
13184
padding: 1rem;
13285
}
13386

@@ -143,7 +96,7 @@
14396
gap: 0.75rem;
14497
}
14598

146-
.container{
99+
.container {
147100
height: 100vh;
148101
}
149102
}
@@ -160,9 +113,9 @@
160113

161114
.footer {
162115
flex-direction: column;
163-
align-items: center;
116+
align-items: center;
164117
justify-content: center;
165-
gap: 0.5rem;
118+
gap: 0.5rem;
166119
padding: 1rem;
167120
}
168121

@@ -171,8 +124,8 @@
171124
align-items: center;
172125
justify-content: center;
173126
padding: 1.2rem 1rem;
174-
font-size: 0.7rem;
175-
height: auto;
127+
font-size: 0.7rem;
128+
height: auto;
176129
max-width: 100%;
177130
border-radius: 0.75rem;
178131
}

packages/apps/esm-login-app/src/login/login.test.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ describe('Login', () => {
3535
renderWithRouter(
3636
Login,
3737
{},
38-
{
38+
{
3939
route: '/login',
4040
},
4141
);
4242

43-
screen.getByRole('img', { name: /OpenMRS logo/i });
43+
expect(screen.getAllByRole('img', { name: /OpenMRS logo/i })).toHaveLength(2);
4444
expect(screen.queryByAltText(/^logo$/i)).not.toBeInTheDocument();
4545
screen.getByRole('textbox', { name: /Username/i });
4646
screen.getByRole('button', { name: /Continue/i });

packages/apps/esm-login-app/translations/en.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
2-
"back": "Back",
3-
"backToUserNameIconLabel": "Back to username",
2+
"builtWith": "Built with",
43
"cancel": "Cancel",
54
"change": "Change",
65
"changePassword": "Change password",
@@ -10,6 +9,7 @@
109
"errorChangingPassword": "Error changing password",
1110
"footerlogo": "Footer Logo",
1211
"invalidCredentials": "Invalid username or password",
12+
"learnMore": "Learn more",
1313
"locationPreferenceRemoved": "Login location preference removed",
1414
"locationPreferenceRemovedMessage": "You will need to select a location on each login",
1515
"locationSaved": "Location saved",
@@ -18,6 +18,7 @@
1818
"locationUpdateMessage": "Your preferred login location has been updated",
1919
"loggingIn": "Logging in",
2020
"login": "Log in",
21+
"loginButtonIconDescription": "Log in button",
2122
"Logout": "Logout",
2223
"newPassword": "New password",
2324
"newPasswordRequired": "New password is required",
@@ -28,7 +29,7 @@
2829
"passwordChangedSuccessfully": "Password changed successfully",
2930
"passwordConfirmationRequired": "Password confirmation is required",
3031
"passwordsDoNotMatch": "Passwords do not match",
31-
"poweredBy": "Powered by",
32+
"poweredBySubtext": "An open-source medical record system and global community",
3233
"rememberLocationForFutureLogins": "Remember my location for future logins",
3334
"selectYourLocation": "Select your location from the list below. Use the search bar to find your location.",
3435
"showPassword": "Show password",

0 commit comments

Comments
 (0)