@financial-times/dotcom-ui-header 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -0
- package/bower.json +9 -0
- package/browser.js +25 -0
- package/component.js +1 -0
- package/dist/node/components/drawer/additionalPartials.js +36 -0
- package/dist/node/components/drawer/topLevelPartials.js +56 -0
- package/dist/node/components/navigation/partials.js +83 -0
- package/dist/node/components/search/partials.js +20 -0
- package/dist/node/components/sticky/partials.js +59 -0
- package/dist/node/components/sub-navigation/partials.js +36 -0
- package/dist/node/components/svg-components/BrandFtMasthead.js +11 -0
- package/dist/node/components/top/partials.js +35 -0
- package/dist/node/index.js +78 -0
- package/dist/node/utils.js +6 -0
- package/package.json +44 -0
- package/screenshots/header-navigation.png +0 -0
- package/screenshots/header-sub-navigation.png +0 -0
- package/screenshots/header-top-search.png +0 -0
- package/scripts/convertSvgsToReactComponents.js +23 -0
- package/src/components/drawer/additionalPartials.tsx +97 -0
- package/src/components/drawer/topLevelPartials.tsx +153 -0
- package/src/components/navigation/partials.tsx +212 -0
- package/src/components/search/partials.tsx +52 -0
- package/src/components/sticky/partials.tsx +137 -0
- package/src/components/sub-navigation/partials.tsx +101 -0
- package/src/components/svg-components/BrandFtMasthead.tsx +16 -0
- package/src/components/top/partials.tsx +90 -0
- package/src/header.scss +29 -0
- package/src/index.tsx +123 -0
- package/src/interfaces.d.ts +18 -0
- package/src/utils.ts +5 -0
- package/styles.scss +14 -0
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@financial-times/dotcom-ui-header",
|
|
3
|
+
"version": "2.6.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"browser": "browser.js",
|
|
6
|
+
"main": "component.js",
|
|
7
|
+
"types": "src/index.tsx",
|
|
8
|
+
"styles": "styles.scss",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
11
|
+
"tsc": "../../node_modules/.bin/tsc --incremental",
|
|
12
|
+
"clean": "npm run clean:dist && npm run clean:node_modules",
|
|
13
|
+
"clean:dist": "rm -rf dist",
|
|
14
|
+
"clean:node_modules": "rm -rf node_modules",
|
|
15
|
+
"clean:install": "npm run clean && npm i",
|
|
16
|
+
"build": "npm run build:node",
|
|
17
|
+
"build:node": "npm run tsc -- --module commonjs --outDir ./dist/node",
|
|
18
|
+
"build:svg-to-react": "node scripts/convertSvgsToReactComponents.js",
|
|
19
|
+
"dev": "npm run build:node -- --watch"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@financial-times/dotcom-types-navigation": "^2.6.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@financial-times/logo-images": "^1.10.1",
|
|
29
|
+
"@svgr/core": "^5.0.0",
|
|
30
|
+
"camelcase": "^6.0.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"react": "^16.8.6"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">= 12.0.0"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"repository": "https://github.com/Financial-Times/dotcom-page-kit.git",
|
|
41
|
+
"directory": "packages/dotcom-ui-header"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/Financial-Times/dotcom-page-kit/tree/HEAD/packages/dotcom-ui-header"
|
|
44
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
|
|
3
|
+
const camelCase = require('camelcase')
|
|
4
|
+
const svgr = require('@svgr/core').default
|
|
5
|
+
|
|
6
|
+
const logoNames = ['brand-ft-masthead']
|
|
7
|
+
|
|
8
|
+
logoNames.forEach((logoName) => {
|
|
9
|
+
const pathToSvg = require.resolve(`@financial-times/logo-images/src/${logoName}.svg`)
|
|
10
|
+
|
|
11
|
+
const svgString = fs.readFileSync(pathToSvg).toString()
|
|
12
|
+
|
|
13
|
+
const componentName = camelCase(logoName, { pascalCase: true })
|
|
14
|
+
|
|
15
|
+
svgr(svgString, { titleProp: true }, { componentName }).then((jsCode) => {
|
|
16
|
+
const comment =
|
|
17
|
+
'// **THIS IS AN AUTO-GENERATED FILE (`npm run build:svg-to-react`) - DO NOT EDIT MANUALLY.**\n\n'
|
|
18
|
+
|
|
19
|
+
const fileExtension = 'tsx'
|
|
20
|
+
|
|
21
|
+
fs.writeFileSync(`src/components/svg-components/${componentName}.${fileExtension}`, comment + jsCode)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { ariaSelected } from '../../utils'
|
|
3
|
+
import { TNavMenuItem, TNavEditions } from '@financial-times/dotcom-types-navigation'
|
|
4
|
+
|
|
5
|
+
export type TDrawerParentItemProps = {
|
|
6
|
+
item: TNavMenuItem
|
|
7
|
+
idSuffix: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const DrawerParentItem = ({ item, idSuffix }: TDrawerParentItemProps) => {
|
|
11
|
+
const selected = item.selected ? 'selected' : 'unselected'
|
|
12
|
+
return (
|
|
13
|
+
<React.Fragment>
|
|
14
|
+
<div key={item.url} className="o-header__drawer-menu-toggle-wrapper">
|
|
15
|
+
<a
|
|
16
|
+
className={`o-header__drawer-menu-link o-header__drawer-menu-link--${selected} o-header__drawer-menu-link--parent`}
|
|
17
|
+
href={item.url}
|
|
18
|
+
{...ariaSelected(item)}
|
|
19
|
+
data-trackable={item.label}
|
|
20
|
+
>
|
|
21
|
+
{item.label}
|
|
22
|
+
</a>
|
|
23
|
+
<button
|
|
24
|
+
className={`o-header__drawer-menu-toggle o-header__drawer-menu-toggle--${selected}`}
|
|
25
|
+
aria-controls={`o-header-drawer-child-${idSuffix}`}
|
|
26
|
+
data-trackable={`sub-level-toggle | ${item.label}`}
|
|
27
|
+
>
|
|
28
|
+
{`Show more ${item.label}`}
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
<ul
|
|
32
|
+
className="o-header__drawer-menu-list o-header__drawer-menu-list--child"
|
|
33
|
+
id={`o-header-drawer-child-${idSuffix}`}
|
|
34
|
+
data-trackable="sub-level"
|
|
35
|
+
>
|
|
36
|
+
{(item.submenu.items as TNavMenuItem[]).map((item) => {
|
|
37
|
+
const selected = item.selected ? 'selected' : 'unselected'
|
|
38
|
+
return (
|
|
39
|
+
<li key={item.url} className="o-header__drawer-menu-item">
|
|
40
|
+
<a
|
|
41
|
+
className={`o-header__drawer-menu-link o-header__drawer-menu-link--${selected} o-header__drawer-menu-link--child`}
|
|
42
|
+
href={item.url}
|
|
43
|
+
data-trackable={item.label}
|
|
44
|
+
{...ariaSelected(item)}
|
|
45
|
+
>
|
|
46
|
+
{item.label}
|
|
47
|
+
</a>
|
|
48
|
+
</li>
|
|
49
|
+
)
|
|
50
|
+
})}
|
|
51
|
+
</ul>
|
|
52
|
+
</React.Fragment>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const DrawerSingleItem = (item: TNavMenuItem) => {
|
|
57
|
+
const selected = item.selected ? 'selected' : 'unselected'
|
|
58
|
+
return (
|
|
59
|
+
<a
|
|
60
|
+
className={`o-header__drawer-menu-link o-header__drawer-menu-link--${selected}`}
|
|
61
|
+
href={item.url}
|
|
62
|
+
data-trackable={item.label}
|
|
63
|
+
{...ariaSelected(item)}
|
|
64
|
+
>
|
|
65
|
+
{item.label}
|
|
66
|
+
</a>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const DrawerSpecialItem = (item: TNavMenuItem) => {
|
|
71
|
+
const selected = item.selected ? 'selected' : 'unselected'
|
|
72
|
+
return (
|
|
73
|
+
<a
|
|
74
|
+
className={`o-header__drawer-menu-link o-header__drawer-menu-link--${selected} o-header__drawer-menu-link--secondary`}
|
|
75
|
+
href={item.url}
|
|
76
|
+
data-trackable={item.label}
|
|
77
|
+
{...ariaSelected(item)}
|
|
78
|
+
>
|
|
79
|
+
{item.label}
|
|
80
|
+
</a>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const EditionsSwitcher = (editions: TNavEditions) => (
|
|
85
|
+
<ul className="o-header__drawer-menu-list">
|
|
86
|
+
{editions.others.map(({ id, name, url }) => {
|
|
87
|
+
const href = `${url}?edition=${id}`
|
|
88
|
+
return (
|
|
89
|
+
<li key={id} className="o-header__drawer-menu-item" data-trackable="edition-switcher">
|
|
90
|
+
<a className="o-header__drawer-menu-link" href={href} data-trackable={id}>
|
|
91
|
+
Switch to {name} Edition
|
|
92
|
+
</a>
|
|
93
|
+
</li>
|
|
94
|
+
)
|
|
95
|
+
})}
|
|
96
|
+
</ul>
|
|
97
|
+
)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { DrawerParentItem, DrawerSingleItem, DrawerSpecialItem, EditionsSwitcher } from './additionalPartials'
|
|
3
|
+
import { THeaderProps } from '../../interfaces'
|
|
4
|
+
import { TNavMenuItem, TNavMenu, TNavEditions } from '@financial-times/dotcom-types-navigation'
|
|
5
|
+
|
|
6
|
+
const IncludeDrawer = (props) => <Drawer {...props} />
|
|
7
|
+
|
|
8
|
+
const Drawer = (props: THeaderProps) => {
|
|
9
|
+
const editions = props.data.editions
|
|
10
|
+
const [primary, secondary, tertiary] = props.data.drawer.items
|
|
11
|
+
const user = props.userIsLoggedIn ? props.data.user : props.data.anon
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className="o-header__drawer"
|
|
16
|
+
id="o-header-drawer"
|
|
17
|
+
role="navigation"
|
|
18
|
+
aria-label="Drawer menu"
|
|
19
|
+
data-o-header-drawer
|
|
20
|
+
data-o-header-drawer--no-js
|
|
21
|
+
data-trackable="drawer"
|
|
22
|
+
data-trackable-terminate
|
|
23
|
+
>
|
|
24
|
+
<div className="o-header__drawer-inner">
|
|
25
|
+
<DrawerTools {...editions} />
|
|
26
|
+
<Search />
|
|
27
|
+
<nav className="o-header__drawer-menu o-header__drawer-menu--primary o-header__drawer-menu--border">
|
|
28
|
+
{editions && <EditionsSwitcher {...editions} />}
|
|
29
|
+
<ul className="o-header__drawer-menu-list">
|
|
30
|
+
{primary ? <SectionPrimary {...primary} /> : null}
|
|
31
|
+
{secondary ? <SectionSecondary {...secondary} /> : null}
|
|
32
|
+
{tertiary ? <SectionTertiary {...tertiary} /> : null}
|
|
33
|
+
</ul>
|
|
34
|
+
</nav>
|
|
35
|
+
<UserMenu {...user} />
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DrawerTools = (props: TNavEditions) => (
|
|
42
|
+
<div className="o-header__drawer-tools">
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
className="o-header__drawer-tools-close"
|
|
46
|
+
title="Close drawer menu"
|
|
47
|
+
aria-controls="o-header-drawer"
|
|
48
|
+
data-trackable="close"
|
|
49
|
+
>
|
|
50
|
+
<span className="o-header__visually-hidden">Close drawer menu</span>
|
|
51
|
+
</button>
|
|
52
|
+
<a className="o-header__drawer-tools-logo" href="/" data-trackable="logo">
|
|
53
|
+
<span className="o-header__visually-hidden">Financial Times</span>
|
|
54
|
+
</a>
|
|
55
|
+
{props.current && <p className="o-header__drawer-current-edition">{`${props.current.name} Edition`}</p>}
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const Search = () => (
|
|
60
|
+
<div className="o-header__drawer-search">
|
|
61
|
+
<form
|
|
62
|
+
className="o-header__drawer-search-form"
|
|
63
|
+
action="/search"
|
|
64
|
+
role="search"
|
|
65
|
+
aria-label="Site search"
|
|
66
|
+
data-n-topic-search
|
|
67
|
+
data-n-topic-search-categories="concepts,equities"
|
|
68
|
+
data-n-topic-search-view-all
|
|
69
|
+
>
|
|
70
|
+
<label className="o-header__visually-hidden" htmlFor="o-header-drawer-search-term">
|
|
71
|
+
Search the <abbr title="Financial Times">FT</abbr>
|
|
72
|
+
</label>
|
|
73
|
+
<input
|
|
74
|
+
className="o-header__drawer-search-term"
|
|
75
|
+
id="o-header-drawer-search-term"
|
|
76
|
+
name="q"
|
|
77
|
+
type="text"
|
|
78
|
+
autoComplete="off"
|
|
79
|
+
autoCorrect="off"
|
|
80
|
+
autoCapitalize="off"
|
|
81
|
+
spellCheck={false}
|
|
82
|
+
placeholder="Search the FT"
|
|
83
|
+
data-trackable="search-term"
|
|
84
|
+
data-n-topic-search-input
|
|
85
|
+
/>
|
|
86
|
+
<button className="o-header__drawer-search-submit" type="submit" data-trackable="search-submit">
|
|
87
|
+
<span className="o-header__visually-hidden">Search</span>
|
|
88
|
+
</button>
|
|
89
|
+
</form>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const SectionPrimary = (props: TNavMenuItem) => {
|
|
94
|
+
return (
|
|
95
|
+
<React.Fragment>
|
|
96
|
+
<li className="o-header__drawer-menu-item o-header__drawer-menu-item--heading">{props.label}</li>
|
|
97
|
+
{(props.submenu.items as TNavMenuItem[]).map((item, index) => (
|
|
98
|
+
<li key={item.url} className="o-header__drawer-menu-item">
|
|
99
|
+
{item.submenu ? (
|
|
100
|
+
<DrawerParentItem item={item} idSuffix={`${index}`} />
|
|
101
|
+
) : (
|
|
102
|
+
<DrawerSingleItem {...item} />
|
|
103
|
+
)}
|
|
104
|
+
</li>
|
|
105
|
+
))}
|
|
106
|
+
</React.Fragment>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const SectionSecondary = (props: TNavMenuItem) => (
|
|
111
|
+
<React.Fragment>
|
|
112
|
+
<li className="o-header__drawer-menu-item o-header__drawer-menu-item--heading">{props.label}</li>
|
|
113
|
+
{(props.submenu.items as TNavMenuItem[]).map((item, index) => (
|
|
114
|
+
<li key={item.url} className="o-header__drawer-menu-item">
|
|
115
|
+
{item.submenu ? (
|
|
116
|
+
<DrawerParentItem item={item} idSuffix={'inner' + index} />
|
|
117
|
+
) : (
|
|
118
|
+
<DrawerSingleItem {...item} />
|
|
119
|
+
)}
|
|
120
|
+
</li>
|
|
121
|
+
))}
|
|
122
|
+
</React.Fragment>
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
const SectionTertiary = (props: TNavMenuItem) => (
|
|
126
|
+
<React.Fragment>
|
|
127
|
+
{(props.submenu.items as TNavMenuItem[]).map((item, index) => {
|
|
128
|
+
const divideItem = index === 0 ? 'o-header__drawer-menu-item--divide' : ''
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<li key={item.url} className={`o-header__drawer-menu-item ${divideItem}`}>
|
|
132
|
+
<DrawerSpecialItem {...item} />
|
|
133
|
+
</li>
|
|
134
|
+
)
|
|
135
|
+
})}
|
|
136
|
+
</React.Fragment>
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const UserMenu = (props: TNavMenu) => (
|
|
140
|
+
<nav className="o-header__drawer-menu o-header__drawer-menu--user" data-trackable="user-nav">
|
|
141
|
+
<ul className="o-header__drawer-menu-list">
|
|
142
|
+
{props.items.map((item) => (
|
|
143
|
+
<li key={item.url} className="o-header__drawer-menu-item">
|
|
144
|
+
<a className="o-header__drawer-menu-link" href={item.url} data-trackable={item.label}>
|
|
145
|
+
{item.label}
|
|
146
|
+
</a>
|
|
147
|
+
</li>
|
|
148
|
+
))}
|
|
149
|
+
</ul>
|
|
150
|
+
</nav>
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
export { IncludeDrawer }
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { THeaderProps } from '../../interfaces'
|
|
3
|
+
import { ariaSelected } from '../../utils'
|
|
4
|
+
import {
|
|
5
|
+
TNavMenuItem,
|
|
6
|
+
TNavMeganav,
|
|
7
|
+
INavMeganavSections,
|
|
8
|
+
INavMeganavArticles
|
|
9
|
+
} from '@financial-times/dotcom-types-navigation'
|
|
10
|
+
|
|
11
|
+
const MobileNav = (props: THeaderProps) => {
|
|
12
|
+
// Only display navigation on pages which are included in this menu
|
|
13
|
+
const targetUrls = props.data['navbar-simple'].items.map((item) => item.url)
|
|
14
|
+
|
|
15
|
+
return targetUrls.includes(props.data.currentPath) ? (
|
|
16
|
+
<NavMobile items={props.data['navbar-simple'].items} />
|
|
17
|
+
) : null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const NavMobile = ({ items }: { items: TNavMenuItem[] }) => {
|
|
21
|
+
return (
|
|
22
|
+
<nav
|
|
23
|
+
id="o-header-nav-mobile"
|
|
24
|
+
className="o-header__row o-header__nav o-header__nav--mobile"
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
data-trackable="header-nav:mobile">
|
|
27
|
+
<ul className="o-header__nav-list">
|
|
28
|
+
{items.map((item, index) => (
|
|
29
|
+
<li className="o-header__nav-item" key={`link-${index}`}>
|
|
30
|
+
<a
|
|
31
|
+
className="o-header__nav-link o-header__nav-link--primary"
|
|
32
|
+
href={item.url}
|
|
33
|
+
{...ariaSelected(item)}
|
|
34
|
+
data-trackable={item.label}>
|
|
35
|
+
{item.label}
|
|
36
|
+
</a>
|
|
37
|
+
</li>
|
|
38
|
+
))}
|
|
39
|
+
</ul>
|
|
40
|
+
</nav>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const NavDesktop = (props) => (
|
|
45
|
+
<nav
|
|
46
|
+
id="o-header-nav-desktop"
|
|
47
|
+
className="o-header__row o-header__nav o-header__nav--desktop"
|
|
48
|
+
role="navigation"
|
|
49
|
+
aria-label="Primary navigation"
|
|
50
|
+
data-trackable="header-nav:desktop">
|
|
51
|
+
<div className="o-header__container">{props.children}</div>
|
|
52
|
+
</nav>
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const NavListLeft = (props: THeaderProps) => (
|
|
56
|
+
<ul className="o-header__nav-list o-header__nav-list--left" data-trackable="primary-nav">
|
|
57
|
+
{props.data.navbar.items.map((item, index) => (
|
|
58
|
+
<li className="o-header__nav-item" key={`link-${index}`}>
|
|
59
|
+
<a
|
|
60
|
+
className="o-header__nav-link o-header__nav-link--primary"
|
|
61
|
+
href={item.url}
|
|
62
|
+
id={`o-header-link-${index}`}
|
|
63
|
+
{...ariaSelected(item)}
|
|
64
|
+
data-trackable={item.label}>
|
|
65
|
+
{item.label}
|
|
66
|
+
</a>
|
|
67
|
+
{props.showMegaNav && Array.isArray(item.meganav) ? (
|
|
68
|
+
<MegaNav meganav={item.meganav} label={item.label} index={index} />
|
|
69
|
+
) : null}
|
|
70
|
+
</li>
|
|
71
|
+
))}
|
|
72
|
+
</ul>
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const NavListRight = (props: THeaderProps) => {
|
|
76
|
+
if (props.userIsLoggedIn) {
|
|
77
|
+
return <NavListRightLoggedIn items={props.data['navbar-right'].items} />
|
|
78
|
+
} else {
|
|
79
|
+
return <NavListRightAnon items={props.data['navbar-right-anon'].items} />
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const NavListRightLoggedIn = ({ items }: { items: TNavMenuItem[] }) => {
|
|
84
|
+
return (
|
|
85
|
+
<ul className="o-header__nav-list o-header__nav-list--right" data-trackable="user-nav">
|
|
86
|
+
{items.map((item, index) => (
|
|
87
|
+
<li className="o-header__nav-item" key={`link-${index}`}>
|
|
88
|
+
<a className="o-header__nav-link" href={item.url} data-trackable={item.label}>
|
|
89
|
+
{item.label}
|
|
90
|
+
</a>
|
|
91
|
+
</li>
|
|
92
|
+
))}
|
|
93
|
+
</ul>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const NavListRightAnon = ({ items, variant }: { items: TNavMenuItem[]; variant?: string }) => {
|
|
98
|
+
// If user is anonymous the second list item is styled as a button
|
|
99
|
+
const [first, second] = items
|
|
100
|
+
const setTabIndex = variant === 'sticky' ? { tabIndex: -1 } : null
|
|
101
|
+
return (
|
|
102
|
+
<ul className="o-header__nav-list o-header__nav-list--right" data-trackable="user-nav">
|
|
103
|
+
<li className="o-header__nav-item">
|
|
104
|
+
<a className="o-header__nav-link" href={first.url} data-trackable={first.label} {...setTabIndex}>
|
|
105
|
+
{first.label}
|
|
106
|
+
</a>
|
|
107
|
+
</li>
|
|
108
|
+
<li className="o-header__nav-item o-header__nav-item--hide-s">
|
|
109
|
+
<a
|
|
110
|
+
className="o-header__nav-button"
|
|
111
|
+
// Added as the result of a DAC audit. This will be confusing for users of voice activation software
|
|
112
|
+
// as it looks like a button but behaves like a link without this role.
|
|
113
|
+
role="button"
|
|
114
|
+
href={second.url}
|
|
115
|
+
data-trackable={second.label}
|
|
116
|
+
{...setTabIndex}>
|
|
117
|
+
{second.label}
|
|
118
|
+
</a>
|
|
119
|
+
</li>
|
|
120
|
+
</ul>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const MegaNav = ({ label, meganav, index }: { label: string; meganav: TNavMeganav[]; index: number }) => {
|
|
125
|
+
const sections = meganav.find(({ component }) => component === 'sectionlist')
|
|
126
|
+
const articles = meganav.find(({ component }) => component === 'articlelist')
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
className="o-header__mega"
|
|
131
|
+
id={`o-header-mega-${index}`}
|
|
132
|
+
role="group"
|
|
133
|
+
aria-labelledby={`o-header-link-${index}`}
|
|
134
|
+
data-o-header-mega
|
|
135
|
+
data-trackable={`meganav | ${label}`}>
|
|
136
|
+
<div className="o-header__container">
|
|
137
|
+
<div className="o-header__mega-wrapper">
|
|
138
|
+
{sections ? <SectionList {...(sections as INavMeganavSections)} /> : null}
|
|
139
|
+
{articles ? <ArticleList {...(articles as INavMeganavArticles)} /> : null}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const SectionList = ({ title, data }: INavMeganavSections) => {
|
|
147
|
+
return (
|
|
148
|
+
<div className="o-header__mega-column o-header__mega-column--subsections" data-trackable="sections">
|
|
149
|
+
<div className="o-header__mega-heading">{title}</div>
|
|
150
|
+
<div className="o-header__mega-content">
|
|
151
|
+
<ul className="o-header__mega-list">
|
|
152
|
+
{data.map((column) =>
|
|
153
|
+
column.map((item, index) => (
|
|
154
|
+
<li className="o-header__mega-item" key={`link-${index}`}>
|
|
155
|
+
<a
|
|
156
|
+
className="o-header__mega-link"
|
|
157
|
+
href={item.url}
|
|
158
|
+
{...ariaSelected(item)}
|
|
159
|
+
data-trackable="link">
|
|
160
|
+
{item.label}
|
|
161
|
+
</a>
|
|
162
|
+
</li>
|
|
163
|
+
))
|
|
164
|
+
)}
|
|
165
|
+
</ul>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const ArticleList = ({ title, data }: INavMeganavArticles) => {
|
|
172
|
+
return (
|
|
173
|
+
<div className="o-header__mega-column o-header__mega-column--articles" data-trackable="popular">
|
|
174
|
+
<div className="o-header__mega-heading">{title}</div>
|
|
175
|
+
<div className="o-header__mega-content">
|
|
176
|
+
<ul className="o-header__mega-list">
|
|
177
|
+
{data.map((item, index) => (
|
|
178
|
+
<li className="o-header__mega-item" key={`link-${index}`}>
|
|
179
|
+
<a
|
|
180
|
+
className="o-header__mega-link"
|
|
181
|
+
href={item.url}
|
|
182
|
+
{...ariaSelected(item)}
|
|
183
|
+
data-trackable="link">
|
|
184
|
+
{item.label}
|
|
185
|
+
</a>
|
|
186
|
+
</li>
|
|
187
|
+
))}
|
|
188
|
+
</ul>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const UserActionsNav = (props: THeaderProps) => {
|
|
195
|
+
const userNavItems = props.data['navbar-right-anon'].items
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div className="o-header__row o-header__anon" data-trackable="header-anon">
|
|
199
|
+
<ul className="o-header__anon-list">
|
|
200
|
+
{userNavItems.map((item, index) => (
|
|
201
|
+
<li className="o-header__anon-item" key={`link-${index}`}>
|
|
202
|
+
<a className="o-header__anon-link" href={item.url} data-trackable={item.label}>
|
|
203
|
+
{item.label}
|
|
204
|
+
</a>
|
|
205
|
+
</li>
|
|
206
|
+
))}
|
|
207
|
+
</ul>
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export { NavDesktop, NavListLeft, NavListRight, NavListRightAnon, UserActionsNav, MobileNav }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
const Search = ({ instance }) => {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
id={`o-header-search-${instance}`}
|
|
7
|
+
className={`o-header__row o-header__search o-header__search--${instance}`}
|
|
8
|
+
data-trackable="header-search"
|
|
9
|
+
data-o-header-search>
|
|
10
|
+
<div className="o-header__container">
|
|
11
|
+
<form
|
|
12
|
+
className="o-header__search-form"
|
|
13
|
+
action="/search"
|
|
14
|
+
role="search"
|
|
15
|
+
aria-label="Site search"
|
|
16
|
+
data-n-topic-search
|
|
17
|
+
data-n-topic-search-categories="concepts,equities"
|
|
18
|
+
data-n-topic-search-view-all>
|
|
19
|
+
<label className="o-header__visually-hidden" htmlFor={`o-header-search-term-${instance}`}>
|
|
20
|
+
Search the <abbr title="Financial Times">FT</abbr>
|
|
21
|
+
</label>
|
|
22
|
+
<input
|
|
23
|
+
className="o-header__search-term"
|
|
24
|
+
id={`o-header-search-term-${instance}`}
|
|
25
|
+
name="q"
|
|
26
|
+
type="text"
|
|
27
|
+
autoComplete="off"
|
|
28
|
+
autoCorrect="off"
|
|
29
|
+
autoCapitalize="off"
|
|
30
|
+
spellCheck={false}
|
|
31
|
+
data-trackable="search-term"
|
|
32
|
+
placeholder="Search the FT"
|
|
33
|
+
data-n-topic-search-input
|
|
34
|
+
/>
|
|
35
|
+
<button className="o-header__search-submit" type="submit" data-trackable="search-submit">
|
|
36
|
+
Search
|
|
37
|
+
</button>
|
|
38
|
+
<button
|
|
39
|
+
className="o-header__search-close o--if-js"
|
|
40
|
+
type="button"
|
|
41
|
+
aria-controls={`o-header-search-${instance}`}
|
|
42
|
+
title="Close search bar"
|
|
43
|
+
data-trackable="close">
|
|
44
|
+
<span className="o-header__visually-hidden">Close search bar</span>
|
|
45
|
+
</button>
|
|
46
|
+
</form>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { Search }
|