diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index aa193d368..2255445ca 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,4 +11,7 @@ packages/dotcom-build-sass/ @financial-times/origami-core @financial-times/platf # The Pro Personalisation and Discovery are responsible for the pro navigation - UI header's dropdown navigation -packages/dotcom-ui-header/src/components/dropdown-navigation @financial-times/platforms @financial-times/professional-personalisation-and-discovery \ No newline at end of file +packages/dotcom-ui-header/src/components/dropdown-navigation @financial-times/platforms @financial-times/professional-personalisation-and-discovery + +# The Pro Personalisation and Discovery are responsible for the pro bar in the new Header Coving of Layout +packages/dotcom-ui-layout/src/components/professional/headerCoving @financial-times/platforms @financial-times/professional-personalisation-and-discovery diff --git a/packages/dotcom-ui-layout/README.md b/packages/dotcom-ui-layout/README.md index 84afff0ef..7563ba9d5 100644 --- a/packages/dotcom-ui-layout/README.md +++ b/packages/dotcom-ui-layout/README.md @@ -86,6 +86,8 @@ This component includes styles written in Sass which includes the styles the [he | footerOptions | TFooterProps | true | `undefined` | Pass options to the footer component | | footerComponent | ReactElement | true | `undefined` | Pass a custom footer | | contents | string | true | `undefined` | A prerendered string of HTML used to insert the page contents when not using JSX composition | +| options | TLayoutProps | true | `undefined` | Pass options to the layout component | +| options.showProBar | boolean | true | `undefined` | Enable rendering of FT Pro Bar in the header coving area. | \* Navigation data is required to render all [header] variants except for `"logo-only"`. Navigation data is required to render all built in [footer] components. It is recommended to integrate the [navigation package] with your application to get navigation data. diff --git a/packages/dotcom-ui-layout/browser.js b/packages/dotcom-ui-layout/browser.js index bf5db6f9e..c551649a8 100644 --- a/packages/dotcom-ui-layout/browser.js +++ b/packages/dotcom-ui-layout/browser.js @@ -1,5 +1,6 @@ import * as footer from '@financial-times/dotcom-ui-footer/browser' import * as header from '@financial-times/dotcom-ui-header/browser' +import { ProBar } from './src/components/professional/headerCoving' // Polyfill for :focus-visible https://github.com/WICG/focus-visible // NOTE: v5 of this polyfill is not yet supported by o-normalise // https://github.com/WICG/focus-visible/pull/196/files @@ -9,4 +10,5 @@ import 'focus-visible' export function init({ headerOptions = {}, footerOptions = {} } = {}) { header.init(headerOptions) footer.init(footerOptions) + ProBar.init() } diff --git a/packages/dotcom-ui-layout/src/components/Layout.tsx b/packages/dotcom-ui-layout/src/components/Layout.tsx index 5df395221..3be0f79a0 100644 --- a/packages/dotcom-ui-layout/src/components/Layout.tsx +++ b/packages/dotcom-ui-layout/src/components/Layout.tsx @@ -10,6 +10,7 @@ import { import { TNavigationData } from '@financial-times/dotcom-types-navigation' import { Footer, LegalFooter, TFooterOptions } from '@financial-times/dotcom-ui-footer/component' import Template from './Template' +import { HeaderCoving } from './professional/headerCoving' enum Headers { simple = HeaderSimple, @@ -23,6 +24,10 @@ enum Footers { legal = LegalFooter } +export type TLayoutOptions = { + showProBar?: boolean +} + export type TLayoutProps = { navigationData?: TNavigationData headerOptions?: THeaderOptions @@ -37,6 +42,7 @@ export type TLayoutProps = { footerAfter?: string | React.ReactNode children?: React.ReactNode contents?: string + options?: TLayoutOptions } export function Layout({ @@ -52,7 +58,8 @@ export function Layout({ footerComponent, footerAfter, children, - contents + contents, + options }: TLayoutProps) { let header = null let drawer = null @@ -101,6 +108,7 @@ export function Layout({
+ {options && options.showProBar && } {headerComponent || header || null}
diff --git a/packages/dotcom-ui-layout/src/components/__test__/Layout.spec.tsx b/packages/dotcom-ui-layout/src/components/__test__/Layout.spec.tsx index 041b9e353..32c6f49b4 100644 --- a/packages/dotcom-ui-layout/src/components/__test__/Layout.spec.tsx +++ b/packages/dotcom-ui-layout/src/components/__test__/Layout.spec.tsx @@ -40,6 +40,20 @@ describe('dotcom-ui-layout/src/components/Layout', () => { }) }) +describe('layout variations', () => { + it('renders Pro Bar in the header coving when option is enabled', () => { + const { container } = render() + + expect(container.querySelector('.n-layout__pro-coving')).not.toBeNull() + }) + + it('does not render Pro Bar in the header coving when option is disabled', () => { + const { container } = render() + + expect(container.querySelector('.n-layout__pro-coving')).toBeNull() + }) +}) + describe('header variations', () => { describe('with the simple variant', () => { it('renders the header component', () => { diff --git a/packages/dotcom-ui-layout/src/components/professional/headerCoving.js b/packages/dotcom-ui-layout/src/components/professional/headerCoving.js new file mode 100644 index 000000000..e9b8960cd --- /dev/null +++ b/packages/dotcom-ui-layout/src/components/professional/headerCoving.js @@ -0,0 +1,169 @@ +/** + * Track a view event for a given DOM selector by dispatching an `oTracking.event`. + * + * @param {Object} [options] - Optional configuration. + * @param {string} [options.selector] - CSS selector for the element to track. + * @returns {void} + */ +const trackBarView = (options) => { + const selector = options && options.selector + const elementToTrack = document.querySelector(selector) + if (!elementToTrack) { + return + } + + const eventData = { + action: 'view', + category: 'component', + className: elementToTrack.className, + url: window.document.location.href || null, + nodeName: elementToTrack.nodeName, + component_name: 'pro-bar' + } + + document.body.dispatchEvent(new CustomEvent('oTracking.event', { detail: eventData, bubbles: true })) +} + +/** + * Update the organisation title shown in the coving element. + * + * Fetches licence information from the provided proNavigationApi URL and updates + * the organisation name in the coving element. Emits tracking events on error. + * + * @param {Object} options - Configuration options. + * @param {string} options.proNavigationApi - URL to fetch licence info from. + * @returns {Promise} + */ +const updateTitle = async (options) => { + if (!isDesktopOrTabletView()) { + return + } + const { proNavigationApi } = options + + const coving = document.querySelector(`.n-layout__pro-coving`) + const textContainer = document.querySelector('.n-layout__pro-coving-text') + if (!coving || !textContainer) { + return + } + + try { + const licenceInfo = await fetchLicenceInfo(proNavigationApi) + + if (!licenceInfo || !licenceInfo.organisationName) { + return + } + + if (licenceInfo.organisationName && licenceInfo.organisationName.length < 51) { + textContainer.classList.add('is-fading-out') + textContainer.addEventListener('transitionend', () => { + updateOrganisationName(coving, licenceInfo.organisationName) + textContainer.classList.remove('is-fading-out') + textContainer.classList.add('is-fading-in') + + requestAnimationFrame(() => { + textContainer.classList.remove('is-fading-in') + }) + }, { once: true }) + } + } catch (error) { + const eventData = { + action: 'fetch', + category: 'error', + component_name: 'pro-bar', + errorMessage: error.message + } + document.body.dispatchEvent(new CustomEvent('oTracking.event', { detail: eventData, bubbles: true })) + } +} + +/** + * Fetch licence information from the given URL. + * + * Uses fetch with credentials included. Throws an Error if the response is not ok. + * + * @param {string} url - The API endpoint to fetch licence info from. + * @returns {Promise} Resolves with the parsed JSON response. + * @throws {Error} If the network response is not ok. + */ +const fetchLicenceInfo = async (url) => { + const response = await fetch(url, { credentials: 'include' }) + if (!response.ok) { + throw new Error(`Error during licence info fetch! Status: ${response.status}`) + } + return response.json() +} + +/** + * Update the organisation name within the coving element. + * + * @param {Element} covingEl - The coving DOM element that contains the organisation name element. + * @param {string} organisationName - The organisation name to display. + * @returns {void} + */ +const updateOrganisationName = (covingEl, organisationName) => { + if (!covingEl || !organisationName) { + return + } + + const organisationNameEl = covingEl.querySelector('.n-layout__pro-coving-organisation') + if (organisationNameEl) { + organisationNameEl.textContent = organisationName + } +} + +/** + * Determine if the current device is a desktop or tablet. + * + * Checks the user agent to identify mobile devices. Uses the modern `navigator.userAgentData` API + * if available, otherwise falls back to parsing `navigator.userAgent`. Returns `true` for desktop + * and tablet devices, `false` for mobile phones. + * + * @returns {boolean} `true` if the device is desktop or tablet, `false` if mobile. + */ +function isDesktopOrTabletView() { + if (navigator.userAgentData && navigator.userAgentData.mobile) { + return !navigator.userAgentData.mobile + } + + const ua = navigator.userAgent.toLowerCase() + + if (ua.includes('ipad') || (ua.includes('macintosh') && 'ontouchend' in window)) { + return true + } + + if (ua.includes('iphone') || ua.includes('ipod')) { + return false + } + + if (ua.includes('android') && ua.includes('mobile')) { + return false + } + + if ( + ua.includes('windows phone') || + ua.includes('blackberry') || + ua.includes('bb10') || + ua.includes('opera mini') + ) { + return false + } + + return true +} + +/** + * Initialise the ProBar component: track view and update title. + * + * @returns {void} + */ +const init = () => { + trackBarView({ selector: '.n-layout__pro-coving' }) + + updateTitle({ + proNavigationApi: 'https://pro-navigation.ft.com/api/licence/info' + }) +} + +export const ProBar = { + init +} diff --git a/packages/dotcom-ui-layout/src/components/professional/headerCoving.scss b/packages/dotcom-ui-layout/src/components/professional/headerCoving.scss new file mode 100644 index 000000000..31e96f59f --- /dev/null +++ b/packages/dotcom-ui-layout/src/components/professional/headerCoving.scss @@ -0,0 +1,44 @@ +@import '@financial-times/o3-foundation/css/professional.css'; + +.n-layout__pro-coving { + display: flex; + width: 100%; + background-color: var(--o3-color-palette-mint); + padding-block: var(--o3-spacing-4xs); + justify-content: center; + border-block: 1px solid rgba(0, 0, 0, 0.20); +} + +.n-layout__pro-coving-text { + opacity: 1; + transition: opacity 0.5s ease; + + font-size: var(--o3-type-label-font-size); + font-weight: var(--o3-font-weight-medium); + font-family: var(--o3-font-family-metric); + color: var(--o3-color-palette-slate); +} + +.n-layout__pro-coving-text.is-fading-out { + opacity: 0; +} + +.n-layout__pro-coving-text.is-fading-in { + opacity: 1; +} + +.n-layout__pro-coving-brand { + text-transform: uppercase; +} + +@media (prefers-reduced-motion: reduce) { + .n-layout__pro-coving-text { + transition: none; + } +} + +.n-layout__pro-coving-organisation:not(:empty)::before { + content: "|"; + padding-inline: var(--o3-spacing-3xs); + color: var(--o3-color-palette-slate); +} diff --git a/packages/dotcom-ui-layout/src/components/professional/headerCoving.tsx b/packages/dotcom-ui-layout/src/components/professional/headerCoving.tsx new file mode 100644 index 000000000..b09b831b8 --- /dev/null +++ b/packages/dotcom-ui-layout/src/components/professional/headerCoving.tsx @@ -0,0 +1,12 @@ +import React from 'react' + +export const HeaderCoving = () => { + return ( +
+
+ FT Professional + +
+
+ ) +} diff --git a/packages/dotcom-ui-layout/styles.scss b/packages/dotcom-ui-layout/styles.scss index d37cbc936..4cf3b73c8 100644 --- a/packages/dotcom-ui-layout/styles.scss +++ b/packages/dotcom-ui-layout/styles.scss @@ -48,3 +48,6 @@ $system-code: 'page-kit-layout' !default; @import '@financial-times/dotcom-ui-header/styles'; @import '@financial-times/dotcom-ui-footer/styles'; + +// Import Header Coving (Pro Bar) styles +@import './src/components/professional/headerCoving.scss';