diff --git a/scripts/bundleSize/bundleSizeConfig.js b/scripts/bundleSize/bundleSizeConfig.js index 5591a683ec5..9f24b7dd0d6 100644 --- a/scripts/bundleSize/bundleSizeConfig.js +++ b/scripts/bundleSize/bundleSizeConfig.js @@ -9,5 +9,5 @@ export const VARIANCE = 5; -export const MIN_SIZE = 913; -export const MAX_SIZE = 1282; +export const MIN_SIZE = 938; +export const MAX_SIZE = 1305; diff --git a/src/app/components/Header/brand-svgs/ArabicBrandSVG.jsx b/src/app/components/Header/brand-svgs/ArabicBrandSVG.jsx new file mode 100644 index 00000000000..ae0b5250c10 --- /dev/null +++ b/src/app/components/Header/brand-svgs/ArabicBrandSVG.jsx @@ -0,0 +1,44 @@ +const ArabicBrandSVG = props => ( + + + + + + + + + + + + + + + +); + +export default ArabicBrandSVG; diff --git a/src/app/components/Navigation/config.ts b/src/app/components/Navigation/config.ts new file mode 100644 index 00000000000..8a2444420ee --- /dev/null +++ b/src/app/components/Navigation/config.ts @@ -0,0 +1,5 @@ +import { Services } from '#app/models/types/global'; + +const SERVICES_WITH_NEW_NAV: Services[] = ['arabic', 'tamil'] as const; + +export default SERVICES_WITH_NEW_NAV; diff --git a/src/app/components/Navigation/index.amp.test.tsx b/src/app/components/Navigation/index.amp.test.tsx new file mode 100644 index 00000000000..1eab58efd84 --- /dev/null +++ b/src/app/components/Navigation/index.amp.test.tsx @@ -0,0 +1,5 @@ +describe('Navigation - AMP', () => { + it('should render', () => { + expect(true).toBeTruthy(); + }); +}); diff --git a/src/app/components/Navigation/index.amp.tsx b/src/app/components/Navigation/index.amp.tsx new file mode 100644 index 00000000000..22ac39290c6 --- /dev/null +++ b/src/app/components/Navigation/index.amp.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import Navigation from '#psammead/psammead-navigation/src'; +import { ScrollableNavigation } from '#psammead/psammead-navigation/src/ScrollableNavigation'; +import { + AmpDropdown, + AmpMenuButton, +} from '#psammead/psammead-navigation/src/DropdownNavigation'; +import { GEL_GROUP_2_SCREEN_WIDTH_MAX } from '#psammead/gel-foundations/src/breakpoints'; +import styled from '@emotion/styled'; + +import type { AmpNavigationContainerProps } from './types'; + +const DROPDOWN_ID = 'si-nav-dropdown-menu'; +const NAVIGATION_ID = 'si-nav'; +const SCROLLABLE_ID = 'si-nav-scrollable'; +const HIDDEN_CLASS_NAME = 'si-nav-scrollable-hidden'; +const OPEN_CLASS_NAME = 'si-nav-open'; + +const StyledAmpScrollableNavigation = styled(ScrollableNavigation)` + &.${HIDDEN_CLASS_NAME} { + @media (max-width: ${GEL_GROUP_2_SCREEN_WIDTH_MAX}) { + display: none; + visibility: hidden; + } + } +`; + +const AmpNavigationContainer: React.FC = ({ + dir, + menuAnnouncedText, + bottomScrollableListItems, + dropdownListItems, +}) => ( + + + {/* Hidden attribute allows us to toggle visibility on the dropdown + using AMP actions. */} + + {dropdownListItems} + + {/* TODO: Implement the new navigation in AMP */} + + {bottomScrollableListItems} + + +); + +export default AmpNavigationContainer; diff --git a/src/app/components/Navigation/index.canonical.test.tsx b/src/app/components/Navigation/index.canonical.test.tsx new file mode 100644 index 00000000000..ef85c308783 --- /dev/null +++ b/src/app/components/Navigation/index.canonical.test.tsx @@ -0,0 +1,5 @@ +describe('Navigation - Canonical', () => { + it('should render', () => { + expect(true).toBeTruthy(); + }); +}); diff --git a/src/app/components/Navigation/index.canonical.tsx b/src/app/components/Navigation/index.canonical.tsx new file mode 100644 index 00000000000..7e01692560a --- /dev/null +++ b/src/app/components/Navigation/index.canonical.tsx @@ -0,0 +1,93 @@ +import React, { useState, use } from 'react'; +import Navigation from '#psammead/psammead-navigation/src'; +import { ScrollableNavigation } from '#psammead/psammead-navigation/src/ScrollableNavigation'; +import { + CanonicalDropdown, + CanonicalMenuButton, +} from '#psammead/psammead-navigation/src/DropdownNavigation'; +import { GROUP_2_MAX_WIDTH_BP } from '#app/components/ThemeProvider/mediaQueries'; +import useMediaQuery from '#hooks/useMediaQuery'; +import { RequestContext } from '#app/contexts/RequestContext'; +import TopBarOJs from '#app/components/TopBarOJs'; +import useToggle from '#app/hooks/useToggle'; +import { TopStoryItem } from '#app/pages/ArticlePage/PagePromoSections/TopStoriesSection/types'; +import { Direction } from '#app/models/types/global'; +import styles from './index.styles'; + +interface CanonicalNavigationContainerProps { + dir: Direction; + menuAnnouncedText: string; + topScrollableListItems?: React.ReactNode; + topDivider?: React.ReactNode; + bottomScrollableListItems: React.ReactNode; + dropdownListItems: React.ReactNode; + menuButton?: React.ReactNode; + isOpen?: boolean; + setIsOpen?: (open: boolean) => void; + blocks?: TopStoryItem[]; +} + +const CanonicalNavigationContainer: React.FC< + CanonicalNavigationContainerProps +> = ({ + dir, + menuAnnouncedText, + topScrollableListItems, + bottomScrollableListItems, + dropdownListItems, + blocks, +}) => { + const { isLite } = use(RequestContext); + const { enabled } = useToggle('topBarOJs'); + const [isOpen, setIsOpen] = useState(false); + + useMediaQuery(`(max-width: ${GROUP_2_MAX_WIDTH_BP}rem)`, event => { + if (!event.matches) { + setIsOpen(false); + } + }); + + return ( + + + + + + {topScrollableListItems} + + {!isLite && ( + setIsOpen(!isOpen)} + dir={dir} + /> + )} + + + {dropdownListItems} + + + + + + {bottomScrollableListItems} + + + + + {enabled && } + + ); +}; + +export default CanonicalNavigationContainer; diff --git a/src/app/components/Navigation/index.stories.tsx b/src/app/components/Navigation/index.stories.tsx new file mode 100644 index 00000000000..d2291ddba5f --- /dev/null +++ b/src/app/components/Navigation/index.stories.tsx @@ -0,0 +1,8 @@ +const Component = () => Navigation; + +export default { + title: 'Components/NewNavigation', + Component, +}; + +export const Example = Component; diff --git a/src/app/components/Navigation/index.styles.ts b/src/app/components/Navigation/index.styles.ts new file mode 100644 index 00000000000..f8703b5f4af --- /dev/null +++ b/src/app/components/Navigation/index.styles.ts @@ -0,0 +1,198 @@ +import pixelsToRem from '#app/utilities/pixelsToRem'; +import { css, Theme } from '@emotion/react'; + +export default { + brandDivider: css({ + position: 'relative', + width: '100%', + margin: '0 auto', + + '&::after': { + content: "''", + display: 'block', + width: '100%', + borderBottom: `${pixelsToRem(1)}rem solid #d77272`, + }, + }), + bottomDivider: ({ palette }: Theme) => + css({ + position: 'absolute', + width: '100%', + insetInlineStart: 0, + '@media (min-width: 1041px)': { + width: '100%', + insetInlineStart: '0', + }, + '&::after': { + content: "''", + position: 'absolute', + insetBlockEnd: 0, + width: '100%', + borderBottom: `${pixelsToRem(1)}rem solid ${palette.GREY_3}`, + }, + }), + navStack: css({ + display: 'flex', + flexDirection: 'column', + width: '100%', + }), + topRow: ({ palette }: Theme) => + css({ + display: 'flex', + flexDirection: 'row', + alignItems: 'stretch', + justifyContent: 'space-between', + position: 'relative', + zIndex: 0, + + '&::before': { + content: "''", + position: 'absolute', + zIndex: -1, + top: 0, + bottom: 0, + width: '100vw', + left: '50%', + transform: 'translateX(-50%)', + background: palette.POSTBOX, + pointerEvents: 'none' /* ensure it never interferes with clicks */, + }, + }), + topRowItems: ({ palette }: Theme) => + css({ + li: { + a: { + color: palette.WHITE, + + '&:hover::after': { + borderBottomColor: palette.WHITE, + }, + '&:focus::after': { + borderBottomColor: palette.WHITE, + }, + '&:focus-visible::after': { + borderBottomColor: palette.WHITE, + }, + }, + + 'a[data-active="true"]': { + span: { + '&::after': { + borderBottomColor: palette.WHITE, + }, + }, + }, + + '&:before': { + content: '""', + position: 'absolute', + insetInlineEnd: 0, + top: '50%', + transform: 'translateY(-50%)', + height: '60%', + width: `${pixelsToRem(1)}rem`, + background: '#D77272', + display: 'block', + opacity: 1, + }, + + '&:last-child:before': { + display: 'none', + }, + }, + + '&:after': { + background: 'none', + }, + }), + bottomRowItems: ({ palette }: Theme) => + css({ + li: { + '&:before': { + content: '""', + position: 'absolute', + insetInlineEnd: 0, + top: '50%', + transform: 'translateY(-50%)', + height: '60%', + width: `${pixelsToRem(1)}rem`, + background: palette.GREY_4, + display: 'block', + opacity: 1, + }, + + '&:last-child:before': { + display: 'none', + }, + }, + }), + dropdown: ({ palette, spacings }: Theme) => + css({ + position: 'absolute', + top: '100%', + left: '0', + width: '100%', + zIndex: 99999, + + borderBottom: `${pixelsToRem(3)}rem solid ${palette.POSTBOX}`, + + ul: { + padding: 0, + border: 'none', + + li: { + padding: 0, + + '&:last-child': { + paddingBottom: 0, + }, + }, + }, + + a: { + display: 'block', + position: 'relative', + paddingInline: `${spacings.FULL}rem`, + + '&:hover': { + backgroundColor: palette.GREY_3, + textDecoration: 'none', + }, + + '&:before': { + content: '""', + position: 'absolute', + top: 0, + insetInlineStart: 0, + height: '100%', + width: `${pixelsToRem(4)}rem`, + background: palette.POSTBOX, + display: 'block', + opacity: 0, + }, + + '&:hover::before': { + opacity: 1, + }, + + '&:focus-visible': { + outlineOffset: `-${pixelsToRem(3)}rem`, + }, + }, + }), + lowerNavWrapper: css({ + width: '100%', + position: 'relative', + zIndex: 1, + }), + menuButton: ({ palette }: Theme) => + css({ + backgroundColor: palette.POSTBOX, + color: palette.WHITE, + + svg: { + verticalAlign: 'middle', + fill: palette.WHITE, + }, + }), +}; diff --git a/src/app/components/Navigation/index.test.tsx b/src/app/components/Navigation/index.test.tsx new file mode 100644 index 00000000000..9ec3f5ede0f --- /dev/null +++ b/src/app/components/Navigation/index.test.tsx @@ -0,0 +1,5 @@ +describe('Navigation', () => { + it('should render', () => { + expect(true).toBeTruthy(); + }); +}); diff --git a/src/app/components/Navigation/index.tsx b/src/app/components/Navigation/index.tsx new file mode 100644 index 00000000000..7692bc9f8c0 --- /dev/null +++ b/src/app/components/Navigation/index.tsx @@ -0,0 +1,297 @@ +import React, { use } from 'react'; +import { NavigationUl, NavigationLi } from '#psammead/psammead-navigation/src'; +import { + DropdownUl, + DropdownLi, +} from '#psammead/psammead-navigation/src/DropdownNavigation'; +import useClickTrackerHandler from '#app/hooks/useClickTrackerHandler'; +import useViewTracker from '#app/hooks/useViewTracker'; +import { RequestContext } from '#contexts/RequestContext'; +import { ServiceContext } from '#contexts/ServiceContext'; +import { Direction, Navigation, PageTypes } from '#app/models/types/global'; +import Canonical from './index.canonical'; +import Amp from './index.amp'; +import type { NavigationContainerProps } from './types'; +import styles from './index.styles'; + +const getTopItemA11yProps = ({ + item, + index, + active, + pageType, +}: { + item: Navigation; + index: number; + active: boolean; + pageType?: PageTypes; +}) => { + const shouldAnnounceCurrentPage = + pageType === 'home' && active && index === 0; + + if (!active || shouldAnnounceCurrentPage) { + return {}; + } + + return { + 'aria-current': undefined, + 'aria-label': item.title, + 'aria-labelledby': undefined, + }; +}; + +/** + * EXPECTED DATA SHAPE (from server): + * navItems: Navigation[] where each item is: + * { + * title: string; + * url: string; // relative e.g. "/arabic" + * hideOnLiteSite?: boolean; + * subItems?: Navigation[]; // child items with same shape (title/url/hideOnLiteSite) + * } + */ + +type RenderListItemsArgs = { + Li: React.ElementType; + navigation: Navigation[]; + currentPage: string; + dir: Direction; + activeIndex: number; + clickTracker: unknown; + viewTracker: unknown; + isLite?: boolean; + pageType?: PageTypes; +}; + +const renderListItems = ({ + Li, + navigation, + currentPage, + dir, + activeIndex, + clickTracker, + viewTracker, + isLite, + pageType, +}: RenderListItemsArgs) => + navigation + // For Lite pages, filter out any items that should be hidden on the Lite site + .filter(item => !(item.hideOnLiteSite && isLite)) + .map((item, index) => { + const { title, url } = item; + const active = index === activeIndex; + const a11yProps = + getTopItemA11yProps({ item, index, active, pageType }) ?? {}; + + return ( + + {title} + + ); + }); + +// this checks if the current pages url matches the navigation item url. We need this to determine which nav item should be active +const matchesUrl = ( + canonicalLink: string | undefined, + origin: string, + navUrl?: string, +) => { + if (!canonicalLink || !navUrl) return false; + const absolute = `${origin}${navUrl}`; + return canonicalLink === absolute; +}; + +/** + * Find which top item should be active: + * - If current page matches a top item url -> that index is active + * - Else if it matches any subItem url -> parent index is active + * - Else if pageType === 'home' -> 0 + * - Else -> -1 (no active) + */ +const getActiveTopIndex = ({ + topItems, + canonicalLink, + origin, + pageType, +}: { + topItems: Navigation[]; + canonicalLink?: string; + origin: string; + pageType?: PageTypes; +}) => { + if (!topItems?.length) return -1; + + // try to find a direct match on the top-level items with the current page URL + // it returns the index of the first item that matches or -1 if none match + const directMatchIndex = topItems.findIndex(item => + matchesUrl(canonicalLink, origin, item.url), + ); + // if a match is found, return the index of the matching top-level item (this is the active item) + if (directMatchIndex > -1) return directMatchIndex; + + // if no direct match in the top level items, check if any of the subItems match the current page URL + // this is so that if a subItem matches the current page, its parent top-level item will be marked as active in the navigation + // as the top level navigation has only one link per item, but the bottom navigation can have many, + // this means that any page categorised under 'Watch' for example, + // will have the 'Watch' top-level nav item highlighted as active, + // even if the user is on a page that doesn't directly match the 'Watch' URL + const parentIndexByChild = topItems.findIndex(parent => + (parent.subItems || []).some(child => + matchesUrl(canonicalLink, origin, child.url), + ), + ); + if (parentIndexByChild > -1) return parentIndexByChild; + + // We always want the first top level nav item to be active on the home page, + // and the first nav item should always be 'Home' + if (pageType === 'home') return 0; + + return -1; +}; + +const NavigationContainer: React.FC = ({ + navItems, + propsForTopBarOJComponent, +}) => { + const { isAmp, isLite, pageType, canonicalLink, origin } = + use(RequestContext); + + const { + translations, + navigation: navFromServiceConfig, + dir, + } = use(ServiceContext); + + const { currentPage, navMenuText } = translations; + + const { blocks = [] } = propsForTopBarOJComponent || {}; + + const navEventTrackingMetadata = { componentName: 'scrollable-navigation' }; + const dropdownNavEventTrackingData = { componentName: 'dropdown-navigation' }; + + const topNavClickTrackerHandler = useClickTrackerHandler( + navEventTrackingMetadata, + ); + const bottomNavClickTrackerHandler = useClickTrackerHandler( + navEventTrackingMetadata, + ); + const dropdownNavClickTrackerHandler = useClickTrackerHandler( + dropdownNavEventTrackingData, + ); + + const topNavViewTracker = useViewTracker(navEventTrackingMetadata); + const bottomNavViewTracker = useViewTracker(navEventTrackingMetadata); + const dropdownNavViewTracker = useViewTracker(dropdownNavEventTrackingData); + + /** + * Prefer server-provided navItems; fallback to ServiceContext.navigation if missing. + */ + const navigationItems = navItems || navFromServiceConfig; + + if (!navigationItems || navigationItems.length === 0) { + return null; + } + + // Compute which top item is active based on current URL + const topActiveIndex = getActiveTopIndex({ + topItems: navigationItems, + canonicalLink, + origin, + pageType, + }); + + const topScrollableListItems = ( + + {renderListItems({ + Li: NavigationLi, + navigation: navigationItems, + currentPage, + dir, + activeIndex: topActiveIndex, + clickTracker: topNavClickTrackerHandler, + viewTracker: topNavViewTracker, + isLite, + pageType, + })} + + ); + + /** + * Build the bottom scrollable nav from the active top item's subItems. + * If nothing matched, you can choose to show the first group's subItems + * (useful for non-matching routes) or show an empty list. + */ + const activeTop = + topActiveIndex > -1 ? navigationItems[topActiveIndex] : navigationItems[0]; + const bottomItems = activeTop?.subItems || []; + + // Find the active subitem index in the bottom nav + const activeBottomIndex = bottomItems.findIndex(item => + matchesUrl(canonicalLink, origin, item.url), + ); + + const bottomScrollableListItems = ( + + {renderListItems({ + Li: NavigationLi, + navigation: bottomItems, + currentPage, + dir, + activeIndex: activeBottomIndex, + clickTracker: bottomNavClickTrackerHandler, + viewTracker: bottomNavViewTracker, + isLite, + pageType, + })} + + ); + + // Dropdown menu: prioritise the first top-level item and all its subitems + // CHANGE WHEN HAVE ANSWER TO THE QUESTION ABOUT THIS + const dropdownSource = (() => { + if (!navigationItems.length) return []; + const [first, ..._] = navigationItems; + return [first, ...(first.subItems || [])]; + })(); + + const dropdownListItems = ( + + {renderListItems({ + Li: DropdownLi, + navigation: dropdownSource, + currentPage, + dir, + activeIndex: -1, + clickTracker: dropdownNavClickTrackerHandler, + viewTracker: dropdownNavViewTracker, + pageType, + })} + + ); + + const NavigationRenderer = isAmp ? Amp : Canonical; + + return ( + <> + + + > + ); +}; + +export default NavigationContainer; diff --git a/src/app/components/Navigation/testHelpers.tsx b/src/app/components/Navigation/testHelpers.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/components/Navigation/types.ts b/src/app/components/Navigation/types.ts new file mode 100644 index 00000000000..f78f26a91e4 --- /dev/null +++ b/src/app/components/Navigation/types.ts @@ -0,0 +1,18 @@ +import { Direction, Navigation } from '#app/models/types/global'; +import { TopStoryItem } from '#app/pages/ArticlePage/PagePromoSections/TopStoriesSection/types'; + +export interface AmpNavigationContainerProps { + dir: Direction; + menuAnnouncedText: string; + topScrollableListItems?: React.ReactNode; + bottomScrollableListItems: React.ReactNode; + dropdownListItems: React.ReactNode; +} + +export interface NavigationContainerProps { + navItems: Navigation[]; + currentPath: string; + propsForTopBarOJComponent?: { + blocks?: TopStoryItem[]; + }; +} diff --git a/src/app/components/ThemeProvider/index.test.tsx b/src/app/components/ThemeProvider/index.test.tsx index 98819cb5a09..f8de98743e2 100644 --- a/src/app/components/ThemeProvider/index.test.tsx +++ b/src/app/components/ThemeProvider/index.test.tsx @@ -223,6 +223,11 @@ describe('ThemeProvider', () => { }); describe.each(SERVICES)(`brandSVG for %s`, service => { + beforeAll(() => { + // TODO: Consider removing this one this check is removed: https://github.com/bbc/simorgh/blob/4bfea6e86e65e3fdd374ff5432bae575366a343b/src/app/legacy/psammead/psammead-brand/src/index.jsx#L155 + process.env.SIMORGH_APP_ENV = 'live'; + }); + const children = child; it(`should match chameleonLogos/${service}.tsx`, async () => { await act(async () => { diff --git a/src/app/legacy/containers/Brand/index.jsx b/src/app/legacy/containers/Brand/index.jsx index 5af2ae96222..43d1444d98e 100644 --- a/src/app/legacy/containers/Brand/index.jsx +++ b/src/app/legacy/containers/Brand/index.jsx @@ -64,6 +64,7 @@ const BrandContainer = ({ skipLink={skipLink} scriptLink={scriptLink} isLongBrand={longBrands.includes(service)} + service={service} ref={brandRef} {...props} > diff --git a/src/app/legacy/containers/Header/NewLogoBanner.tsx b/src/app/legacy/containers/Header/NewLogoBanner.tsx new file mode 100644 index 00000000000..537ddc88a4a --- /dev/null +++ b/src/app/legacy/containers/Header/NewLogoBanner.tsx @@ -0,0 +1,28 @@ +import styles from './index.styles'; + +const NewLogoBanner = () => ( + + + + + + + +); + +export default NewLogoBanner; diff --git a/src/app/legacy/containers/Header/index.jsx b/src/app/legacy/containers/Header/index.jsx index e46d611494a..b836bd537b0 100644 --- a/src/app/legacy/containers/Header/index.jsx +++ b/src/app/legacy/containers/Header/index.jsx @@ -11,11 +11,15 @@ import { LIVE_PAGE, } from '#app/routes/utils/pageTypes'; import LiteSiteSummary from '#app/components/LiteSiteSummary'; +import NewNavigationContainer from '#src/app/components/Navigation'; +import LegacyNavigationContainer from '#src/app/legacy/containers/Navigation'; import AccountHeader from '#app/components/Account/AccountHeader'; +import isLive from '#lib/utilities/isLive'; +import SERVICES_WITH_NEW_NAV from '#app/components/Navigation/config'; import { ServiceContext } from '../../../contexts/ServiceContext'; import ConsentBanner from '../ConsentBanner'; -import NavigationContainer from '../Navigation'; import BrandContainer from '../Brand'; +import NewLogoBanner from './NewLogoBanner'; const Header = ({ brandRef, @@ -101,8 +105,16 @@ const HeaderContainer = ({ navItems, propsForTopBarOJComponent }) => { if (isApp) return null; + const NavigationComponent = + SERVICES_WITH_NEW_NAV.includes(service) && !isLive() + ? NewNavigationContainer + : LegacyNavigationContainer; + return ( + {SERVICES_WITH_NEW_NAV.includes(service) && !isLive() && ( + + )} {isAmp ? ( { )} {isLite && } - diff --git a/src/app/legacy/containers/Header/index.styles.ts b/src/app/legacy/containers/Header/index.styles.ts new file mode 100644 index 00000000000..a19e104fc3b --- /dev/null +++ b/src/app/legacy/containers/Header/index.styles.ts @@ -0,0 +1,37 @@ +import { css, Theme } from '@emotion/react'; + +const SVG_WRAPPER_MAX_WIDTH_ABOVE_1280PX = '63rem'; +const LOGO_ASPECT_RATIO = 168 / 48; +const SVG_HEIGHT = 38; + +export default { + banner: ({ palette, mq, spacings }: Theme) => + css({ + background: palette.WHITE, + width: '100%', + maxWidth: SVG_WRAPPER_MAX_WIDTH_ABOVE_1280PX, + margin: '0 auto', + padding: `0 ${spacings.DOUBLE}rem`, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '4rem', + + [mq.GROUP_3_MIN_WIDTH]: { + justifyContent: 'flex-start', + }, + + [mq.GROUP_4_MIN_WIDTH]: { + padding: 0, + }, + }), + + logoSvg: ({ palette }: Theme) => + css({ + boxSizing: 'content-box', + color: palette.BLACK, + fill: 'currentColor', + height: `${SVG_HEIGHT}px`, + maxWidth: `${LOGO_ASPECT_RATIO * SVG_HEIGHT}px`, + }), +}; diff --git a/src/app/legacy/containers/Navigation/__snapshots__/index.test.jsx.snap b/src/app/legacy/containers/Navigation/__snapshots__/index.test.jsx.snap index 31d566287c3..975bbecc11b 100644 --- a/src/app/legacy/containers/Navigation/__snapshots__/index.test.jsx.snap +++ b/src/app/legacy/containers/Navigation/__snapshots__/index.test.jsx.snap @@ -696,8 +696,10 @@ exports[`Navigation Container should correctly render amp navigation 1`] = ` role="listitem" > ( ); export default { - title: 'Containers/Navigation', + title: 'Containers/Navigation/Legacy', Component, parameters: { chromatic: { disable: true } }, }; diff --git a/src/app/legacy/psammead/psammead-brand/src/index.jsx b/src/app/legacy/psammead/psammead-brand/src/index.jsx index c95041dcef0..d09ffe08423 100644 --- a/src/app/legacy/psammead/psammead-brand/src/index.jsx +++ b/src/app/legacy/psammead/psammead-brand/src/index.jsx @@ -12,6 +12,9 @@ import { GEL_SPACING, GEL_SPACING_DBL, } from '#psammead/gel-foundations/src/spacings'; +import ArabicBrandSVG from '#app/components/Header/brand-svgs/ArabicBrandSVG'; +import isLive from '#lib/utilities/isLive'; +import SERVICES_WITH_NEW_NAV from '#app/components/Navigation/config'; import { focusIndicatorThickness } from '../../../../components/ThemeProvider/focusIndicator'; import VisuallyHiddenText from '../../../../components/VisuallyHiddenText'; @@ -148,7 +151,52 @@ const StyledBrand = ({ serviceLocalisedName = null, svg, isLongBrand, + service, }) => { + if (SERVICES_WITH_NEW_NAV.includes(service) && !isLive()) { + const svgMap = { + arabic: ( + + ), + // TODO: Get new logo for Tamil + tamil: ( + + {svg.group} + + ), + }; + + return ( + <> + {svgMap[service]} + + > + ); + } + return svg ? ( <> { +export const CanonicalDropdown = ({ isOpen, children, className = '' }) => { const heightRef = useRef(null); - return ( {children} @@ -207,14 +207,15 @@ export const CanonicalMenuButton = ({ isOpen, onClick, dir = 'ltr', - script, + script = '', + className = '', }) => ( {isOpen ? navigationIcons.cross : navigationIcons.hamburger} {announcedText} @@ -240,7 +241,7 @@ export const AmpMenuButton = ({ announcedText, onToggle, dir = 'ltr', - script, + script = '', }) => ( <> diff --git a/src/app/legacy/psammead/psammead-navigation/src/ScrollableNavigation/index.jsx b/src/app/legacy/psammead/psammead-navigation/src/ScrollableNavigation/index.jsx index 31dac1773ad..80a508a3b85 100644 --- a/src/app/legacy/psammead/psammead-navigation/src/ScrollableNavigation/index.jsx +++ b/src/app/legacy/psammead/psammead-navigation/src/ScrollableNavigation/index.jsx @@ -72,8 +72,17 @@ const StyledScrollableNav = styled.div` } `; -export const ScrollableNavigation = ({ children, dir = 'ltr', ...props }) => ( - +export const ScrollableNavigation = ({ + children, + dir = 'ltr', + navPosition, + ...props +}) => ( + {children} ); diff --git a/src/app/legacy/psammead/psammead-navigation/src/__snapshots__/index.test.jsx.snap b/src/app/legacy/psammead/psammead-navigation/src/__snapshots__/index.test.jsx.snap index 1703615d2d6..b7482e1d9c3 100644 --- a/src/app/legacy/psammead/psammead-navigation/src/__snapshots__/index.test.jsx.snap +++ b/src/app/legacy/psammead/psammead-navigation/src/__snapshots__/index.test.jsx.snap @@ -250,8 +250,10 @@ exports[`Navigation should render correctly 1`] = ` role="listitem" > @@ -570,8 +572,10 @@ exports[`Navigation should render correctly when ampOpenClass prop is provided 1 role="listitem" > @@ -884,8 +888,10 @@ exports[`Navigation should render correctly when isOpen is true 1`] = ` role="listitem" > @@ -1253,8 +1259,10 @@ exports[`Scrollable Navigation should render correctly 1`] = ` role="listitem" > diff --git a/src/app/legacy/psammead/psammead-navigation/src/index.jsx b/src/app/legacy/psammead/psammead-navigation/src/index.jsx index 72b814722a8..3868a4ca522 100644 --- a/src/app/legacy/psammead/psammead-navigation/src/index.jsx +++ b/src/app/legacy/psammead/psammead-navigation/src/index.jsx @@ -69,15 +69,13 @@ const StyledLink = styled.a` props.theme.palette.POSTBOX}; ${({ currentLink, theme }) => currentLink && - ` - border-bottom: ${CURRENT_ITEM_HOVER_BORDER} solid ${theme.palette.POSTBOX}; - `} + `border-bottom: ${CURRENT_ITEM_HOVER_BORDER} solid ${theme.palette.POSTBOX};`} } &:focus::after { ${ListItemBorder} - border-bottom: ${GEL_SPACING_HLF} solid ${props => - props.theme.palette.POSTBOX}; + border-bottom: ${GEL_SPACING_HLF} solid ${({ theme }) => + theme.palette.POSTBOX}; top: 0; border: ${focusIndicatorThickness} solid ${props => props.theme.palette.BLACK}; @@ -86,8 +84,8 @@ const StyledLink = styled.a` /* Custom focus indicator styling applied to pseudo-element. Global focus indicator styling has been removed. */ &:focus-visible::after { ${ListItemBorder} - border-bottom: ${GEL_SPACING_HLF} solid ${props => - props.theme.palette.POSTBOX}; + border-bottom: ${GEL_SPACING_HLF} solid + ${({ theme }) => theme.palette.POSTBOX}; top: 0; border: ${focusIndicatorThickness} solid ${props => props.theme.palette.BLACK}; @@ -124,8 +122,8 @@ const StyledListItem = styled.li` const StyledSpan = styled.span` &::after { ${ListItemBorder} - border-bottom: ${GEL_SPACING_HLF} solid ${props => - props.theme.palette.POSTBOX}; + border-bottom: ${GEL_SPACING_HLF} solid + ${({ theme }) => theme.palette.POSTBOX}; } `; @@ -175,7 +173,9 @@ export const NavigationLi = ({ currentLink // This is a temporary fix for the a11y nested span's bug experienced in TalkBack, refer to the following issue: https://github.com/bbc/simorgh/issues/9652 aria-labelledby={`NavigationLinks-${link}`} + aria-current="page" className="focusIndicatorRemove" + data-active="true" {...clickTracker} {...props} > @@ -193,6 +193,7 @@ export const NavigationLi = ({ script={script} service={service} className="focusIndicatorRemove" + aria-current={active ? 'page' : undefined} {...clickTracker} {...props} > diff --git a/src/app/lib/utilities/fetchConfig/index.ts b/src/app/lib/utilities/fetchConfig/index.ts index 6eb079b4112..732ea62c85b 100644 --- a/src/app/lib/utilities/fetchConfig/index.ts +++ b/src/app/lib/utilities/fetchConfig/index.ts @@ -6,6 +6,8 @@ import getAgent from '#src/server/utilities/getAgent'; import certsRequired from '#app/routes/utils/certsRequired'; import { FetchError } from '#app/models/types/fetch'; import getEnvironment from '#app/routes/utils/getEnvironment'; +import SERVICES_WITH_NEW_NAV from '#app/components/Navigation/config'; +import isLive from '#lib/utilities/isLive'; import { PRIMARY_DATA_TIMEOUT } from '../getFetchTimeouts'; const logger = nodeLogger(__filename); @@ -35,10 +37,16 @@ const fetchConfig = async ({ const fetchUrl = new URL(process.env.BFF_PATH as string); fetchUrl.searchParams.set('service', service); fetchUrl.searchParams.set('config', configType); + if (variant) { fetchUrl.searchParams.set('variant', variant); } + // Only fetch new nav for Arabic and Tamil services on Local/Test + if (SERVICES_WITH_NEW_NAV.includes(service) && !isLive()) { + fetchUrl.searchParams.set('useNewNav', 'true'); + } + const bffReqPath = fetchUrl.toString(); const cachedResponse = cache.get(bffReqPath); @@ -59,6 +67,10 @@ const fetchConfig = async ({ const fetchOptions = { ...(agent && { agent }), ...(!isLocal && { headers: { 'ctx-service-env': environment } }), + // TODO: Temporary override to fetch from Test data for new navigation until it's ready in Live + ...(fetchUrl.searchParams.get('useNewNav') === 'true' && { + headers: { 'ctx-service-env': 'test' }, + }), signal: AbortSignal.timeout(PRIMARY_DATA_TIMEOUT), }; @@ -70,7 +82,6 @@ const fetchConfig = async ({ cache.set(bffReqPath, res); return res as T; } - const error = new Error() as FetchError; error.status = response.status; diff --git a/src/app/models/types/global.ts b/src/app/models/types/global.ts index 542bc8bb7be..5cefe43e951 100644 --- a/src/app/models/types/global.ts +++ b/src/app/models/types/global.ts @@ -28,6 +28,7 @@ export type Navigation = { title: string; url: string; hideOnLiteSite?: boolean; + subItems?: Navigation[]; }; export type ComponentExperimentProps = { diff --git a/ws-nextjs-app/integration/pages/homePage/arabic/__snapshots__/canonical.test.ts.snap b/ws-nextjs-app/integration/pages/homePage/arabic/__snapshots__/canonical.test.ts.snap index 3ae1a8f647c..8ce02e3670b 100644 --- a/ws-nextjs-app/integration/pages/homePage/arabic/__snapshots__/canonical.test.ts.snap +++ b/ws-nextjs-app/integration/pages/homePage/arabic/__snapshots__/canonical.test.ts.snap @@ -133,7 +133,8 @@ exports[`Canonical Home Page Header I can see the branding 1`] = ` { "brandLink": "BBC News, عربي", "svg": , } `; diff --git a/ws-nextjs-app/integration/pages/live/arabic/__snapshots__/canonical.test.ts.snap b/ws-nextjs-app/integration/pages/live/arabic/__snapshots__/canonical.test.ts.snap index 17cd15de7b9..02d5d21d321 100644 --- a/ws-nextjs-app/integration/pages/live/arabic/__snapshots__/canonical.test.ts.snap +++ b/ws-nextjs-app/integration/pages/live/arabic/__snapshots__/canonical.test.ts.snap @@ -4,7 +4,8 @@ exports[`Canonical Live Header I can see the branding 1`] = ` { "brandLink": "BBC News, عربي", "svg": , } `; diff --git a/ws-nextjs-app/integration/pages/mediaAssetPage/arabicTC2/__snapshots__/canonical.test.ts.snap b/ws-nextjs-app/integration/pages/mediaAssetPage/arabicTC2/__snapshots__/canonical.test.ts.snap index aac85265345..99380c540f5 100644 --- a/ws-nextjs-app/integration/pages/mediaAssetPage/arabicTC2/__snapshots__/canonical.test.ts.snap +++ b/ws-nextjs-app/integration/pages/mediaAssetPage/arabicTC2/__snapshots__/canonical.test.ts.snap @@ -115,7 +115,8 @@ exports[`Canonical Media Asset Page Header I can see the branding 1`] = ` { "brandLink": "BBC News, عربي", "svg": , } `;