@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.
@@ -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
- <form
15
- className="o-header__search-form"
16
- action="/search"
17
- role="search"
18
- aria-label="Site search"
19
- data-n-topic-search
20
- >
21
- <label htmlFor={inputId} className="o-header__search-term o-forms-field o-forms-field--optional">
22
- <span className="o-forms-title o-header__visually-hidden">
23
- <span className="o-forms-title__main">
24
- Search the <abbr title="Financial Times">FT</abbr>
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
- </span>
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
- </label>
56
- </form>
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-label={title || 'Navigation menu'}
24
+ aria-labelledby={ariaLabelledBy}
24
25
  >
25
26
  {title && <h2 className="o-header__subnav-dropdown-title">{title}</h2>}
26
- <span
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
- role="button"
31
+ type="button"
31
32
  >
32
33
  <span className="o-header__subnav-dropdown-close-icon" />
33
- </span>
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
- <span
16
- role="button"
17
- tabIndex={0}
18
- className={`o-header__subnav-link ${selected}`}
19
- {...ariaSelected(item)}
20
- data-trackable={item.label}
21
- >
22
- {item.label}
23
- <SubNavDropdown items={dropdownItems} title={item.label} />
24
- </span>
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
- .o-header__subnav-link:has([data-o-header-subnav-dropdown]) {
4
- cursor: pointer;
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
- .o-header__subnav-link:has([data-o-header-subnav-dropdown]) {
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-hidden='false']) .o-header__subnav-wrap-outside,
65
- .o-header__subnav:has([data-o-header-subnav-dropdown][aria-hidden='false']) .o-header__subnav-wrap-inside {
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