@ons/design-system 50.0.0 → 52.0.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.
Files changed (166) hide show
  1. package/README.md +35 -15
  2. package/components/access-code/_macro.njk +1 -1
  3. package/components/access-code/_macro.spec.js +162 -0
  4. package/components/access-code/uac.spec.js +26 -0
  5. package/components/accordion/_macro.spec.js +224 -0
  6. package/components/accordion/accordion.spec.js +134 -0
  7. package/components/address-input/_macro.njk +1 -1
  8. package/components/address-input/_macro.spec.js +465 -0
  9. package/components/address-input/autosuggest.address.js +5 -4
  10. package/components/address-input/autosuggest.address.setter.js +9 -3
  11. package/components/address-input/autosuggest.address.spec.js +733 -0
  12. package/components/address-output/_macro.njk +6 -6
  13. package/components/address-output/_macro.spec.js +122 -0
  14. package/components/autosuggest/_macro.njk +1 -1
  15. package/components/autosuggest/_macro.spec.js +229 -0
  16. package/components/autosuggest/autosuggest.helpers.js +2 -3
  17. package/components/autosuggest/autosuggest.helpers.spec.js +85 -0
  18. package/components/autosuggest/autosuggest.js +4 -2
  19. package/components/autosuggest/autosuggest.spec.js +625 -0
  20. package/components/autosuggest/autosuggest.ui.js +6 -2
  21. package/components/breadcrumbs/_macro.spec.js +129 -0
  22. package/components/button/_macro.njk +5 -5
  23. package/components/button/_macro.spec.js +446 -0
  24. package/components/button/button.spec.js +290 -0
  25. package/components/call-to-action/_macro.njk +3 -1
  26. package/components/call-to-action/_macro.spec.js +52 -0
  27. package/components/card/_macro.njk +26 -19
  28. package/components/card/_macro.spec.js +261 -0
  29. package/components/char-check-limit/_macro.spec.js +73 -0
  30. package/components/char-check-limit/character-check.spec.js +196 -0
  31. package/components/char-check-limit/character-limit.js +1 -1
  32. package/components/checkboxes/_checkbox-macro.spec.js +419 -0
  33. package/components/checkboxes/_macro.njk +1 -3
  34. package/components/checkboxes/_macro.spec.js +306 -0
  35. package/components/checkboxes/checkbox-with-autoselect.js +2 -1
  36. package/components/checkboxes/checkboxes.spec.js +208 -0
  37. package/components/code-highlight/_macro.spec.js +56 -0
  38. package/components/code-highlight/code-highlight.spec.js +18 -0
  39. package/components/collapsible/_macro.spec.js +204 -0
  40. package/components/collapsible/collapsible.js +2 -1
  41. package/components/collapsible/collapsible.spec.js +236 -0
  42. package/components/content-pagination/_macro.spec.js +199 -0
  43. package/components/cookies-banner/_macro.njk +1 -1
  44. package/components/cookies-banner/_macro.spec.js +171 -0
  45. package/components/cookies-banner/cookies-banner.spec.js +90 -0
  46. package/components/date-input/_macro.njk +6 -3
  47. package/components/date-input/_macro.spec.js +286 -0
  48. package/components/document-list/_macro.njk +3 -5
  49. package/components/document-list/_macro.spec.js +491 -0
  50. package/components/download-resources/download-resources.spec.js +540 -0
  51. package/components/duration/_macro.njk +7 -6
  52. package/components/duration/_macro.spec.js +251 -0
  53. package/components/error/_macro.spec.js +97 -0
  54. package/components/external-link/_macro.spec.js +60 -0
  55. package/components/feedback/_macro.njk +5 -3
  56. package/components/feedback/_macro.spec.js +122 -0
  57. package/components/field/_macro.njk +2 -2
  58. package/components/field/_macro.spec.js +97 -0
  59. package/components/fieldset/_macro.njk +3 -3
  60. package/components/fieldset/_macro.spec.js +173 -0
  61. package/components/footer/_macro.njk +12 -49
  62. package/components/footer/_macro.spec.js +549 -0
  63. package/components/header/_macro.njk +3 -3
  64. package/components/header/_macro.spec.js +562 -0
  65. package/components/hero/_hero.scss +0 -3
  66. package/components/hero/_macro.njk +4 -4
  67. package/components/hero/_macro.spec.js +224 -0
  68. package/components/icons/_macro.njk +15 -15
  69. package/components/icons/_macro.spec.js +140 -0
  70. package/components/images/_macro.njk +1 -1
  71. package/components/images/_macro.spec.js +121 -0
  72. package/components/input/_input-type.scss +12 -5
  73. package/components/input/_macro.njk +4 -5
  74. package/components/input/_macro.spec.js +658 -0
  75. package/components/label/_macro.spec.js +189 -0
  76. package/components/language-selector/_macro.spec.js +129 -0
  77. package/components/lists/_list.scss +4 -0
  78. package/components/lists/_macro.njk +4 -7
  79. package/components/lists/_macro.spec.js +618 -0
  80. package/components/message/_macro.spec.js +137 -0
  81. package/components/message-list/_macro.njk +7 -7
  82. package/components/message-list/_macro.spec.js +159 -0
  83. package/components/metadata/_macro.spec.js +167 -0
  84. package/components/modal/_macro.njk +6 -6
  85. package/components/modal/_macro.spec.js +87 -0
  86. package/components/modal/modal.js +0 -16
  87. package/components/modal/modal.spec.js +59 -0
  88. package/components/mutually-exclusive/_macro.njk +2 -2
  89. package/components/mutually-exclusive/_macro.spec.js +184 -0
  90. package/components/mutually-exclusive/mutually-exclusive.checkboxes.spec.js +203 -0
  91. package/components/mutually-exclusive/mutually-exclusive.date.spec.js +142 -0
  92. package/components/mutually-exclusive/mutually-exclusive.duration.spec.js +141 -0
  93. package/components/mutually-exclusive/mutually-exclusive.email.spec.js +117 -0
  94. package/components/mutually-exclusive/mutually-exclusive.multiple-options.checkboxes.spec.js +213 -0
  95. package/components/mutually-exclusive/mutually-exclusive.number.spec.js +125 -0
  96. package/components/mutually-exclusive/mutually-exclusive.textarea.spec.js +131 -0
  97. package/components/navigation/_macro.njk +6 -6
  98. package/components/navigation/_macro.spec.js +327 -0
  99. package/components/navigation/navigation.dom.js +1 -1
  100. package/components/navigation/navigation.spec.js +232 -0
  101. package/components/pagination/_macro.njk +1 -1
  102. package/components/pagination/_macro.spec.js +411 -0
  103. package/components/panel/_macro.njk +6 -6
  104. package/components/panel/_macro.spec.js +423 -0
  105. package/components/password/_macro.spec.js +137 -0
  106. package/components/password/password.spec.js +40 -0
  107. package/components/phase-banner/_macro.spec.js +73 -0
  108. package/components/promotional-banner/_macro.spec.js +97 -0
  109. package/components/question/_macro.njk +16 -22
  110. package/components/question/_macro.spec.js +309 -0
  111. package/components/quote/_macro.spec.js +81 -0
  112. package/components/radios/_macro.njk +3 -6
  113. package/components/radios/_macro.spec.js +575 -0
  114. package/components/radios/radios.spec.js +180 -0
  115. package/components/related-content/_macro.njk +1 -0
  116. package/components/related-content/_macro.spec.js +142 -0
  117. package/components/relationships/_macro.spec.js +108 -0
  118. package/components/relationships/relationships.spec.js +84 -0
  119. package/components/reply/_macro.njk +2 -2
  120. package/components/reply/_macro.spec.js +69 -0
  121. package/components/reply/reply.spec.js +78 -0
  122. package/components/search/_macro.njk +14 -12
  123. package/components/search/_macro.spec.js +44 -0
  124. package/components/search/_search.scss +7 -7
  125. package/components/section-navigation/_macro.njk +7 -2
  126. package/components/section-navigation/_macro.spec.js +206 -0
  127. package/components/select/_macro.njk +3 -3
  128. package/components/select/_macro.spec.js +203 -0
  129. package/components/select/select.spec.js +56 -0
  130. package/components/share-page/_macro.njk +2 -2
  131. package/components/share-page/_macro.spec.js +110 -0
  132. package/components/skip-to-content/_macro.spec.js +57 -0
  133. package/components/skip-to-content/skip-to-content.spec.js +44 -0
  134. package/components/status/_macro.spec.js +77 -0
  135. package/components/summary/_macro.njk +5 -5
  136. package/components/summary/_macro.spec.js +472 -0
  137. package/components/table/_macro.njk +2 -2
  138. package/components/table/_macro.spec.js +557 -0
  139. package/components/table/table.spec.js +155 -0
  140. package/components/table-of-contents/_macro.njk +35 -35
  141. package/components/table-of-contents/_macro.spec.js +178 -0
  142. package/components/table-of-contents/toc.js +29 -25
  143. package/components/table-of-contents/toc.spec.js +61 -0
  144. package/components/tabs/_macro.njk +1 -1
  145. package/components/tabs/_macro.spec.js +79 -0
  146. package/components/tabs/tabs.spec.js +162 -0
  147. package/components/text-indent/_macro.spec.js +52 -0
  148. package/components/textarea/_macro.njk +5 -3
  149. package/components/textarea/_macro.spec.js +300 -0
  150. package/components/textarea/textarea.spec.js +98 -0
  151. package/components/timeline/_macro.njk +3 -3
  152. package/components/timeline/_macro.spec.js +81 -0
  153. package/components/timeout-modal/_macro.spec.js +68 -0
  154. package/components/timeout-modal/timeout-modal.spec.js +226 -0
  155. package/components/timeout-panel/_macro.njk +0 -1
  156. package/components/timeout-panel/_macro.spec.js +54 -0
  157. package/components/timeout-panel/timeout-panel.dom.js +1 -2
  158. package/components/timeout-panel/timeout-panel.spec.js +161 -0
  159. package/components/upload/_macro.spec.js +75 -0
  160. package/components/video/_macro.spec.js +34 -0
  161. package/css/census.css +1 -1
  162. package/css/main.css +1 -1
  163. package/js/cookies-settings.spec.js +154 -0
  164. package/package.json +10 -23
  165. package/scripts/main.es5.js +1 -1
  166. package/scripts/main.js +2 -2
@@ -0,0 +1,155 @@
1
+ import { renderComponent, setTestPage } from '../../tests/helpers/rendering';
2
+
3
+ describe('script: table', () => {
4
+ describe('variant: scrollable', () => {
5
+ // Construct a table with 15 columns and 15 rows with long labels.
6
+ const params = {
7
+ variants: ['scrollable'],
8
+ ths: Array.from({ length: 15 }, (_, i) => ({ value: `Column ${i + 1}` })),
9
+ trs: [
10
+ {
11
+ tds: Array.from({ length: 15 }, (_, i) => ({ value: `Business Register and Employment Survey ${i + 1}` })),
12
+ },
13
+ ],
14
+ };
15
+
16
+ beforeEach(async () => {
17
+ await setTestPage('/test', renderComponent('table', params));
18
+ });
19
+
20
+ it('should add shadow elements', async () => {
21
+ const leftShadowCount = await page.$$eval('.ons-table__left-shadow', nodes => nodes.length);
22
+ expect(leftShadowCount).not.toBe(0);
23
+ const rightShadowCount = await page.$$eval('.ons-table__right-shadow', nodes => nodes.length);
24
+ expect(rightShadowCount).not.toBe(0);
25
+ });
26
+
27
+ describe('When the table component is scrolled,', () => {
28
+ beforeEach(async () => {
29
+ await page.focus('.ons-table-scrollable__content');
30
+ await page.keyboard.press('ArrowRight');
31
+ });
32
+
33
+ it('should show both shadow elements', async () => {
34
+ await page.waitForTimeout(200);
35
+
36
+ const leftShadowVisibleCount = await page.$$eval('.ons-table__left-shadow.ons-visible', nodes => nodes.length);
37
+ expect(leftShadowVisibleCount).not.toBe(0);
38
+ const rightShadowVisibleCount = await page.$$eval('.ons-table__right-shadow.ons-visible', nodes => nodes.length);
39
+ expect(rightShadowVisibleCount).not.toBe(0);
40
+ });
41
+ });
42
+ });
43
+
44
+ describe('variant: sortable', () => {
45
+ const params = {
46
+ variants: ['sortable'],
47
+ sortBy: 'Sort by',
48
+ ariaAsc: 'ascending',
49
+ ariaDesc: 'descending',
50
+ ths: [
51
+ { value: 'Column 1', ariaSort: 'none' },
52
+ { value: 'Column 2', ariaSort: 'none' },
53
+ { value: 'Column 3', ariaSort: 'none' },
54
+ { value: 'Column 4', ariaSort: 'none' },
55
+ ],
56
+ trs: [
57
+ {
58
+ tds: [
59
+ { value: 'A', dataSort: '1' },
60
+ { value: 'A', dataSort: '4' },
61
+ { value: 'A', dataSort: '0' },
62
+ { value: 'A', dataSort: '2' },
63
+ ],
64
+ },
65
+ {
66
+ tds: [
67
+ { value: 'B', dataSort: '2' },
68
+ { value: 'B', dataSort: '4' },
69
+ { value: 'B', dataSort: '0' },
70
+ { value: 'B', dataSort: '2' },
71
+ ],
72
+ },
73
+ {
74
+ tds: [
75
+ { value: 'C', dataSort: '3' },
76
+ { value: 'C', dataSort: '4' },
77
+ { value: 'C', dataSort: '0' },
78
+ { value: 'C', dataSort: '2' },
79
+ ],
80
+ },
81
+ ],
82
+ };
83
+
84
+ beforeEach(async () => {
85
+ await setTestPage('/test', renderComponent('table', params));
86
+ });
87
+
88
+ it('should create a button element in each TH', async () => {
89
+ const buttonCount = await page.$$eval('.ons-table__header .ons-table__sort-button', nodes => nodes.length);
90
+ expect(buttonCount).toBe(4);
91
+ });
92
+
93
+ it('should create a status element with aria attributes', async () => {
94
+ const ariaLiveAttribute = await page.$eval('.ons-sortable-table-status', node => node.getAttribute('aria-live'));
95
+ expect(ariaLiveAttribute).toBe('polite');
96
+ const roleAttribute = await page.$eval('.ons-sortable-table-status', node => node.getAttribute('role'));
97
+ expect(roleAttribute).toBe('status');
98
+ const ariaAtomicAttribute = await page.$eval('.ons-sortable-table-status', node => node.getAttribute('aria-atomic'));
99
+ expect(ariaAtomicAttribute).toBe('true');
100
+ });
101
+
102
+ describe('Each sort button element', () => {
103
+ it('should contain an aria-label attribute', async () => {
104
+ const ariaLabelValues = await page.$$eval('.ons-table__sort-button', nodes => nodes.map(node => node.getAttribute('aria-label')));
105
+ expect(ariaLabelValues).toEqual(['Sort by Column 1', 'Sort by Column 2', 'Sort by Column 3', 'Sort by Column 4']);
106
+ });
107
+
108
+ it('should contain a data-index attribute', async () => {
109
+ const dataIndexValues = await page.$$eval('.ons-table__sort-button', nodes => nodes.map(node => node.getAttribute('data-index')));
110
+ expect(dataIndexValues).toEqual(['0', '1', '2', '3']);
111
+ });
112
+ });
113
+
114
+ describe('When a sort button is clicked', () => {
115
+ beforeEach(async () => {
116
+ await page.click('.ons-table__header:nth-child(1) .ons-table__sort-button');
117
+ });
118
+
119
+ it('should update aria-sort value for each column header', async () => {
120
+ const ariaSortValues = await page.$$eval('.ons-table__header', nodes => nodes.map(node => node.getAttribute('aria-sort')));
121
+ expect(ariaSortValues).toEqual(['descending', 'none', 'none', 'none']);
122
+ });
123
+
124
+ it('should sort the column into descending order', async () => {
125
+ const firstColumnValues = await page.$$eval('.ons-table__row .ons-table__cell:first-child', nodes =>
126
+ nodes.map(node => node.textContent.trim()),
127
+ );
128
+ expect(firstColumnValues).toEqual(['C', 'B', 'A']);
129
+ });
130
+
131
+ it('should update the aria-live status', async () => {
132
+ const statusText = await page.$eval('.ons-sortable-table-status', node => node.textContent);
133
+ expect(statusText).toBe('Sort by Column 1 (descending)');
134
+ });
135
+
136
+ describe('When a sort button is clicked again', () => {
137
+ beforeEach(async () => {
138
+ await page.click('.ons-table__header:nth-child(1) .ons-table__sort-button');
139
+ });
140
+
141
+ it('should update aria-sort value for each column header', async () => {
142
+ const ariaSortValues = await page.$$eval('.ons-table__header', nodes => nodes.map(node => node.getAttribute('aria-sort')));
143
+ expect(ariaSortValues).toEqual(['ascending', 'none', 'none', 'none']);
144
+ });
145
+
146
+ it('should sort the column into ascending order', async () => {
147
+ const firstColumnValues = await page.$$eval('.ons-table__row .ons-table__cell:first-child', nodes =>
148
+ nodes.map(node => node.textContent.trim()),
149
+ );
150
+ expect(firstColumnValues).toEqual(['A', 'B', 'C']);
151
+ });
152
+ });
153
+ });
154
+ });
155
+ });
@@ -1,43 +1,43 @@
1
1
  {% macro onsTableOfContents(params) %}
2
- {% from "components/lists/_macro.njk" import onsList %}
3
- {% from "components/skip-to-content/_macro.njk" import onsSkipToContent %}
2
+ {% from "components/lists/_macro.njk" import onsList %}
3
+ {% from "components/skip-to-content/_macro.njk" import onsSkipToContent %}
4
4
 
5
- <aside class="ons-toc-container" role="complementary">
5
+ <aside class="ons-toc-container" role="complementary">
6
6
  {% if params.skipLink is defined and params.skipLink %}
7
- {{
8
- onsSkipToContent({
9
- "url": params.skipLink.url,
10
- "text": params.skipLink.text
11
- })
12
- }}
7
+ {{
8
+ onsSkipToContent({
9
+ "url": params.skipLink.url,
10
+ "text": params.skipLink.text
11
+ })
12
+ }}
13
13
  {% endif %}
14
14
 
15
15
  <nav class="ons-toc" aria-label="{{ params.ariaLabel | default('Table of contents') }}">
16
- <h2 class="ons-toc__title ons-u-fs-r--b ons-u-mb-s">{{ params.title }}</h2>
17
- {% if params.lists is defined and params.lists %}
18
- {% for list in params.lists %}
19
- {% if list.listHeading is defined and list.listHeading %}
20
- <h3 class="ons-u-fs-r ons-u-mb-xs">{{ list.listHeading }}<span class="ons-u-vh"> {{ list.listHeadingHidden }}</span>:</h3>
21
- {% endif %}
22
- {{
23
- onsList({
24
- "element": 'ol',
25
- "classes": 'ons-u-mb-m',
26
- "variants": 'dashed',
27
- "itemsList": list.itemsList
28
- })
29
- }}
30
- {% endfor %}
31
- {% elif params.itemsList is defined and params.itemsList %}
32
- {{
33
- onsList({
34
- "element": 'ol',
35
- "classes": 'ons-u-mb-m',
36
- "variants": 'dashed',
37
- "itemsList": params.itemsList
38
- })
39
- }}
40
- {% endif %}
16
+ <h2 class="ons-toc__title ons-u-fs-r--b ons-u-mb-s">{{ params.title }}</h2>
17
+ {% if params.lists is defined and params.lists %}
18
+ {% for list in params.lists %}
19
+ {% if list.listHeading is defined and list.listHeading %}
20
+ <h3 class="ons-u-fs-r ons-u-mb-xs">{{ list.listHeading }}<span class="ons-u-vh"> {{ list.listHeadingHidden }}</span>:</h3>
21
+ {% endif %}
22
+ {{
23
+ onsList({
24
+ "element": 'ol',
25
+ "classes": 'ons-u-mb-m',
26
+ "variants": 'dashed',
27
+ "itemsList": list.itemsList
28
+ })
29
+ }}
30
+ {% endfor %}
31
+ {% elif params.itemsList is defined and params.itemsList %}
32
+ {{
33
+ onsList({
34
+ "element": 'ol',
35
+ "classes": 'ons-u-mb-m',
36
+ "variants": 'dashed',
37
+ "itemsList": params.itemsList
38
+ })
39
+ }}
40
+ {% endif %}
41
41
  </nav>
42
- </aside>
42
+ </aside>
43
43
  {% endmacro %}
@@ -0,0 +1,178 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import * as cheerio from 'cheerio';
4
+
5
+ import axe from '../../tests/helpers/axe';
6
+ import { mapAll } from '../../tests/helpers/cheerio';
7
+ import { renderComponent, templateFaker } from '../../tests/helpers/rendering';
8
+
9
+ const EXAMPLE_TABLE_OF_CONTENTS_SKIP_LINK = {
10
+ title: 'Contents',
11
+ skipLink: {
12
+ url: '#the-content',
13
+ text: 'Skip to guide content',
14
+ },
15
+ itemsList: [],
16
+ };
17
+
18
+ const EXAMPLE_TABLE_OF_CONTENTS_SINGLE = {
19
+ title: 'Contents',
20
+ itemsList: [
21
+ {
22
+ url: '#overview',
23
+ text: 'Overview',
24
+ },
25
+ {
26
+ url: '#who-should-take-part-and-why',
27
+ text: 'Who should take part and why',
28
+ },
29
+ ],
30
+ };
31
+
32
+ const EXAMPLE_TABLE_OF_CONTENTS_MULTIPLE = {
33
+ title: 'Contents',
34
+ lists: [
35
+ {
36
+ listHeading: 'Household questions',
37
+ listHeadingHidden: 'help topics',
38
+ itemsList: [
39
+ {
40
+ url: '#household1',
41
+ text: 'Household and who lives here',
42
+ },
43
+ ],
44
+ },
45
+ {
46
+ listHeading: 'Individual questions',
47
+ listHeadingHidden: 'help topics',
48
+ itemsList: [
49
+ {
50
+ url: '#individual1',
51
+ text: 'Name, date of birth and marital status',
52
+ },
53
+ ],
54
+ },
55
+ ],
56
+ };
57
+
58
+ describe('macro: table-of-contents', () => {
59
+ it('renders a default aria label', () => {
60
+ const $ = cheerio.load(renderComponent('table-of-contents', EXAMPLE_TABLE_OF_CONTENTS_SINGLE));
61
+
62
+ expect($('.ons-toc').attr('aria-label')).toBe('Table of contents');
63
+ });
64
+
65
+ it('renders the provided `ariaLabel`', () => {
66
+ const $ = cheerio.load(
67
+ renderComponent('table-of-contents', {
68
+ ...EXAMPLE_TABLE_OF_CONTENTS_SINGLE,
69
+ ariaLabel: 'Contents',
70
+ }),
71
+ );
72
+
73
+ expect($('.ons-toc').attr('aria-label')).toBe('Contents');
74
+ });
75
+
76
+ it('renders title as heading element', () => {
77
+ const $ = cheerio.load(renderComponent('table-of-contents', EXAMPLE_TABLE_OF_CONTENTS_SINGLE));
78
+
79
+ expect(
80
+ $('.ons-toc__title')
81
+ .text()
82
+ .trim(),
83
+ ).toBe('Contents');
84
+ });
85
+
86
+ describe('skip to content when `skipLink` is provided', () => {
87
+ it('passes jest-axe checks', async () => {
88
+ const $ = cheerio.load(renderComponent('table-of-contents', EXAMPLE_TABLE_OF_CONTENTS_SKIP_LINK));
89
+
90
+ const results = await axe($.html());
91
+ expect(results).toHaveNoViolations();
92
+ });
93
+
94
+ it('outputs `skip-to-content` component', () => {
95
+ const faker = templateFaker();
96
+ const skipToContentSpy = faker.spy('skip-to-content');
97
+
98
+ faker.renderComponent('table-of-contents', EXAMPLE_TABLE_OF_CONTENTS_SKIP_LINK);
99
+
100
+ expect(skipToContentSpy.occurrences[0]).toEqual({
101
+ url: '#the-content',
102
+ text: 'Skip to guide content',
103
+ });
104
+ });
105
+ });
106
+
107
+ describe('single list', () => {
108
+ it('passes jest-axe checks', async () => {
109
+ const $ = cheerio.load(renderComponent('table-of-contents', EXAMPLE_TABLE_OF_CONTENTS_SINGLE));
110
+
111
+ const results = await axe($.html());
112
+ expect(results).toHaveNoViolations();
113
+ });
114
+
115
+ it('outputs `lists` component', () => {
116
+ const faker = templateFaker();
117
+ const listsSpy = faker.spy('lists');
118
+
119
+ faker.renderComponent('table-of-contents', EXAMPLE_TABLE_OF_CONTENTS_SINGLE);
120
+
121
+ expect(listsSpy.occurrences[0]).toEqual({
122
+ element: 'ol',
123
+ classes: 'ons-u-mb-m',
124
+ variants: 'dashed',
125
+ itemsList: EXAMPLE_TABLE_OF_CONTENTS_SINGLE.itemsList,
126
+ });
127
+ });
128
+ });
129
+
130
+ describe('multiple lists', () => {
131
+ it('passes jest-axe checks', async () => {
132
+ const $ = cheerio.load(renderComponent('table-of-contents', EXAMPLE_TABLE_OF_CONTENTS_MULTIPLE));
133
+
134
+ const results = await axe($.html());
135
+ expect(results).toHaveNoViolations();
136
+ });
137
+
138
+ it('renders a heading for each list', () => {
139
+ const $ = cheerio.load(renderComponent('table-of-contents', EXAMPLE_TABLE_OF_CONTENTS_MULTIPLE));
140
+
141
+ $('.ons-u-vh').remove();
142
+
143
+ const headings = mapAll($('h3'), node =>
144
+ $(node)
145
+ .text()
146
+ .trim(),
147
+ );
148
+ expect(headings).toEqual(['Household questions:', 'Individual questions:']);
149
+ });
150
+
151
+ it('renders visually hidden heading for each list', () => {
152
+ const $ = cheerio.load(renderComponent('table-of-contents', EXAMPLE_TABLE_OF_CONTENTS_MULTIPLE));
153
+
154
+ const headings = mapAll($('h3 .ons-u-vh'), node => node.text().trim());
155
+ expect(headings).toEqual(['help topics', 'help topics']);
156
+ });
157
+
158
+ it('outputs `lists` component for each list', () => {
159
+ const faker = templateFaker();
160
+ const listsSpy = faker.spy('lists');
161
+
162
+ faker.renderComponent('table-of-contents', EXAMPLE_TABLE_OF_CONTENTS_MULTIPLE);
163
+
164
+ expect(listsSpy.occurrences[0]).toEqual({
165
+ element: 'ol',
166
+ classes: 'ons-u-mb-m',
167
+ variants: 'dashed',
168
+ itemsList: EXAMPLE_TABLE_OF_CONTENTS_MULTIPLE.lists[0].itemsList,
169
+ });
170
+ expect(listsSpy.occurrences[1]).toEqual({
171
+ element: 'ol',
172
+ classes: 'ons-u-mb-m',
173
+ variants: 'dashed',
174
+ itemsList: EXAMPLE_TABLE_OF_CONTENTS_MULTIPLE.lists[1].itemsList,
175
+ });
176
+ });
177
+ });
178
+ });
@@ -1,35 +1,39 @@
1
1
  export default class Toc {
2
2
  constructor(component) {
3
3
  this.component = component;
4
- this.page = this.component.closest('html');
5
- this.headings = this.component.querySelectorAll('section[id]');
6
- this.observerOptions = {
7
- root: null,
8
- rootMargin: '0px 0px -70% 0px',
9
- threshold: [0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1],
10
- };
11
-
12
- this.observe = new IntersectionObserver(this.setCurrent, this.observerOptions);
13
- this.addIntersectionObserver();
4
+ this.sections = [...this.component.querySelectorAll('section[id]')];
5
+ this.refreshIntervalId = setInterval(() => this.setCurrent(), 100);
6
+ this.setCurrent();
14
7
  }
15
8
 
16
- addIntersectionObserver() {
17
- this.headings.forEach(heading => {
18
- this.observe.observe(heading);
19
- });
20
- }
9
+ setCurrent() {
10
+ let activeSection = this.sections[0];
11
+ for (let section of this.sections) {
12
+ const top = section.getBoundingClientRect().top;
13
+ if (top > 100) {
14
+ break;
15
+ }
16
+
17
+ activeSection = section;
18
+
19
+ if (top >= 0 && top <= 100) {
20
+ break;
21
+ }
22
+ }
23
+
24
+ if (activeSection === this.activeSection) {
25
+ return;
26
+ }
27
+
28
+ this.activeSection = activeSection;
21
29
 
22
- setCurrent(event) {
23
- event.map(element => {
24
- const position = element.boundingClientRect;
25
- const link = document.querySelector(`.ons-toc li a[href="#${element.target.id}"]`);
26
- if (link) {
27
- link.classList[element.isIntersecting === true && position.top < 70 && position.top > -100 ? 'add' : 'remove'](
28
- 'ons-toc__link-active',
29
- );
30
+ for (let section of this.sections) {
31
+ const tocItem = document.querySelector(`.ons-toc .ons-list__link[href="#${section.id}"]`);
32
+ if (section === activeSection) {
33
+ tocItem.classList.add('ons-toc__link-active');
30
34
  } else {
31
- console.warn(`element ".ons-toc li a[href="#${element.target.id}"]" is missing`);
35
+ tocItem.classList.remove('ons-toc__link-active');
32
36
  }
33
- });
37
+ }
34
38
  }
35
39
  }
@@ -0,0 +1,61 @@
1
+ import { renderComponent, setTestPage } from '../../tests/helpers/rendering';
2
+
3
+ describe('script: table-of-contents', () => {
4
+ beforeEach(async () => {
5
+ await setTestPage(
6
+ '/test',
7
+ `
8
+ <div class="ons-page__container ons-container">
9
+ <div class="ons-grid ons-js-toc-container">
10
+ <div class="ons-grid__col ons-grid__col--sticky@m ons-col-4@m">
11
+ ${renderComponent('table-of-contents', {
12
+ title: 'Contents',
13
+ ariaLabel: 'Sections in this page',
14
+ itemsList: [
15
+ {
16
+ url: '#section1',
17
+ text: 'First section',
18
+ },
19
+ {
20
+ url: '#section2',
21
+ text: 'Second section',
22
+ },
23
+ {
24
+ url: '#section3',
25
+ text: 'Third section',
26
+ },
27
+ ],
28
+ })}
29
+ </div>
30
+ <div class="ons-grid__col ons-col-7@m ons-push-1@m">
31
+ <section id="section1">
32
+ <h2>First section</h2>
33
+ <p>${'<br/>'.repeat(20)}</p>
34
+ </section>
35
+ <section id="section2">
36
+ <h2>Second section</h2>
37
+ <p>${'<br/>'.repeat(10)}</p>
38
+ </section>
39
+ <section id="section3">
40
+ <h2>Third section</h2>
41
+ <p>${'<br/>'.repeat(50)}</p>
42
+ </section>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ `,
47
+ );
48
+ });
49
+
50
+ it.each([
51
+ ['section1', 'First section'],
52
+ ['section2', 'Second section'],
53
+ ['section3', 'Third section'],
54
+ ])('marks "%s" as the current section', async (sectionId, sectionTitle) => {
55
+ await page.$eval(`#${sectionId}`, node => node.scrollIntoView());
56
+ await page.waitForTimeout(250);
57
+
58
+ const activeSection = await page.$eval('.ons-toc__link-active', node => node.innerText.trim());
59
+ expect(activeSection).toBe(sectionTitle);
60
+ });
61
+ });
@@ -1,5 +1,5 @@
1
1
  {% macro onsTabs(params) %}
2
- <section role="region" class="ons-tabs">
2
+ <section class="ons-tabs">
3
3
  <h2 class="ons-tabs__title">{{params.title}}</h2>
4
4
  <ul class="ons-tabs__list">
5
5
  {% for tab in params.tabs %}
@@ -0,0 +1,79 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import * as cheerio from 'cheerio';
4
+
5
+ import axe from '../../tests/helpers/axe';
6
+ import { renderComponent } from '../../tests/helpers/rendering';
7
+
8
+ const EXAMPLE_TABS = {
9
+ title: 'Example tabs',
10
+ tabs: [
11
+ {
12
+ title: 'Tab 1',
13
+ content: 'Example content...',
14
+ },
15
+ {
16
+ title: 'Tab 2',
17
+ content: 'Some nested <strong>strong element</strong>...',
18
+ },
19
+ ],
20
+ };
21
+
22
+ describe('macro: tabs', () => {
23
+ it('passes jest-axe checks', async () => {
24
+ const $ = cheerio.load(renderComponent('tabs', EXAMPLE_TABS));
25
+
26
+ const results = await axe($.html());
27
+ expect(results).toHaveNoViolations();
28
+ });
29
+
30
+ it('has the provided `title`', () => {
31
+ const $ = cheerio.load(renderComponent('tabs', EXAMPLE_TABS));
32
+
33
+ expect(
34
+ $('.ons-tabs__title')
35
+ .text()
36
+ .trim(),
37
+ ).toBe('Example tabs');
38
+ });
39
+
40
+ it('has expected label text in tab links', () => {
41
+ const $ = cheerio.load(renderComponent('tabs', EXAMPLE_TABS));
42
+
43
+ expect(
44
+ $('.ons-tab:first')
45
+ .text()
46
+ .trim(),
47
+ ).toBe('Tab 1');
48
+ expect(
49
+ $('.ons-tab:last')
50
+ .text()
51
+ .trim(),
52
+ ).toBe('Tab 2');
53
+ });
54
+
55
+ it('has Google Analytics integration on tab links', () => {
56
+ const $ = cheerio.load(renderComponent('tabs', EXAMPLE_TABS));
57
+
58
+ const tabItem = $('.ons-tab');
59
+ expect(tabItem.attr('data-ga')).toBe('click');
60
+ expect(tabItem.attr('data-ga-category')).toBe('tabs');
61
+ expect(tabItem.attr('data-ga-action')).toBe('Show: Tab 1');
62
+ expect(tabItem.attr('data-ga-label')).toBe('Show: Tab 1');
63
+ });
64
+
65
+ it('has expected content in tab panels', () => {
66
+ const $ = cheerio.load(renderComponent('tabs', EXAMPLE_TABS));
67
+
68
+ expect(
69
+ $('.ons-tabs__panel:first')
70
+ .html()
71
+ .trim(),
72
+ ).toBe('Example content...');
73
+ expect(
74
+ $('.ons-tabs__panel:last')
75
+ .html()
76
+ .trim(),
77
+ ).toBe('Some nested <strong>strong element</strong>...');
78
+ });
79
+ });