From 9743dd7c78bb236db48db6d63164eac6b2a041cc Mon Sep 17 00:00:00 2001 From: Meis Date: Thu, 22 Jun 2023 09:50:44 -0600 Subject: [PATCH 1/9] WIP: Footer --- src/components/Footer/Footer.less | 198 ++++++++++++++++++ src/components/Footer/Footer.stories.tsx | 15 ++ src/components/Footer/Footer.tsx | 249 +++++++++++++++++++++++ src/components/Footer/SocialMedia.less | 102 ++++++++++ 4 files changed, 564 insertions(+) create mode 100644 src/components/Footer/Footer.less create mode 100644 src/components/Footer/Footer.stories.tsx create mode 100644 src/components/Footer/Footer.tsx create mode 100644 src/components/Footer/SocialMedia.less diff --git a/src/components/Footer/Footer.less b/src/components/Footer/Footer.less new file mode 100644 index 00000000..8863af0f --- /dev/null +++ b/src/components/Footer/Footer.less @@ -0,0 +1,198 @@ +@import (reference) url('@cfpb/cfpb-design-system/src/cfpb-design-system.less'); + +// 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; +} \ No newline at end of file diff --git a/src/components/Footer/Footer.stories.tsx b/src/components/Footer/Footer.stories.tsx new file mode 100644 index 00000000..119673cb --- /dev/null +++ b/src/components/Footer/Footer.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Footer from './Footer'; + +const meta: Meta = { + component: Footer, + argTypes: {} +}; + +export default meta; + +type Story = StoryObj; + +export const CFGov: Story = { + args: {} +}; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx new file mode 100644 index 00000000..102b81b2 --- /dev/null +++ b/src/components/Footer/Footer.tsx @@ -0,0 +1,249 @@ +import React from 'react'; +import { Icon } from '../Icon/Icon'; +import './Footer.less'; +import './SocialMedia.less'; + +const BackToTop = (): JSX.Element => ( + + Back to top + + +); + +interface NavLinksProperties { + children: JSX.Element[]; +} +const NavLinks = ({ children }: NavLinksProperties): JSX.Element => ( +
    + {children.map(link => ( +
  • + {React.cloneElement(link, { + className: `m-list_link ${link.props?.className ?? ''}` + })} +
  • + ))} +
+); + +const FooterBanner = (): JSX.Element => ( +
+
+ +
+ An official website of the  + United States government +
+
+
+); + +export default function Footer(): JSX.Element { + return ( + + ); +} diff --git a/src/components/Footer/SocialMedia.less b/src/components/Footer/SocialMedia.less new file mode 100644 index 00000000..dac535ae --- /dev/null +++ b/src/components/Footer/SocialMedia.less @@ -0,0 +1,102 @@ +@import (reference) url('@cfpb/cfpb-design-system/src/cfpb-design-system.less'); + +// https://github.com/cfpb/consumerfinance.gov/blob/main/cfgov/unprocessed/css/molecules/social-media.less + +/* topdoc + name: Social Media + family: cfgov-molecules + patterns: + - name: Default example + markup: | + + codenotes: + - | + Structural cheat sheet: + ----------------------- + .m-social-media.m-social-media__share + .m-social-media_heading + ul.m-list + li.m-list_item + .m-social-media_icon + .cf-icon-svg + span.u-visually-hidden + notes: + - "m-social-media__follow modifier is identical + but does not include the heading." + tags: + - cfgov-molecules +*/ + +.m-social-media { + display: inline-block; + + // This could be wrapped in the `m-social-media__share` modifier, + // but it's ok on its own too. + &_heading { + .heading-5(); + display: inline-block; + // 0.25em subtracted to compensate for inline spacing + padding-right: unit((@grid_gutter-width / @font-size - 0.25em), em); + margin-bottom: 0; + vertical-align: middle; + } + + .m-list { + display: inline-block; + vertical-align: middle; + + .m-list_item { + // 0.25em subtracted to compensate for inline spacing + margin-right: unit( + ((@grid_gutter-width / 2) / @base-font-size-px - 0.25em), + em + ); + margin-bottom: 0; + } + + .m-list_item:last-child { + margin-right: 0; + } + } + + &_icon { + color: @gray-dark; + font-size: unit((@grid_gutter-width / @base-font-size-px), em); + line-height: 1; + .u-link__colors( @gray-dark, @pacific-80 ); + .u-link__no-border(); + } + + &_print { + padding-left: unit(((@grid_gutter-width / 2) / @base-font-size-px), em); + border-left: 1px solid @black; + } + + // Hide on print. + .respond-to-print( { + & { + display: none; + } + } ); +} + +.no-js .m-social-media_print { + display: none; +} + +/* topdoc + name: EOF + eof: true +*/ From 82e4bf3d2d1f85d1ce5ee7f11758243a2a8184f4 Mon Sep 17 00:00:00 2001 From: Meis Date: Fri, 23 Jun 2023 13:00:59 -0600 Subject: [PATCH 2/9] fix: make List component more customizable by propagating the className prop --- src/components/List/List.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/List/List.tsx b/src/components/List/List.tsx index f730e71a..cec0dbe2 100644 --- a/src/components/List/List.tsx +++ b/src/components/List/List.tsx @@ -1,23 +1,25 @@ import classnames from 'classnames'; interface ListProperties { + children: JSX.Element | JSX.Element[]; + className?: string; isHorizontal?: boolean; isLinks?: boolean; isOrdered?: boolean; - isUnstyled?: boolean; isSpaced?: boolean; - children: JSX.Element | JSX.Element[]; + isUnstyled?: boolean; } export default function List({ children, + className, isHorizontal, isLinks = false, isOrdered, isSpaced, isUnstyled }: ListProperties): JSX.Element { - const cnames = ['m-list']; + const cnames = [className, 'm-list']; if (isHorizontal) cnames.push('m-list__horizontal'); if (isLinks) cnames.push('m-list__links'); if (isSpaced) cnames.push('m-list__spaced'); From 6f6237a1041a0e8f00a4550c0e1e1d31733592a7 Mon Sep 17 00:00:00 2001 From: Meis Date: Fri, 23 Jun 2023 13:01:41 -0600 Subject: [PATCH 3/9] feat: Add a ListItemBuilder component that handles the wrapping of child elements in
  • s --- src/components/List/ListItem.tsx | 47 ++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/components/List/ListItem.tsx b/src/components/List/ListItem.tsx index 6d56d5cb..4dffa807 100644 --- a/src/components/List/ListItem.tsx +++ b/src/components/List/ListItem.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + interface ListItemProperties { children: JSX.Element | string; } @@ -7,3 +9,48 @@ export default function ListItem({ }: ListItemProperties): JSX.Element { return
  • {children}
  • ; } + +interface ListItemBuilderProperties { + children: JSX.Element[]; + className?: string; +} +/** + * A utility component that wraps each child element in an
  • , + * while adding the provided `className` prop to the newly wrapped element. + * + * Example: + * + * Homepage + * Other page + * + * + * Example Output: + * <> + *
  • + * Homepage + *
  • + *
  • + * Other page + *
  • + * + * + * @param children Elements to be wrapped in
  • + * @param className Class name to be applied each of the `children` elements (not the
  • ) + * @returns Single nestable JSX element + */ +export function ListItemBuilder({ + children, + className = '' +}: ListItemBuilderProperties): JSX.Element { + return ( + <> + {children.map((element: JSX.Element) => ( + + {React.cloneElement(element, { + className: `${className} ${element.props?.className ?? ''}` + })} + + ))} + + ); +} From ed5a99a829e8f94412ce6c104d31faa43701879c Mon Sep 17 00:00:00 2001 From: Meis Date: Fri, 23 Jun 2023 13:03:00 -0600 Subject: [PATCH 4/9] feat: Supporting components for