From da8388a7fa0c7c81a3db170ed1dad15fef720bca Mon Sep 17 00:00:00 2001 From: Matthew Revell Date: Mon, 12 Aug 2024 10:40:19 +0100 Subject: [PATCH 1/2] feat: add NabarLite --- .../organisms/Navbar/Navbar/NavbarLite.tsx | 261 ++++++++++++++++++ .../organisms/NavbarLite/NavbarLite.tsx | 238 ++++++++++++++++ src/components/styles/Theme/ThemeProvider.tsx | 2 + .../ProductTemplate/ProductTemplateV2.tsx | 1 + src/index.ts | 1 + 5 files changed, 503 insertions(+) create mode 100644 src/components/organisms/Navbar/Navbar/NavbarLite.tsx create mode 100644 src/components/organisms/NavbarLite/NavbarLite.tsx diff --git a/src/components/organisms/Navbar/Navbar/NavbarLite.tsx b/src/components/organisms/Navbar/Navbar/NavbarLite.tsx new file mode 100644 index 000000000..cdcae1ede --- /dev/null +++ b/src/components/organisms/Navbar/Navbar/NavbarLite.tsx @@ -0,0 +1,261 @@ +import React, { Dispatch, SetStateAction } from 'react'; +import styled, { css } from 'styled-components'; +import Headroom from 'react-headroom'; + +import { colors, navbarOpenHeight, navbarClosedHeight, mobileNavbarHeight, spacing } from '../../../../constants'; +import { maxMedia, minMedia } from '../../../../helpers/responsiveness'; +import useScrollThreshold from '../useScrollThreshold/useScrollThreshold'; +import { NavbarLinkProps } from '../NavbarLink/NavbarLink'; +import Button from '../../../atoms/Button/Button'; +import { AppThemeProps, useThemeContext } from '../../../styles/Theme'; +import { ConditionalWrapper } from '../../../atoms/ConditionalWrapper/ConditionalWrapper'; + +export interface NavigationItem { + label: React.ReactNode; + href?: string; + 'data-automation'?: string; + onClick?: (event?: React.MouseEvent) => void; + isDropdownHeading?: boolean; + children?: Exclude[]; +} +export interface NavbarLinksListProps { + /** + * Array of links + */ + links?: NavigationItem[]; + /** + * Link component + */ + renderLink?: (item: NavigationItem, index: number, props?: any) => React.ReactNode; + setOpen: Dispatch>; +} + +export interface NavbarProps extends Omit { + /** + * allows you to overlay the logo with a button or link + */ + overlayLogoWith?: React.ReactNode; + /** + * flag to display CTA + */ + withCTA?: boolean; + /** + * CTA component + */ + cta?: React.ReactNode; + /** + * Displayed scrolled styling + */ + collapsed?: boolean; +} + +export interface HamburgerContainerProps extends React.HTMLAttributes { + open: boolean; +} + +export interface PageNavigationProps extends AppThemeProps { + overlap?: boolean; + collapsed?: boolean; +} + +export interface NavbarLinksListLinkProps { + item: NavigationItem; + index: number; + props: NavbarLinkProps; +} + +const PageNavigation = styled.header` + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1; + + ${minMedia.desktop` + ${css` + z-index: 1; + background-color: ${colors.white}; + max-height: ${navbarOpenHeight}px; + + transition: max-height 0.3s ease; + + ${({ collapsed }: PageNavigationProps) => + collapsed && + css` + max-height: ${navbarClosedHeight}px; + `} + + ${({ overlap }: PageNavigationProps) => + overlap && + css` + max-height: ${navbarClosedHeight}px; + `} + `} + `} + + .headroom { + z-index: 1; + background-color: ${({ theme }) => theme.navbar.mobile.bgColor}; + transition: transform 200ms ease-in-out; + + ${minMedia.desktop` + ${css` + background-color: ${colors.white}; + `} + `} + } + .headroom--unfixed { + transform: translateY(0); + } + + .headroom--unpinned { + transform: translateY(-100%); + } + .headroom--pinned { + transform: translateY(0%); + } +`; + +const Spacer = styled.div` + height: ${mobileNavbarHeight}px; + + ${minMedia.desktop` + ${css` + height: ${({ collapsed }: PageNavigationProps) => (collapsed ? navbarClosedHeight : navbarOpenHeight)}px; + `} + `} +`; + +const LayoutInner = styled.nav` + display: flex; + align-items: center; + justify-content: ${({ theme }: PageNavigationProps) => theme.navbar.layoutInner?.justifyContent ?? 'space-between'}; + height: 100%; + width: 100%; + box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px; + + ${minMedia.desktop` + ${css` + box-shadow: none; + + ${({ overlap }: PageNavigationProps) => + overlap && + css` + box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px; + `} + `} + `} +`; + +// TODO: We have to manually type the theme, I suspect this is because we're using outdated typings +// The generic passed does not actually get passed and the generic theme from styled-components is used instead +// This should be fixed in the future +export const LogoContainer = styled.div` + position: relative; + min-height: ${({ theme }) => theme.navbar.mobile.minHeight}px; + ${minMedia.desktop` + ${css` + display: flex; + align-items: center; + width: 490px; + transition: 0.3s min-height ease; + min-height: ${({ overlap }: PageNavigationProps) => (overlap ? navbarClosedHeight : navbarOpenHeight)}px; + padding-left: ${({ theme }: PageNavigationProps) => + theme.navbar.logoContainer?.desktopMinMedia.paddingLeft ?? spacing[10]}; + height: ${({ theme }: PageNavigationProps) => theme.navbar.logoContainer?.desktopMinMedia.height}; + width: ${({ theme }: PageNavigationProps) => theme.navbar.logoContainer?.desktopMinMedia.width}; + justify-content: ${({ theme }: PageNavigationProps) => + theme.navbar.logoContainer?.desktopMinMedia.justifyContent}; + `} + `} + + ${maxMedia.desktop` + ${css` + display: ${({ theme }: PageNavigationProps) => theme.navbar.logoContainer?.desktopMaxMedia.display}; + align-items: ${({ theme }: PageNavigationProps) => theme.navbar.logoContainer?.desktopMaxMedia.alignItems}; + `} + `} + + } + + & > a, + & > button, + & > div { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } +`; + +const NavbarLinksListContainer = styled.ul` + ${minMedia.desktop` + ${css` + margin-right: ${spacing[10]}; + padding-left: 0; + `} + `} +`; + +const IconContainer = styled(Button).attrs({ 'aria-label': 'Navigation' })` + background: transparent; + display: ${({ theme }) => theme.navbar.iconContainer.display}; + height: ${mobileNavbarHeight}px; + width: ${mobileNavbarHeight}px; + font-size: 24px; + justify-content: center; + align-items: center; + border-radius: 0; +`; + +const LargeDeviceNavbar = styled.div` + display: none; + + ${minMedia.desktop` + ${` + display:block; + `} + `} +`; + +const SmallDeviceNavbar = styled.div` + display: block; + + ${minMedia.desktop` + ${` + display:none; + `} + `} +`; + +const NavbarLiteWrapper = ({ collapsed = false }: React.PropsWithChildren) => { + const overThreshold = useScrollThreshold(20); + const theme = useThemeContext(); + + return ( + <> + + {children}} + > + + + + + + + + + + + + + ); +}; + +export default NavbarLiteWrapper; diff --git a/src/components/organisms/NavbarLite/NavbarLite.tsx b/src/components/organisms/NavbarLite/NavbarLite.tsx new file mode 100644 index 000000000..7b0d69043 --- /dev/null +++ b/src/components/organisms/NavbarLite/NavbarLite.tsx @@ -0,0 +1,238 @@ +import React from 'react'; +import styled, { css } from 'styled-components'; +import Headroom from 'react-headroom'; + +import { colors, navbarOpenHeight, navbarClosedHeight, mobileNavbarHeight, breakpoints } from '../../../constants'; +import { minMedia } from '../../../helpers/responsiveness'; +import useScrollThreshold from '../Navbar/useScrollThreshold/useScrollThreshold'; +import { AppThemeProps, useThemeContext } from '../../styles/Theme'; +import { ConditionalWrapper } from '../../atoms/ConditionalWrapper/ConditionalWrapper'; +import { useViewport } from '../../../hooks/useViewport'; +import Logo from '../../atoms/Logo/Logo'; + +export interface NavbarProps { + /** + * allows you to overlay the logo with a button or link + */ + overlayLogoWith?: React.ReactNode; + /** + * flag to display CTA + */ + withCTA?: boolean; + /** + * CTA component + */ + cta?: React.ReactNode; + /** + * Show the zopa logo on the right and the parnter logo on the left + * @default false + */ + isCobranded?: boolean; + + containerClassName?: string; +} + +export interface PageNavigationProps extends AppThemeProps { + overlap?: boolean; + collapsed?: boolean; +} + +const PageNavigation = styled.header` + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1; + + ${minMedia.desktop` + ${css` + z-index: 1; + background-color: ${colors.white}; + max-height: ${navbarOpenHeight}px; + + transition: max-height 0.3s ease; + + ${({ collapsed }: PageNavigationProps) => + collapsed && + css` + max-height: ${navbarClosedHeight}px; + `} + + ${({ overlap }: PageNavigationProps) => + overlap && + css` + max-height: ${navbarClosedHeight}px; + `} + `} + `} + + .headroom { + z-index: 1; + background-color: ${({ theme }) => theme.navbar.mobile.bgColor}; + transition: transform 200ms ease-in-out; + + ${minMedia.desktop` + ${css` + background-color: ${colors.white}; + `} + `} + } + .headroom--unfixed { + transform: translateY(0); + } + + .headroom--unpinned { + transform: translateY(-100%); + } + .headroom--pinned { + transform: translateY(0%); + } +`; + +const Spacer = styled.div` + height: ${mobileNavbarHeight}px; + + ${minMedia.desktop` + ${css` + height: ${({ collapsed }: PageNavigationProps) => (collapsed ? navbarClosedHeight : navbarOpenHeight)}px; + `} + `} +`; + +const LayoutInner = styled.nav` + display: flex; + align-items: center; + justify-content: ${({ theme }: PageNavigationProps) => theme.navbar.layoutInner?.justifyContent ?? 'space-between'}; + height: 100%; + width: 100%; + box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px; + + ${minMedia.desktop` + ${css` + box-shadow: none; + + ${({ overlap }: PageNavigationProps) => + overlap && + css` + box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px; + `} + `} + `} +`; + +const LargeDeviceNavbar = styled.div` + display: none; + + ${minMedia.desktop` + ${` + display:block; + `} + `} +`; + +const SmallDeviceNavbar = styled.div` + display: block; + + ${minMedia.desktop` + ${` + display:none; + `} + `} +`; + +const CenteredContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex: 1; + height: 100%; + padding: 0; +`; + +const LogoWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 100%; +`; + +const CobrandedContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + height: 100%; + padding: 0 24px; // Add horizontal padding +`; + +const LeftLogoWrapper = styled.div` + display: flex; + align-items: center; + height: 100%; +`; + +const RightLogoWrapper = styled.div` + display: flex; + align-items: center; + height: 100%; +`; + +const LogoRenderer = ({ children, isCobranded, containerClassName }: React.PropsWithChildren) => { + if (!isCobranded) { + return ( + + {children} + + ); + } + + return ( + + {children} + + + + + ); +}; + +const NavbarLiteWrapper = ({ children, isCobranded }: React.PropsWithChildren) => { + const { width } = useViewport(); + const overThreshold = useScrollThreshold(20); + const theme = useThemeContext(); + + return ( + <> + + {children}} + > + = breakpoints.desktop)} + > + + + + {children} + + + + + + + {children} + + + + + + + + + ); +}; + +export default NavbarLiteWrapper; diff --git a/src/components/styles/Theme/ThemeProvider.tsx b/src/components/styles/Theme/ThemeProvider.tsx index 79335e47a..a9f64a2e5 100644 --- a/src/components/styles/Theme/ThemeProvider.tsx +++ b/src/components/styles/Theme/ThemeProvider.tsx @@ -241,6 +241,7 @@ export interface NavbarTheme { iconContainer: { display: string; }; + /* @deprecated */ logo: { render: boolean; }; @@ -248,6 +249,7 @@ export interface NavbarTheme { minHeight: string; bgColor: string; }; + containerClassName?: string; logoContainer?: { desktopMinMedia: { width?: string; diff --git a/src/components/templates/ProductTemplate/ProductTemplate/ProductTemplateV2.tsx b/src/components/templates/ProductTemplate/ProductTemplate/ProductTemplateV2.tsx index 970022c70..04433d4c0 100644 --- a/src/components/templates/ProductTemplate/ProductTemplate/ProductTemplateV2.tsx +++ b/src/components/templates/ProductTemplate/ProductTemplate/ProductTemplateV2.tsx @@ -16,6 +16,7 @@ export interface ProductTemplateV2 { subheadingPostion: 'before' | 'after'; headingClassName: string; subheadingClassName: string; + containerClassName: string; }; row?: { className: string; diff --git a/src/index.ts b/src/index.ts index 7f79307c0..60a33cfce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,6 +66,7 @@ export * from './components/organisms/Form'; export type { FormButtonProps } from './components/organisms/Form/FormButton/FormButton'; export * from './components/organisms/Accordion'; export { default as Navbar, navbarLinkStyles } from './components/organisms/Navbar/'; +export { default as NavbarLite } from './components/organisms/NavbarLite/NavbarLite'; export { default as Card } from './components/organisms/Card'; export * from './components/organisms/Tabs'; export * from './components/organisms/Carousel'; From 14d097eb15b5d63773ba384fd9d8287cb99e4b00 Mon Sep 17 00:00:00 2001 From: Matthew Revell Date: Mon, 12 Aug 2024 11:35:24 +0100 Subject: [PATCH 2/2] chore: changeset --- .changeset/cyan-eyes-rest.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cyan-eyes-rest.md diff --git a/.changeset/cyan-eyes-rest.md b/.changeset/cyan-eyes-rest.md new file mode 100644 index 000000000..62c182b4d --- /dev/null +++ b/.changeset/cyan-eyes-rest.md @@ -0,0 +1,5 @@ +--- +'@zopauk/react-components': patch +--- + +Cobranded navigation