Skip to content

Commit

Permalink
[Component] Footer (#90)
Browse files Browse the repository at this point in the history
* WIP: Footer

* fix: make List component more customizable by propagating the className prop

* feat: Add a ListItemBuilder component that handles the wrapping of child elements in <li>s

* feat: Supporting components for <Footer>

* feat: Footer

* feat: Use Footer to build the CF.gov footer

* feat: Footer stories

* Update src/components/Footer/Footer.less

* Update src/components/Footer/SocialMedia.less

---------

Co-authored-by: Chris Contolini <contolini@users.noreply.github.com>
  • Loading branch information
meissadia and contolini committed Jun 27, 2023
1 parent 05bef37 commit 2ebfc99
Show file tree
Hide file tree
Showing 10 changed files with 688 additions and 3 deletions.
14 changes: 14 additions & 0 deletions src/components/Footer/BackToTop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
import { Icon } from '../Icon/Icon';

export const BackToTop = (): JSX.Element => (
<a
className='a-btn a-btn__secondary o-footer_top-button'
data-gtm_ignore='true'
data-js-hook='behavior_return-to-top'
href='#'
>
Back to top
<Icon name='arrow-up' />
</a>
);
198 changes: 198 additions & 0 deletions src/components/Footer/Footer.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
@import (reference) '@cfpb/cfpb-design-system';

// https://github.com/cfpb/consumerfinance.gov/blob/main/cfgov/unprocessed/css/organisms/footer.less

/* ==========================================================================
consumerfinance.gov
footer
========================================================================== */

/* topdoc
name: CF.gov site-wide footer.
family: cfgov-organisms
patterns:
- notes:
- "For the markup please see _layouts/organisms/footer.html."
tags:
- cfgov-organisms
*/

.o-footer {
// Adding an extra 5px to the top to account for the absolute positioned
// social media icons.
padding-top: unit((50px / @base-font-size-px), em);
// There is a 10px margin-bottom on the last .o-footer_list li's, plus the
// 50px bottom padding = 60px of total padding at the bottom of the footer.
padding-bottom: unit((50px / @base-font-size-px), em);
border-top: 5px solid @green;
background: @gray-5;

&_nav-list {
.m-list__links();

.m-list_link {
font-size: unit((18px / @base-font-size-px), em);
.u-link__colors( @black );

.respond-to-min( @bp-sm-min, {
margin-right: 1em;

.u-link__hover-border();
} );

.respond-to-min( @bp-med-min, {
margin-right: unit(( @grid_gutter-width / 22px), em );
font-size: unit( (20px / @base-font-size-px), em );
} );
}

.m-list_link.m-list_link__disabled {
border-bottom: 1px dotted;

.respond-to-min( @bp-med-min, {
.u-link__no-border();
} );
}
}

&_list {
.m-list__links();

.m-list_link {
.u-link__colors( @gray-dark );
}
}

&_pre {
position: relative;
margin-bottom: unit((45px / @base-font-size-px), em);

.o-footer_top-button {
display: block;
width: 100%;
text-align: center;
// There's a standard margin between the top
// of the footer and the links. The button comes between
// that margin in the wireframes.
margin: -2em auto 2em;
}

.o-footer_nav-list {
margin-bottom: 0;
}

.respond-to-min( @bp-sm-min, {
padding-bottom: unit( (@grid_gutter-width / @base-font-size-px), em );
margin-bottom: unit( (@grid_gutter-width / @base-font-size-px), em );
border-bottom: 1px solid @gray-40;

.o-footer_top-button {
display: none;
}

.o-footer_nav-list {
.m-list__horizontal();

.m-list_item {
margin-bottom: 0;
}
}
} );

.respond-to-print( {
// !important used here to avoid being overriden by a much more specific
// selector that sets the display property for this element
// and to avoid using a selector that specific here.
display: none !important;
} );
}

// TODO: Refactor to use Design System Layout package.
&-middle-left {
.o-footer_list {
margin: 0;
}

/* Fix doubled border in mobile view */
.respond-to-max( @bp-xs-max, {
.o-footer_col:nth-child( n+2 ) {
.o-footer_list {
.m-list_item .m-list_link {
border-top-width: 0;
}
}
}
} );

.respond-to-min( @bp-sm-min, {
.grid_column(8);
border-right: 1px solid @gray-40;
border-left: 0;

.o-footer_col {
.grid_column(6);
border-left: 0;
border-right: 0;
padding-right: unit( (@grid_gutter-width / 2 / @base-font-size-px), em );
}
} );

.respond-to-print( {
// !important used here to avoid being overriden by a much more specific
// selector that sets the display property for this element
// and to avoid using a selector that specific here.
display: none !important;
} );
}

&-middle-right {
/* Fix doubled border in mobile view */
.respond-to-max( @bp-xs-max, {
.o-footer_list {
.m-list_item .m-list_link {
border-top-width: 0;
}
}
} );

.respond-to-min( @bp-sm-min, {
.grid_column(4);

.o-footer_list {
padding-left: @grid_gutter-width;
padding-right: @grid_gutter-width;
}
} );

.respond-to-print( {
// !important used here to avoid being overriden by a much more specific
// selector that sets the display property for this element
// and to avoid using a selector that specific here.
display: none !important;
} );
}

&-post {
margin-top: unit((@grid_gutter-width / @base-font-size-px), em);

.respond-to-min( @bp-sm-min, {
padding-top: unit( (@grid_gutter-width / @base-font-size-px), em );
border-top: 1px solid @gray-40;
} );

.respond-to-print( {
padding: 0;
border: none;
margin: 0;
} );
}
}

/* topdoc
name: EOF
eof: true
*/

.o-footer .cf-icon-svg__external-link {
margin-left: 3px;
}
56 changes: 56 additions & 0 deletions src/components/Footer/Footer.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Icon } from '../Icon/Icon';
import Footer from './Footer';
import { FooterCfGov } from './FooterCfGov';

const meta: Meta<typeof Footer> = {
component: Footer,
argTypes: {}
};

export default meta;

type Story = StoryObj<typeof meta>;

const makeLink = (label: string, isExternal?: boolean): JSX.Element => (
<a
key={label}
href={window.location.href}
className={`m-list_link a-link ${isExternal ? 'a-link__icon' : ''}`}
>
<span className='a-link_text'>{label}</span>
{isExternal ? <Icon name='external-link' alt='External link' /> : null}
</a>
);

const makeSocialLink = (label: string): JSX.Element => (
<a key={label} href={window.location.href}>
<Icon name={label} withBg />
</a>
);

export const Example: Story = {
args: {
navLinks: [makeLink('Nav 1'), makeLink('Nav 2'), makeLink('Nav 3')],
socialLinks: [makeSocialLink('facebook'), makeSocialLink('youtube')],
linksCol1: [
makeLink('Col1 Link 1'),
makeLink('Col1 Link 2'),
makeLink('Col1 Link 3')
],
linksCol2: [
makeLink('Col2 Link 1'),
makeLink('Col2 Link 2'),
makeLink('Col2 Link 3')
],
linksCol3: [
makeLink('Col3 Link 1', true),
makeLink('Col3 Link 2', true),
makeLink('Col3 Link 3', true)
]
}
};

export const CFGov: Story = {
render: () => <FooterCfGov />
};
53 changes: 53 additions & 0 deletions src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import List from '../List/List';
import { ListItemBuilder } from '../List/ListItem';
import { BackToTop } from './BackToTop';
import './Footer.less';
import { FooterBanner } from './FooterBanner';
import { FooterLinksColumn, NavLinks, SocialLinks } from './FooterLinks';
import './SocialMedia.less';

interface FooterProperties {
navLinks: JSX.Element[];
socialLinks: JSX.Element[];
linksCol1: JSX.Element[];
linksCol2: JSX.Element[];
linksCol3: JSX.Element[];
}

/**
* Simply define the anchor elements for each section to compose your Footer
*/
export default function Footer({
navLinks = [],
socialLinks = [],
linksCol1 = [],
linksCol2 = [],
linksCol3 = []
}: FooterProperties): JSX.Element {
return (
<footer className='o-footer' data-js-hook='state_atomic_init'>
<div className='wrapper wrapper__match-content'>
<div className='o-footer_pre'>
<BackToTop />
<NavLinks>{navLinks}</NavLinks>
<SocialLinks>{socialLinks}</SocialLinks>
</div>

<div className='o-footer-middle-left'>
<FooterLinksColumn>{linksCol1}</FooterLinksColumn>
<FooterLinksColumn>{linksCol2}</FooterLinksColumn>
</div>

<div className='o-footer-middle-right'>
<List className='o-footer_list'>
<ListItemBuilder className='m-list_link'>
{linksCol3}
</ListItemBuilder>
</List>
</div>

<FooterBanner />
</div>
</footer>
);
}
14 changes: 14 additions & 0 deletions src/components/Footer/FooterBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const FooterBanner = (): JSX.Element => (
<div className='o-footer-post'>
<div
className='a-tagline a-tagline__large'
aria-label='Official website of the United States government'
>
<span className='u-usa-flag' />
<div className='a-tagline_text'>
An official website of the&nbsp;
<span className='u-nowrap'>United States government</span>
</div>
</div>
</div>
);
Loading

0 comments on commit 2ebfc99

Please sign in to comment.