Skip to content

Commit 4b32557

Browse files
authored
feat: Implement page footer with USWDS components (#1285)
**Related Ticket:** #1135 ### Description of Changes TO DO: - [x] Create slim footer front-end, using USWDS. - [x] Pulling USWDS Styles. - [x] Connect to veda.config.js data. - [x] Setup USWDS feature flag. - [x] hideFooter functionality (props) - [x] Fix Nasa svg file to not rely on css translate. - [x] Checkin with Fausto on progress. - [x] Import into and use in next-veda-ui (NASA-IMPACT/next-veda-ui#26) - [ ] Connect to instance theme styling (font, color, etc.) - [x] Ensure styling looks as expected on next-veda-ui - [x] Add Feature Flag documentation ### Notes & Questions About Changes _{Add additonal notes and outstanding questions here related to changes in this pull request}_ ### Validation / Testing _{Update with info on what can be manually validated in the Deploy Preview link for example "Validate style updates to selection modal do NOT affect cards"}_
2 parents a84cc34 + cd105ca commit 4b32557

File tree

31 files changed

+838
-297
lines changed

31 files changed

+838
-297
lines changed

.env

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ GOOGLE_FORM = 'https://docs.google.com/forms/d/e/1FAIpQLSfGcd3FDsM3kQIOVKjzdPn4f
1515
SHOW_CONFIGURABLE_COLOR_MAP = 'TRUE'
1616

1717
# Enables the refactor page header component that uses the USWDS design system
18-
ENABLE_USWDS_PAGE_HEADER = 'TRUE'
18+
ENABLE_USWDS_PAGE_HEADER = 'FALSE'
19+
# Enables the refactor page footer component that uses the USWDS design system
20+
ENABLE_USWDS_PAGE_FOOTER = 'FALSE'

app/scripts/components/common/layout-root/index.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,18 @@ import { useDeepCompareEffect } from 'use-deep-compare';
1010
import styled from 'styled-components';
1111
import { Outlet } from 'react-router';
1212
import { reveal } from '@devseed-ui/animation';
13+
import { NavLink } from 'react-router-dom';
14+
1315
import {
1416
getBannerFromVedaConfig,
1517
getCookieConsentFromVedaConfig,
1618
getSiteAlertFromVedaConfig
1719
} from 'veda';
1820
import MetaTags from '../meta-tags';
1921
import PageFooter from '../page-footer';
22+
import PageFooterLegacy from '../page-footer-legacy';
23+
import NasaLogoColor from '../nasa-logo-color';
24+
2025
const Banner = React.lazy(() => import('../banner'));
2126
const SiteAlert = React.lazy(() => import('../site-alert'));
2227
const CookieConsent = React.lazy(() => import('../cookie-consent'));
@@ -31,9 +36,11 @@ import {
3136
mainNavItems,
3237
subNavItems
3338
} from '$components/common/page-header/default-config';
39+
import { checkEnvFlag } from '$utils/utils';
3440

3541
const appTitle = process.env.APP_TITLE;
3642
const appDescription = process.env.APP_DESCRIPTION;
43+
const isUswdsFooterEnabled = checkEnvFlag(process.env.ENABLE_USWDS_PAGE_FOOTER);
3744

3845
export const PAGE_BODY_ID = 'pagebody';
3946

@@ -64,6 +71,7 @@ function LayoutRoot(props: { children?: ReactNode }) {
6471
useEffect(() => {
6572
// When there is no cookie consent form set up
6673
!cookieConsentContent && setGoogleTagManager();
74+
// eslint-disable-next-line react-hooks/exhaustive-deps
6775
}, []);
6876

6977
const { title, thumbnail, description, hideFooter } =
@@ -105,7 +113,17 @@ function LayoutRoot(props: { children?: ReactNode }) {
105113
/>
106114
)}
107115
</PageBody>
108-
<PageFooter isHidden={hideFooter} />
116+
{isUswdsFooterEnabled ? (
117+
<PageFooter
118+
mainNavItems={mainNavItems}
119+
subNavItems={subNavItems}
120+
hideFooter={hideFooter}
121+
linkProperties={{ LinkElement: NavLink, pathAttributeKeyName: 'to' }}
122+
logoSvg={<NasaLogoColor />}
123+
/>
124+
) : (
125+
<PageFooterLegacy hideFooter={hideFooter} />
126+
)}
109127
</Page>
110128
);
111129
}

app/scripts/components/common/nasa-logo-color.js

Lines changed: 245 additions & 207 deletions
Large diffs are not rendered by default.

app/scripts/components/common/page-footer.js renamed to app/scripts/components/common/page-footer-legacy.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ const InfoList = styled.dl`
109109
}
110110
`;
111111

112-
function PageFooter(props) {
112+
function PageFooterLegacy(props) {
113113
const nowDate = new Date();
114114

115115
return (
@@ -174,8 +174,8 @@ function PageFooter(props) {
174174
);
175175
}
176176

177-
export default PageFooter;
177+
export default PageFooterLegacy;
178178

179-
PageFooter.propTypes = {
179+
PageFooterLegacy.propTypes = {
180180
isHidden: T.bool
181181
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { getFooterSettingsFromVedaConfig } from 'veda';
2+
const defaultFooterSettings = {
3+
secondarySection: {
4+
division: 'NASA EarthData 2024',
5+
version: 'BETA VERSION',
6+
title: 'NASA Official',
7+
name: 'Manil Maskey',
8+
to: 'test@example.com',
9+
type: 'email'
10+
},
11+
returnToTop: true
12+
};
13+
const footerSettings =
14+
getFooterSettingsFromVedaConfig() ?? defaultFooterSettings;
15+
16+
export { footerSettings };
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React, { ComponentType } from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { navItems } from '../../../../../mock/veda.config.js';
4+
import NasaLogoColor from '../nasa-logo-color';
5+
import { NavItem } from '../page-header/types.js';
6+
7+
import PageFooter from './index';
8+
9+
const mockMainNavItems: NavItem[] = navItems.mainNavItems;
10+
const mockSubNavItems: NavItem[] = navItems.subNavItems;
11+
// const mockFooterSettings = footerSettings;
12+
const hideFooter = false;
13+
const mockLinkProperties = {
14+
pathAttributeKeyName: 'to',
15+
LinkElement: 'a' as unknown as ComponentType
16+
};
17+
jest.mock('./default-config', () => ({
18+
footerSettings: {
19+
secondarySection: {
20+
division: 'NASA EarthData 2024',
21+
version: 'BETA VERSION',
22+
title: 'NASA Official',
23+
name: 'test',
24+
to: 'test@example.com',
25+
type: 'email'
26+
},
27+
returnToTop: true
28+
}
29+
}));
30+
31+
describe('PageFooter', () => {
32+
afterEach(() => {
33+
jest.clearAllMocks();
34+
});
35+
test('renders the PageFooter', () => {
36+
render(
37+
<PageFooter
38+
mainNavItems={mockMainNavItems}
39+
subNavItems={mockSubNavItems}
40+
hideFooter={hideFooter}
41+
logoSvg={<NasaLogoColor />}
42+
linkProperties={mockLinkProperties}
43+
/>
44+
);
45+
const footerElement = document.querySelector('footer');
46+
47+
expect(footerElement).toBeInTheDocument();
48+
expect(footerElement).not.toHaveClass('display-none');
49+
});
50+
51+
test('renders correct buttons and links', () => {
52+
render(
53+
<PageFooter
54+
mainNavItems={mockMainNavItems}
55+
subNavItems={mockSubNavItems}
56+
hideFooter={hideFooter}
57+
logoSvg={<NasaLogoColor />}
58+
linkProperties={mockLinkProperties}
59+
/>
60+
);
61+
expect(screen.getByText('Data Catalog')).toBeInTheDocument();
62+
expect(screen.getByText('Exploration')).toBeInTheDocument();
63+
expect(screen.getByText('Stories')).toBeInTheDocument();
64+
65+
expect(screen.getByText('About')).toBeInTheDocument();
66+
expect(screen.getByText('Return to top')).toBeInTheDocument();
67+
});
68+
});
69+
70+
describe('PageFooter dynamic settings', () => {
71+
test('Hide footer should function correctly', () => {
72+
jest.mock('./default-config', () => ({
73+
footerSettings: {
74+
secondarySection: {
75+
division: 'NASA EarthData 2024',
76+
version: 'BETA VERSION',
77+
title: 'NASA Official',
78+
name: 'test',
79+
to: 'test@example.com',
80+
type: 'email'
81+
},
82+
returnToTop: true
83+
}
84+
}));
85+
render(
86+
<PageFooter
87+
linkProperties={mockLinkProperties}
88+
mainNavItems={mockMainNavItems}
89+
subNavItems={mockSubNavItems}
90+
hideFooter={true}
91+
logoSvg={<NasaLogoColor />}
92+
/>
93+
);
94+
const footerElement = document.querySelector('footer');
95+
expect(footerElement).toHaveClass('display-none');
96+
});
97+
});
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import React, { useMemo } from 'react';
2+
import { Icon } from '@trussworks/react-uswds';
3+
import { DropdownNavLink, NavLinkItem } from '../types';
4+
import { ActionNavItem, NavItemType } from '../page-header/types';
5+
import { NavItemCTA } from '../page-header/nav/nav-item-cta';
6+
import ReturnToTopButton from './return-to-top-button';
7+
import { footerSettings } from './default-config';
8+
9+
import { LinkProperties } from '$types/veda';
10+
import {
11+
USWDSFooter,
12+
USWDSFooterNav,
13+
USWDSAddress
14+
} from '$components/common/uswds';
15+
16+
interface PageFooterProps {
17+
mainNavItems: (NavLinkItem | DropdownNavLink | ActionNavItem)[];
18+
subNavItems: (NavLinkItem | DropdownNavLink | ActionNavItem)[];
19+
hideFooter?: boolean;
20+
logoSvg?: SVGElement | JSX.Element;
21+
linkProperties: LinkProperties;
22+
}
23+
24+
25+
export default function PageFooter({
26+
mainNavItems,
27+
subNavItems,
28+
hideFooter,
29+
logoSvg,
30+
linkProperties
31+
}: PageFooterProps) {
32+
const { returnToTop, secondarySection } = footerSettings;
33+
const FooterNavItemInternalLink = (item) => {
34+
const { item: linkContents, linkClasses, linkProperties } = item;
35+
if (linkProperties.LinkElement) {
36+
const path = {
37+
[linkProperties.pathAttributeKeyName]: linkContents.to
38+
};
39+
const LinkElement = linkProperties.LinkElement;
40+
return (
41+
<LinkElement
42+
key={linkContents.id}
43+
{...path}
44+
className={linkClasses}
45+
id={linkContents.id}
46+
>
47+
<span>{linkContents.title}</span>
48+
</LinkElement>
49+
);
50+
}
51+
// If the link provided is invalid, do not render the element
52+
return null;
53+
};
54+
55+
const createNavElement = (navItems, linkClasses) => {
56+
//removing 'dropdown' items from array
57+
const cleanedNavItems = navItems.filter((a) => {
58+
if (a.type !== 'dropdown') {
59+
return a;
60+
}
61+
});
62+
63+
return cleanedNavItems.map((item) => {
64+
switch (item.type) {
65+
case NavItemType.ACTION:
66+
return <NavItemCTA item={item} customClasses={linkClasses} />;
67+
68+
case NavItemType.EXTERNAL_LINK:
69+
return (
70+
<a className={linkClasses} href={item.to} key={item.id}>
71+
{item.title}
72+
</a>
73+
);
74+
case NavItemType.INTERNAL_LINK:
75+
return (
76+
<FooterNavItemInternalLink
77+
item={item}
78+
linkClasses={linkClasses}
79+
linkProperties={linkProperties}
80+
/>
81+
);
82+
83+
default:
84+
return <></>;
85+
}
86+
});
87+
};
88+
89+
const primaryItems = useMemo(
90+
() => createNavElement(mainNavItems, 'usa-footer__primary-link'),
91+
[mainNavItems]
92+
);
93+
const secondaryItems = useMemo(
94+
() =>
95+
createNavElement(subNavItems, 'usa-link text-base-dark text-underline'),
96+
[mainNavItems]
97+
);
98+
99+
return (
100+
<USWDSFooter
101+
size='slim'
102+
returnToTop={returnToTop && <ReturnToTopButton />}
103+
className={hideFooter && 'display-none'}
104+
primary={
105+
<div className='grid-row usa-footer__primary-container footer_primary_container'>
106+
<div className='mobile-lg:grid-col-8'>
107+
<USWDSFooterNav
108+
aria-label='Footer navigation'
109+
size='slim'
110+
links={primaryItems}
111+
/>
112+
</div>
113+
<div className='tablet:grid-col-4'>
114+
<USWDSAddress
115+
size='slim'
116+
className='flex-justify-end'
117+
items={secondaryItems}
118+
/>
119+
</div>
120+
</div>
121+
}
122+
secondary={
123+
<div id='footer_secondary_container' className='grid-row'>
124+
<div id='logo-container'>
125+
<a id='logo-container-link' href='#'>
126+
{logoSvg as JSX.Element}
127+
<span className='footer-text'>
128+
{secondarySection.division}{secondarySection.version}
129+
</span>
130+
</a>
131+
</div>
132+
<div className='grid-col-4 footer-text grid-gap-6 flex-justify-end'>
133+
<span>{secondarySection.title}: </span>
134+
<a
135+
key={secondarySection.type}
136+
href={`mailto:${secondarySection.to}`}
137+
className='text-primary-light'
138+
>
139+
<Icon.Mail
140+
className='margin-right-1 width-205 height-auto position-relative'
141+
id='mail_icon'
142+
/>
143+
{secondarySection.name}
144+
</a>
145+
</div>
146+
</div>
147+
}
148+
/>
149+
);
150+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
@use '$styles/veda-ui-theme-vars.scss' as themeVars;
2+
3+
.footer-text {
4+
color: themeVars.$veda-uswds-color-base-white;
5+
align-self: center;
6+
display: flex;
7+
font-size: themeVars.$veda-uswds-fontsize-sm;
8+
font-weight: themeVars.$veda-uswds-fontweight-regular;
9+
gap: themeVars.$veda-uswds-spacing-105;
10+
}
11+
12+
.return-to-top-container {
13+
padding: themeVars.$veda-uswds-padding-4 themeVars.$veda-uswds-padding-5;
14+
max-width: themeVars.$veda-uswds-spacing-desktop;
15+
}
16+
.footer_primary_container {
17+
padding: themeVars.$veda-uswds-padding-4 themeVars.$veda-uswds-padding-5;
18+
background-color: themeVars.$veda-uswds-color-base-lightest;
19+
}
20+
.usa-footer__secondary-section {
21+
background-color: themeVars.$veda-uswds-color-base-darkest;
22+
}
23+
24+
.usa-footer__primary-section {
25+
background-color: themeVars.$veda-uswds-color-base-lightest;
26+
text-underline-offset: themeVars.$veda-uswds-spacing-5;
27+
}
28+
29+
#mail_icon {
30+
top: 50%;
31+
transform: translateY(-50%);
32+
}
33+
34+
.usa-footer__address > .grid-row {
35+
justify-content: flex-end;
36+
}
37+
38+
address {
39+
font-style: normal;
40+
font-weight: themeVars.$veda-uswds-fontweight-regular;
41+
font-size: themeVars.$veda-uswds-fontsize-sm;
42+
text-underline-offset: themeVars.$veda-uswds-spacing-5;
43+
}
44+
.usa-footer__contact-info {
45+
line-height: unset;
46+
}

0 commit comments

Comments
 (0)