From e25838a05347e00e90ec951a67f1cd2a243ab501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Wed, 4 Feb 2026 21:55:17 +0000 Subject: [PATCH 01/82] wip duplication of nav --- .../Navigation/LanguageNavigation/index.tsx | 27 ++ .../Navigation/LanguageNavigation/lazy.tsx | 9 + .../components/Navigation/index.amp.test.jsx | 41 ++ src/app/components/Navigation/index.amp.tsx | 63 +++ .../Navigation/index.canonical.test.jsx | 93 +++++ .../components/Navigation/index.canonical.tsx | 85 ++++ .../components/Navigation/index.stories.tsx | 70 ++++ src/app/components/Navigation/index.test.tsx | 370 ++++++++++++++++++ src/app/components/Navigation/index.tsx | 160 ++++++++ src/app/components/Navigation/testHelpers.tsx | 14 + src/app/components/Navigation/types.ts | 54 +++ .../legacy/containers/Navigation/index.jsx | 2 +- .../containers/Navigation/index.stories.jsx | 2 +- 13 files changed, 988 insertions(+), 2 deletions(-) create mode 100644 src/app/components/Navigation/LanguageNavigation/index.tsx create mode 100644 src/app/components/Navigation/LanguageNavigation/lazy.tsx create mode 100644 src/app/components/Navigation/index.amp.test.jsx create mode 100644 src/app/components/Navigation/index.amp.tsx create mode 100644 src/app/components/Navigation/index.canonical.test.jsx create mode 100644 src/app/components/Navigation/index.canonical.tsx create mode 100644 src/app/components/Navigation/index.stories.tsx create mode 100644 src/app/components/Navigation/index.test.tsx create mode 100644 src/app/components/Navigation/index.tsx create mode 100644 src/app/components/Navigation/testHelpers.tsx create mode 100644 src/app/components/Navigation/types.ts diff --git a/src/app/components/Navigation/LanguageNavigation/index.tsx b/src/app/components/Navigation/LanguageNavigation/index.tsx new file mode 100644 index 00000000000..94869d4c042 --- /dev/null +++ b/src/app/components/Navigation/LanguageNavigation/index.tsx @@ -0,0 +1,27 @@ +import { Fragment, use } from 'react'; +import VisuallyHiddenText from '#app/components/VisuallyHiddenText'; +import CollapsibleNavigation from '#app/components/CollapsibleNavigation'; +import Navigation from '#app/legacy/psammead/psammead-navigation/src'; +import { ServiceContext } from '#app/contexts/ServiceContext'; + +const LanguageNavigation = () => { + const { script, service, dir, collapsibleNavigation } = use(ServiceContext); + + if (!collapsibleNavigation?.length) { + return null; + } + + return ( + + + Navigation, BBC World Service regions + + + + ); +}; + +export default LanguageNavigation; diff --git a/src/app/components/Navigation/LanguageNavigation/lazy.tsx b/src/app/components/Navigation/LanguageNavigation/lazy.tsx new file mode 100644 index 00000000000..30614835c8f --- /dev/null +++ b/src/app/components/Navigation/LanguageNavigation/lazy.tsx @@ -0,0 +1,9 @@ +import loadable from 'next/dynamic'; + +export default loadable( + () => + import( + /* webpackChunkName: "language_navigation" */ + '.' + ), +); diff --git a/src/app/components/Navigation/index.amp.test.jsx b/src/app/components/Navigation/index.amp.test.jsx new file mode 100644 index 00000000000..34be2dc10b8 --- /dev/null +++ b/src/app/components/Navigation/index.amp.test.jsx @@ -0,0 +1,41 @@ +import latin from '../ThemeProvider/fontScripts/latin'; +import AmpNavigation from './index.amp'; +import { + dropdownTestId, + scrollableTestId, + dropdownListItems, + scrollableListItems, +} from './testHelpers'; +import { render } from '../react-testing-library-with-providers'; + +const navigation = ( + +); + +describe('AMP Navigation', () => { + describe('Snapshots', () => { + it('should correctly render AMP navigation', () => { + const { container } = render(navigation); + expect(container).toMatchSnapshot(); + }); + }); + + describe('Assertions', () => { + it('should render scrollable nav and a hidden dropdown', () => { + const { queryByTestId } = render(navigation); + const dropdown = queryByTestId(dropdownTestId).parentElement; + const scrollableNav = queryByTestId(scrollableTestId); + expect(scrollableNav.innerHTML).toBe('
  • List Items
  • '); + expect(dropdown).not.toBeVisible(); + }); + }); + + // AMP state toggling tested by an e2e. +}); diff --git a/src/app/components/Navigation/index.amp.tsx b/src/app/components/Navigation/index.amp.tsx new file mode 100644 index 00000000000..0c105114c20 --- /dev/null +++ b/src/app/components/Navigation/index.amp.tsx @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +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 { 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 = ({ + script, + service, + dir, + menuAnnouncedText, + scrollableListItems, + dropdownListItems, +}: AmpNavigationContainerProps) => ( + + + {/* Hidden attribute allows us to toggle visibility on the dropdown + using AMP actions. */} + + + {scrollableListItems} + + +); + +export default AmpNavigationContainer; diff --git a/src/app/components/Navigation/index.canonical.test.jsx b/src/app/components/Navigation/index.canonical.test.jsx new file mode 100644 index 00000000000..d4c5e888b44 --- /dev/null +++ b/src/app/components/Navigation/index.canonical.test.jsx @@ -0,0 +1,93 @@ +import CanonicalNavigation from './index.canonical'; +import { + dropdownTestId, + scrollableTestId, + dropdownListItems, + scrollableListItems, +} from './testHelpers'; +import { render, fireEvent } from '../react-testing-library-with-providers'; +import latin from '../ThemeProvider/fontScripts/latin'; + +const blocks = [{ id: '1', title: 'Story' }]; + +const navigationProps = { + scrollableListItems, + dropdownListItems, + menuAnnouncedText: 'menu', + script: latin, + service: 'pidgin', + dir: 'ltr', +}; + +const navigation = ( + +); + +describe('Canonical Navigation', () => { + describe('snapshots', () => { + it('should correctly render Canonical navigation', () => { + const { container } = render(navigation); + expect(container).toMatchSnapshot(); + }); + }); + + describe('assertions', () => { + it('should render scrollable nav and hide dropdown', () => { + const { queryByTestId } = render(navigation); + const dropdown = queryByTestId(dropdownTestId).parentElement; + const scrollableNav = queryByTestId(scrollableTestId); + expect(scrollableNav.innerHTML).toBe('
  • List Items
  • '); + expect(dropdown).toHaveAttribute('height', '0'); + }); + + it('should render dropdown and no scrollable nav after menu button clicked', () => { + const { queryByTestId, queryByText } = render(navigation); + + fireEvent.click(queryByText('menu')); + + const dropdown = queryByTestId(dropdownTestId); + const scrollableNav = queryByTestId(scrollableTestId); + expect(scrollableNav).toBeNull(); + expect(dropdown.innerHTML).toBe('
  • Dropdown Items
  • '); + }); + + describe('Top Bar OJs', () => { + it.each([ + [ + 'should not render TopBarOJs when toggle is off', + { ...navigationProps, blocks }, + { topBarOJs: { enabled: false } }, + queryByTestId => + expect(queryByTestId('top-bar-onward-journeys')).toBeNull(), + ], + [ + 'should render TopBarOJs when toggle is on and blocks are provided', + { ...navigationProps, blocks }, + { topBarOJs: { enabled: true } }, + queryByTestId => + expect(queryByTestId('top-bar-onward-journeys')).not.toBeNull(), + ], + [ + 'should not render TopBarOJs when blocks are empty even if toggle is on', + { ...navigationProps, blocks: [] }, + { topBarOJs: { enabled: true } }, + queryByTestId => + expect(queryByTestId('top-bar-onward-journeys')).toBeNull(), + ], + ])('%s', (_, props, toggles, assertion) => { + const { queryByTestId } = render(, { + toggles, + service: props.service, + }); + assertion(queryByTestId); + }); + }); + }); +}); diff --git a/src/app/components/Navigation/index.canonical.tsx b/src/app/components/Navigation/index.canonical.tsx new file mode 100644 index 00000000000..49b172da6c4 --- /dev/null +++ b/src/app/components/Navigation/index.canonical.tsx @@ -0,0 +1,85 @@ +import { useState, use } from 'react'; +import styled from '@emotion/styled'; +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 { GEL_GROUP_2_SCREEN_WIDTH_MAX } from '#psammead/gel-foundations/src/breakpoints'; +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 { CanonicalNavigationContainerProps } from './types'; + +const ScrollableWrapper = styled.div` + position: relative; +`; +const Divider = styled.div` + position: absolute; + width: calc(100vw - 0.8rem); + inset-inline-start: 0; + @media (min-width: 1041px) { + width: calc(100vw + 0.8rem); + inset-inline-start: calc(-1 * (100vw - 1014px) / 2); + } + &::after { + content: ''; + position: absolute; + inset-block-end: 0; + inset-inline: -0.8rem 0; + width: calc(100% + 0.8rem); + border-bottom: 0.0625rem solid ${props => props.theme.palette.GREY_3}; + } + @media (min-width: 1008px) { + display: none; + } +`; +const CanonicalNavigationContainer = ({ + script, + service, + dir, + menuAnnouncedText, + scrollableListItems, + dropdownListItems, + blocks, +}: CanonicalNavigationContainerProps) => { + const { isLite } = use(RequestContext); + const { enabled } = useToggle('topBarOJs'); + const [isOpen, setIsOpen] = useState(false); + useMediaQuery(`(max-width: ${GEL_GROUP_2_SCREEN_WIDTH_MAX})`, event => { + if (!event.matches) { + setIsOpen(false); + } + }); + + const topBarBlocks = Array.isArray(blocks) ? (blocks as TopStoryItem[]) : []; + + return ( + + + {!isLite && ( + setIsOpen(!isOpen)} + dir={dir} + script={script} + /> + )} + {!isOpen && ( + + {scrollableListItems} + + )} + + {dropdownListItems} + + {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..abc83bedbfb --- /dev/null +++ b/src/app/components/Navigation/index.stories.tsx @@ -0,0 +1,70 @@ +import { RequestContextProvider } from '#contexts/RequestContext'; +import { HOME_PAGE } from '#app/routes/utils/pageTypes'; +import { + topStoriesBlocks, + mostReadBlocks, +} from '../ArticleLinksBlock/helpers/fixtureData'; +import AmpDecorator from '../../../../.storybook/helpers/ampDecorator/index.jsx'; +import Navigation from '.'; + +import type { Services } from '#models/types/global'; +import type { PropsForTopBarOJComponent } from './types'; + +type StoryComponentProps = { + isAmp?: boolean; + service: Services; + propsForTopBarOJComponent?: PropsForTopBarOJComponent | null; +}; + +const Component = ({ + isAmp = false, + service, + propsForTopBarOJComponent, +}: StoryComponentProps) => ( + + + +); + +export default { + title: 'Containers/Navigation', + Component, + parameters: { chromatic: { disable: true } }, +}; + +export const Canonical = (_, { service }) => ; +export const Amp = (_, { service }) => ; +Amp.decorators = [AmpDecorator]; + +export const CanonicalWithOJTopBarExperimentTopStories = (_, { service }) => { + const propsForTopBarOJComponent = { + blocks: topStoriesBlocks, + experimentVariant: 'A', + }; + return ( + + ); +}; + +export const CanonicalWithOJTopBarExperimentMostRead = (_, { service }) => { + const propsForTopBarOJComponent = { + blocks: mostReadBlocks, + experimentVariant: 'B', + }; + return ( + + ); +}; diff --git a/src/app/components/Navigation/index.test.tsx b/src/app/components/Navigation/index.test.tsx new file mode 100644 index 00000000000..1178ff0f457 --- /dev/null +++ b/src/app/components/Navigation/index.test.tsx @@ -0,0 +1,370 @@ +import { fireEvent } from '@testing-library/dom'; +// import { RequestContextProvider } from '#contexts/RequestContext'; +import { ARTICLE_PAGE, HOME_PAGE } from '#app/routes/utils/pageTypes'; +import { + // ServiceContextProvider, + ServiceContext, +} from '#contexts/ServiceContext'; +import * as viewTracking from '#app/hooks/useViewTracker'; +import * as clickTracking from '#app/hooks/useClickTrackerHandler'; +import { service as newsConfig } from '#lib/config/services/news'; +import { service as indonesiaConfig } from '#lib/config/services/indonesia'; +// import { within } from '@testing-library/react'; +import { render, act } from '../react-testing-library-with-providers'; +import Navigation from './index'; + +describe('Navigation Container', () => { + it('should correctly render amp navigation', () => { + const { container } = render(, { + bbcOrigin: 'https://www.test.bbc.co.uk', + id: 'c0000000000o', + isAmp: true, + pageType: ARTICLE_PAGE, + service: 'news', + statusCode: 200, + pathname: '/news', + }); + expect(container).toMatchSnapshot(); + }); + + it('should correctly render canonical navigation', () => { + const { container } = render(, { + bbcOrigin: 'https://www.test.bbc.co.uk', + id: 'c0000000000o', + isAmp: false, + pageType: ARTICLE_PAGE, + service: 'news', + statusCode: 200, + pathname: '/news', + }); + expect(container).toMatchSnapshot(); + }); + + it('should correctly render amp navigation on non-home navigation page', () => { + const { container } = render(, { + bbcOrigin: 'https://www.test.bbc.co.uk', + id: 'c0000000000o', + isAmp: true, + pageType: ARTICLE_PAGE, + service: 'news', + statusCode: 200, + pathname: '/uk', + }); + expect(container).toMatchSnapshot(); + }); + + it('should correctly render canonical navigation on non-home navigation page', () => { + const { container } = render(, { + bbcOrigin: 'https://www.test.bbc.co.uk', + id: 'c0000000000o', + isAmp: false, + pageType: ARTICLE_PAGE, + service: 'news', + statusCode: 200, + pathname: '/uk', + }); + expect(container).toMatchSnapshot(); + }); + + it('should correctly render amp navigation on non-navigation page', () => { + const { container } = render(, { + bbcOrigin: 'https://www.test.bbc.co.uk', + id: 'c0000000000o', + isAmp: true, + pageType: ARTICLE_PAGE, + service: 'news', + statusCode: 200, + pathname: '/not-a-navigation-page', + }); + expect(container).toMatchSnapshot(); + }); + + it('should correctly render canonical navigation on non-navigation page', () => { + const { container } = render(, { + bbcOrigin: 'https://www.test.bbc.co.uk', + id: 'c0000000000o', + isAmp: false, + pageType: ARTICLE_PAGE, + service: 'news', + statusCode: 200, + pathname: '/not-a-navigation-page', + }); + expect(container).toMatchSnapshot(); + }); + + // it.skip('should not render listItem in scrollable list when hideOnLiteSite is true and isLite is true', () => { + // const { ...rest } = newsConfig.default; + // const mockNavigation = [ + // { title: 'Home', url: '/home', hideOnLiteSite: true }, + // { title: 'News', url: '/news' }, + // { title: 'Sport', url: '/sport' }, + // ]; + + // const navigationComponent = ( + // + // + // + // ); + + // const { getAllByText, container } = render(navigationComponent, { + // bbcOrigin: 'https://www.test.bbc.co.uk', + // id: 'c0000000000o', + // isAmp: false, + // pageType: ARTICLE_PAGE, + // service: 'news', + // statusCode: 200, + // pathname: '/news', + // isLite: true, + // }); + + // const scrollableList = container.querySelector('[role="list"]'); + // expect(scrollableList).toBeInTheDocument(); + + // if (scrollableList) { + // // Only "News" and "Sport" should be inside the scrollable list + // const newsInList = within(scrollableList as HTMLElement).queryByText( + // 'News', + // ); + // const sportInList = within(scrollableList as HTMLElement).queryByText( + // 'Sport', + // ); + // const homeInList = within(scrollableList as HTMLElement).queryByText( + // 'Home', + // ); + + // expect(newsInList).toBeVisible(); + // expect(sportInList).toBeVisible(); + // expect(homeInList).toBeNull(); + // } + + // // "Home" should be rendered somewhere outside the scrollable list + // const homeElements = getAllByText('Home'); + // expect(homeElements.length).toBeGreaterThan(0); + // homeElements.forEach(element => { + // expect(element.closest('[role="list"]') as HTMLElement | null).toBeNull(); + // }); + // }); + + it('should prefer navItems prop over service config', () => { + const navItems = [ + { title: 'Home', url: '/home' }, + { title: 'About', url: '/about' }, + ]; + + const { getAllByText, queryAllByText } = render( + , + { + bbcOrigin: 'https://www.test.bbc.co.uk', + id: 'c0000000000o', + isAmp: false, + pageType: ARTICLE_PAGE, + service: 'news', + statusCode: 200, + pathname: '/news', + }, + ); + + expect(getAllByText('Home').length).toBeGreaterThan(0); + expect(getAllByText('About').length).toBeGreaterThan(0); + expect(queryAllByText('World')).toHaveLength(0); + }); + + it('should fall back to service config when navItems is null', () => { + const { navigation = [] } = indonesiaConfig.default; + + const { getAllByText } = render(, { + bbcOrigin: 'https://www.test.bbc.co.uk', + id: 'c0000000000o', + isAmp: false, + pageType: ARTICLE_PAGE, + service: 'indonesia', + statusCode: 200, + pathname: '/indonesian', + }); + + navigation.forEach(({ title }) => { + const elements = getAllByText(title); + elements.forEach(element => { + expect(element).toHaveTextContent(title); + }); + }); + }); + + it('should render nothing when navItems is an empty array', () => { + const { container } = render(, { + bbcOrigin: 'https://www.test.bbc.co.uk', + id: 'c0000000000o', + isAmp: false, + pageType: ARTICLE_PAGE, + service: 'news', + statusCode: 200, + pathname: '/news', + }); + + expect(container.firstChild).toBeNull(); + }); + + it.skip('should not render listItem in scrollable list when hideOnLiteSite is true and isLite is true', () => { + const { ...rest } = newsConfig.default; + const mockNavigation = [ + { title: 'Home', url: '/home', hideOnLiteSite: true }, + { title: 'News', url: '/news' }, + { title: 'Sport', url: '/sport' }, + ]; + + const navigationComponent = ( + + + + ); + + const { queryByText } = render(navigationComponent, { + bbcOrigin: 'https://www.test.bbc.co.uk', + id: 'c0000000000o', + isAmp: false, + pageType: ARTICLE_PAGE, + service: 'news', + statusCode: 200, + pathname: '/news', + isLite: true, + }); + + expect(queryByText(mockNavigation[0].title)).not.toBeInTheDocument(); + }); + + it('should render listItem in scrollable list when hideOnLiteSite is true and isLite is false', () => { + const { ...rest } = newsConfig.default; + const mockNavigation = [ + { title: 'Home', url: '/home', hideOnLiteSite: true }, + { title: 'News', url: '/news' }, + { title: 'Sport', url: '/sport' }, + ]; + + const navigationComponent = ( + + + + ); + + const { queryAllByText } = render(navigationComponent, { + bbcOrigin: 'https://www.test.bbc.co.uk', + id: 'c0000000000o', + isAmp: false, + pageType: ARTICLE_PAGE, + service: 'news', + statusCode: 200, + pathname: '/news', + isLite: false, + }); + + expect(queryAllByText(mockNavigation[0].title)[0]).toBeVisible(); + }); + + it('should render listItem in scrollable list when hideOnLiteSite is false/not set', () => { + const { ...rest } = newsConfig.default; + const mockNavigation = [ + { title: 'Home', url: '/home' }, + { title: 'News', url: '/news' }, + { title: 'Sport', url: '/sport' }, + ]; + + const navigationComponent = ( + + + + ); + + const { queryAllByText } = render(navigationComponent, { + bbcOrigin: 'https://www.test.bbc.co.uk', + id: 'c0000000000o', + isAmp: false, + pageType: ARTICLE_PAGE, + service: 'news', + statusCode: 200, + pathname: '/news', + isLite: false, + }); + + expect(queryAllByText(mockNavigation[0].title)[0]).toBeVisible(); + }); + + describe('View and click tracking', () => { + const scrollEventTrackingData = { + componentName: 'scrollable-navigation', + }; + + const dropdownEventTrackingData = { + componentName: 'dropdown-navigation', + }; + + const clickTrackerSpy = jest + .spyOn(clickTracking, 'default') + .mockImplementation(); + + beforeEach(() => { + clickTrackerSpy.mockRestore(); + }); + + it('should call the view tracking hook when on scrollable navigation', () => { + const viewTrackerSpy = jest.spyOn(viewTracking, 'default'); + render(, { + bbcOrigin: 'https://www.test.bbc.co.uk', + id: 'c0000000000o', + isAmp: true, + pageType: ARTICLE_PAGE, + service: 'news', + statusCode: 200, + pathname: '/news', + }); + expect(viewTrackerSpy).toHaveBeenCalledWith(scrollEventTrackingData); + }); + + it('should call the view tracking hook when on dropdown navigation', () => { + const viewTrackerSpy = jest.spyOn(viewTracking, 'default'); + render(, { + bbcOrigin: 'https://www.test.bbc.co.uk', + id: 'c0000000000o', + isAmp: true, + pageType: ARTICLE_PAGE, + service: 'news', + statusCode: 200, + pathname: '/news', + }); + expect(viewTrackerSpy).toHaveBeenCalledWith(dropdownEventTrackingData); + }); + + it('should call the click tracking hook when scrollable navigation is clicked', () => { + const { container } = render(, { + bbcOrigin: 'https://www.test.bbc.co.uk', + id: 'c0000000000o', + isAmp: true, + pageType: ARTICLE_PAGE, + service: 'news', + statusCode: 200, + pathname: '/news', + }); + + fireEvent.click(container); + + expect(container.onclick).toBeTruthy(); + }); + }); + + describe('Language Navigation', () => { + it('should render LanguageNavigation for WS service in all environment', async () => { + const { getByTestId } = await act(async () => + render(, { + bbcOrigin: 'https://www.test.bbc.co.uk', + id: 'c0000000000o', + isAmp: false, + pageType: HOME_PAGE, + service: 'ws', + statusCode: 200, + pathname: '/ws/languages', + }), + ); + + expect(getByTestId('collapsible-nav')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/app/components/Navigation/index.tsx b/src/app/components/Navigation/index.tsx new file mode 100644 index 00000000000..2222a6cf43b --- /dev/null +++ b/src/app/components/Navigation/index.tsx @@ -0,0 +1,160 @@ +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 LanguageNavigation from './LanguageNavigation/lazy'; +import Canonical from './index.canonical'; +import Amp from './index.amp'; +import { NavigationItem, NavigationContainerProps } from './types'; + +const renderListItems = ( + Li: React.ElementType, + navigation: NavigationItem[], + script: unknown, + currentPage: string, + service: string, + dir: string, + activeIndex: number, + clickTracker: unknown, + viewTracker: unknown, + isLite?: boolean, +) => + navigation.reduce((listAcc, item, index) => { + const { title, url, hideOnLiteSite } = item; + const active = index === activeIndex; + + if (hideOnLiteSite && isLite) return listAcc; + + const listItem = ( +
  • + {title} +
  • + ); + + return [...listAcc, listItem]; + }, [] as React.ReactNode[]); + +const NavigationContainer = ({ + navItems, + propsForTopBarOJComponent, +}: NavigationContainerProps) => { + const { isAmp, isLite } = use(RequestContext); + const { blocks = [] } = propsForTopBarOJComponent || {}; + const { + script, + translations, + navigation: navFromServiceConfig = [], + service, + dir, + collapsibleNavigation, + } = use(ServiceContext); + + const { canonicalLink, origin } = use(RequestContext); + const { currentPage, navMenuText } = translations; + + const scrollableNavEventTrackingData = { + componentName: 'scrollable-navigation', + }; + + const dropdownNavEventTrackingData = { + componentName: 'dropdown-navigation', + }; + + const scrollableNavClickTrackerHandler = useClickTrackerHandler( + scrollableNavEventTrackingData, + ); + + const dropdownNavClickTrackerHandler = useClickTrackerHandler( + dropdownNavEventTrackingData, + ); + + const scrollableNavViewTracker = useViewTracker( + scrollableNavEventTrackingData, + ); + + const dropdownNavViewTracker = useViewTracker(dropdownNavEventTrackingData); + + const renderLanguageNavigation = collapsibleNavigation?.length; + + if (renderLanguageNavigation) { + return ; + } + + // Prefer navItems passed from props over service config + // Eventually all services will migrate to passing navItems via props + const navigation = navItems || navFromServiceConfig; + + if (!navigation || navigation.length === 0) { + return null; + } + + const activeIndex = navigation.findIndex( + link => `${origin}${link.url}` === canonicalLink, + ); + + const scrollableListItems = ( + + {renderListItems( + NavigationLi, + navigation, + script, + currentPage, + service, + dir, + activeIndex, + scrollableNavClickTrackerHandler, + scrollableNavViewTracker, + isLite, + )} + + ); + + const dropdownListItems = ( + + {renderListItems( + DropdownLi, + navigation, + script, + currentPage, + service, + dir, + activeIndex, + dropdownNavClickTrackerHandler, + dropdownNavViewTracker, + isLite, + )} + + ); + + const Navigation = 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..deb40eea112 --- /dev/null +++ b/src/app/components/Navigation/testHelpers.tsx @@ -0,0 +1,14 @@ +export const dropdownTestId = 'dropdown'; +export const scrollableTestId = 'scrollable-list'; + +export const scrollableListItems = ( +
      +
    • List Items
    • +
    +); + +export const dropdownListItems = ( +
      +
    • Dropdown Items
    • +
    +); diff --git a/src/app/components/Navigation/types.ts b/src/app/components/Navigation/types.ts new file mode 100644 index 00000000000..cdd380728bb --- /dev/null +++ b/src/app/components/Navigation/types.ts @@ -0,0 +1,54 @@ +import { ReactNode } from 'react'; + +export type NavigationSubItem = { + title: string; + url: string; + hideOnLiteSite?: boolean; + subItems?: NavigationSubItem[]; +}; + +export type NavigationItem = { + title: string; + url: string; + hideOnLiteSite?: boolean; + subItems?: NavigationSubItem[]; +}; + +export type PropsForTopBarOJComponent = { + blocks?: unknown[]; + experimentVariant?: string; +}; + +export type NavigationContainerProps = { + navItems?: NavigationItem[] | null; + propsForTopBarOJComponent?: PropsForTopBarOJComponent | null; +}; + +export type CanonicalNavigationContainerProps = { + script: unknown; + service: string; + dir: string; + menuAnnouncedText: string; + scrollableListItems: ReactNode; + dropdownListItems: ReactNode; + blocks?: unknown[]; +}; + +export type AmpNavigationContainerProps = { + script: unknown; + service: string; + dir: string; + menuAnnouncedText: string; + scrollableListItems: React.ReactNode; + dropdownListItems: React.ReactNode; +}; + +export type NavigationBaseProps = { + script: unknown; + service: string; + dir: string; + children: ReactNode; + isOpen?: boolean; + ampOpenClass?: string | null; + id?: string; +}; diff --git a/src/app/legacy/containers/Navigation/index.jsx b/src/app/legacy/containers/Navigation/index.jsx index d82367898da..7150332b88f 100644 --- a/src/app/legacy/containers/Navigation/index.jsx +++ b/src/app/legacy/containers/Navigation/index.jsx @@ -60,7 +60,7 @@ const NavigationContainer = ({ navItems, propsForTopBarOJComponent }) => { dir, collapsibleNavigation, } = use(ServiceContext); - + console.log('navItems:', navItems); const { canonicalLink, origin } = use(RequestContext); const { currentPage, navMenuText } = translations; diff --git a/src/app/legacy/containers/Navigation/index.stories.jsx b/src/app/legacy/containers/Navigation/index.stories.jsx index f3723da0dda..3d3e6981064 100644 --- a/src/app/legacy/containers/Navigation/index.stories.jsx +++ b/src/app/legacy/containers/Navigation/index.stories.jsx @@ -19,7 +19,7 @@ const Component = ({ isAmp = false, service, propsForOJExperiment = null }) => ( ); export default { - title: 'Containers/Navigation', + title: 'Containers/Navigation/Legacy', Component, parameters: { chromatic: { disable: true } }, }; From c9a639a17cea83a56d382959b858448ac82d851e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Mon, 9 Feb 2026 14:14:25 +0000 Subject: [PATCH 02/82] wip --- .../Navigation/TopLevelNav/index.tsx | 74 +++++++++++++++++++ src/app/components/Navigation/index.amp.tsx | 46 ++++++------ .../components/Navigation/index.canonical.tsx | 61 +++++++++------ .../components/Navigation/index.stories.tsx | 23 +++++- src/app/components/Navigation/index.tsx | 27 ++++--- src/app/components/Navigation/types.ts | 17 +++++ src/app/legacy/containers/Header/index.jsx | 2 +- 7 files changed, 195 insertions(+), 55 deletions(-) create mode 100644 src/app/components/Navigation/TopLevelNav/index.tsx diff --git a/src/app/components/Navigation/TopLevelNav/index.tsx b/src/app/components/Navigation/TopLevelNav/index.tsx new file mode 100644 index 00000000000..f8207a51190 --- /dev/null +++ b/src/app/components/Navigation/TopLevelNav/index.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { useTheme, CSSObject } from '@emotion/react'; +import { DropdownUl } from '#psammead/psammead-navigation/src/DropdownNavigation'; + +const topLevelNavLinks = [ + { title: 'Home', url: '/' }, + { title: 'Watch', url: '/watch' }, + { title: 'Listen', url: '/listen' }, +]; + +const topLevelNavContainerStyles = (theme): CSSObject => ({ + backgroundColor: theme.palette.BRAND_BACKGROUND, + padding: '0.5rem 0', + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center' as const, +}); + +const topLevelNavListStyles: CSSObject = { + display: 'flex', + justifyContent: 'center', + listStyle: 'none', + margin: 0, + padding: 0, +}; + +const topLevelNavItemStyles: CSSObject = { + margin: '0 1rem', +}; + +const topLevelNavLinkStyles = (theme): CSSObject => ({ + color: theme.palette.WHITE, + textDecoration: 'none', + fontWeight: 700, + fontSize: '1rem', + padding: '0.5rem 1rem', + borderRadius: '0.25rem', + transition: 'background 0.2s', + '&:hover, &:focus': { + textDecoration: 'underline', + backgroundColor: theme.palette.POSTBOX_30, + }, +}); + +export type TopLevelNavProps = { + dropdownList: React.ReactNode; + dir: string; +}; + +const TopLevelNav = ({ dropdownList }: TopLevelNavProps) => { + const theme = useTheme(); + + return ( + + ); +}; + +export default TopLevelNav; diff --git a/src/app/components/Navigation/index.amp.tsx b/src/app/components/Navigation/index.amp.tsx index 0c105114c20..8a2357acd4d 100644 --- a/src/app/components/Navigation/index.amp.tsx +++ b/src/app/components/Navigation/index.amp.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import Navigation from '#psammead/psammead-navigation/src'; +import Navigation from '#src/app/components/Navigation'; import { ScrollableNavigation } from '#psammead/psammead-navigation/src/ScrollableNavigation'; import { AmpDropdown, @@ -38,26 +38,30 @@ const AmpNavigationContainer = ({ dir={dir} id={NAVIGATION_ID} ampOpenClass={OPEN_CLASS_NAME as any} - > - - {/* Hidden attribute allows us to toggle visibility on the dropdown - using AMP actions. */} - - - {scrollableListItems} - -
    + scrollableListItems={ + + {scrollableListItems} + + } + dropdownListItems={ + + } + menuAnnouncedText={menuAnnouncedText} + ampMenuButton={ + + } + /> ); export default AmpNavigationContainer; diff --git a/src/app/components/Navigation/index.canonical.tsx b/src/app/components/Navigation/index.canonical.tsx index 49b172da6c4..91370a2d99e 100644 --- a/src/app/components/Navigation/index.canonical.tsx +++ b/src/app/components/Navigation/index.canonical.tsx @@ -1,6 +1,6 @@ import { useState, use } from 'react'; import styled from '@emotion/styled'; -import Navigation from '#psammead/psammead-navigation/src'; +import Navigation from '#src/app/components/Navigation'; import { ScrollableNavigation } from '#psammead/psammead-navigation/src/ScrollableNavigation'; import { CanonicalDropdown, @@ -13,10 +13,12 @@ import TopBarOJs from '#app/components/TopBarOJs'; import useToggle from '#app/hooks/useToggle'; import { TopStoryItem } from '#app/pages/ArticlePage/PagePromoSections/TopStoriesSection/types'; import { CanonicalNavigationContainerProps } from './types'; +import TopLevelNav from './TopLevelNav'; const ScrollableWrapper = styled.div` position: relative; `; + const Divider = styled.div` position: absolute; width: calc(100vw - 0.8rem); @@ -37,6 +39,7 @@ const Divider = styled.div` display: none; } `; + const CanonicalNavigationContainer = ({ script, service, @@ -45,10 +48,13 @@ const CanonicalNavigationContainer = ({ scrollableListItems, dropdownListItems, blocks, + navItems, + propsForTopBarOJComponent, }: CanonicalNavigationContainerProps) => { const { isLite } = use(RequestContext); const { enabled } = useToggle('topBarOJs'); const [isOpen, setIsOpen] = useState(false); + useMediaQuery(`(max-width: ${GEL_GROUP_2_SCREEN_WIDTH_MAX})`, event => { if (!event.matches) { setIsOpen(false); @@ -58,27 +64,38 @@ const CanonicalNavigationContainer = ({ const topBarBlocks = Array.isArray(blocks) ? (blocks as TopStoryItem[]) : []; return ( - - - {!isLite && ( - setIsOpen(!isOpen)} - dir={dir} - script={script} - /> - )} - {!isOpen && ( - - {scrollableListItems} - - )} - - {dropdownListItems} - - {enabled && } - + <> + + + {!isLite && ( + setIsOpen(!isOpen)} + dir={dir} + script={script} + /> + )} + {!isOpen && ( + + {scrollableListItems} + + )} + + } + divider={} + topBarOJs={enabled ? : null} + /> + ); }; diff --git a/src/app/components/Navigation/index.stories.tsx b/src/app/components/Navigation/index.stories.tsx index abc83bedbfb..3264dbbdb37 100644 --- a/src/app/components/Navigation/index.stories.tsx +++ b/src/app/components/Navigation/index.stories.tsx @@ -10,6 +10,21 @@ import Navigation from '.'; import type { Services } from '#models/types/global'; import type { PropsForTopBarOJComponent } from './types'; +const mockScript = {}; // Replace with a realistic script object if needed +const mockDir = 'ltr'; // or 'rtl' depending on the service +const mockScrollableListItems = ( +
      +
    • Mock Item
    • +
    +); +const mockDropdownListItems = ( +
      +
    • Mock Dropdown
    • +
    +); +const mockMenuAnnouncedText = 'Menu'; +const mockService = 'arabic'; + type StoryComponentProps = { isAmp?: boolean; service: Services; @@ -18,7 +33,7 @@ type StoryComponentProps = { const Component = ({ isAmp = false, - service, + service = mockService, propsForTopBarOJComponent, }: StoryComponentProps) => ( diff --git a/src/app/components/Navigation/index.tsx b/src/app/components/Navigation/index.tsx index 2222a6cf43b..c5f63a9ed2b 100644 --- a/src/app/components/Navigation/index.tsx +++ b/src/app/components/Navigation/index.tsx @@ -12,6 +12,7 @@ import LanguageNavigation from './LanguageNavigation/lazy'; import Canonical from './index.canonical'; import Amp from './index.amp'; import { NavigationItem, NavigationContainerProps } from './types'; +import TopLevelNav from './TopLevelNav'; const renderListItems = ( Li: React.ElementType, @@ -99,7 +100,6 @@ const NavigationContainer = ({ // Prefer navItems passed from props over service config // Eventually all services will migrate to passing navItems via props const navigation = navItems || navFromServiceConfig; - if (!navigation || navigation.length === 0) { return null; } @@ -144,16 +144,23 @@ const NavigationContainer = ({ const Navigation = isAmp ? Amp : Canonical; + // --- Render TopLevelNav above the main navigation list --- return ( - + <> +
    + Navigation test: This is showing on the page. +
    + + + ); }; diff --git a/src/app/components/Navigation/types.ts b/src/app/components/Navigation/types.ts index cdd380728bb..1673fef31cd 100644 --- a/src/app/components/Navigation/types.ts +++ b/src/app/components/Navigation/types.ts @@ -20,8 +20,21 @@ export type PropsForTopBarOJComponent = { }; export type NavigationContainerProps = { + script: unknown; + service: string; + dir: string; + id?: string; + ampOpenClass?: string; + scrollableListItems: ReactNode; + dropdownListItems: ReactNode; + menuAnnouncedText: string; + ampMenuButton?: ReactNode; + blocks?: unknown[]; navItems?: NavigationItem[] | null; propsForTopBarOJComponent?: PropsForTopBarOJComponent | null; + divider?: ReactNode; + topBarOJs?: ReactNode; + isOpen?: boolean; }; export type CanonicalNavigationContainerProps = { @@ -32,6 +45,8 @@ export type CanonicalNavigationContainerProps = { scrollableListItems: ReactNode; dropdownListItems: ReactNode; blocks?: unknown[]; + navItems?: NavigationItem[] | null; + propsForTopBarOJComponent?: PropsForTopBarOJComponent | null; }; export type AmpNavigationContainerProps = { @@ -41,6 +56,8 @@ export type AmpNavigationContainerProps = { menuAnnouncedText: string; scrollableListItems: React.ReactNode; dropdownListItems: React.ReactNode; + navItems?: NavigationItem[] | null; + propsForTopBarOJComponent?: PropsForTopBarOJComponent | null; }; export type NavigationBaseProps = { diff --git a/src/app/legacy/containers/Header/index.jsx b/src/app/legacy/containers/Header/index.jsx index 5d499e942b9..356859b93e7 100644 --- a/src/app/legacy/containers/Header/index.jsx +++ b/src/app/legacy/containers/Header/index.jsx @@ -11,9 +11,9 @@ import { LIVE_PAGE, } from '#app/routes/utils/pageTypes'; import LiteSiteSummary from '#app/components/LiteSiteSummary'; +import NavigationContainer from '#src/app/components/Navigation'; import { ServiceContext } from '../../../contexts/ServiceContext'; import ConsentBanner from '../ConsentBanner'; -import NavigationContainer from '../Navigation'; import BrandContainer from '../Brand'; const Header = ({ brandRef, borderBottom, skipLink, scriptLink, linkId }) => { From af56a2c70b16d0d56575775631e3f816183ddc9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Tue, 10 Feb 2026 15:56:07 +0000 Subject: [PATCH 03/82] top nav bar added --- .../Navigation/TopLevelNav/index.tsx | 74 ---- .../components/Navigation/index.amp.test.jsx | 41 -- .../components/Navigation/index.amp.test.tsx | 0 src/app/components/Navigation/index.amp.tsx | 55 ++- .../Navigation/index.canonical.test.jsx | 93 ----- .../Navigation/index.canonical.test.tsx | 0 .../components/Navigation/index.canonical.tsx | 144 ++++--- .../components/Navigation/index.stories.tsx | 91 ----- src/app/components/Navigation/index.test.tsx | 370 ------------------ src/app/components/Navigation/index.tsx | 116 +++--- src/app/components/Navigation/testHelpers.tsx | 14 - src/app/components/Navigation/types.ts | 77 +--- .../legacy/containers/Navigation/index.jsx | 1 - .../src/ScrollableNavigation/index.jsx | 44 ++- 14 files changed, 236 insertions(+), 884 deletions(-) delete mode 100644 src/app/components/Navigation/TopLevelNav/index.tsx delete mode 100644 src/app/components/Navigation/index.amp.test.jsx create mode 100644 src/app/components/Navigation/index.amp.test.tsx delete mode 100644 src/app/components/Navigation/index.canonical.test.jsx create mode 100644 src/app/components/Navigation/index.canonical.test.tsx diff --git a/src/app/components/Navigation/TopLevelNav/index.tsx b/src/app/components/Navigation/TopLevelNav/index.tsx deleted file mode 100644 index f8207a51190..00000000000 --- a/src/app/components/Navigation/TopLevelNav/index.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import { useTheme, CSSObject } from '@emotion/react'; -import { DropdownUl } from '#psammead/psammead-navigation/src/DropdownNavigation'; - -const topLevelNavLinks = [ - { title: 'Home', url: '/' }, - { title: 'Watch', url: '/watch' }, - { title: 'Listen', url: '/listen' }, -]; - -const topLevelNavContainerStyles = (theme): CSSObject => ({ - backgroundColor: theme.palette.BRAND_BACKGROUND, - padding: '0.5rem 0', - display: 'flex', - flexDirection: 'column' as const, - alignItems: 'center' as const, -}); - -const topLevelNavListStyles: CSSObject = { - display: 'flex', - justifyContent: 'center', - listStyle: 'none', - margin: 0, - padding: 0, -}; - -const topLevelNavItemStyles: CSSObject = { - margin: '0 1rem', -}; - -const topLevelNavLinkStyles = (theme): CSSObject => ({ - color: theme.palette.WHITE, - textDecoration: 'none', - fontWeight: 700, - fontSize: '1rem', - padding: '0.5rem 1rem', - borderRadius: '0.25rem', - transition: 'background 0.2s', - '&:hover, &:focus': { - textDecoration: 'underline', - backgroundColor: theme.palette.POSTBOX_30, - }, -}); - -export type TopLevelNavProps = { - dropdownList: React.ReactNode; - dir: string; -}; - -const TopLevelNav = ({ dropdownList }: TopLevelNavProps) => { - const theme = useTheme(); - - return ( - - ); -}; - -export default TopLevelNav; diff --git a/src/app/components/Navigation/index.amp.test.jsx b/src/app/components/Navigation/index.amp.test.jsx deleted file mode 100644 index 34be2dc10b8..00000000000 --- a/src/app/components/Navigation/index.amp.test.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import latin from '../ThemeProvider/fontScripts/latin'; -import AmpNavigation from './index.amp'; -import { - dropdownTestId, - scrollableTestId, - dropdownListItems, - scrollableListItems, -} from './testHelpers'; -import { render } from '../react-testing-library-with-providers'; - -const navigation = ( - -); - -describe('AMP Navigation', () => { - describe('Snapshots', () => { - it('should correctly render AMP navigation', () => { - const { container } = render(navigation); - expect(container).toMatchSnapshot(); - }); - }); - - describe('Assertions', () => { - it('should render scrollable nav and a hidden dropdown', () => { - const { queryByTestId } = render(navigation); - const dropdown = queryByTestId(dropdownTestId).parentElement; - const scrollableNav = queryByTestId(scrollableTestId); - expect(scrollableNav.innerHTML).toBe('
  • List Items
  • '); - expect(dropdown).not.toBeVisible(); - }); - }); - - // AMP state toggling tested by an e2e. -}); 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..e69de29bb2d diff --git a/src/app/components/Navigation/index.amp.tsx b/src/app/components/Navigation/index.amp.tsx index 8a2357acd4d..059baa30fd1 100644 --- a/src/app/components/Navigation/index.amp.tsx +++ b/src/app/components/Navigation/index.amp.tsx @@ -1,5 +1,5 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import Navigation from '#src/app/components/Navigation'; +import React from 'react'; +import Navigation from '#psammead/psammead-navigation/src'; import { ScrollableNavigation } from '#psammead/psammead-navigation/src/ScrollableNavigation'; import { AmpDropdown, @@ -7,7 +7,8 @@ import { } 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 { AmpNavigationContainerProps } from './types'; + +import type { AmpNavigationContainerProps } from './types'; const DROPDOWN_ID = 'si-nav-dropdown-menu'; const NAVIGATION_ID = 'si-nav'; @@ -24,44 +25,40 @@ const StyledAmpScrollableNavigation = styled(ScrollableNavigation)` } `; -const AmpNavigationContainer = ({ +const AmpNavigationContainer: React.FC = ({ script, service, dir, menuAnnouncedText, scrollableListItems, dropdownListItems, -}: AmpNavigationContainerProps) => ( +}) => ( - {scrollableListItems} - - } - dropdownListItems={ - - } - menuAnnouncedText={menuAnnouncedText} - ampMenuButton={ - - } - /> + > + + {/* Hidden attribute allows us to toggle visibility on the dropdown + using AMP actions. */} + + + {scrollableListItems} + + ); export default AmpNavigationContainer; diff --git a/src/app/components/Navigation/index.canonical.test.jsx b/src/app/components/Navigation/index.canonical.test.jsx deleted file mode 100644 index d4c5e888b44..00000000000 --- a/src/app/components/Navigation/index.canonical.test.jsx +++ /dev/null @@ -1,93 +0,0 @@ -import CanonicalNavigation from './index.canonical'; -import { - dropdownTestId, - scrollableTestId, - dropdownListItems, - scrollableListItems, -} from './testHelpers'; -import { render, fireEvent } from '../react-testing-library-with-providers'; -import latin from '../ThemeProvider/fontScripts/latin'; - -const blocks = [{ id: '1', title: 'Story' }]; - -const navigationProps = { - scrollableListItems, - dropdownListItems, - menuAnnouncedText: 'menu', - script: latin, - service: 'pidgin', - dir: 'ltr', -}; - -const navigation = ( - -); - -describe('Canonical Navigation', () => { - describe('snapshots', () => { - it('should correctly render Canonical navigation', () => { - const { container } = render(navigation); - expect(container).toMatchSnapshot(); - }); - }); - - describe('assertions', () => { - it('should render scrollable nav and hide dropdown', () => { - const { queryByTestId } = render(navigation); - const dropdown = queryByTestId(dropdownTestId).parentElement; - const scrollableNav = queryByTestId(scrollableTestId); - expect(scrollableNav.innerHTML).toBe('
  • List Items
  • '); - expect(dropdown).toHaveAttribute('height', '0'); - }); - - it('should render dropdown and no scrollable nav after menu button clicked', () => { - const { queryByTestId, queryByText } = render(navigation); - - fireEvent.click(queryByText('menu')); - - const dropdown = queryByTestId(dropdownTestId); - const scrollableNav = queryByTestId(scrollableTestId); - expect(scrollableNav).toBeNull(); - expect(dropdown.innerHTML).toBe('
  • Dropdown Items
  • '); - }); - - describe('Top Bar OJs', () => { - it.each([ - [ - 'should not render TopBarOJs when toggle is off', - { ...navigationProps, blocks }, - { topBarOJs: { enabled: false } }, - queryByTestId => - expect(queryByTestId('top-bar-onward-journeys')).toBeNull(), - ], - [ - 'should render TopBarOJs when toggle is on and blocks are provided', - { ...navigationProps, blocks }, - { topBarOJs: { enabled: true } }, - queryByTestId => - expect(queryByTestId('top-bar-onward-journeys')).not.toBeNull(), - ], - [ - 'should not render TopBarOJs when blocks are empty even if toggle is on', - { ...navigationProps, blocks: [] }, - { topBarOJs: { enabled: true } }, - queryByTestId => - expect(queryByTestId('top-bar-onward-journeys')).toBeNull(), - ], - ])('%s', (_, props, toggles, assertion) => { - const { queryByTestId } = render(, { - toggles, - service: props.service, - }); - assertion(queryByTestId); - }); - }); - }); -}); 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..e69de29bb2d diff --git a/src/app/components/Navigation/index.canonical.tsx b/src/app/components/Navigation/index.canonical.tsx index 91370a2d99e..bcd9b56ee70 100644 --- a/src/app/components/Navigation/index.canonical.tsx +++ b/src/app/components/Navigation/index.canonical.tsx @@ -1,6 +1,6 @@ -import { useState, use } from 'react'; +import React, { useState, use } from 'react'; import styled from '@emotion/styled'; -import Navigation from '#src/app/components/Navigation'; +import Navigation from '#psammead/psammead-navigation/src'; import { ScrollableNavigation } from '#psammead/psammead-navigation/src/ScrollableNavigation'; import { CanonicalDropdown, @@ -11,13 +11,23 @@ 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 { CanonicalNavigationContainerProps } from './types'; -import TopLevelNav from './TopLevelNav'; -const ScrollableWrapper = styled.div` - position: relative; -`; +interface CanonicalNavigationContainerProps { + script: unknown; + service: string; + dir: string; + menuAnnouncedText: string; + topScrollableListItems?: React.ReactNode; + topDivider?: React.ReactNode; + scrollableListItems: React.ReactNode; + dropdownListItems: React.ReactNode; + menuButton?: React.ReactNode; + isOpen?: boolean; + setIsOpen?: (open: boolean) => void; + blocks?: TopStoryItem[]; +} const Divider = styled.div` position: absolute; @@ -40,62 +50,106 @@ const Divider = styled.div` } `; -const CanonicalNavigationContainer = ({ +const NavStack = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +const NavRow = styled.div` + display: flex; + flex-direction: ${({ dir }) => (dir === 'rtl' ? 'row-reverse' : 'row')}; + align-items: stretch; + justify-content: ${({ dir }) => (dir === 'rtl' ? 'flex-end' : 'flex-start')}; + width: 100%; +`; + +const LowerNavWrapper = styled.div` + width: 100%; + margin-top: 0.25rem; + position: relative; + z-index: 1; +`; + +const CanonicalNavigationContainer: React.FC< + CanonicalNavigationContainerProps +> = ({ script, service, dir, menuAnnouncedText, + topScrollableListItems, scrollableListItems, dropdownListItems, blocks, - navItems, - propsForTopBarOJComponent, -}: CanonicalNavigationContainerProps) => { +}) => { const { isLite } = use(RequestContext); const { enabled } = useToggle('topBarOJs'); const [isOpen, setIsOpen] = useState(false); - useMediaQuery(`(max-width: ${GEL_GROUP_2_SCREEN_WIDTH_MAX})`, event => { if (!event.matches) { setIsOpen(false); } }); - - const topBarBlocks = Array.isArray(blocks) ? (blocks as TopStoryItem[]) : []; - return ( - <> - - - {!isLite && ( - setIsOpen(!isOpen)} + + + + {dir === 'rtl' ? ( + <> + {!isLite && ( +
    + setIsOpen(!isOpen)} + dir={dir} + navType="top" + /> +
    + )} + + {topScrollableListItems} + + + ) : ( + <> + - )} - {!isOpen && ( - - {scrollableListItems} + style={{ flex: '1 1 0', color: '#fff' }} + navType="top" + > + {topScrollableListItems} - )} - - } - divider={} - topBarOJs={enabled ? : null} - /> - + {!isLite && ( +
    + setIsOpen(!isOpen)} + dir={dir} + script={script} + navType="top" + /> +
    + )} + + )} +
    + + + + {scrollableListItems} + + + + {dropdownListItems} + + + {enabled && } +
    +
    ); }; diff --git a/src/app/components/Navigation/index.stories.tsx b/src/app/components/Navigation/index.stories.tsx index 3264dbbdb37..e69de29bb2d 100644 --- a/src/app/components/Navigation/index.stories.tsx +++ b/src/app/components/Navigation/index.stories.tsx @@ -1,91 +0,0 @@ -import { RequestContextProvider } from '#contexts/RequestContext'; -import { HOME_PAGE } from '#app/routes/utils/pageTypes'; -import { - topStoriesBlocks, - mostReadBlocks, -} from '../ArticleLinksBlock/helpers/fixtureData'; -import AmpDecorator from '../../../../.storybook/helpers/ampDecorator/index.jsx'; -import Navigation from '.'; - -import type { Services } from '#models/types/global'; -import type { PropsForTopBarOJComponent } from './types'; - -const mockScript = {}; // Replace with a realistic script object if needed -const mockDir = 'ltr'; // or 'rtl' depending on the service -const mockScrollableListItems = ( -
      -
    • Mock Item
    • -
    -); -const mockDropdownListItems = ( -
      -
    • Mock Dropdown
    • -
    -); -const mockMenuAnnouncedText = 'Menu'; -const mockService = 'arabic'; - -type StoryComponentProps = { - isAmp?: boolean; - service: Services; - propsForTopBarOJComponent?: PropsForTopBarOJComponent | null; -}; - -const Component = ({ - isAmp = false, - service = mockService, - propsForTopBarOJComponent, -}: StoryComponentProps) => ( - - - -); - -export default { - title: 'Containers/Navigation', - Component, - parameters: { chromatic: { disable: true } }, -}; - -export const Canonical = (_, { service }) => ; -export const Amp = (_, { service }) => ; -Amp.decorators = [AmpDecorator]; - -export const CanonicalWithOJTopBarExperimentTopStories = (_, { service }) => { - const propsForTopBarOJComponent = { - blocks: topStoriesBlocks, - experimentVariant: 'A', - }; - return ( - - ); -}; - -export const CanonicalWithOJTopBarExperimentMostRead = (_, { service }) => { - const propsForTopBarOJComponent = { - blocks: mostReadBlocks, - experimentVariant: 'B', - }; - return ( - - ); -}; diff --git a/src/app/components/Navigation/index.test.tsx b/src/app/components/Navigation/index.test.tsx index 1178ff0f457..e69de29bb2d 100644 --- a/src/app/components/Navigation/index.test.tsx +++ b/src/app/components/Navigation/index.test.tsx @@ -1,370 +0,0 @@ -import { fireEvent } from '@testing-library/dom'; -// import { RequestContextProvider } from '#contexts/RequestContext'; -import { ARTICLE_PAGE, HOME_PAGE } from '#app/routes/utils/pageTypes'; -import { - // ServiceContextProvider, - ServiceContext, -} from '#contexts/ServiceContext'; -import * as viewTracking from '#app/hooks/useViewTracker'; -import * as clickTracking from '#app/hooks/useClickTrackerHandler'; -import { service as newsConfig } from '#lib/config/services/news'; -import { service as indonesiaConfig } from '#lib/config/services/indonesia'; -// import { within } from '@testing-library/react'; -import { render, act } from '../react-testing-library-with-providers'; -import Navigation from './index'; - -describe('Navigation Container', () => { - it('should correctly render amp navigation', () => { - const { container } = render(, { - bbcOrigin: 'https://www.test.bbc.co.uk', - id: 'c0000000000o', - isAmp: true, - pageType: ARTICLE_PAGE, - service: 'news', - statusCode: 200, - pathname: '/news', - }); - expect(container).toMatchSnapshot(); - }); - - it('should correctly render canonical navigation', () => { - const { container } = render(, { - bbcOrigin: 'https://www.test.bbc.co.uk', - id: 'c0000000000o', - isAmp: false, - pageType: ARTICLE_PAGE, - service: 'news', - statusCode: 200, - pathname: '/news', - }); - expect(container).toMatchSnapshot(); - }); - - it('should correctly render amp navigation on non-home navigation page', () => { - const { container } = render(, { - bbcOrigin: 'https://www.test.bbc.co.uk', - id: 'c0000000000o', - isAmp: true, - pageType: ARTICLE_PAGE, - service: 'news', - statusCode: 200, - pathname: '/uk', - }); - expect(container).toMatchSnapshot(); - }); - - it('should correctly render canonical navigation on non-home navigation page', () => { - const { container } = render(, { - bbcOrigin: 'https://www.test.bbc.co.uk', - id: 'c0000000000o', - isAmp: false, - pageType: ARTICLE_PAGE, - service: 'news', - statusCode: 200, - pathname: '/uk', - }); - expect(container).toMatchSnapshot(); - }); - - it('should correctly render amp navigation on non-navigation page', () => { - const { container } = render(, { - bbcOrigin: 'https://www.test.bbc.co.uk', - id: 'c0000000000o', - isAmp: true, - pageType: ARTICLE_PAGE, - service: 'news', - statusCode: 200, - pathname: '/not-a-navigation-page', - }); - expect(container).toMatchSnapshot(); - }); - - it('should correctly render canonical navigation on non-navigation page', () => { - const { container } = render(, { - bbcOrigin: 'https://www.test.bbc.co.uk', - id: 'c0000000000o', - isAmp: false, - pageType: ARTICLE_PAGE, - service: 'news', - statusCode: 200, - pathname: '/not-a-navigation-page', - }); - expect(container).toMatchSnapshot(); - }); - - // it.skip('should not render listItem in scrollable list when hideOnLiteSite is true and isLite is true', () => { - // const { ...rest } = newsConfig.default; - // const mockNavigation = [ - // { title: 'Home', url: '/home', hideOnLiteSite: true }, - // { title: 'News', url: '/news' }, - // { title: 'Sport', url: '/sport' }, - // ]; - - // const navigationComponent = ( - // - // - // - // ); - - // const { getAllByText, container } = render(navigationComponent, { - // bbcOrigin: 'https://www.test.bbc.co.uk', - // id: 'c0000000000o', - // isAmp: false, - // pageType: ARTICLE_PAGE, - // service: 'news', - // statusCode: 200, - // pathname: '/news', - // isLite: true, - // }); - - // const scrollableList = container.querySelector('[role="list"]'); - // expect(scrollableList).toBeInTheDocument(); - - // if (scrollableList) { - // // Only "News" and "Sport" should be inside the scrollable list - // const newsInList = within(scrollableList as HTMLElement).queryByText( - // 'News', - // ); - // const sportInList = within(scrollableList as HTMLElement).queryByText( - // 'Sport', - // ); - // const homeInList = within(scrollableList as HTMLElement).queryByText( - // 'Home', - // ); - - // expect(newsInList).toBeVisible(); - // expect(sportInList).toBeVisible(); - // expect(homeInList).toBeNull(); - // } - - // // "Home" should be rendered somewhere outside the scrollable list - // const homeElements = getAllByText('Home'); - // expect(homeElements.length).toBeGreaterThan(0); - // homeElements.forEach(element => { - // expect(element.closest('[role="list"]') as HTMLElement | null).toBeNull(); - // }); - // }); - - it('should prefer navItems prop over service config', () => { - const navItems = [ - { title: 'Home', url: '/home' }, - { title: 'About', url: '/about' }, - ]; - - const { getAllByText, queryAllByText } = render( - , - { - bbcOrigin: 'https://www.test.bbc.co.uk', - id: 'c0000000000o', - isAmp: false, - pageType: ARTICLE_PAGE, - service: 'news', - statusCode: 200, - pathname: '/news', - }, - ); - - expect(getAllByText('Home').length).toBeGreaterThan(0); - expect(getAllByText('About').length).toBeGreaterThan(0); - expect(queryAllByText('World')).toHaveLength(0); - }); - - it('should fall back to service config when navItems is null', () => { - const { navigation = [] } = indonesiaConfig.default; - - const { getAllByText } = render(, { - bbcOrigin: 'https://www.test.bbc.co.uk', - id: 'c0000000000o', - isAmp: false, - pageType: ARTICLE_PAGE, - service: 'indonesia', - statusCode: 200, - pathname: '/indonesian', - }); - - navigation.forEach(({ title }) => { - const elements = getAllByText(title); - elements.forEach(element => { - expect(element).toHaveTextContent(title); - }); - }); - }); - - it('should render nothing when navItems is an empty array', () => { - const { container } = render(, { - bbcOrigin: 'https://www.test.bbc.co.uk', - id: 'c0000000000o', - isAmp: false, - pageType: ARTICLE_PAGE, - service: 'news', - statusCode: 200, - pathname: '/news', - }); - - expect(container.firstChild).toBeNull(); - }); - - it.skip('should not render listItem in scrollable list when hideOnLiteSite is true and isLite is true', () => { - const { ...rest } = newsConfig.default; - const mockNavigation = [ - { title: 'Home', url: '/home', hideOnLiteSite: true }, - { title: 'News', url: '/news' }, - { title: 'Sport', url: '/sport' }, - ]; - - const navigationComponent = ( - - - - ); - - const { queryByText } = render(navigationComponent, { - bbcOrigin: 'https://www.test.bbc.co.uk', - id: 'c0000000000o', - isAmp: false, - pageType: ARTICLE_PAGE, - service: 'news', - statusCode: 200, - pathname: '/news', - isLite: true, - }); - - expect(queryByText(mockNavigation[0].title)).not.toBeInTheDocument(); - }); - - it('should render listItem in scrollable list when hideOnLiteSite is true and isLite is false', () => { - const { ...rest } = newsConfig.default; - const mockNavigation = [ - { title: 'Home', url: '/home', hideOnLiteSite: true }, - { title: 'News', url: '/news' }, - { title: 'Sport', url: '/sport' }, - ]; - - const navigationComponent = ( - - - - ); - - const { queryAllByText } = render(navigationComponent, { - bbcOrigin: 'https://www.test.bbc.co.uk', - id: 'c0000000000o', - isAmp: false, - pageType: ARTICLE_PAGE, - service: 'news', - statusCode: 200, - pathname: '/news', - isLite: false, - }); - - expect(queryAllByText(mockNavigation[0].title)[0]).toBeVisible(); - }); - - it('should render listItem in scrollable list when hideOnLiteSite is false/not set', () => { - const { ...rest } = newsConfig.default; - const mockNavigation = [ - { title: 'Home', url: '/home' }, - { title: 'News', url: '/news' }, - { title: 'Sport', url: '/sport' }, - ]; - - const navigationComponent = ( - - - - ); - - const { queryAllByText } = render(navigationComponent, { - bbcOrigin: 'https://www.test.bbc.co.uk', - id: 'c0000000000o', - isAmp: false, - pageType: ARTICLE_PAGE, - service: 'news', - statusCode: 200, - pathname: '/news', - isLite: false, - }); - - expect(queryAllByText(mockNavigation[0].title)[0]).toBeVisible(); - }); - - describe('View and click tracking', () => { - const scrollEventTrackingData = { - componentName: 'scrollable-navigation', - }; - - const dropdownEventTrackingData = { - componentName: 'dropdown-navigation', - }; - - const clickTrackerSpy = jest - .spyOn(clickTracking, 'default') - .mockImplementation(); - - beforeEach(() => { - clickTrackerSpy.mockRestore(); - }); - - it('should call the view tracking hook when on scrollable navigation', () => { - const viewTrackerSpy = jest.spyOn(viewTracking, 'default'); - render(, { - bbcOrigin: 'https://www.test.bbc.co.uk', - id: 'c0000000000o', - isAmp: true, - pageType: ARTICLE_PAGE, - service: 'news', - statusCode: 200, - pathname: '/news', - }); - expect(viewTrackerSpy).toHaveBeenCalledWith(scrollEventTrackingData); - }); - - it('should call the view tracking hook when on dropdown navigation', () => { - const viewTrackerSpy = jest.spyOn(viewTracking, 'default'); - render(, { - bbcOrigin: 'https://www.test.bbc.co.uk', - id: 'c0000000000o', - isAmp: true, - pageType: ARTICLE_PAGE, - service: 'news', - statusCode: 200, - pathname: '/news', - }); - expect(viewTrackerSpy).toHaveBeenCalledWith(dropdownEventTrackingData); - }); - - it('should call the click tracking hook when scrollable navigation is clicked', () => { - const { container } = render(, { - bbcOrigin: 'https://www.test.bbc.co.uk', - id: 'c0000000000o', - isAmp: true, - pageType: ARTICLE_PAGE, - service: 'news', - statusCode: 200, - pathname: '/news', - }); - - fireEvent.click(container); - - expect(container.onclick).toBeTruthy(); - }); - }); - - describe('Language Navigation', () => { - it('should render LanguageNavigation for WS service in all environment', async () => { - const { getByTestId } = await act(async () => - render(, { - bbcOrigin: 'https://www.test.bbc.co.uk', - id: 'c0000000000o', - isAmp: false, - pageType: HOME_PAGE, - service: 'ws', - statusCode: 200, - pathname: '/ws/languages', - }), - ); - - expect(getByTestId('collapsible-nav')).toBeInTheDocument(); - }); - }); -}); diff --git a/src/app/components/Navigation/index.tsx b/src/app/components/Navigation/index.tsx index c5f63a9ed2b..809dd87a017 100644 --- a/src/app/components/Navigation/index.tsx +++ b/src/app/components/Navigation/index.tsx @@ -8,11 +8,9 @@ import useClickTrackerHandler from '#app/hooks/useClickTrackerHandler'; import useViewTracker from '#app/hooks/useViewTracker'; import { RequestContext } from '#contexts/RequestContext'; import { ServiceContext } from '#contexts/ServiceContext'; -import LanguageNavigation from './LanguageNavigation/lazy'; import Canonical from './index.canonical'; import Amp from './index.amp'; -import { NavigationItem, NavigationContainerProps } from './types'; -import TopLevelNav from './TopLevelNav'; +import type { NavigationItem, NavigationContainerProps } from './types'; const renderListItems = ( Li: React.ElementType, @@ -26,7 +24,7 @@ const renderListItems = ( viewTracker: unknown, isLite?: boolean, ) => - navigation.reduce((listAcc, item, index) => { + navigation.reduce((listAcc, item, index) => { const { title, url, hideOnLiteSite } = item; const active = index === activeIndex; @@ -49,57 +47,44 @@ const renderListItems = ( ); return [...listAcc, listItem]; - }, [] as React.ReactNode[]); - -const NavigationContainer = ({ - navItems, + }, []); + +const getTopNavLinks = (navigation: NavigationItem[]): NavigationItem[] => [ + { + title: navigation?.[0]?.title || 'Home', + url: 'https://www.bbc.com/arabic', + }, + { title: 'Watch', url: 'https://www.bbc.com/arabic/topics/crgyknwdlwnt' }, + { title: 'Listen', url: 'https://www.bbc.com/arabic/topics/cljddp5lw0dt' }, +]; + +const NavigationContainer: React.FC = ({ propsForTopBarOJComponent, -}: NavigationContainerProps) => { - const { isAmp, isLite } = use(RequestContext); +}) => { + const { isAmp, isLite, pageType } = use(RequestContext); + console.log('pageType', pageType); const { blocks = [] } = propsForTopBarOJComponent || {}; - const { - script, - translations, - navigation: navFromServiceConfig = [], - service, - dir, - collapsibleNavigation, - } = use(ServiceContext); - + const { script, translations, navigation, service, dir } = + use(ServiceContext); const { canonicalLink, origin } = use(RequestContext); const { currentPage, navMenuText } = translations; const scrollableNavEventTrackingData = { - componentName: 'scrollable-navigation', - }; - - const dropdownNavEventTrackingData = { - componentName: 'dropdown-navigation', + componentName: `scrollable-navigation`, }; + const dropdownNavEventTrackingData = { componentName: `dropdown-navigation` }; const scrollableNavClickTrackerHandler = useClickTrackerHandler( scrollableNavEventTrackingData, ); - const dropdownNavClickTrackerHandler = useClickTrackerHandler( dropdownNavEventTrackingData, ); - const scrollableNavViewTracker = useViewTracker( scrollableNavEventTrackingData, ); - const dropdownNavViewTracker = useViewTracker(dropdownNavEventTrackingData); - const renderLanguageNavigation = collapsibleNavigation?.length; - - if (renderLanguageNavigation) { - return ; - } - - // Prefer navItems passed from props over service config - // Eventually all services will migrate to passing navItems via props - const navigation = navItems || navFromServiceConfig; if (!navigation || navigation.length === 0) { return null; } @@ -108,16 +93,42 @@ const NavigationContainer = ({ link => `${origin}${link.url}` === canonicalLink, ); + // Top scrollable nav (static links) + const topNavLinks = getTopNavLinks(navigation); + // Set activeIndex to 0 only if pageType is 'home', otherwise -1 (no active) + const topActiveIndex = pageType === 'home' ? 0 : -1; + + const topScrollableListItems = ( + + {renderListItems( + NavigationLi, + topNavLinks, + script, + currentPage, + service, + dir, + topActiveIndex, + undefined, + undefined, + isLite, + )} + + ); + + // Remove the first item (Home) from the main navigation list + const mainNavLinks = navigation.slice(1); + const mainActiveIndex = activeIndex > 0 ? activeIndex - 1 : -1; + // Main scrollable nav (dynamic) const scrollableListItems = ( {renderListItems( NavigationLi, - navigation, + mainNavLinks, script, currentPage, service, dir, - activeIndex, + mainActiveIndex, scrollableNavClickTrackerHandler, scrollableNavViewTracker, isLite, @@ -126,7 +137,7 @@ const NavigationContainer = ({ ); const dropdownListItems = ( - + {renderListItems( DropdownLi, navigation, @@ -137,30 +148,23 @@ const NavigationContainer = ({ activeIndex, dropdownNavClickTrackerHandler, dropdownNavViewTracker, - isLite, )} ); const Navigation = isAmp ? Amp : Canonical; - // --- Render TopLevelNav above the main navigation list --- return ( - <> -
    - Navigation test: This is showing on the page. -
    - - - + ); }; diff --git a/src/app/components/Navigation/testHelpers.tsx b/src/app/components/Navigation/testHelpers.tsx index deb40eea112..e69de29bb2d 100644 --- a/src/app/components/Navigation/testHelpers.tsx +++ b/src/app/components/Navigation/testHelpers.tsx @@ -1,14 +0,0 @@ -export const dropdownTestId = 'dropdown'; -export const scrollableTestId = 'scrollable-list'; - -export const scrollableListItems = ( -
      -
    • List Items
    • -
    -); - -export const dropdownListItems = ( -
      -
    • Dropdown Items
    • -
    -); diff --git a/src/app/components/Navigation/types.ts b/src/app/components/Navigation/types.ts index 1673fef31cd..c337c267cd3 100644 --- a/src/app/components/Navigation/types.ts +++ b/src/app/components/Navigation/types.ts @@ -1,71 +1,22 @@ -import { ReactNode } from 'react'; +import { TopStoryItem } from '#app/pages/ArticlePage/PagePromoSections/TopStoriesSection/types'; -export type NavigationSubItem = { - title: string; - url: string; - hideOnLiteSite?: boolean; - subItems?: NavigationSubItem[]; -}; - -export type NavigationItem = { - title: string; - url: string; - hideOnLiteSite?: boolean; - subItems?: NavigationSubItem[]; -}; - -export type PropsForTopBarOJComponent = { - blocks?: unknown[]; - experimentVariant?: string; -}; - -export type NavigationContainerProps = { - script: unknown; - service: string; - dir: string; - id?: string; - ampOpenClass?: string; - scrollableListItems: ReactNode; - dropdownListItems: ReactNode; - menuAnnouncedText: string; - ampMenuButton?: ReactNode; - blocks?: unknown[]; - navItems?: NavigationItem[] | null; - propsForTopBarOJComponent?: PropsForTopBarOJComponent | null; - divider?: ReactNode; - topBarOJs?: ReactNode; - isOpen?: boolean; -}; - -export type CanonicalNavigationContainerProps = { - script: unknown; - service: string; - dir: string; - menuAnnouncedText: string; - scrollableListItems: ReactNode; - dropdownListItems: ReactNode; - blocks?: unknown[]; - navItems?: NavigationItem[] | null; - propsForTopBarOJComponent?: PropsForTopBarOJComponent | null; -}; - -export type AmpNavigationContainerProps = { +export interface AmpNavigationContainerProps { script: unknown; service: string; dir: string; menuAnnouncedText: string; scrollableListItems: React.ReactNode; dropdownListItems: React.ReactNode; - navItems?: NavigationItem[] | null; - propsForTopBarOJComponent?: PropsForTopBarOJComponent | null; -}; +} -export type NavigationBaseProps = { - script: unknown; - service: string; - dir: string; - children: ReactNode; - isOpen?: boolean; - ampOpenClass?: string | null; - id?: string; -}; +export interface NavigationItem { + title: string; + url: string; + hideOnLiteSite?: boolean; +} + +export interface NavigationContainerProps { + propsForTopBarOJComponent?: { + blocks?: TopStoryItem[]; + }; +} diff --git a/src/app/legacy/containers/Navigation/index.jsx b/src/app/legacy/containers/Navigation/index.jsx index 7150332b88f..68628a599fe 100644 --- a/src/app/legacy/containers/Navigation/index.jsx +++ b/src/app/legacy/containers/Navigation/index.jsx @@ -60,7 +60,6 @@ const NavigationContainer = ({ navItems, propsForTopBarOJComponent }) => { dir, collapsibleNavigation, } = use(ServiceContext); - console.log('navItems:', navItems); const { canonicalLink, origin } = use(RequestContext); const { currentPage, navMenuText } = translations; 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..69874f9350f 100644 --- a/src/app/legacy/psammead/psammead-navigation/src/ScrollableNavigation/index.jsx +++ b/src/app/legacy/psammead/psammead-navigation/src/ScrollableNavigation/index.jsx @@ -22,6 +22,22 @@ const scrollableNavOutline = ` `; const StyledScrollableNav = styled.div` + ${({ navType, theme }) => + navType === 'top' + ? ` + background: ${theme.palette.POSTBOX}; + color: #fff; + width: 100%; + display: flex; + flex-direction: row; + align-items: stretch; + position: relative; + z-index: 2; + * { + color: #fff !important; + } + ` + : ''} @media (max-width: ${GEL_GROUP_2_SCREEN_WIDTH_MAX}) { white-space: nowrap; overflow-x: scroll; @@ -63,17 +79,31 @@ const StyledScrollableNav = styled.div` z-index: 3; overflow: hidden; pointer-events: none; - background: linear-gradient( - ${({ dir }) => (dir === 'ltr' ? 'to right' : 'to left')}, - ${props => hexToRGB(props.theme.palette.WHITE, 0)} 0%, - ${props => hexToRGB(props.theme.palette.WHITE, 1)} 100% - ); + /* Only show gradient if navType is not top */ + ${props => + props.navType === 'top' + ? 'background: none !important;' + : `background: linear-gradient( + ${props.dir === 'ltr' ? 'to right' : 'to left'}, + ${hexToRGB(props.theme.palette.WHITE, 0)} 0%, + ${hexToRGB(props.theme.palette.WHITE, 1)} 100% + );`} } } `; -export const ScrollableNavigation = ({ children, dir = 'ltr', ...props }) => ( - +export const ScrollableNavigation = ({ + children, + dir = 'ltr', + navType, + ...props +}) => ( + {children} ); From c58d015e7ca5e8e9ca6317570fc94e73f82b44f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Tue, 10 Feb 2026 17:19:11 +0000 Subject: [PATCH 04/82] menu button colours and hover state --- .../components/Navigation/index.canonical.tsx | 5 ++- src/app/components/Navigation/index.tsx | 3 ++ src/app/legacy/containers/Header/index.jsx | 21 +++++++++++ .../src/DropdownNavigation/index.jsx | 8 ++++- .../psammead-navigation/src/index.jsx | 36 ++++++++++++------- 5 files changed, 56 insertions(+), 17 deletions(-) diff --git a/src/app/components/Navigation/index.canonical.tsx b/src/app/components/Navigation/index.canonical.tsx index bcd9b56ee70..6003bdddf79 100644 --- a/src/app/components/Navigation/index.canonical.tsx +++ b/src/app/components/Navigation/index.canonical.tsx @@ -137,7 +137,6 @@ const CanonicalNavigationContainer: React.FC< )} - {scrollableListItems} @@ -146,9 +145,9 @@ const CanonicalNavigationContainer: React.FC< {dropdownListItems} - - {enabled && } + + {enabled && }
    ); }; diff --git a/src/app/components/Navigation/index.tsx b/src/app/components/Navigation/index.tsx index 809dd87a017..5f7f548b4c8 100644 --- a/src/app/components/Navigation/index.tsx +++ b/src/app/components/Navigation/index.tsx @@ -23,6 +23,7 @@ const renderListItems = ( clickTracker: unknown, viewTracker: unknown, isLite?: boolean, + navType?: string, ) => navigation.reduce((listAcc, item, index) => { const { title, url, hideOnLiteSite } = item; @@ -41,6 +42,7 @@ const renderListItems = ( dir={dir} clickTracker={clickTracker} viewTracker={viewTracker} + navType={navType} > {title} @@ -111,6 +113,7 @@ const NavigationContainer: React.FC = ({ undefined, undefined, isLite, + 'top', )}
    ); diff --git a/src/app/legacy/containers/Header/index.jsx b/src/app/legacy/containers/Header/index.jsx index 356859b93e7..bb4b638b29f 100644 --- a/src/app/legacy/containers/Header/index.jsx +++ b/src/app/legacy/containers/Header/index.jsx @@ -12,10 +12,30 @@ import { } from '#app/routes/utils/pageTypes'; import LiteSiteSummary from '#app/components/LiteSiteSummary'; import NavigationContainer from '#src/app/components/Navigation'; +import styled from '@emotion/styled'; import { ServiceContext } from '../../../contexts/ServiceContext'; import ConsentBanner from '../ConsentBanner'; import BrandContainer from '../Brand'; +const Divider = styled.div` + position: relative; + width: 100%; + margin: 0 auto; + &::after { + content: ''; + display: block; + width: 100%; + border-bottom: 0.0625rem solid ${props => props.theme.palette.GREY_3}; + } + @media (min-width: 1041px) { + width: calc(100vw + 0.8rem); + margin-left: calc(-1 * (100vw - 1014px) / 2); + } + @media (min-width: 1008px) { + display: none; + } +`; + const Header = ({ brandRef, borderBottom, skipLink, scriptLink, linkId }) => { const [showConsentBanner, setShowConsentBanner] = useState(true); @@ -107,6 +127,7 @@ const HeaderContainer = ({ navItems, propsForTopBarOJComponent }) => { /> )} {isLite && } + + navType === 'top' ? theme.palette.POSTBOX : 'transparent'}; + color: ${({ navType }) => (navType === 'top' ? '#fff' : 'inherit')}; border: 0; ${({ dir }) => (dir === 'ltr' ? `float: left;` : `float: right;`)} @@ -199,6 +201,8 @@ const MenuButton = styled(Button)` & svg { vertical-align: middle; + color: ${({ navType }) => (navType === 'top' ? '#fff' : 'inherit')}; + fill: ${({ navType }) => (navType === 'top' ? '#fff' : 'inherit')}; } `; @@ -208,6 +212,7 @@ export const CanonicalMenuButton = ({ onClick, dir = 'ltr', script, + navType, }) => ( {isOpen ? navigationIcons.cross : navigationIcons.hamburger} {announcedText} diff --git a/src/app/legacy/psammead/psammead-navigation/src/index.jsx b/src/app/legacy/psammead/psammead-navigation/src/index.jsx index 72b814722a8..121e9aac7e5 100644 --- a/src/app/legacy/psammead/psammead-navigation/src/index.jsx +++ b/src/app/legacy/psammead/psammead-navigation/src/index.jsx @@ -65,19 +65,21 @@ const StyledLink = styled.a` &:hover::after { ${ListItemBorder} - border-bottom: ${GEL_SPACING_HLF} solid ${props => - props.theme.palette.POSTBOX}; - ${({ currentLink, theme }) => - currentLink && - ` - border-bottom: ${CURRENT_ITEM_HOVER_BORDER} solid ${theme.palette.POSTBOX}; - `} + border-bottom: ${GEL_SPACING_HLF} solid + ${({ navType, theme }) => + navType === 'top' ? '#fff' : theme.palette.POSTBOX}; + ${({ currentLink, theme, navType }) => + currentLink && navType === 'top' + ? `border-bottom: ${CURRENT_ITEM_HOVER_BORDER} solid #fff;` + : currentLink && + `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 + ${({ navType, theme }) => + navType === 'top' ? '#fff' : theme.palette.POSTBOX}; top: 0; border: ${focusIndicatorThickness} solid ${props => props.theme.palette.BLACK}; @@ -86,8 +88,9 @@ 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 + ${({ navType, theme }) => + navType === 'top' ? '#fff' : theme.palette.POSTBOX}; top: 0; border: ${focusIndicatorThickness} solid ${props => props.theme.palette.BLACK}; @@ -124,8 +127,9 @@ 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 + ${({ navType }) => + navType === 'top' ? '#fff' : props => props.theme.palette.POSTBOX}; } `; @@ -134,11 +138,13 @@ const CurrentLink = ({ children: link, script, currentPageText = null, + navType, }) => ( @@ -163,6 +169,7 @@ export const NavigationLi = ({ service, dir = 'ltr', viewTracker = null, + navType, ...props }) => { return ( @@ -173,6 +180,7 @@ export const NavigationLi = ({ script={script} service={service} currentLink + navType={navType} // 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}`} className="focusIndicatorRemove" @@ -183,6 +191,7 @@ export const NavigationLi = ({ linkId={link} script={script} currentPageText={currentPageText} + navType={navType} > {link} @@ -193,6 +202,7 @@ export const NavigationLi = ({ script={script} service={service} className="focusIndicatorRemove" + navType={navType} {...clickTracker} {...props} > From 3c05b157e1ac62c129527969e6b1b68df228f2ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Sun, 15 Feb 2026 16:19:32 +0000 Subject: [PATCH 05/82] vary nav version on service --- src/app/legacy/containers/Header/index.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/legacy/containers/Header/index.jsx b/src/app/legacy/containers/Header/index.jsx index bb4b638b29f..bfe413449b8 100644 --- a/src/app/legacy/containers/Header/index.jsx +++ b/src/app/legacy/containers/Header/index.jsx @@ -11,8 +11,9 @@ import { LIVE_PAGE, } from '#app/routes/utils/pageTypes'; import LiteSiteSummary from '#app/components/LiteSiteSummary'; -import NavigationContainer from '#src/app/components/Navigation'; +import NewNavigationContainer from '#src/app/components/Navigation'; import styled from '@emotion/styled'; +import LegacyNavigationContainer from '../Navigation'; import { ServiceContext } from '../../../contexts/ServiceContext'; import ConsentBanner from '../ConsentBanner'; import BrandContainer from '../Brand'; @@ -111,6 +112,8 @@ const HeaderContainer = ({ navItems, propsForTopBarOJComponent }) => { if (isApp) return null; + const NavigationComponent = + service === 'arabic' ? NewNavigationContainer : LegacyNavigationContainer; return (
    {isAmp ? ( @@ -128,7 +131,7 @@ const HeaderContainer = ({ navItems, propsForTopBarOJComponent }) => { )} {isLite && } - From f3158549feb73c808ec5ca3e1a78ec9be0990bf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Sun, 15 Feb 2026 18:22:33 +0000 Subject: [PATCH 06/82] logo on top --- src/app/legacy/containers/Brand/index.jsx | 1 + .../containers/Header/NewLogoBanner.jsx | 55 +++++++++++++++++++ src/app/legacy/containers/Header/index.jsx | 4 +- 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/app/legacy/containers/Header/NewLogoBanner.jsx diff --git a/src/app/legacy/containers/Brand/index.jsx b/src/app/legacy/containers/Brand/index.jsx index 6abea032356..30ecdaecf2c 100644 --- a/src/app/legacy/containers/Brand/index.jsx +++ b/src/app/legacy/containers/Brand/index.jsx @@ -48,6 +48,7 @@ const BrandContainer = ({ 'serbian', 'ws', ]; + const newNavBrands = ['arabic']; const brandPath = getBrandPath(service, variant); diff --git a/src/app/legacy/containers/Header/NewLogoBanner.jsx b/src/app/legacy/containers/Header/NewLogoBanner.jsx new file mode 100644 index 00000000000..ce9455592e2 --- /dev/null +++ b/src/app/legacy/containers/Header/NewLogoBanner.jsx @@ -0,0 +1,55 @@ +import styled from '@emotion/styled'; + +const Banner = styled.div` + background: #fff; + width: 100%; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + height: ${44 / 16}rem; + @media (min-width: 37.5rem) { + height: ${60 / 16}rem; + } + @media (min-width: 48rem) { + height: ${64 / 16}rem; + } +`; + +const LOGO_ASPECT_RATIO = 168 / 48; +const SVG_HEIGHT = 38; +const LogoSvg = styled.svg` + box-sizing: content-box; + color: #000; + fill: currentColor; + height: ${SVG_HEIGHT}px; + max-width: ${LOGO_ASPECT_RATIO * SVG_HEIGHT}px; + width: 100%; + display: block; +`; + +const NewLogoBanner = () => ( + + + +); + +export default NewLogoBanner; diff --git a/src/app/legacy/containers/Header/index.jsx b/src/app/legacy/containers/Header/index.jsx index bfe413449b8..4fddd1c3500 100644 --- a/src/app/legacy/containers/Header/index.jsx +++ b/src/app/legacy/containers/Header/index.jsx @@ -12,11 +12,12 @@ import { } 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 styled from '@emotion/styled'; -import LegacyNavigationContainer from '../Navigation'; import { ServiceContext } from '../../../contexts/ServiceContext'; import ConsentBanner from '../ConsentBanner'; import BrandContainer from '../Brand'; +import NewLogoBanner from './NewLogoBanner'; const Divider = styled.div` position: relative; @@ -116,6 +117,7 @@ const HeaderContainer = ({ navItems, propsForTopBarOJComponent }) => { service === 'arabic' ? NewNavigationContainer : LegacyNavigationContainer; return (
    + {service === 'arabic' && } {isAmp ? (
    Date: Sun, 15 Feb 2026 18:32:13 +0000 Subject: [PATCH 07/82] break point logo position changes --- .../containers/Header/NewLogoBanner.jsx | 82 ++++++++++++++----- 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/src/app/legacy/containers/Header/NewLogoBanner.jsx b/src/app/legacy/containers/Header/NewLogoBanner.jsx index ce9455592e2..65c03391d97 100644 --- a/src/app/legacy/containers/Header/NewLogoBanner.jsx +++ b/src/app/legacy/containers/Header/NewLogoBanner.jsx @@ -1,4 +1,12 @@ import styled from '@emotion/styled'; +import { + GEL_GROUP_2_SCREEN_WIDTH_MIN, // 37.5rem = 600px + GEL_GROUP_3_SCREEN_WIDTH_MIN, // 48rem = 768px +} from '#psammead/gel-foundations/src/breakpoints'; +import { + GEL_SPACING, + GEL_SPACING_DBL, +} from '#psammead/gel-foundations/src/spacings'; const Banner = styled.div` background: #fff; @@ -8,6 +16,7 @@ const Banner = styled.div` align-items: center; justify-content: center; height: ${44 / 16}rem; + @media (min-width: 37.5rem) { height: ${60 / 16}rem; } @@ -16,8 +25,40 @@ const Banner = styled.div` } `; +/** + * Match Brand's internal content width so the logo aligns with brandSvg + * This mirrors SVG_WRAPPER_MAX_WIDTH_ABOVE_1280PX used by Brand/SvgWrapper + */ +const SVG_WRAPPER_MAX_WIDTH_ABOVE_1280PX = '63rem'; + +const Inner = styled.div` + /* Constrain to the same column as Brand's SvgWrapper */ + width: 100%; + max-width: ${SVG_WRAPPER_MAX_WIDTH_ABOVE_1280PX}; + margin: 0 auto; + + /* Center by default */ + display: flex; + justify-content: center; + + /** + * Between 600px (37.5rem) and 1008px (63rem), + * left-align and apply the same horizontal paddings as Brand Banner + */ + @media (min-width: ${GEL_GROUP_2_SCREEN_WIDTH_MIN}) and (max-width: ${SVG_WRAPPER_MAX_WIDTH_ABOVE_1280PX}) { + justify-content: flex-start; + padding: 0 ${GEL_SPACING}; + } + + /* Match Brand's double padding from 768px upwards within this band */ + @media (min-width: ${GEL_GROUP_3_SCREEN_WIDTH_MIN}) and (max-width: ${SVG_WRAPPER_MAX_WIDTH_ABOVE_1280PX}) { + padding: 0 ${GEL_SPACING_DBL}; + } +`; + const LOGO_ASPECT_RATIO = 168 / 48; const SVG_HEIGHT = 38; + const LogoSvg = styled.svg` box-sizing: content-box; color: #000; @@ -30,25 +71,28 @@ const LogoSvg = styled.svg` const NewLogoBanner = () => ( - + {/* ✅ Inner wrapper controls alignment and width per breakpoint */} + + + ); From 7e0db897d3a5aa9f03445316be2359c8af54ce28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Mon, 16 Feb 2026 12:02:51 +0000 Subject: [PATCH 08/82] make postbox coloour full width and divider right colour --- .../components/Navigation/index.canonical.tsx | 47 +++++++++++++++---- .../containers/Header/NewLogoBanner.jsx | 9 +--- src/app/legacy/containers/Header/index.jsx | 9 +--- 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/src/app/components/Navigation/index.canonical.tsx b/src/app/components/Navigation/index.canonical.tsx index 6003bdddf79..5ab6a86353f 100644 --- a/src/app/components/Navigation/index.canonical.tsx +++ b/src/app/components/Navigation/index.canonical.tsx @@ -56,7 +56,7 @@ const NavStack = styled.div` width: 100%; `; -const NavRow = styled.div` +const NavRow = styled.div<{ dir: string }>` display: flex; flex-direction: ${({ dir }) => (dir === 'rtl' ? 'row-reverse' : 'row')}; align-items: stretch; @@ -64,6 +64,34 @@ const NavRow = styled.div` width: 100%; `; +/** + * ✅ TopRow is the same as NavRow, but it paints a full-bleed POSTBOX background behind it. + * This works even though ancestors are width-constrained because we use a centered 100vw pseudo-element. + * No spacing/height changes; content stays exactly where it is. + */ +const TopRow = styled(NavRow)` + position: relative; + z-index: 0; + + &::before { + content: ''; + position: absolute; + z-index: -1; + /* Cover this row vertically */ + top: 0; + bottom: 0; + + /* Full-bleed horizontally, independent of the constrained container */ + width: 100vw; + left: 50%; + transform: translateX(-50%); + + /* POSTBOX red from theme */ + background: ${props => props.theme.palette.POSTBOX}; + pointer-events: none; /* ensure it never interferes with clicks */ + } +`; + const LowerNavWrapper = styled.div` width: 100%; margin-top: 0.25rem; @@ -86,15 +114,17 @@ const CanonicalNavigationContainer: React.FC< const { isLite } = use(RequestContext); const { enabled } = useToggle('topBarOJs'); const [isOpen, setIsOpen] = useState(false); + useMediaQuery(`(max-width: ${GEL_GROUP_2_SCREEN_WIDTH_MAX})`, event => { if (!event.matches) { setIsOpen(false); } }); + return ( - + {dir === 'rtl' ? ( <> {!isLite && ( @@ -109,19 +139,17 @@ const CanonicalNavigationContainer: React.FC< /> )} + {topScrollableListItems} ) : ( <> - + {topScrollableListItems} + {!isLite && (
    )} - + + {scrollableListItems} + {dropdownListItems} + {enabled && } diff --git a/src/app/legacy/containers/Header/NewLogoBanner.jsx b/src/app/legacy/containers/Header/NewLogoBanner.jsx index 65c03391d97..ace0f594df4 100644 --- a/src/app/legacy/containers/Header/NewLogoBanner.jsx +++ b/src/app/legacy/containers/Header/NewLogoBanner.jsx @@ -15,14 +15,7 @@ const Banner = styled.div` display: flex; align-items: center; justify-content: center; - height: ${44 / 16}rem; - - @media (min-width: 37.5rem) { - height: ${60 / 16}rem; - } - @media (min-width: 48rem) { - height: ${64 / 16}rem; - } + height: 4rem; `; /** diff --git a/src/app/legacy/containers/Header/index.jsx b/src/app/legacy/containers/Header/index.jsx index 4fddd1c3500..b06f04832b8 100644 --- a/src/app/legacy/containers/Header/index.jsx +++ b/src/app/legacy/containers/Header/index.jsx @@ -27,14 +27,7 @@ const Divider = styled.div` content: ''; display: block; width: 100%; - border-bottom: 0.0625rem solid ${props => props.theme.palette.GREY_3}; - } - @media (min-width: 1041px) { - width: calc(100vw + 0.8rem); - margin-left: calc(-1 * (100vw - 1014px) / 2); - } - @media (min-width: 1008px) { - display: none; + border-bottom: 0.0625rem solid #d77272; } `; From 4dcc57fff74e8d166a2a1c47a0f9968486d79f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Mon, 16 Feb 2026 13:05:33 +0000 Subject: [PATCH 09/82] new brand svg --- src/app/legacy/containers/Brand/index.jsx | 2 +- .../psammead-brand/src/ArabicBrandSVG.jsx | 45 +++++++++++++++++++ .../psammead/psammead-brand/src/index.jsx | 19 ++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 src/app/legacy/psammead/psammead-brand/src/ArabicBrandSVG.jsx diff --git a/src/app/legacy/containers/Brand/index.jsx b/src/app/legacy/containers/Brand/index.jsx index 30ecdaecf2c..9e9fd8d7982 100644 --- a/src/app/legacy/containers/Brand/index.jsx +++ b/src/app/legacy/containers/Brand/index.jsx @@ -48,7 +48,6 @@ const BrandContainer = ({ 'serbian', 'ws', ]; - const newNavBrands = ['arabic']; const brandPath = getBrandPath(service, variant); @@ -64,6 +63,7 @@ const BrandContainer = ({ skipLink={skipLink} scriptLink={scriptLink} isLongBrand={longBrands.includes(service)} + service={service} ref={brandRef} {...props} /> diff --git a/src/app/legacy/psammead/psammead-brand/src/ArabicBrandSVG.jsx b/src/app/legacy/psammead/psammead-brand/src/ArabicBrandSVG.jsx new file mode 100644 index 00000000000..a3f91552509 --- /dev/null +++ b/src/app/legacy/psammead/psammead-brand/src/ArabicBrandSVG.jsx @@ -0,0 +1,45 @@ +// This file exports the BBC News World Service Arabic SVG as a React component +// [copilot] + +import React from 'react'; + +const ArabicBrandSVG = props => ( + + + + + + + + + + + + + +); + +export default ArabicBrandSVG; diff --git a/src/app/legacy/psammead/psammead-brand/src/index.jsx b/src/app/legacy/psammead/psammead-brand/src/index.jsx index ca41762c46d..4ca36758ac0 100644 --- a/src/app/legacy/psammead/psammead-brand/src/index.jsx +++ b/src/app/legacy/psammead/psammead-brand/src/index.jsx @@ -14,6 +14,7 @@ import { } from '#psammead/gel-foundations/src/spacings'; import { focusIndicatorThickness } from '../../../../components/ThemeProvider/focusIndicator'; import VisuallyHiddenText from '../../../../components/VisuallyHiddenText'; +import ArabicBrandSVG from './ArabicBrandSVG'; const SVG_WRAPPER_MAX_WIDTH_ABOVE_1280PX = '63rem'; const SIZE_OF_BRAND_LINK_WITH_VARIANT_BELOW_239PX = '2.625rem'; @@ -148,7 +149,25 @@ const StyledBrand = ({ serviceLocalisedName = null, svg, isLongBrand, + service, }) => { + if (service === 'arabic') { + return ( + <> +

    Child element

    Child element