diff --git a/.eslintrc.js b/.eslintrc.js index bce2049712f..72998947bf6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -74,6 +74,7 @@ module.exports = { 'custom-element', 'custom-template', 'fallback', + 'on', // AMP event handler attribute — https://amp.dev/documentation/guides-and-tutorials/learn/amp-actions-and-events/ ], }, ], diff --git a/scripts/bundleSize/bundleSizeConfig.js b/scripts/bundleSize/bundleSizeConfig.js index b6c38b33d1f..fee45d8ffc4 100644 --- a/scripts/bundleSize/bundleSizeConfig.js +++ b/scripts/bundleSize/bundleSizeConfig.js @@ -9,5 +9,5 @@ export const VARIANCE = 5; -export const MIN_SIZE = 935; -export const MAX_SIZE = 1309; +export const MIN_SIZE = 948; +export const MAX_SIZE = 1320; diff --git a/src/app/components/Navigation/DropdownNavigation/index.styles.ts b/src/app/components/Navigation/DropdownNavigation/index.styles.ts new file mode 100644 index 00000000000..d3babf6542a --- /dev/null +++ b/src/app/components/Navigation/DropdownNavigation/index.styles.ts @@ -0,0 +1,142 @@ +import { css, Theme } from '@emotion/react'; +import pixelsToRem from '#app/utilities/pixelsToRem'; +import { GROUP_B_MIN_WIDTH } from '#app/components/ThemeProvider/fontMediaQueries'; +import { MAX_NAV_ITEM_HEIGHT } from '../index.styles'; + +export default { + dropdown: ({ palette, mq }: Theme) => + css({ + position: 'absolute', + top: '100%', + left: 0, + width: '100%', + zIndex: 99999, + backgroundColor: palette.WHITE, + borderBottom: `${pixelsToRem(3)}rem solid ${palette.POSTBOX}`, + clear: 'both', + overflow: 'hidden', + height: 0, + transition: 'all 0.2s ease-out', + transitionTimingFunction: 'cubic-bezier(0, 0, 0.58, 1)', + visibility: 'hidden', + [mq.GROUP_3_MIN_WIDTH]: { + display: 'none', + visibility: 'hidden', + }, + '@media (prefers-reduced-motion: reduce)': { + transition: 'none', + }, + }), + + dropdownOpen: css({ + visibility: 'visible', + }), + + ampDropdown: ({ palette, mq }: Theme) => + css({ + position: 'absolute', + top: '100%', + left: 0, + width: '100%', + zIndex: 99999, + backgroundColor: palette.WHITE, + borderBottom: `${pixelsToRem(3)}rem solid ${palette.POSTBOX}`, + clear: 'both', + [mq.GROUP_3_MIN_WIDTH]: { + display: 'none', + visibility: 'hidden', + }, + }), + + dropdownList: css({ + listStyleType: 'none', + margin: 0, + padding: 0, + }), + + dropdownListItem: ({ palette }: Theme) => + css({ + padding: 0, + borderBottom: `${pixelsToRem(1)}rem solid ${palette.GREY_3}`, + '&:last-child': { + border: 0, + }, + }), + + dropdownLink: ({ palette, spacings, fontSizes, fontVariants }: Theme) => + css({ + ...fontSizes.pica, + ...fontVariants.sansRegular, + color: palette.GREY_10, + textDecoration: 'none', + display: 'block', + position: 'relative', + padding: `${pixelsToRem(12)}rem ${spacings.FULL}rem`, + '&:hover': { + backgroundColor: palette.GREY_3, + textDecoration: 'none', + '&::before': { + opacity: 1, + }, + }, + '&::before': { + content: '""', + position: 'absolute', + top: 0, + insetInlineStart: 0, + height: '100%', + width: `${pixelsToRem(4)}rem`, + background: palette.POSTBOX, + display: 'block', + opacity: 0, + }, + '&:focus-visible': { + textDecoration: 'underline', + textDecorationColor: palette.POSTBOX, + outlineOffset: `-${pixelsToRem(3)}rem`, + }, + }), + + // Active link indicator: inline-start border + padding for the current page item + currentLink: ({ palette, spacings }: Theme) => + css({ + borderInlineStart: `${pixelsToRem(4)}rem solid ${palette.POSTBOX}`, + paddingInlineStart: `${spacings.FULL}rem`, + }), + + menuButton: ({ palette, mq }: Theme) => + css({ + position: 'relative', + padding: 0, + margin: 0, + border: 0, + flexShrink: 0, // never let the flex container squeeze the button off-screen + float: 'inline-start', + backgroundColor: palette.POSTBOX, + color: palette.WHITE, + width: `${pixelsToRem(MAX_NAV_ITEM_HEIGHT)}rem`, + height: `${pixelsToRem(MAX_NAV_ITEM_HEIGHT)}rem`, + [mq.GROUP_3_MIN_WIDTH]: { + display: 'none', + visibility: 'hidden', + }, + [GROUP_B_MIN_WIDTH]: { + width: `${pixelsToRem(MAX_NAV_ITEM_HEIGHT)}rem`, + height: `${pixelsToRem(MAX_NAV_ITEM_HEIGHT)}rem`, + }, + svg: { + verticalAlign: 'middle', + fill: palette.WHITE, + }, + '&:hover, &:focus': { + cursor: 'pointer', + boxShadow: `inset 0 0 0 ${pixelsToRem(4)}rem ${palette.WHITE}`, + '&::after': { + content: "''", + position: 'absolute', + inset: 0, + border: `${pixelsToRem(4)}rem solid ${palette.BLACK}`, + }, + }, + }), +}; diff --git a/src/app/components/Navigation/DropdownNavigation/index.tsx b/src/app/components/Navigation/DropdownNavigation/index.tsx new file mode 100644 index 00000000000..13416d31bdf --- /dev/null +++ b/src/app/components/Navigation/DropdownNavigation/index.tsx @@ -0,0 +1,185 @@ +import { PropsWithChildren, useRef, cloneElement } from 'react'; +import { Helmet } from 'react-helmet'; +import { navigationIcons } from '#psammead/psammead-assets/src/svgs'; +import VisuallyHiddenText from '#app/components/VisuallyHiddenText'; +import { Direction } from '#app/models/types/global'; +import styles from './index.styles'; + +type CanonicalDropdownProps = { + isOpen: boolean; +}; + +export const CanonicalDropdown = ({ + isOpen, + children, +}: PropsWithChildren) => { + const heightRef = useRef(null); + return ( +
+ {children} +
+ ); +}; + +type AmpDropdownProps = { + id?: string; + hidden?: boolean; +}; + +export const AmpDropdown = ({ + children, + id, + hidden, +}: PropsWithChildren) => ( + +); + +export const DropdownList = ({ + children, + ...props +}: PropsWithChildren>) => ( +
    + {children} +
+); + +type DropdownListItemProps = { + url: string; + active?: boolean; + currentPageText?: string; + clickTracker?: Record | null; + viewTracker?: Record | null; +}; + +export const DropdownListItem = ({ + children, + clickTracker = null, + currentPageText, + active = false, + url, + viewTracker = null, +}: PropsWithChildren) => { + const ariaId = `dropdownNavigation-${(children as string) + .replace(/\s+/g, '-') + .toLowerCase()}`; + return ( + // aria-labelledby is a temporary fix for the a11y nested span bug in TalkBack: https://github.com/bbc/simorgh/issues/9652 +
  • + + {active && currentPageText ? ( + // ID is a temporary fix for the a11y nested span bug in TalkBack: https://github.com/bbc/simorgh/issues/9652 + // eslint-disable-next-line jsx-a11y/aria-role + + {`${currentPageText}, `} + {children} + + ) : ( + // ID is a temporary fix for the a11y nested span bug in TalkBack: https://github.com/bbc/simorgh/issues/9652 + {children} + )} + +
  • + ); +}; + +type CanonicalMenuButtonProps = { + announcedText: string; + isOpen: boolean; + onClick: () => void; + dir?: Direction; +}; + +export const CanonicalMenuButton = ({ + announcedText, + isOpen, + onClick, + dir = 'ltr', +}: CanonicalMenuButtonProps) => ( + +); + +const AmpHead = () => ( + +