@financial-times/dotcom-ui-header 13.8.1 → 13.9.1-beta.1
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 +14 -0
- package/browser.js +13 -13
- package/dist/node/components/search/partials.js +17 -14
- package/dist/node/components/sub-navigation/SubNavDropdown.d.ts +2 -0
- package/dist/node/components/sub-navigation/SubNavDropdown.js +3 -3
- package/dist/node/components/sub-navigation/SubNavWithDropdown.js +7 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/__test__/components/SubNavDropdown.spec.tsx +16 -15
- package/src/__test__/components/__snapshots__/MainHeader.spec.tsx.snap +608 -504
- package/src/__test__/components/__snapshots__/StickyHeader.spec.tsx.snap +380 -315
- package/src/__test__/output/component.spec.tsx +32 -0
- package/src/components/search/partials.tsx +22 -15
- package/src/components/sub-navigation/SubNavDropdown.tsx +9 -8
- package/src/components/sub-navigation/SubNavWithDropdown.tsx +19 -10
- package/src/components/sub-navigation/subNavDropdown.scss +18 -5
- package/src/header.scss +32 -0
|
@@ -67,6 +67,38 @@ describe('dotcom-ui-header', () => {
|
|
|
67
67
|
expect(mobileHeader).not.toBeNull()
|
|
68
68
|
})
|
|
69
69
|
|
|
70
|
+
it('renders one search widget anchor in the primary search row', () => {
|
|
71
|
+
const { container } = render(commonHeader)
|
|
72
|
+
const primarySearch = container.querySelector('#o-header-search-primary')
|
|
73
|
+
const primarySearchMain = primarySearch?.querySelector('.o-header__search-main')
|
|
74
|
+
const primaryForm = primarySearch?.querySelector('.o-header__search-form')
|
|
75
|
+
const primaryAnchor = primarySearch?.querySelector('[data-o-header-search-widget-anchor="primary"]')
|
|
76
|
+
const primaryAnchors = container.querySelectorAll('[data-o-header-search-widget-anchor="primary"]')
|
|
77
|
+
const widgetAnchors = container.querySelectorAll('[data-o-header-search-widget-anchor]')
|
|
78
|
+
|
|
79
|
+
expect(primarySearchMain).not.toBeNull()
|
|
80
|
+
expect(primaryForm?.parentElement).toBe(primarySearchMain)
|
|
81
|
+
expect(primarySearchMain?.nextElementSibling).toBe(primaryAnchor)
|
|
82
|
+
expect(primaryAnchors).toHaveLength(1)
|
|
83
|
+
expect(widgetAnchors).toHaveLength(2)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('renders one search widget anchor in sticky search', () => {
|
|
87
|
+
const { container } = render(commonHeader)
|
|
88
|
+
const searchMainNodes = container.querySelectorAll('.o-header__search-main')
|
|
89
|
+
const stickySearch = container.querySelector('#o-header-search-sticky')
|
|
90
|
+
const stickySearchMain = stickySearch?.querySelector('.o-header__search-main')
|
|
91
|
+
const stickyForm = stickySearch?.querySelector('.o-header__search-form')
|
|
92
|
+
const stickyAnchor = stickySearch?.querySelector('[data-o-header-search-widget-anchor="sticky"]')
|
|
93
|
+
const stickyAnchors = container.querySelectorAll('[data-o-header-search-widget-anchor="sticky"]')
|
|
94
|
+
|
|
95
|
+
expect(searchMainNodes).toHaveLength(2)
|
|
96
|
+
expect(stickySearchMain).not.toBeNull()
|
|
97
|
+
expect(stickyForm?.parentElement).toBe(stickySearchMain)
|
|
98
|
+
expect(stickySearchMain?.nextElementSibling).toBe(stickyAnchor)
|
|
99
|
+
expect(stickyAnchors).toHaveLength(1)
|
|
100
|
+
})
|
|
101
|
+
|
|
70
102
|
describe('When the user is subscribed', () => {
|
|
71
103
|
it('renders the expected logged in user header links', () => {
|
|
72
104
|
const { container } = render(subscribedUserHeader)
|
|
@@ -2,6 +2,7 @@ import React from 'react'
|
|
|
2
2
|
|
|
3
3
|
const Search = ({ instance }) => {
|
|
4
4
|
const inputId = `o-header-search-term-${instance}`
|
|
5
|
+
|
|
5
6
|
return (
|
|
6
7
|
<div
|
|
7
8
|
id={`o-header-search-${instance}`}
|
|
@@ -11,19 +12,22 @@ const Search = ({ instance }) => {
|
|
|
11
12
|
data-o-header-search
|
|
12
13
|
>
|
|
13
14
|
<div className="o-header__container">
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
<div className="o-header__search-main">
|
|
16
|
+
<form
|
|
17
|
+
className="o-header__search-form"
|
|
18
|
+
action="/search"
|
|
19
|
+
role="search"
|
|
20
|
+
aria-label="Site search"
|
|
21
|
+
data-n-topic-search
|
|
22
|
+
>
|
|
23
|
+
<div className="o-header__search-term o-forms-field o-forms-field--optional">
|
|
24
|
+
<label htmlFor={inputId}>
|
|
25
|
+
<span className="o-forms-title o-header__visually-hidden">
|
|
26
|
+
<span className="o-forms-title__main">
|
|
27
|
+
Search the <abbr title="Financial Times">FT</abbr>
|
|
28
|
+
</span>
|
|
25
29
|
</span>
|
|
26
|
-
</
|
|
30
|
+
</label>
|
|
27
31
|
<span className="o-forms-input o-forms-input--text o-forms-input--suffix">
|
|
28
32
|
<input
|
|
29
33
|
id={inputId}
|
|
@@ -37,7 +41,7 @@ const Search = ({ instance }) => {
|
|
|
37
41
|
role="combobox"
|
|
38
42
|
aria-controls={`suggestions-${inputId}`}
|
|
39
43
|
/>
|
|
40
|
-
<button className="o-header__search-submit" type="submit">
|
|
44
|
+
<button className="o-header__search-submit" type="submit" aria-label="search">
|
|
41
45
|
<span aria-hidden="true" className="o-header__search-icon"></span>
|
|
42
46
|
<span>Search</span>
|
|
43
47
|
</button>
|
|
@@ -47,13 +51,16 @@ const Search = ({ instance }) => {
|
|
|
47
51
|
aria-controls={`o-header-search-${instance}`}
|
|
48
52
|
title="Close search bar"
|
|
49
53
|
data-trackable="close"
|
|
54
|
+
aria-label="close"
|
|
50
55
|
>
|
|
51
56
|
<span className="o-header__visually-hidden">Close search bar</span>
|
|
52
57
|
<span>Close</span>
|
|
53
58
|
</button>
|
|
54
59
|
</span>
|
|
55
|
-
</
|
|
56
|
-
|
|
60
|
+
</div>
|
|
61
|
+
</form>
|
|
62
|
+
</div>
|
|
63
|
+
<div className="o-header__search-widget-anchor" data-o-header-search-widget-anchor={instance} />
|
|
57
64
|
</div>
|
|
58
65
|
</div>
|
|
59
66
|
)
|
|
@@ -7,30 +7,31 @@ interface DropdownItem {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
interface SubNavDropdownProps {
|
|
10
|
+
id: string
|
|
10
11
|
items: DropdownItem[]
|
|
12
|
+
ariaLabelledBy: string
|
|
11
13
|
title?: string
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
const SubNavDropdown: React.FC<SubNavDropdownProps> = ({ items, title }) => {
|
|
16
|
+
const SubNavDropdown: React.FC<SubNavDropdownProps> = ({ id, items, ariaLabelledBy, title }) => {
|
|
15
17
|
return (
|
|
16
18
|
<div
|
|
19
|
+
id={id}
|
|
17
20
|
className="o-header__subnav-dropdown"
|
|
18
|
-
data-o-header-subnav-dropdown
|
|
19
|
-
aria-hidden="true"
|
|
20
|
-
aria-expanded="false"
|
|
21
|
+
data-o-header-subnav-dropdown-modal
|
|
21
22
|
role="dialog"
|
|
22
23
|
aria-modal="true"
|
|
23
|
-
aria-
|
|
24
|
+
aria-labelledby={ariaLabelledBy}
|
|
24
25
|
>
|
|
25
26
|
{title && <h2 className="o-header__subnav-dropdown-title">{title}</h2>}
|
|
26
|
-
<
|
|
27
|
+
<button
|
|
27
28
|
className="o-header__subnav-dropdown-close"
|
|
28
29
|
data-o-header-subnav-dropdown-close
|
|
29
30
|
aria-label="Close menu"
|
|
30
|
-
|
|
31
|
+
type="button"
|
|
31
32
|
>
|
|
32
33
|
<span className="o-header__subnav-dropdown-close-icon" />
|
|
33
|
-
</
|
|
34
|
+
</button>
|
|
34
35
|
<ul className="o-header__subnav-dropdown-list">
|
|
35
36
|
{items.map((item, index) => (
|
|
36
37
|
<li key={index} className="o-header__subnav-dropdown-item">
|
|
@@ -10,18 +10,27 @@ interface SubNavWithDropdownProps {
|
|
|
10
10
|
|
|
11
11
|
const SubNavWithDropdown: React.FC<SubNavWithDropdownProps> = ({ item, selected }) => {
|
|
12
12
|
const dropdownItems = item.subnavDropdownOptions || []
|
|
13
|
+
const labelSlug = item.label?.toLowerCase().replace(/\s+/g, '-')
|
|
14
|
+
const buttonId = `subnav-dropdown-button-${labelSlug}`
|
|
15
|
+
const modalId = `subnav-dropdown-modal-${labelSlug}`
|
|
13
16
|
|
|
14
17
|
return (
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
<div data-o-header-subnav-dropdown-parent>
|
|
19
|
+
<button
|
|
20
|
+
id={buttonId}
|
|
21
|
+
type="button"
|
|
22
|
+
className={`o-header__subnav-link ${selected}`}
|
|
23
|
+
{...ariaSelected(item)}
|
|
24
|
+
data-trackable={item.label}
|
|
25
|
+
aria-controls={modalId}
|
|
26
|
+
aria-haspopup="dialog"
|
|
27
|
+
aria-expanded="false"
|
|
28
|
+
data-o-header-subnav-dropdown-button
|
|
29
|
+
>
|
|
30
|
+
{item.label}
|
|
31
|
+
</button>
|
|
32
|
+
<SubNavDropdown id={modalId} ariaLabelledBy={buttonId} items={dropdownItems} title={item.label} />
|
|
33
|
+
</div>
|
|
25
34
|
)
|
|
26
35
|
}
|
|
27
36
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
@import '@financial-times/o3-foundation/css/professional.css';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
cursor:
|
|
3
|
+
[data-o-header-subnav-dropdown-parent] {
|
|
4
|
+
cursor: default;
|
|
5
5
|
display: inline-block;
|
|
6
6
|
position: relative;
|
|
7
7
|
}
|
|
@@ -50,8 +50,21 @@
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
[data-o-header-subnav-dropdown-button] {
|
|
54
|
+
background-color: inherit;
|
|
55
|
+
font-family: inherit;
|
|
56
|
+
font-size: inherit;
|
|
57
|
+
font-weight: inherit;
|
|
58
|
+
line-height: inherit;
|
|
59
|
+
&:focus,
|
|
60
|
+
&:focus-visible {
|
|
61
|
+
box-shadow: var(--o3-focus-use-case-outline-color);
|
|
62
|
+
}
|
|
63
|
+
cursor: default !important; // On desktop the hover action will open the modal with click doing nothing additional. Therefore the pointer is misleading.
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
@media (hover: none) {
|
|
54
|
-
|
|
67
|
+
[data-o-header-subnav-dropdown-parent] {
|
|
55
68
|
-webkit-tap-highlight-color: transparent;
|
|
56
69
|
}
|
|
57
70
|
|
|
@@ -61,8 +74,8 @@
|
|
|
61
74
|
* limited to iOS to prevent android widening the layout when items overflow.
|
|
62
75
|
*/
|
|
63
76
|
@supports (-webkit-touch-callout: none) {
|
|
64
|
-
.o-header__subnav:has([data-o-header-subnav-dropdown][aria-
|
|
65
|
-
.o-header__subnav:has([data-o-header-subnav-dropdown][aria-
|
|
77
|
+
.o-header__subnav:has([data-o-header-subnav-dropdown-button][aria-expanded='true']) .o-header__subnav-wrap-outside,
|
|
78
|
+
.o-header__subnav:has([data-o-header-subnav-dropdown-button][aria-expanded='true']) .o-header__subnav-wrap-inside {
|
|
66
79
|
overflow: visible;
|
|
67
80
|
}
|
|
68
81
|
}
|
package/src/header.scss
CHANGED
|
@@ -16,6 +16,38 @@
|
|
|
16
16
|
display: none;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
.o-header__search-widget-anchor[data-o-header-search-widget-anchor] {
|
|
20
|
+
display: none;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Grid layout on large sizes to create right hand rail for widget anchor
|
|
24
|
+
@include oGridRespondTo('L') {
|
|
25
|
+
.o-header__search--primary .o-header__container,
|
|
26
|
+
.o-header__search--sticky .o-header__container {
|
|
27
|
+
max-width: none;
|
|
28
|
+
display: grid;
|
|
29
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 840px) minmax(0, 1fr);
|
|
30
|
+
column-gap: var(--o3-spacing-3xs);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.o-header__search--primary .o-header__search-main,
|
|
34
|
+
.o-header__search--sticky .o-header__search-main {
|
|
35
|
+
grid-column: 2;
|
|
36
|
+
grid-row: 1;
|
|
37
|
+
min-width: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.o-header__search--primary .o-header__search-widget-anchor[data-o-header-search-widget-anchor='primary'],
|
|
41
|
+
.o-header__search--sticky .o-header__search-widget-anchor[data-o-header-search-widget-anchor='sticky'] {
|
|
42
|
+
grid-column: 3;
|
|
43
|
+
display: block;
|
|
44
|
+
width: 100%;
|
|
45
|
+
min-width: 0;
|
|
46
|
+
overflow-x: hidden;
|
|
47
|
+
overflow-y: visible;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
19
51
|
// Import the dropdown navigation styles
|
|
20
52
|
@import 'components/dropdown-navigation/dropdownNavigation.scss';
|
|
21
53
|
|