bbc/psammead

View on GitHub
packages/components/psammead-brand/src/index.jsx

Summary

Maintainability
A
2 hrs
Test Coverage
A
100%
import React, { forwardRef } from 'react';
import styled from '@emotion/styled';
import { string, number, node, shape, bool } from 'prop-types';
import VisuallyHiddenText from '@bbc/psammead-visually-hidden-text';
import {
  GEL_GROUP_0_SCREEN_WIDTH_MAX,
  GEL_GROUP_2_SCREEN_WIDTH_MIN,
  GEL_GROUP_3_SCREEN_WIDTH_MIN,
} from '@bbc/gel-foundations/breakpoints';
import {
  GEL_SPACING_HLF,
  GEL_SPACING,
  GEL_SPACING_DBL,
} from '@bbc/gel-foundations/spacings';

const SVG_TOP_OFFSET_BELOW_400PX = '0.625rem'; // 10px
const SVG_BOTTOM_OFFSET_BELOW_400PX = '0.375rem'; // 6px
const SVG_BOTTOM_OFFSET_ABOVE_400PX = '0.75rem'; // 12px
const SVG_BOTTOM_OFFSET_ABOVE_600PX = '1.25rem'; // 20px
const SVG_WRAPPER_MAX_WIDTH_ABOVE_1280PX = '78rem';
const SCRIPT_LINK_OFFSET_BELOW_240PX = 52;
const PADDING_AROUND_SVG_BELOW_400PX = 16;
const PADDING_AROUND_SVG_ABOVE_400PX = 28;

const conditionallyRenderHeight = (svgHeight, padding) =>
  svgHeight ? `min-height: ${(svgHeight + padding) / 16}rem;` : '';

const TRANSPARENT_BORDER = `0.0625rem solid transparent`;

const SvgWrapper = styled.div`
  position: relative;
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  max-width: ${SVG_WRAPPER_MAX_WIDTH_ABOVE_1280PX};
  margin: 0 auto;

  @media (max-width: ${GEL_GROUP_0_SCREEN_WIDTH_MAX}) {
    display: block;
  }
`;

const Banner = styled.div`
  background-color: ${props => props.backgroundColour};
  ${({ svgHeight }) =>
    conditionallyRenderHeight(svgHeight, PADDING_AROUND_SVG_BELOW_400PX)}
  width: 100%;
  padding: 0 ${GEL_SPACING};

  @media (min-width: ${GEL_GROUP_2_SCREEN_WIDTH_MIN}) {
    ${({ svgHeight }) =>
      conditionallyRenderHeight(svgHeight, PADDING_AROUND_SVG_ABOVE_400PX)}
    padding: 0 ${GEL_SPACING_DBL};
  }

  @media (max-width: ${GEL_GROUP_0_SCREEN_WIDTH_MAX}) {
    ${({ scriptLink, svgHeight }) =>
      scriptLink &&
      `min-height: ${
        (svgHeight +
          PADDING_AROUND_SVG_BELOW_400PX +
          SCRIPT_LINK_OFFSET_BELOW_240PX) /
        16
      }rem;`}
  }

  ${({ borderTop }) => borderTop && `border-top: ${TRANSPARENT_BORDER}`};
  ${({ borderBottom }) =>
    borderBottom && `border-bottom: ${TRANSPARENT_BORDER}`};
`;

const brandWidth = (minWidth, maxWidth) => `
  width: 100%;
  max-width: ${maxWidth / 16}rem;
  min-width: ${minWidth / 16}rem;
  flex: 1 1 ${minWidth / 16}rem;
  -ms-flex-preferred-size: ${maxWidth / 16}rem;
`;

const StyledLink = styled.a`
  display: inline-block;
  ${({ maxWidth, minWidth }) => brandWidth(minWidth, maxWidth)}

  @media (max-width: ${GEL_GROUP_0_SCREEN_WIDTH_MAX}) {
    display: block;
  }
`;

// `currentColor` has been used to address high contrast mode in Firefox.
const BrandSvg = styled.svg`
  box-sizing: content-box;
  color: ${props => props.logoColour};
  fill: currentColor;
  padding-top: ${SVG_TOP_OFFSET_BELOW_400PX};
  padding-bottom: ${SVG_BOTTOM_OFFSET_BELOW_400PX};
  height: ${props => props.height / 16}rem;

  ${({ maxWidth, minWidth }) => brandWidth(minWidth, maxWidth)}

  @media (min-width: ${GEL_GROUP_2_SCREEN_WIDTH_MIN}) {
    padding-top: ${GEL_SPACING_DBL};
    padding-bottom: ${SVG_BOTTOM_OFFSET_ABOVE_400PX};
  }

  @media (min-width: ${GEL_GROUP_3_SCREEN_WIDTH_MIN}) {
    padding-top: ${SVG_BOTTOM_OFFSET_ABOVE_600PX};
    padding-bottom: ${GEL_SPACING_DBL};
  }

  @media screen and (-ms-high-contrast: active), print {
    fill: windowText;
  }

  /* stylelint-disable */
  ${StyledLink}:hover &,
    ${StyledLink}:focus & {
    text-decoration: none;
    border-bottom: ${GEL_SPACING_HLF} solid ${props => props.logoColour};
    margin-bottom: -${GEL_SPACING_HLF};
  }
  /* stylelint-enable */
`;

const LocalisedBrandName = ({ linkId, product, serviceLocalisedName }) => {
  const brandId = `BrandLink-${linkId}`;
  return serviceLocalisedName ? (
    // id={`BrandLink-${linkId}` 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
    // eslint-disable-next-line jsx-a11y/aria-role
    <VisuallyHiddenText role="text" id={brandId}>
      <span lang="en-GB">{`${product}, `}</span>
      <span>{serviceLocalisedName}</span>
    </VisuallyHiddenText>
  ) : (
    <VisuallyHiddenText id={brandId}>{product}</VisuallyHiddenText>
  );
};

LocalisedBrandName.propTypes = {
  linkId: string.isRequired,
  product: string.isRequired,
  serviceLocalisedName: string,
};

LocalisedBrandName.defaultProps = {
  serviceLocalisedName: null,
};

const StyledBrand = ({
  linkId,
  product,
  serviceLocalisedName,
  svgHeight,
  svg,
  maxWidth,
  minWidth,
  backgroundColour,
  logoColour,
}) => (
  <>
    {svg && (
      <>
        <BrandSvg
          height={svgHeight}
          viewBox={`0 0 ${svg.viewbox.width} ${svg.viewbox.height}`}
          xmlns="http://www.w3.org/2000/svg"
          focusable="false"
          aria-hidden="true"
          ratio={svg.ratio}
          maxWidth={maxWidth}
          minWidth={minWidth}
          backgroundColour={backgroundColour}
          logoColour={logoColour}
        >
          {svg.group}
        </BrandSvg>
        <LocalisedBrandName
          linkId={linkId}
          product={product}
          serviceLocalisedName={serviceLocalisedName}
        />
      </>
    )}
  </>
);

const brandProps = {
  linkId: string.isRequired,
  product: string.isRequired,
  serviceLocalisedName: string,
  maxWidth: number.isRequired,
  minWidth: number.isRequired,
  svgHeight: number.isRequired,
  svg: shape({
    group: node.isRequired,
    ratio: number.isRequired,
    viewbox: shape({
      height: number.isRequired,
      width: number.isRequired,
    }).isRequired,
  }).isRequired,
  backgroundColour: string.isRequired,
  logoColour: string.isRequired,
};

StyledBrand.propTypes = brandProps;

StyledBrand.defaultProps = {
  serviceLocalisedName: null,
};

const Brand = forwardRef((props, ref) => {
  const {
    svgHeight,
    maxWidth,
    minWidth,
    url,
    borderTop,
    borderBottom,
    backgroundColour,
    logoColour,
    scriptLink,
    skipLink,
    linkId,
    ...rest
  } = props;

  return (
    <Banner
      svgHeight={svgHeight}
      borderTop={borderTop}
      borderBottom={borderBottom}
      backgroundColour={backgroundColour}
      logoColour={logoColour}
      scriptLink={scriptLink}
      {...rest}
    >
      <SvgWrapper ref={ref}>
        {url ? (
          <StyledLink
            href={url}
            maxWidth={maxWidth}
            minWidth={minWidth}
            id={linkId}
            // 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={`BrandLink-${linkId}`}
          >
            <StyledBrand {...props} />
          </StyledLink>
        ) : (
          <StyledBrand {...props} />
        )}
        {skipLink}
        {scriptLink && <div>{scriptLink}</div>}
      </SvgWrapper>
    </Banner>
  );
});

Brand.defaultProps = {
  url: null,
  serviceLocalisedName: null,
  borderTop: false,
  borderBottom: false,
  scriptLink: null,
  skipLink: null,
  linkId: null,
};

Brand.propTypes = {
  ...brandProps,
  url: string,
  serviceLocalisedName: string,
  borderTop: bool,
  borderBottom: bool,
  scriptLink: node,
  skipLink: node,
  linkId: string,
};

export default Brand;