@ons/design-system 73.3.0 → 73.4.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.
@@ -35,6 +35,100 @@ describe('FOR: Macro: Header', () => {
35
35
  });
36
36
  });
37
37
  });
38
+
39
+ describe('Accessibility: search heading', () => {
40
+ test('THEN: axe passes when search heading is present (params.search.links.heading)', async () => {
41
+ const params = {
42
+ ...EXAMPLE_HEADER_BASIC,
43
+ variants: 'basic',
44
+ search: {
45
+ links: {
46
+ heading: 'Popular searches',
47
+ itemsList: [{ text: 'Census', url: '/census' }],
48
+ },
49
+ form: { action: '/search' },
50
+ },
51
+ searchLinks: undefined,
52
+ };
53
+ const $ = cheerio.load(renderComponent('header', params));
54
+ const results = await axe($.html());
55
+ expect(results).toHaveNoViolations();
56
+ expect($('.ons-header-nav-search__heading').text()).toBe('Popular searches');
57
+ });
58
+
59
+ test('THEN: axe passes when search heading is present (params.searchLinks.heading)', async () => {
60
+ const params = {
61
+ ...EXAMPLE_HEADER_BASIC,
62
+ variants: 'basic',
63
+ search: undefined,
64
+ searchLinks: {
65
+ heading: 'Other searches',
66
+ itemsList: [{ text: 'Population', url: '/population' }],
67
+ },
68
+ };
69
+ const $ = cheerio.load(renderComponent('header', params));
70
+ const results = await axe($.html());
71
+ expect(results).toHaveNoViolations();
72
+ expect($('.ons-header-nav-search__heading').text()).toBe('Other searches');
73
+ });
74
+
75
+ test('THEN: axe passes and no h2 is rendered when no heading present', async () => {
76
+ const params = {
77
+ ...EXAMPLE_HEADER_BASIC,
78
+ variants: 'basic',
79
+ search: { links: { itemsList: [] }, form: { action: '/search' } },
80
+ searchLinks: undefined,
81
+ };
82
+ const $ = cheerio.load(renderComponent('header', params));
83
+ const results = await axe($.html());
84
+ expect(results).toHaveNoViolations();
85
+ expect($('.ons-header-nav-search__heading').length).toBe(0);
86
+ });
87
+ });
88
+
89
+ describe('Snapshot: search heading', () => {
90
+ test('matches snapshot when search heading is present (params.search.links.heading)', () => {
91
+ const params = {
92
+ ...EXAMPLE_HEADER_BASIC,
93
+ variants: 'basic',
94
+ search: {
95
+ links: {
96
+ heading: 'Popular searches',
97
+ itemsList: [{ text: 'Census', url: '/census' }],
98
+ },
99
+ form: { action: '/search' },
100
+ },
101
+ searchLinks: undefined,
102
+ };
103
+ const $ = cheerio.load(renderComponent('header', params));
104
+ expect($.html()).toMatchSnapshot();
105
+ });
106
+
107
+ test('matches snapshot when search heading is present (params.searchLinks.heading)', () => {
108
+ const params = {
109
+ ...EXAMPLE_HEADER_BASIC,
110
+ variants: 'basic',
111
+ search: undefined,
112
+ searchLinks: {
113
+ heading: 'Other searches',
114
+ itemsList: [{ text: 'Population', url: '/population' }],
115
+ },
116
+ };
117
+ const $ = cheerio.load(renderComponent('header', params));
118
+ expect($.html()).toMatchSnapshot();
119
+ });
120
+
121
+ test('matches snapshot when no search heading is present', () => {
122
+ const params = {
123
+ ...EXAMPLE_HEADER_BASIC,
124
+ variants: 'basic',
125
+ search: { links: { itemsList: [] }, form: { action: '/search' } },
126
+ searchLinks: undefined,
127
+ };
128
+ const $ = cheerio.load(renderComponent('header', params));
129
+ expect($.html()).toMatchSnapshot();
130
+ });
131
+ });
38
132
  describe('GIVEN: Params: variants', () => {
39
133
  describe('WHEN: variants are provided', () => {
40
134
  const $ = cheerio.load(
@@ -823,12 +917,12 @@ describe('FOR: Macro: Header', () => {
823
917
  test('THEN: renders search icon button', () => {
824
918
  expect(buttonSpy.occurrences[0]).toEqual({
825
919
  iconType: 'search',
826
- classes: 'ons-u-fs-s--b ons-js-toggle-header-search ons-btn--close ons-btn--search-icon active disabled',
920
+ classes: 'ons-u-fs-s--b ons-js-toggle-header-search ons-u-db-no-js_disabled ons-btn--search-icon disabled',
827
921
  type: 'button',
828
922
  variants: 'search',
829
923
  attributes: {
830
924
  'aria-controls': 'search-id',
831
- 'aria-expanded': 'true',
925
+ 'aria-expanded': 'false',
832
926
  'aria-label': 'Custom search button aria label',
833
927
  'aria-disabled': 'true',
834
928
  },
@@ -911,12 +1005,12 @@ describe('FOR: Macro: Header', () => {
911
1005
  });
912
1006
  });
913
1007
 
914
- describe('WHEN: using basic header variant and search is active & disabled by default before JS loads', () => {
1008
+ describe('WHEN: using basic header variant and search is disabled by default before JS loads', () => {
915
1009
  const $ = cheerio.load(renderComponent('header', { ...EXAMPLE_HEADER_SEARCH, variants: 'basic' }));
916
1010
  const $searchBtn = $('.ons-js-toggle-header-search');
917
1011
 
918
- test('THEN: adds the "active" class to the search toggle button', () => {
919
- expect($searchBtn.hasClass('active')).toBe(true);
1012
+ test('THEN: does not add the "active" class to the search toggle button', () => {
1013
+ expect($searchBtn.hasClass('active')).toBe(false);
920
1014
  });
921
1015
 
922
1016
  test('THEN: adds the "disabled" class to the search toggle button', () => {
@@ -926,6 +1020,10 @@ describe('FOR: Macro: Header', () => {
926
1020
  test('THEN: sets aria-disabled="true" on the search toggle button', () => {
927
1021
  expect($searchBtn.attr('aria-disabled')).toBe('true');
928
1022
  });
1023
+
1024
+ test('THEN: sets aria-expanded="false" on the search toggle button', () => {
1025
+ expect($searchBtn.attr('aria-expanded')).toBe('false');
1026
+ });
929
1027
  });
930
1028
 
931
1029
  describe('WHEN: search is provided with all custom properties', () => {
@@ -1044,12 +1142,12 @@ describe('FOR: Macro: Header', () => {
1044
1142
  test('THEN: renders search icon button', () => {
1045
1143
  expect(buttonSpy.occurrences[0]).toEqual({
1046
1144
  iconType: 'search',
1047
- classes: 'ons-u-fs-s--b ons-js-toggle-header-search ons-btn--close ons-btn--search-icon active disabled',
1145
+ classes: 'ons-u-fs-s--b ons-js-toggle-header-search ons-u-db-no-js_disabled ons-btn--search-icon disabled',
1048
1146
  type: 'button',
1049
1147
  variants: 'search',
1050
1148
  attributes: {
1051
1149
  'aria-controls': 'search-links-id',
1052
- 'aria-expanded': 'true',
1150
+ 'aria-expanded': 'false',
1053
1151
  'aria-label': 'Custom search button aria label',
1054
1152
  'aria-disabled': 'true',
1055
1153
  },
@@ -1132,12 +1230,12 @@ describe('FOR: Macro: Header', () => {
1132
1230
  });
1133
1231
  });
1134
1232
 
1135
- describe('WHEN: using basic header variant and search is active & disabled by default before JS loads', () => {
1233
+ describe('WHEN: using basic header variant and search is disabled by default before JS loads', () => {
1136
1234
  const $ = cheerio.load(renderComponent('header', { ...EXAMPLE_HEADER_SEARCH_LINKS, variants: 'basic' }));
1137
1235
  const $searchBtn = $('.ons-js-toggle-header-search');
1138
1236
 
1139
- test('THEN: adds the "active" class to the search toggle button', () => {
1140
- expect($searchBtn.hasClass('active')).toBe(true);
1237
+ test('THEN: does not add the "active" class to the search toggle button', () => {
1238
+ expect($searchBtn.hasClass('active')).toBe(false);
1141
1239
  });
1142
1240
 
1143
1241
  test('THEN: adds the "disabled" class to the search toggle button', () => {
@@ -1147,6 +1245,10 @@ describe('FOR: Macro: Header', () => {
1147
1245
  test('THEN: sets aria-disabled="true" on the search toggle button', () => {
1148
1246
  expect($searchBtn.attr('aria-disabled')).toBe('true');
1149
1247
  });
1248
+
1249
+ test('THEN: sets aria-expanded="false" on the search toggle button', () => {
1250
+ expect($searchBtn.attr('aria-expanded')).toBe('false');
1251
+ });
1150
1252
  });
1151
1253
 
1152
1254
  describe('WHEN: searchLinks are provided with all custom properties', () => {
@@ -1259,8 +1361,8 @@ describe('FOR: Macro: Header', () => {
1259
1361
  const $ = cheerio.load(renderComponent('header', EXAMPLE_HEADER_MENU_LINKS));
1260
1362
  const $menuBtn = $('.ons-js-toggle-nav-menu');
1261
1363
 
1262
- test('THEN: adds the "active" class to the menu toggle button', () => {
1263
- expect($menuBtn.hasClass('active')).toBe(true);
1364
+ test('THEN: does not add the "active" class to the menu toggle button', () => {
1365
+ expect($menuBtn.hasClass('active')).toBe(false);
1264
1366
  });
1265
1367
 
1266
1368
  test('THEN: adds the "disabled" class to the menu toggle button', () => {
@@ -1271,8 +1373,8 @@ describe('FOR: Macro: Header', () => {
1271
1373
  expect($menuBtn.attr('aria-disabled')).toBe('true');
1272
1374
  });
1273
1375
 
1274
- test('THEN: sets aria-expanded="true" on the menu toggle button', () => {
1275
- expect($menuBtn.attr('aria-expanded')).toBe('true');
1376
+ test('THEN: sets aria-expanded="false" on the menu toggle button', () => {
1377
+ expect($menuBtn.attr('aria-expanded')).toBe('false');
1276
1378
  });
1277
1379
 
1278
1380
  test('THEN: sets aria-controls to the correct menu ID', () => {
@@ -0,0 +1,22 @@
1
+ ---
2
+ 'fullWidth': true
3
+ ---
4
+
5
+ {% from "components/header/_macro.njk" import onsHeader %}
6
+
7
+ {{
8
+ onsHeader({
9
+ "variants": 'basic',
10
+ "search": {
11
+ "id": "search",
12
+ "navAriaLabel": 'Nav Search',
13
+ "toggleAriaLabel": 'Toggle search',
14
+ "form": {
15
+ "action": "https://www.ons.gov.uk/search" ,
16
+ "inputName": "q",
17
+ "inputLabel": "Search the ONS",
18
+ "buttonText": "Search"
19
+ }
20
+ }
21
+ })
22
+ }}
@@ -1,6 +1,6 @@
1
1
  import { renderComponent, setTestPage } from '../../tests/helpers/rendering';
2
2
  import { EXAMPLE_HEADER_SEARCH_AND_MENU_LINKS } from './_test-examples';
3
- import { getNodeAttributes } from '../../tests/helpers/puppeteer';
3
+ import { getNodeAttributes, setViewport } from '../../tests/helpers/puppeteer';
4
4
 
5
5
  describe('script: header', () => {
6
6
  beforeEach(async () => {
@@ -107,6 +107,23 @@ describe('script: header', () => {
107
107
  const isAriaHiddenFalse = await page.$eval('.ons-header-nav-menu', (el) => el.getAttribute('aria-hidden') === 'false');
108
108
  expect(isAriaHiddenFalse).toBe(true);
109
109
  });
110
+
111
+ describe('and only the viewport height changes', () => {
112
+ beforeEach(async () => {
113
+ const viewport = page.viewport();
114
+ await setViewport(page, { width: viewport.width, height: viewport.height - 150 });
115
+ });
116
+
117
+ it('then the navigation menu stays displayed', async () => {
118
+ const isMenuNavVisible = await page.$eval('.ons-header-nav-menu', (el) => !el.classList.contains('ons-u-d-no'));
119
+ expect(isMenuNavVisible).toBe(true);
120
+ });
121
+
122
+ it('then the menu button stays marked as expanded', async () => {
123
+ const menuButtonAriaExpanded = await page.$eval('.ons-btn--menu', (el) => el.getAttribute('aria-expanded'));
124
+ expect(menuButtonAriaExpanded).toBe('true');
125
+ });
126
+ });
110
127
  });
111
128
 
112
129
  describe('when the menu button is not clicked', () => {
@@ -0,0 +1,27 @@
1
+ ---
2
+ 'fullWidth': true
3
+ ---
4
+
5
+ {% from "components/hero/_macro.njk" import onsHero %}
6
+ {{
7
+ onsHero({
8
+ "variants": 'grey',
9
+ "detailsColumns": '12',
10
+ "title": 'Lorem Ipsum',
11
+ "text": 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
12
+ "descriptionList": {
13
+ "termCol": "5",
14
+ "descriptionCol": "7",
15
+ "itemsList": [
16
+ {
17
+ "term": "Last updated:",
18
+ "descriptions": [
19
+ {
20
+ "description": "23 February 2024"
21
+ }
22
+ ]
23
+ }
24
+ ]
25
+ }
26
+ })
27
+ }}
@@ -90,15 +90,33 @@ export default class NavigationToggle {
90
90
  }
91
91
 
92
92
  isHidden(el) {
93
- return el.offsetParent === null;
93
+ if (!el) {
94
+ return true;
95
+ }
96
+
97
+ const style = window.getComputedStyle(el);
98
+ if (style.display === 'none') {
99
+ return true;
100
+ }
101
+
102
+ // `offsetParent` can be `null` for reasons other than `display: none` (e.g. fixed positioning),
103
+ // so use layout rects as a more reliable proxy for visibility.
104
+ return el.getClientRects().length === 0;
94
105
  }
95
106
 
96
- setAria() {
107
+ setAria(viewportDetails = null) {
97
108
  const isToggleHidden = this.isHidden(this.toggle);
98
109
  this.toggle.setAttribute(attrDisabled, 'false');
99
110
 
100
111
  this.searchToggleBtn?.setAttribute(attrDisabled, 'false');
101
112
 
113
+ // iOS Safari/Chrome can change viewport height during scroll when the browser chrome
114
+ // hides/shows, which can trigger resize events. If the user has the navigation open
115
+ // on a small screen, don't force-close it in response to these resizes.
116
+ if (viewportDetails && !isToggleHidden && this.navigation.getAttribute(attrHidden) === 'false') {
117
+ return;
118
+ }
119
+
102
120
  if (!isToggleHidden) {
103
121
  // close nav by default if toggle button is visible
104
122
  this.closeNav();
@@ -259,6 +259,28 @@ describe('script: navigation', () => {
259
259
  expect(hasClass).toBe(false);
260
260
  });
261
261
  });
262
+
263
+ describe('when the navigation is open and only the viewport height changes', () => {
264
+ beforeEach(async () => {
265
+ await page.focus(buttonEl);
266
+ await page.keyboard.press('Enter');
267
+ // Simulate iOS address bar show/hide which changes height during scroll
268
+ // without any meaningful layout/breakpoint change.
269
+ await setViewport(page, { width: 600, height: 900 });
270
+ });
271
+
272
+ it('keeps aria-hidden set as `false` on the navigation list', async () => {
273
+ const nav = await page.$(navEl);
274
+ const hasAriaAttribute = await nav.evaluate((node) => node.getAttribute('aria-hidden') === 'false');
275
+ expect(hasAriaAttribute).toBe(true);
276
+ });
277
+
278
+ it('keeps aria-expanded set as `true` on the navigation toggle button', async () => {
279
+ const button = await page.$(buttonEl);
280
+ const ariaExpandedIsTrue = await button.evaluate((node) => node.getAttribute('aria-expanded') === 'true');
281
+ expect(ariaExpandedIsTrue).toBe(true);
282
+ });
283
+ });
262
284
  });
263
285
  });
264
286