@madgex/design-system 13.6.4 → 13.7.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.
@@ -0,0 +1,462 @@
1
+ // eslint-disable-next-line n/no-unpublished-import
2
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
3
+ import { MdsScrollSpy } from './scroll-spy.js';
4
+
5
+ // Polyfill CSS.escape for jsdom (not available by default)
6
+ if (typeof CSS === 'undefined' || !CSS.escape) {
7
+ globalThis.CSS = {
8
+ escape: (str) => str.replace(/([^\w-])/g, (match) => `\\${match}`),
9
+ };
10
+ }
11
+
12
+ // Mock IntersectionObserver
13
+ let lastObserverInstance = null;
14
+
15
+ class MockIntersectionObserver {
16
+ constructor(callback, options) {
17
+ this.callback = callback;
18
+ this.options = options;
19
+ this.observedElements = [];
20
+ lastObserverInstance = this;
21
+ }
22
+ observe(el) {
23
+ this.observedElements.push(el);
24
+ }
25
+ unobserve(el) {
26
+ this.observedElements = this.observedElements.filter((e) => e !== el);
27
+ }
28
+ disconnect() {
29
+ this.observedElements = [];
30
+ }
31
+ // Helper to simulate intersection events
32
+ triggerIntersect(entries) {
33
+ this.callback(entries, this);
34
+ }
35
+ }
36
+
37
+ vi.stubGlobal('IntersectionObserver', MockIntersectionObserver);
38
+
39
+ // Register the custom element
40
+ if (!customElements.get('mds-scroll-spy')) {
41
+ customElements.define('mds-scroll-spy', MdsScrollSpy);
42
+ }
43
+
44
+ describe('MdsScrollSpy', () => {
45
+ let container;
46
+
47
+ beforeEach(() => {
48
+ container = document.createElement('div');
49
+ document.body.appendChild(container);
50
+ });
51
+
52
+ afterEach(() => {
53
+ container.remove();
54
+ vi.restoreAllMocks();
55
+ });
56
+
57
+ /**
58
+ * Helper to create a scroll-spy and attach it to the container with links.
59
+ * Uses innerHTML to ensure proper URL resolution in jsdom.
60
+ */
61
+ function createScrollSpyWithLinks(anchors) {
62
+ const linksHtml = anchors.map(({ href, text }) => `<a href="${href}">${text}</a>`).join('');
63
+ container.innerHTML = `<mds-scroll-spy>${linksHtml}</mds-scroll-spy>`;
64
+ return container.querySelector('mds-scroll-spy');
65
+ }
66
+
67
+ // ------------------------------
68
+ // Constructor
69
+ // ------------------------------
70
+ describe('constructor', () => {
71
+ it('creates instance without errors', () => {
72
+ const scrollSpy = new MdsScrollSpy();
73
+ expect(scrollSpy).toBeInstanceOf(MdsScrollSpy);
74
+ expect(scrollSpy).toBeInstanceOf(HTMLElement);
75
+ });
76
+ });
77
+
78
+ // ------------------------------
79
+ // observer-threshold attribute parsing (tested via behavior)
80
+ // ------------------------------
81
+ describe('observer-threshold attribute', () => {
82
+ it('accepts single numeric threshold value', () => {
83
+ const section = document.createElement('section');
84
+ section.id = 'test-section';
85
+ document.body.appendChild(section);
86
+
87
+ container.innerHTML = `
88
+ <mds-scroll-spy observer-threshold="0.5">
89
+ <a href="#test-section">Link</a>
90
+ </mds-scroll-spy>
91
+ `;
92
+ const scrollSpy = container.querySelector('mds-scroll-spy');
93
+
94
+ // Element should initialize without errors
95
+ expect(scrollSpy).toBeInstanceOf(MdsScrollSpy);
96
+
97
+ section.remove();
98
+ });
99
+
100
+ it('accepts JSON array threshold value', () => {
101
+ const section = document.createElement('section');
102
+ section.id = 'test-section';
103
+ document.body.appendChild(section);
104
+
105
+ container.innerHTML = `
106
+ <mds-scroll-spy observer-threshold="[0, 0.5, 1]">
107
+ <a href="#test-section">Link</a>
108
+ </mds-scroll-spy>
109
+ `;
110
+ const scrollSpy = container.querySelector('mds-scroll-spy');
111
+
112
+ expect(scrollSpy).toBeInstanceOf(MdsScrollSpy);
113
+
114
+ section.remove();
115
+ });
116
+
117
+ it('warns and uses default for invalid threshold', () => {
118
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
119
+ const section = document.createElement('section');
120
+ section.id = 'test-section';
121
+ document.body.appendChild(section);
122
+
123
+ container.innerHTML = `
124
+ <mds-scroll-spy observer-threshold="invalid">
125
+ <a href="#test-section">Link</a>
126
+ </mds-scroll-spy>
127
+ `;
128
+ const scrollSpy = container.querySelector('mds-scroll-spy');
129
+
130
+ expect(scrollSpy).toBeInstanceOf(MdsScrollSpy);
131
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid observer-threshold'));
132
+
133
+ section.remove();
134
+ });
135
+
136
+ it('warns and uses default for empty array threshold', () => {
137
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
138
+ const section = document.createElement('section');
139
+ section.id = 'test-section';
140
+ document.body.appendChild(section);
141
+
142
+ container.innerHTML = `
143
+ <mds-scroll-spy observer-threshold="[]">
144
+ <a href="#test-section">Link</a>
145
+ </mds-scroll-spy>
146
+ `;
147
+ const scrollSpy = container.querySelector('mds-scroll-spy');
148
+
149
+ expect(scrollSpy).toBeInstanceOf(MdsScrollSpy);
150
+ expect(warnSpy).toHaveBeenCalled();
151
+
152
+ section.remove();
153
+ });
154
+ });
155
+
156
+ // ------------------------------
157
+ // Section discovery (via links)
158
+ // ------------------------------
159
+ describe('section discovery', () => {
160
+ it('observes sections referenced by links', () => {
161
+ const section1 = document.createElement('section');
162
+ section1.id = 'section-1';
163
+ const section2 = document.createElement('section');
164
+ section2.id = 'section-2';
165
+ document.body.appendChild(section1);
166
+ document.body.appendChild(section2);
167
+
168
+ container.innerHTML = `
169
+ <mds-scroll-spy>
170
+ <a href="#section-1">Section 1</a>
171
+ <a href="#section-2">Section 2</a>
172
+ </mds-scroll-spy>
173
+ `;
174
+ const scrollSpy = container.querySelector('mds-scroll-spy');
175
+
176
+ // Verify observer was created and is observing sections
177
+ expect(scrollSpy).toBeInstanceOf(MdsScrollSpy);
178
+
179
+ section1.remove();
180
+ section2.remove();
181
+ });
182
+
183
+ it('warns when section element does not exist', () => {
184
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
185
+
186
+ createScrollSpyWithLinks([{ href: '#nonexistent', text: 'Link' }]);
187
+
188
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("doesn't exist on this page"));
189
+ });
190
+
191
+ it('ignores external links', () => {
192
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
193
+
194
+ container.innerHTML = `
195
+ <mds-scroll-spy>
196
+ <a href="https://external.com/page#section">External</a>
197
+ </mds-scroll-spy>
198
+ `;
199
+
200
+ // No warning should be logged for external links
201
+ expect(warnSpy).not.toHaveBeenCalled();
202
+ });
203
+
204
+ it('ignores links without hash', () => {
205
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
206
+
207
+ container.innerHTML = `
208
+ <mds-scroll-spy>
209
+ <a href="/page">No hash</a>
210
+ </mds-scroll-spy>
211
+ `;
212
+
213
+ // No warning should be logged for links without hash
214
+ expect(warnSpy).not.toHaveBeenCalled();
215
+ });
216
+
217
+ it('handles IDs with special characters', () => {
218
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
219
+ const section = document.createElement('section');
220
+ section.id = 'section:with.special-chars';
221
+ document.body.appendChild(section);
222
+
223
+ createScrollSpyWithLinks([{ href: '#section:with.special-chars', text: 'Special' }]);
224
+
225
+ // No warning means section was found
226
+ expect(warnSpy).not.toHaveBeenCalled();
227
+
228
+ section.remove();
229
+ });
230
+ });
231
+
232
+ // ------------------------------
233
+ // Observer options (via attributes)
234
+ // ------------------------------
235
+ describe('observer options', () => {
236
+ it('uses default threshold when no attribute provided', () => {
237
+ const section = document.createElement('section');
238
+ section.id = 'test-section';
239
+ document.body.appendChild(section);
240
+
241
+ container.innerHTML = `
242
+ <mds-scroll-spy>
243
+ <a href="#test-section">Link</a>
244
+ </mds-scroll-spy>
245
+ `;
246
+ const scrollSpy = container.querySelector('mds-scroll-spy');
247
+
248
+ // Check that observer was created (via mock)
249
+ expect(scrollSpy).toBeInstanceOf(MdsScrollSpy);
250
+
251
+ section.remove();
252
+ });
253
+
254
+ it('uses observer-root attribute', () => {
255
+ const rootEl = document.createElement('div');
256
+ rootEl.id = 'scroll-root';
257
+ document.body.appendChild(rootEl);
258
+
259
+ const section = document.createElement('section');
260
+ section.id = 'test-section';
261
+ rootEl.appendChild(section);
262
+
263
+ container.innerHTML = `
264
+ <mds-scroll-spy observer-root="#scroll-root">
265
+ <a href="#test-section">Link</a>
266
+ </mds-scroll-spy>
267
+ `;
268
+ const scrollSpy = container.querySelector('mds-scroll-spy');
269
+
270
+ expect(scrollSpy).toBeInstanceOf(MdsScrollSpy);
271
+
272
+ rootEl.remove();
273
+ });
274
+
275
+ it('uses observer-threshold attribute', () => {
276
+ const section = document.createElement('section');
277
+ section.id = 'test-section';
278
+ document.body.appendChild(section);
279
+
280
+ container.innerHTML = `
281
+ <mds-scroll-spy observer-threshold="0.75">
282
+ <a href="#test-section">Link</a>
283
+ </mds-scroll-spy>
284
+ `;
285
+ const scrollSpy = container.querySelector('mds-scroll-spy');
286
+
287
+ expect(scrollSpy).toBeInstanceOf(MdsScrollSpy);
288
+
289
+ section.remove();
290
+ });
291
+ });
292
+
293
+ // ------------------------------
294
+ // connectedCallback
295
+ // ------------------------------
296
+ describe('connectedCallback', () => {
297
+ it('finds links within the element', () => {
298
+ container.innerHTML = `
299
+ <mds-scroll-spy>
300
+ <a href="#a">A</a>
301
+ <a href="#b">B</a>
302
+ </mds-scroll-spy>
303
+ `;
304
+
305
+ const scrollSpy = container.querySelector('mds-scroll-spy');
306
+ const links = scrollSpy.querySelectorAll('a');
307
+ expect(links).toHaveLength(2);
308
+ });
309
+
310
+ it('uses aria-current-value attribute when setting active state', () => {
311
+ const section = document.createElement('section');
312
+ section.id = 'test';
313
+ document.body.appendChild(section);
314
+
315
+ container.innerHTML = `
316
+ <mds-scroll-spy aria-current-value="location">
317
+ <a href="#test">Test</a>
318
+ </mds-scroll-spy>
319
+ `;
320
+
321
+ const scrollSpy = container.querySelector('mds-scroll-spy');
322
+ expect(scrollSpy.getAttribute('aria-current-value')).toBe('location');
323
+
324
+ section.remove();
325
+ });
326
+
327
+ it('does not create observer when no sections found', () => {
328
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
329
+
330
+ container.innerHTML = `
331
+ <mds-scroll-spy>
332
+ <a href="#nonexistent">Link</a>
333
+ </mds-scroll-spy>
334
+ `;
335
+
336
+ // Element should still be created without errors
337
+ const scrollSpy = container.querySelector('mds-scroll-spy');
338
+ expect(scrollSpy).toBeInstanceOf(MdsScrollSpy);
339
+ });
340
+
341
+ it('creates observer when sections exist', () => {
342
+ const section = document.createElement('section');
343
+ section.id = 'test-section';
344
+ document.body.appendChild(section);
345
+
346
+ const scrollSpy = createScrollSpyWithLinks([{ href: '#test-section', text: 'Link' }]);
347
+
348
+ expect(scrollSpy).toBeInstanceOf(MdsScrollSpy);
349
+
350
+ section.remove();
351
+ });
352
+ });
353
+
354
+ // ------------------------------
355
+ // disconnectedCallback
356
+ // ------------------------------
357
+ describe('disconnectedCallback', () => {
358
+ it('cleans up without errors when removed', () => {
359
+ const section = document.createElement('section');
360
+ section.id = 'cleanup-test';
361
+ document.body.appendChild(section);
362
+
363
+ const scrollSpy = createScrollSpyWithLinks([{ href: '#cleanup-test', text: 'Link' }]);
364
+
365
+ // Should not throw when removing
366
+ expect(() => scrollSpy.remove()).not.toThrow();
367
+
368
+ section.remove();
369
+ });
370
+
371
+ it('handles disconnect when no observer was created', () => {
372
+ container.innerHTML = `<mds-scroll-spy></mds-scroll-spy>`;
373
+ const scrollSpy = container.querySelector('mds-scroll-spy');
374
+
375
+ // Should not throw
376
+ expect(() => scrollSpy.remove()).not.toThrow();
377
+ });
378
+ });
379
+
380
+ // ------------------------------
381
+ // setObserver / aria-current behavior
382
+ // ------------------------------
383
+ describe('setObserver', () => {
384
+ it('sets up intersection observer for sections', () => {
385
+ const section = document.createElement('section');
386
+ section.id = 'observer-test';
387
+ document.body.appendChild(section);
388
+
389
+ container.innerHTML = `
390
+ <mds-scroll-spy observer-threshold="0.5">
391
+ <a href="#observer-test">Link</a>
392
+ </mds-scroll-spy>
393
+ `;
394
+ const scrollSpy = container.querySelector('mds-scroll-spy');
395
+
396
+ expect(scrollSpy).toBeInstanceOf(MdsScrollSpy);
397
+
398
+ section.remove();
399
+ });
400
+
401
+ it('sets aria-current on most visible section link', () => {
402
+ const section1 = document.createElement('section');
403
+ section1.id = 'section-1';
404
+ const section2 = document.createElement('section');
405
+ section2.id = 'section-2';
406
+ document.body.appendChild(section1);
407
+ document.body.appendChild(section2);
408
+
409
+ container.innerHTML = `
410
+ <mds-scroll-spy>
411
+ <a href="#section-1">Section 1</a>
412
+ <a href="#section-2">Section 2</a>
413
+ </mds-scroll-spy>
414
+ `;
415
+ const scrollSpy = container.querySelector('mds-scroll-spy');
416
+ const link1 = scrollSpy.querySelector('a[href="#section-1"]');
417
+ const link2 = scrollSpy.querySelector('a[href="#section-2"]');
418
+
419
+ // Verify initial state (no aria-current)
420
+ expect(link1.getAttribute('aria-current')).toBeNull();
421
+ expect(link2.getAttribute('aria-current')).toBeNull();
422
+
423
+ // Simulate intersection: section-1 is more visible
424
+ lastObserverInstance.triggerIntersect([
425
+ { target: section1, intersectionRatio: 0.8 },
426
+ { target: section2, intersectionRatio: 0.2 },
427
+ ]);
428
+
429
+ expect(link1.getAttribute('aria-current')).toBe('step');
430
+ expect(link2.getAttribute('aria-current')).toBeNull();
431
+
432
+ // Simulate scrolling: section-2 becomes more visible
433
+ lastObserverInstance.triggerIntersect([
434
+ { target: section1, intersectionRatio: 0.1 },
435
+ { target: section2, intersectionRatio: 0.9 },
436
+ ]);
437
+
438
+ expect(link1.getAttribute('aria-current')).toBeNull();
439
+ expect(link2.getAttribute('aria-current')).toBe('step');
440
+
441
+ section1.remove();
442
+ section2.remove();
443
+ });
444
+
445
+ it('uses custom aria-current-value attribute', () => {
446
+ const section = document.createElement('section');
447
+ section.id = 'custom-aria';
448
+ document.body.appendChild(section);
449
+
450
+ container.innerHTML = `
451
+ <mds-scroll-spy aria-current-value="page">
452
+ <a href="#custom-aria">Link</a>
453
+ </mds-scroll-spy>
454
+ `;
455
+ const scrollSpy = container.querySelector('mds-scroll-spy');
456
+
457
+ expect(scrollSpy.getAttribute('aria-current-value')).toBe('page');
458
+
459
+ section.remove();
460
+ });
461
+ });
462
+ });
package/src/js/index.js CHANGED
@@ -15,6 +15,7 @@ import { MdsCardLink } from '../components/card/card-link';
15
15
  import { MdsConditionalSection } from '../components/conditional-section/conditional-section';
16
16
  import { MdsImageCropper } from '../components/image-cropper/image-cropper';
17
17
  import { MdsCategoryPicker } from '../components/inputs/category-picker/category-picker';
18
+ import { MdsScrollSpy } from '../components/scroll-spy/scroll-spy';
18
19
 
19
20
  if (!window.customElements.get('mds-dropdown-nav')) {
20
21
  window.customElements.define('mds-dropdown-nav', MdsDropdownNav);
@@ -37,6 +38,9 @@ if (!window.customElements.get('mds-file-upload')) {
37
38
  if (!window.customElements.get('mds-category-picker')) {
38
39
  window.customElements.define('mds-category-picker', MdsCategoryPicker);
39
40
  }
41
+ if (!window.customElements.get('mds-scroll-spy')) {
42
+ window.customElements.define('mds-scroll-spy', MdsScrollSpy);
43
+ }
40
44
 
41
45
  const initAll = () => {
42
46
  tabs.init();
@@ -126,5 +126,26 @@
126
126
  color: $constant-color-neutral-light;
127
127
  }
128
128
  }
129
+
130
+ &.mds-step-list__item--has-subnav:before {
131
+ margin-bottom: 0;
132
+ }
129
133
  }
130
134
  }
135
+
136
+ .mds-step-list-subnav {
137
+ list-style-type: none;
138
+ border-left: 1px solid $constant-color-neutral-light;
139
+ margin-left: 1.25em;
140
+ }
141
+ .mds-step-list-subnav__link {
142
+ display: block;
143
+ padding: $constant-size-baseline * 5;
144
+ font-weight: normal;
145
+ border-left: 4px solid transparent;
146
+
147
+ &[aria-current] {
148
+ color: var(--mds-color-text-base);
149
+ border-left-color: $link-color;
150
+ }
151
+ }
@@ -12,3 +12,4 @@
12
12
  @import 'text-formatting';
13
13
  @import 'table';
14
14
  @import 'container-queries';
15
+ @import 'position';
@@ -0,0 +1,4 @@
1
+ .mds-position-sticky {
2
+ position: sticky;
3
+ top: $constant-size-baseline * 5;
4
+ }
@@ -10,14 +10,12 @@
10
10
  <a href="#three">three</a>
11
11
  </li>
12
12
  </ul>
13
-
14
13
  <h2>Ordered list</h2>
15
14
  <ol class="mds-list mds-list--number">
16
15
  <li class="mds-list__item">one</li>
17
16
  <li class="mds-list__item">two</li>
18
17
  <li class="mds-list__item">three</li>
19
18
  </ol>
20
-
21
19
  <h2>Definition List with border</h2>
22
20
  <dl class="mds-list mds-list--definition mds-list--border">
23
21
  <dt class="mds-list__key">Recruiter</dt>
@@ -27,21 +25,18 @@
27
25
  <dt class="mds-list__key">Expires</dt>
28
26
  <dd class="mds-list__value">12pm, 25th November 1983</dd>
29
27
  </dl>
30
-
31
28
  <h2>Inline list</h2>
32
29
  <ul class="mds-list mds-list--inline">
33
30
  <li class="mds-list__item">One</li>
34
31
  <li class="mds-list__item">Two</li>
35
32
  <li class="mds-list__item">Three</li>
36
33
  </ul>
37
-
38
34
  <h2>Bordered inline list (separated)</h2>
39
35
  <ul class="mds-list mds-list--inline mds-list--pipe">
40
36
  <li class="mds-list__item">Four</li>
41
37
  <li class="mds-list__item">Five</li>
42
38
  <li class="mds-list__item">Six</li>
43
39
  </ul>
44
-
45
40
  <h2>Multilevel list</h2>
46
41
  <ol class="mds-list mds-list--number mds-list--multilevel">
47
42
  <li class="mds-list__item">
@@ -54,7 +49,13 @@
54
49
  <ul class="mds-list mds-list--bullet">
55
50
  <li class="mds-list__item">first</li>
56
51
  <li class="mds-list__item">second</li>
57
- <li class="mds-list__item">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent eget metus nisi. Morbi sollicitudin, erat sed elementum suscipit, dolor nibh sagittis dui, at ultrices massa erat non ipsum. Nam euismod dapibus augue, sit amet efficitur urna malesuada vitae. Sed tincidunt felis a turpis vulputate, vitae sodales nibh imperdiet. Donec ullamcorper risus quis convallis mollis. Nullam nec magna lectus. Etiam in lobortis purus. Duis vestibulum, nibh sit amet commodo porttitor, nunc metus eleifend magna, at venenatis tortor justo at eros. Vivamus eu tincidunt mi, vel fermentum tortor. Aenean tempus, magna a scelerisque consequat, libero tortor porttitor neque, nec aliquam lorem purus sodales diam. Pellentesque a orci vitae orci fringilla sollicitudin quis vitae leo.
52
+ <li class="mds-list__item">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent eget metus nisi. Morbi
53
+ sollicitudin, erat sed elementum suscipit, dolor nibh sagittis dui, at ultrices massa erat non ipsum. Nam euismod
54
+ dapibus augue, sit amet efficitur urna malesuada vitae. Sed tincidunt felis a turpis vulputate, vitae sodales nibh
55
+ imperdiet. Donec ullamcorper risus quis convallis mollis. Nullam nec magna lectus. Etiam in lobortis purus. Duis
56
+ vestibulum, nibh sit amet commodo porttitor, nunc metus eleifend magna, at venenatis tortor justo at eros. Vivamus eu
57
+ tincidunt mi, vel fermentum tortor. Aenean tempus, magna a scelerisque consequat, libero tortor porttitor neque, nec
58
+ aliquam lorem purus sodales diam. Pellentesque a orci vitae orci fringilla sollicitudin quis vitae leo.
58
59
  <ul class="mds-list mds-list--bullet">
59
60
  <li class="mds-list__item">once</li>
60
61
  <li class="mds-list__item">twice</li>
@@ -69,7 +70,6 @@
69
70
  <a href="#two">five</a>
70
71
  </li>
71
72
  </ol>
72
-
73
73
  <h2>List with no indent</h2>
74
74
  <ul class="mds-list mds-list--bullet mds-list--noindent">
75
75
  <li class="mds-list__item">
@@ -82,17 +82,30 @@
82
82
  <a href="#three">three</a>
83
83
  </li>
84
84
  </ul>
85
-
86
85
  <h2>Step List</h2>
87
86
  <ol class="mds-step-list">
88
87
  <li class="mds-step-list__item">Step 1</li>
89
88
  <li class="mds-step-list__item">Step 2</li>
90
89
  <li class="mds-step-list__item">Step 3</li>
91
90
  </ol>
92
-
93
91
  <h2>Step List with page progress</h2>
94
92
  <ol class="mds-step-list">
95
- <li class="mds-step-list__item"><a href="https://www.google.com">Past page with link</a></li>
96
- <li class="mds-step-list__item mds-step-list__item--current" aria-current="page">Current page</li>
93
+ <li class="mds-step-list__item">
94
+ <a href="https://www.google.com">Past page with link</a>
95
+ </li>
96
+ <li class="mds-step-list__item mds-step-list__item--current mds-step-list__item--has-subnav" aria-current="page">Current
97
+ page
98
+ <ul class="mds-step-list-subnav">
99
+ <li class="mds-step-list-subnav__item">
100
+ <a class="mds-step-list-subnav__link" href="#section-1">Section 1</a>
101
+ </li>
102
+ <li class="mds-step-list-subnav__item">
103
+ <a class="mds-step-list-subnav__link" href="#section-2" aria-current="location">Section 2</a>
104
+ </li>
105
+ <li class="mds-step-list-subnav__item">
106
+ <a class="mds-step-list-subnav__link" href="#section-3">Section 3</a>
107
+ </li>
108
+ </ul>
109
+ </li>
97
110
  <li class="mds-step-list__item mds-step-list__item--future">Ghost of pages future</li>
98
111
  </ol>