Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
2 changes: 2 additions & 0 deletions packages/dotcom-ui-layout/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions packages/dotcom-ui-layout/browser.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -9,4 +10,5 @@ import 'focus-visible'
export function init({ headerOptions = {}, footerOptions = {} } = {}) {
header.init(headerOptions)
footer.init(footerOptions)
ProBar.init()
}
10 changes: 9 additions & 1 deletion packages/dotcom-ui-layout/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,6 +24,10 @@ enum Footers {
legal = LegalFooter
}

export type TLayoutOptions = {
showProBar?: boolean
}

export type TLayoutProps = {
navigationData?: TNavigationData
headerOptions?: THeaderOptions
Expand All @@ -37,6 +42,7 @@ export type TLayoutProps = {
footerAfter?: string | React.ReactNode
children?: React.ReactNode
contents?: string
options?: TLayoutOptions
}

export function Layout({
Expand All @@ -52,7 +58,8 @@ export function Layout({
footerComponent,
footerAfter,
children,
contents
contents,
options
}: TLayoutProps) {
let header = null
let drawer = null
Expand Down Expand Up @@ -101,6 +108,7 @@ export function Layout({

<div className="n-layout__row n-layout__row--header">
<Template className="n-layout__header-before">{headerBefore}</Template>
{options && options.showProBar && <HeaderCoving />}
{headerComponent || header || null}
<Template className="n-layout__header-after">{headerAfter}</Template>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Subject options={{ showProBar: true }} />)

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(<Subject options={{ showProBar: false }} />)

expect(container.querySelector('.n-layout__pro-coving')).toBeNull()
})
})

describe('header variations', () => {
describe('with the simple variant', () => {
it('renders the header component', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void>}
*/
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<Object>} 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
}
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: it would be good to get this (and the slate foreground below) added to o3-foundation as use case tokens (e.g. --o3-color-use-case-coving-background/foreground)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a good idea.. however, I will add that to the professional brand only even though the header coving is a layout element now..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apaleslimghost is this a use case value you see being used in FT Pink also?

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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react'

export const HeaderCoving = () => {
return (
<div data-o3-brand="professional" className={`n-layout__pro-coving`}>
<div className={`n-layout__pro-coving-text`}>
<span className={`n-layout__pro-coving-brand`}>FT Professional</span>
<span className={`n-layout__pro-coving-organisation`}></span>
</div>
</div>
)
}
3 changes: 3 additions & 0 deletions packages/dotcom-ui-layout/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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';