-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Implement page footer with USWDS components #1285
base: main
Are you sure you want to change the base?
Changes from all commits
a014680
4e4fef7
7f300ea
545697e
6f58910
f838337
9f059df
e856157
a82d132
65dacea
1aafd0c
18f0173
60398ed
a5b4502
c47e331
e629882
a7a91ec
d0a4cd4
59dd97e
4bdf994
e2afeed
9f4a2fd
fe545d4
f224268
02e5b61
89a9421
0755ef6
4e3a21a
2d175f1
6bf3149
bb5dcad
886912e
8c3c769
d830c03
5089c7e
98fd9ed
c7846f8
fc7d58e
75f6fd3
d72afea
d43dccc
a9fd5ce
e039620
c13d9c1
13c9cb9
8ff3230
b45d108
3584e15
c149d5b
23850fd
0c78b01
c4f1b24
b5fad30
2267afc
c0d279a
8cfe1ce
6846e20
522dab0
e8a7b14
bda0bde
73eff21
9bbd492
9877502
dc2d615
8b758bf
09babe8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,13 +10,18 @@ import { useDeepCompareEffect } from 'use-deep-compare'; | |
import styled from 'styled-components'; | ||
import { Outlet } from 'react-router'; | ||
import { reveal } from '@devseed-ui/animation'; | ||
import { NavLink } from 'react-router-dom'; | ||
|
||
import { | ||
getBannerFromVedaConfig, | ||
getCookieConsentFromVedaConfig, | ||
getSiteAlertFromVedaConfig | ||
} from 'veda'; | ||
import MetaTags from '../meta-tags'; | ||
import PageFooter from '../page-footer'; | ||
import PageFooterLegacy from '../page-footer-legacy'; | ||
import NasaLogoColor from '../nasa-logo-color'; | ||
|
||
const Banner = React.lazy(() => import('../banner')); | ||
const SiteAlert = React.lazy(() => import('../site-alert')); | ||
const CookieConsent = React.lazy(() => import('../cookie-consent')); | ||
|
@@ -31,9 +36,11 @@ import { | |
mainNavItems, | ||
subNavItems | ||
} from '$components/common/page-header/default-config'; | ||
import { checkEnvFlag } from '$utils/utils'; | ||
|
||
const appTitle = process.env.APP_TITLE; | ||
const appDescription = process.env.APP_DESCRIPTION; | ||
const isUswdsFooterEnabled = checkEnvFlag(process.env.ENABLE_USWDS_PAGE_FOOTER); | ||
|
||
export const PAGE_BODY_ID = 'pagebody'; | ||
|
||
|
@@ -64,7 +71,8 @@ function LayoutRoot(props: { children?: ReactNode }) { | |
useEffect(() => { | ||
// When there is no cookie consent form set up | ||
!cookieConsentContent && setGoogleTagManager(); | ||
}, []); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, []); // Empty dependency array ensures this effect runs only once, and not during SSR | ||
|
||
const { title, thumbnail, description, hideFooter } = | ||
useContext(LayoutRootContext); | ||
|
@@ -105,7 +113,17 @@ function LayoutRoot(props: { children?: ReactNode }) { | |
/> | ||
)} | ||
</PageBody> | ||
<PageFooter isHidden={hideFooter} /> | ||
{isUswdsFooterEnabled ? ( | ||
<PageFooter | ||
mainNavItems={mainNavItems} | ||
subNavItems={subNavItems} | ||
hideFooter={hideFooter} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. curious, whats the point of having the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Its a carry over functionality from the old footer. No real documentation to back up its use. But I would infer that certain instances may not want to show this veda footer for some reason. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is for the case that the instance wants to have their own footer. ex. ghg has their own footer https://github.com/US-GHG-Center/veda-config-ghg/blob/bf3e66b7299d474dae01576e850bccaaaca5856c/overrides/components/page-footer/component.tsx#L136 |
||
linkProperties={{ LinkElement: NavLink, pathAttributeKeyName: 'to' }} | ||
logoSvg={<NasaLogoColor />} | ||
/> | ||
snmln marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) : ( | ||
<PageFooterLegacy hideFooter={hideFooter} /> | ||
)} | ||
</Page> | ||
); | ||
} | ||
|
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { getFooterSettingsFromVedaConfig } from 'veda'; | ||
const defaultFooterSettings = { | ||
secondarySection: { | ||
division: 'NASA EarthData 2024', | ||
version: 'BETA VERSION', | ||
title: 'NASA Official', | ||
name: 'Manil Maskey', | ||
to: 'test@example.com', | ||
type: 'email' | ||
}, | ||
returnToTop: false | ||
}; | ||
const footerSettings = | ||
getFooterSettingsFromVedaConfig() ?? defaultFooterSettings; | ||
|
||
export { footerSettings }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import React, { ComponentType } from 'react'; | ||
import { render, screen } from '@testing-library/react'; | ||
import { navItems } from '../../../../../mock/veda.config.js'; | ||
import NasaLogoColor from '../nasa-logo-color'; | ||
import { NavItem } from '../page-header/types.js'; | ||
|
||
import PageFooter from './index'; | ||
|
||
const mockMainNavItems: NavItem[] = navItems.mainNavItems; | ||
const mockSubNavItems: NavItem[] = navItems.subNavItems; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These mocks are mocking what looks to be used for the Header which includes dropdowns which footer doesn't worry about, we should probably explicity define the config for footer separately. And then test the dynamicness? |
||
// const mockFooterSettings = footerSettings; | ||
const hideFooter = false; | ||
const mockLinkProperties = { | ||
pathAttributeKeyName: 'to', | ||
LinkElement: 'a' as unknown as ComponentType | ||
}; | ||
jest.mock('./default-config', () => ({ | ||
footerSettings: { | ||
secondarySection: { | ||
division: 'NASA EarthData 2024', | ||
version: 'BETA VERSION', | ||
title: 'NASA Official', | ||
name: 'test', | ||
to: 'test@example.com', | ||
type: 'email' | ||
}, | ||
returnToTop: true | ||
} | ||
})); | ||
|
||
describe('PageFooter', () => { | ||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
test('renders the PageFooter', () => { | ||
render( | ||
<PageFooter | ||
mainNavItems={mockMainNavItems} | ||
subNavItems={mockSubNavItems} | ||
hideFooter={hideFooter} | ||
logoSvg={<NasaLogoColor />} | ||
linkProperties={mockLinkProperties} | ||
/> | ||
); | ||
const footerElement = document.querySelector('footer'); | ||
|
||
expect(footerElement).toBeInTheDocument(); | ||
expect(footerElement).not.toHaveClass('display-none'); | ||
}); | ||
|
||
test('renders correct buttons and links', () => { | ||
render( | ||
<PageFooter | ||
mainNavItems={mockMainNavItems} | ||
subNavItems={mockSubNavItems} | ||
hideFooter={hideFooter} | ||
logoSvg={<NasaLogoColor />} | ||
linkProperties={mockLinkProperties} | ||
/> | ||
); | ||
expect(screen.getByText('Data Catalog')).toBeInTheDocument(); | ||
expect(screen.getByText('Exploration')).toBeInTheDocument(); | ||
expect(screen.getByText('Stories')).toBeInTheDocument(); | ||
|
||
expect(screen.getByText('About')).toBeInTheDocument(); | ||
expect(screen.getByText('Return to top')).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
describe('PageFooter dynamic settings', () => { | ||
test('Hide footer should function correctly', () => { | ||
jest.mock('./default-config', () => ({ | ||
footerSettings: { | ||
secondarySection: { | ||
division: 'NASA EarthData 2024', | ||
version: 'BETA VERSION', | ||
title: 'NASA Official', | ||
name: 'test', | ||
to: 'test@example.com', | ||
type: 'email' | ||
}, | ||
returnToTop: true | ||
} | ||
})); | ||
render( | ||
<PageFooter | ||
linkProperties={mockLinkProperties} | ||
mainNavItems={mockMainNavItems} | ||
subNavItems={mockSubNavItems} | ||
hideFooter={true} | ||
logoSvg={<NasaLogoColor />} | ||
/> | ||
); | ||
const footerElement = document.querySelector('footer'); | ||
expect(footerElement).toHaveClass('display-none'); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import React, { useMemo } from 'react'; | ||
import { Icon } from '@trussworks/react-uswds'; | ||
import { DropdownNavLink, NavLinkItem } from '../types'; | ||
import { ActionNavItem, NavItemType } from '../page-header/types'; | ||
import { NavItemCTA } from '../page-header/nav/nav-item-cta'; | ||
import ReturnToTopButton from './return-to-top-button'; | ||
import { footerSettings } from './default-config'; | ||
|
||
import { LinkProperties } from '$types/veda'; | ||
import { | ||
USWDSFooter, | ||
USWDSFooterNav, | ||
USWDSAddress | ||
} from '$components/common/uswds'; | ||
|
||
interface PageFooterProps { | ||
//use of NavItem is causing issues with TS and throwing erros in the . | ||
mainNavItems: (NavLinkItem | DropdownNavLink | ActionNavItem)[]; | ||
subNavItems: (NavLinkItem | DropdownNavLink | ActionNavItem)[]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I dont think Footer will need DropDowns. Unsure if we have any designs where it includes dropdowns but I've never seen and I dont see the option in storybook here so we should probably remove this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @AliceR and I determined that it makes sense to reuse the main and sub navigation for the footer navigation and strip out the dropdown options functionality within the footer. So we need to maintain the dropdown types to appropriately digest the existing navigation for now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it makes more sense to be explicit hence why we have these config driven nav/menus and why we created it like so but thats fine. I'll add a note for this to be re-looked at in #1323 . |
||
hideFooter?: boolean; | ||
logoSvg?: SVGElement | JSX.Element; | ||
linkProperties: LinkProperties; | ||
} | ||
|
||
//TODO: clean up PageFooterProps, Unexpected any. Specify a different interface. | ||
|
||
export default function PageFooter({ | ||
mainNavItems, | ||
subNavItems, | ||
hideFooter, | ||
logoSvg, | ||
linkProperties | ||
}: PageFooterProps) { | ||
const { returnToTop, secondarySection } = footerSettings; | ||
const FooterNavItemInternalLink = (item) => { | ||
const { item: linkContents, linkClasses, linkProperties } = item; | ||
if (linkProperties.LinkElement) { | ||
const path = { | ||
[linkProperties.pathAttributeKeyName]: linkContents.to | ||
}; | ||
const LinkElement = linkProperties.LinkElement; | ||
return ( | ||
<LinkElement | ||
key={linkContents.id} | ||
{...path} | ||
className={linkClasses} | ||
id={linkContents.id} | ||
> | ||
<span>{linkContents.title}</span> | ||
</LinkElement> | ||
); | ||
} | ||
// If the link provided is invalid, do not render the element | ||
return null; | ||
}; | ||
|
||
const createNavElement = (navItems, linkClasses) => { | ||
//removing 'dropdown' items from array | ||
const cleanedNavItems = navItems.filter((a) => { | ||
if (a.type !== 'dropdown') { | ||
return a; | ||
} | ||
}); | ||
|
||
return cleanedNavItems.map((item) => { | ||
switch (item.type) { | ||
case NavItemType.ACTION: | ||
return <NavItemCTA item={item} customClasses={linkClasses} />; | ||
|
||
case NavItemType.EXTERNAL_LINK: | ||
return ( | ||
<a className={linkClasses} href={item.to} key={item.id}> | ||
{item.title} | ||
</a> | ||
); | ||
case NavItemType.INTERNAL_LINK: | ||
return ( | ||
<FooterNavItemInternalLink | ||
item={item} | ||
linkClasses={linkClasses} | ||
linkProperties={linkProperties} | ||
/> | ||
); | ||
snmln marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
default: | ||
return <></>; | ||
} | ||
}); | ||
}; | ||
|
||
const primaryItems = useMemo( | ||
() => createNavElement(mainNavItems, 'usa-footer__primary-link'), | ||
[mainNavItems] | ||
); | ||
const secondaryItems = useMemo( | ||
() => | ||
createNavElement(subNavItems, 'usa-link text-base-dark text-underline'), | ||
[mainNavItems] | ||
); | ||
|
||
return ( | ||
<USWDSFooter | ||
size='slim' | ||
returnToTop={returnToTop && <ReturnToTopButton />} | ||
className={hideFooter && 'display-none'} | ||
primary={ | ||
<div | ||
id='footer_primary_container' | ||
className=' grid-row usa-footer__primary-container' | ||
> | ||
<div className='mobile-lg:grid-col-8'> | ||
<USWDSFooterNav | ||
aria-label='Footer navigation' | ||
size='slim' | ||
links={primaryItems} | ||
/> | ||
</div> | ||
<div className='tablet:grid-col-4'> | ||
<USWDSAddress | ||
size='slim' | ||
className='flex-justify-end' | ||
items={secondaryItems} | ||
/> | ||
</div> | ||
</div> | ||
} | ||
secondary={ | ||
<div id='footer_secondary_container' className='grid-row'> | ||
<div id='logo-container'> | ||
<a id='logo-container-link' href='#'> | ||
{logoSvg as JSX.Element} | ||
<span className='footer-text'> | ||
{secondarySection.division} • {secondarySection.version} | ||
</span> | ||
</a> | ||
</div> | ||
<div className='grid-col-4 footer-text grid-gap-6 flex-justify-end'> | ||
snmln marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<span>{secondarySection.title}: </span> | ||
<a | ||
key={secondarySection.type} | ||
href={`mailto:${secondarySection.to}`} | ||
className='text-primary-light' | ||
> | ||
<Icon.Mail | ||
className='margin-right-1 width-205 height-auto position-relative' | ||
id='mail_icon' | ||
/> | ||
{secondarySection.name} | ||
</a> | ||
</div> | ||
</div> | ||
} | ||
/> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't
layout-root
only used for the legacy way?