@madgex/design-system 13.6.3 → 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.
- package/dist/assets/icons-inline.svg +1 -1
- package/dist/assets/icons.json +1 -1
- package/dist/assets/icons.svg +1 -1
- package/dist/css/index.css +1 -1
- package/dist/js/index.js +1 -1
- package/package.json +1 -1
- package/src/components/inputs/text-editor/README.md +14 -9
- package/src/components/inputs/text-editor/_template.njk +1 -0
- package/src/components/inputs/text-editor/text-editor.config.js +10 -0
- package/src/components/inputs/text-editor/text-editor.njk +2 -1
- package/src/components/scroll-spy/README.md +34 -0
- package/src/components/scroll-spy/scroll-spy.js +127 -0
- package/src/components/scroll-spy/scroll-spy.njk +184 -0
- package/src/components/scroll-spy/scroll-spy.spec.js +462 -0
- package/src/icons/arrow-left.svg +11 -0
- package/src/icons/arrow-right.svg +11 -0
- package/src/js/index.js +4 -0
- package/src/scss/core/_lists.scss +21 -0
- package/src/scss/helpers/__index.scss +1 -0
- package/src/scss/helpers/_position.scss +4 -0
- package/src/typography/lists.njk +24 -11
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<div class="mds-grid-row">
|
|
2
|
+
<div class="mds-grid-col-12 mds-grid-col-md-4 mds-grid-col-lg-3 mds-display-none mds-display-md-block">
|
|
3
|
+
<nav aria-label="Page sections" class="mds-position-sticky">
|
|
4
|
+
<ol class="mds-step-list">
|
|
5
|
+
<li class="mds-step-list__item">
|
|
6
|
+
<a href="/">Previous page</a>
|
|
7
|
+
</li>
|
|
8
|
+
<li class="mds-step-list__item mds-step-list__item--current mds-step-list__item--has-subnav" aria-current="page">Current
|
|
9
|
+
page
|
|
10
|
+
<mds-scroll-spy>
|
|
11
|
+
<ul class="mds-step-list-subnav">
|
|
12
|
+
<li class="mds-step-list-subnav__item">
|
|
13
|
+
<a class="mds-step-list-subnav__link" href="#section-1">Section 1</a>
|
|
14
|
+
</li>
|
|
15
|
+
<li class="mds-step-list-subnav__item">
|
|
16
|
+
<a class="mds-step-list-subnav__link" href="#section-2">Section 2</a>
|
|
17
|
+
</li>
|
|
18
|
+
<li class="mds-step-list-subnav__item">
|
|
19
|
+
<a class="mds-step-list-subnav__link" href="#section-3">Section 3</a>
|
|
20
|
+
</li>
|
|
21
|
+
</ul>
|
|
22
|
+
</mds-scroll-spy>
|
|
23
|
+
</li>
|
|
24
|
+
<li class="mds-step-list__item mds-step-list__item--future">
|
|
25
|
+
<a href="/">Next page</a>
|
|
26
|
+
</li>
|
|
27
|
+
</ol>
|
|
28
|
+
</nav>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="mds-grid-col-12 mds-grid-col-md-8 mds-grid-col-lg-9">
|
|
31
|
+
<section id="section-1">
|
|
32
|
+
<h2>Section 1</h2>
|
|
33
|
+
<p>
|
|
34
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris elit sem, mollis ut dui ac, ultricies posuere dolor.
|
|
35
|
+
Pellentesque tincidunt interdum justo et tempor. Mauris varius ac dolor accumsan pellentesque. Suspendisse varius leo
|
|
36
|
+
quis lorem aliquet, ut eleifend turpis malesuada. Duis maximus cursus sem vel iaculis. Nullam bibendum massa et orci
|
|
37
|
+
scelerisque vulputate. Aenean dignissim tempus urna. Fusce vel faucibus lectus, commodo fringilla sem.
|
|
38
|
+
</p>
|
|
39
|
+
<p>
|
|
40
|
+
Nam venenatis a dui nec efficitur. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia
|
|
41
|
+
curae; Integer vel tincidunt risus, et eleifend nisl. Nullam imperdiet ex sit amet sapien iaculis, sit amet consectetur
|
|
42
|
+
justo sollicitudin. Sed vitae tristique arcu, a commodo urna. Curabitur in vulputate magna. Duis iaculis placerat
|
|
43
|
+
consequat.
|
|
44
|
+
</p>
|
|
45
|
+
<p>
|
|
46
|
+
Vestibulum neque erat, pellentesque ut lectus et, tincidunt tincidunt magna. Nam tristique, urna eu dignissim ultricies,
|
|
47
|
+
dui turpis rutrum odio, et fringilla sapien quam vitae nulla. Nulla facilisi. Maecenas gravida, quam in elementum
|
|
48
|
+
sodales, justo magna rutrum mi, et ultricies arcu diam et dolor. Sed sit amet odio vitae urna eleifend sodales et congue
|
|
49
|
+
libero. Etiam sit amet dolor tincidunt, porttitor mi et, ullamcorper tortor. Aenean tellus odio, posuere in condimentum
|
|
50
|
+
vitae, eleifend vel ligula. Donec luctus dolor eu massa luctus pellentesque.
|
|
51
|
+
</p>
|
|
52
|
+
<p>
|
|
53
|
+
Aenean dictum urna at lorem accumsan aliquam. Donec molestie, ante ac consectetur pharetra, nulla dolor tristique odio,
|
|
54
|
+
sit amet dictum elit libero eu velit. Etiam non accumsan lacus, sit amet condimentum eros. Aliquam erat volutpat. Etiam
|
|
55
|
+
rutrum porttitor tortor, ac convallis libero maximus interdum. Nullam ut tellus pulvinar, viverra ex vitae, sodales dui.
|
|
56
|
+
Nulla molestie, lacus eget condimentum vulputate, odio metus feugiat ex, ac consectetur nibh odio in erat. Nunc eget
|
|
57
|
+
justo ac ante euismod tempor. Integer nisi urna, bibendum vitae quam sit amet, laoreet vulputate nisi. Suspendisse
|
|
58
|
+
aliquam ex sit amet turpis iaculis, quis venenatis nibh dignissim.
|
|
59
|
+
</p>
|
|
60
|
+
<p>
|
|
61
|
+
Aliquam semper metus dolor, ac luctus dui posuere id. In facilisis tincidunt rutrum. Nam fermentum purus ante, vel
|
|
62
|
+
elementum lectus vehicula vel. Mauris eu dignissim lectus, quis ultrices turpis. Pellentesque eget accumsan sem, sit
|
|
63
|
+
amet porta tellus. Curabitur sit amet neque et mi porttitor ultricies. Duis eu ipsum finibus, fringilla diam eget,
|
|
64
|
+
fermentum nisl. Ut augue tortor, cursus a congue vel, maximus facilisis massa. Ut gravida, velit eu feugiat faucibus,
|
|
65
|
+
eros magna tincidunt ipsum, sed pellentesque elit diam a ligula. Cras vel viverra massa, laoreet auctor justo. Quisque
|
|
66
|
+
gravida bibendum lorem, at laoreet nisi fermentum a.
|
|
67
|
+
</p>
|
|
68
|
+
</section>
|
|
69
|
+
<section id="section-2">
|
|
70
|
+
<h2>Section 2</h2>
|
|
71
|
+
<p>
|
|
72
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris elit sem, mollis ut dui ac, ultricies posuere dolor.
|
|
73
|
+
Pellentesque tincidunt interdum justo et tempor. Mauris varius ac dolor accumsan pellentesque. Suspendisse varius leo
|
|
74
|
+
quis lorem aliquet, ut eleifend turpis malesuada. Duis maximus cursus sem vel iaculis. Nullam bibendum massa et orci
|
|
75
|
+
scelerisque vulputate. Aenean dignissim tempus urna. Fusce vel faucibus lectus, commodo fringilla sem.
|
|
76
|
+
</p>
|
|
77
|
+
<p>
|
|
78
|
+
Nam venenatis a dui nec efficitur. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia
|
|
79
|
+
curae; Integer vel tincidunt risus, et eleifend nisl. Nullam imperdiet ex sit amet sapien iaculis, sit amet consectetur
|
|
80
|
+
justo sollicitudin. Sed vitae tristique arcu, a commodo urna. Curabitur in vulputate magna. Duis iaculis placerat
|
|
81
|
+
consequat.
|
|
82
|
+
</p>
|
|
83
|
+
<p>
|
|
84
|
+
Vestibulum neque erat, pellentesque ut lectus et, tincidunt tincidunt magna. Nam tristique, urna eu dignissim ultricies,
|
|
85
|
+
dui turpis rutrum odio, et fringilla sapien quam vitae nulla. Nulla facilisi. Maecenas gravida, quam in elementum
|
|
86
|
+
sodales, justo magna rutrum mi, et ultricies arcu diam et dolor. Sed sit amet odio vitae urna eleifend sodales et congue
|
|
87
|
+
libero. Etiam sit amet dolor tincidunt, porttitor mi et, ullamcorper tortor. Aenean tellus odio, posuere in condimentum
|
|
88
|
+
vitae, eleifend vel ligula. Donec luctus dolor eu massa luctus pellentesque.
|
|
89
|
+
</p>
|
|
90
|
+
<p>
|
|
91
|
+
Aenean dictum urna at lorem accumsan aliquam. Donec molestie, ante ac consectetur pharetra, nulla dolor tristique odio,
|
|
92
|
+
sit amet dictum elit libero eu velit. Etiam non accumsan lacus, sit amet condimentum eros. Aliquam erat volutpat. Etiam
|
|
93
|
+
rutrum porttitor tortor, ac convallis libero maximus interdum. Nullam ut tellus pulvinar, viverra ex vitae, sodales dui.
|
|
94
|
+
Nulla molestie, lacus eget condimentum vulputate, odio metus feugiat ex, ac consectetur nibh odio in erat. Nunc eget
|
|
95
|
+
justo ac ante euismod tempor. Integer nisi urna, bibendum vitae quam sit amet, laoreet vulputate nisi. Suspendisse
|
|
96
|
+
aliquam ex sit amet turpis iaculis, quis venenatis nibh dignissim.
|
|
97
|
+
</p>
|
|
98
|
+
<p>
|
|
99
|
+
Aliquam semper metus dolor, ac luctus dui posuere id. In facilisis tincidunt rutrum. Nam fermentum purus ante, vel
|
|
100
|
+
elementum lectus vehicula vel. Mauris eu dignissim lectus, quis ultrices turpis. Pellentesque eget accumsan sem, sit
|
|
101
|
+
amet porta tellus. Curabitur sit amet neque et mi porttitor ultricies. Duis eu ipsum finibus, fringilla diam eget,
|
|
102
|
+
fermentum nisl. Ut augue tortor, cursus a congue vel, maximus facilisis massa. Ut gravida, velit eu feugiat faucibus,
|
|
103
|
+
eros magna tincidunt ipsum, sed pellentesque elit diam a ligula. Cras vel viverra massa, laoreet auctor justo. Quisque
|
|
104
|
+
gravida bibendum lorem, at laoreet nisi fermentum a.
|
|
105
|
+
</p>
|
|
106
|
+
</section>
|
|
107
|
+
<section id="section-3">
|
|
108
|
+
<h2>Section 3</h2>
|
|
109
|
+
<p>
|
|
110
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris elit sem, mollis ut dui ac, ultricies posuere dolor.
|
|
111
|
+
Pellentesque tincidunt interdum justo et tempor. Mauris varius ac dolor accumsan pellentesque. Suspendisse varius leo
|
|
112
|
+
quis lorem aliquet, ut eleifend turpis malesuada. Duis maximus cursus sem vel iaculis. Nullam bibendum massa et orci
|
|
113
|
+
scelerisque vulputate. Aenean dignissim tempus urna. Fusce vel faucibus lectus, commodo fringilla sem.
|
|
114
|
+
</p>
|
|
115
|
+
<p>
|
|
116
|
+
Nam venenatis a dui nec efficitur. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia
|
|
117
|
+
curae; Integer vel tincidunt risus, et eleifend nisl. Nullam imperdiet ex sit amet sapien iaculis, sit amet consectetur
|
|
118
|
+
justo sollicitudin. Sed vitae tristique arcu, a commodo urna. Curabitur in vulputate magna. Duis iaculis placerat
|
|
119
|
+
consequat.
|
|
120
|
+
</p>
|
|
121
|
+
<p>
|
|
122
|
+
Vestibulum neque erat, pellentesque ut lectus et, tincidunt tincidunt magna. Nam tristique, urna eu dignissim ultricies,
|
|
123
|
+
dui turpis rutrum odio, et fringilla sapien quam vitae nulla. Nulla facilisi. Maecenas gravida, quam in elementum
|
|
124
|
+
sodales, justo magna rutrum mi, et ultricies arcu diam et dolor. Sed sit amet odio vitae urna eleifend sodales et congue
|
|
125
|
+
libero. Etiam sit amet dolor tincidunt, porttitor mi et, ullamcorper tortor. Aenean tellus odio, posuere in condimentum
|
|
126
|
+
vitae, eleifend vel ligula. Donec luctus dolor eu massa luctus pellentesque.
|
|
127
|
+
</p>
|
|
128
|
+
<p>
|
|
129
|
+
Aenean dictum urna at lorem accumsan aliquam. Donec molestie, ante ac consectetur pharetra, nulla dolor tristique odio,
|
|
130
|
+
sit amet dictum elit libero eu velit. Etiam non accumsan lacus, sit amet condimentum eros. Aliquam erat volutpat. Etiam
|
|
131
|
+
rutrum porttitor tortor, ac convallis libero maximus interdum. Nullam ut tellus pulvinar, viverra ex vitae, sodales dui.
|
|
132
|
+
Nulla molestie, lacus eget condimentum vulputate, odio metus feugiat ex, ac consectetur nibh odio in erat. Nunc eget
|
|
133
|
+
justo ac ante euismod tempor. Integer nisi urna, bibendum vitae quam sit amet, laoreet vulputate nisi. Suspendisse
|
|
134
|
+
aliquam ex sit amet turpis iaculis, quis venenatis nibh dignissim.
|
|
135
|
+
</p>
|
|
136
|
+
<p>
|
|
137
|
+
Aliquam semper metus dolor, ac luctus dui posuere id. In facilisis tincidunt rutrum. Nam fermentum purus ante, vel
|
|
138
|
+
elementum lectus vehicula vel. Mauris eu dignissim lectus, quis ultrices turpis. Pellentesque eget accumsan sem, sit
|
|
139
|
+
amet porta tellus. Curabitur sit amet neque et mi porttitor ultricies. Duis eu ipsum finibus, fringilla diam eget,
|
|
140
|
+
fermentum nisl. Ut augue tortor, cursus a congue vel, maximus facilisis massa. Ut gravida, velit eu feugiat faucibus,
|
|
141
|
+
eros magna tincidunt ipsum, sed pellentesque elit diam a ligula. Cras vel viverra massa, laoreet auctor justo. Quisque
|
|
142
|
+
gravida bibendum lorem, at laoreet nisi fermentum a.
|
|
143
|
+
</p>
|
|
144
|
+
</section>
|
|
145
|
+
<section id="section-no-scrollspy">
|
|
146
|
+
<h2>Section outside of Scroll Spy</h2>
|
|
147
|
+
<p>
|
|
148
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris elit sem, mollis ut dui ac, ultricies posuere dolor.
|
|
149
|
+
Pellentesque tincidunt interdum justo et tempor. Mauris varius ac dolor accumsan pellentesque. Suspendisse varius leo
|
|
150
|
+
quis lorem aliquet, ut eleifend turpis malesuada. Duis maximus cursus sem vel iaculis. Nullam bibendum massa et orci
|
|
151
|
+
scelerisque vulputate. Aenean dignissim tempus urna. Fusce vel faucibus lectus, commodo fringilla sem.
|
|
152
|
+
</p>
|
|
153
|
+
<p>
|
|
154
|
+
Nam venenatis a dui nec efficitur. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia
|
|
155
|
+
curae; Integer vel tincidunt risus, et eleifend nisl. Nullam imperdiet ex sit amet sapien iaculis, sit amet consectetur
|
|
156
|
+
justo sollicitudin. Sed vitae tristique arcu, a commodo urna. Curabitur in vulputate magna. Duis iaculis placerat
|
|
157
|
+
consequat.
|
|
158
|
+
</p>
|
|
159
|
+
<p>
|
|
160
|
+
Vestibulum neque erat, pellentesque ut lectus et, tincidunt tincidunt magna. Nam tristique, urna eu dignissim ultricies,
|
|
161
|
+
dui turpis rutrum odio, et fringilla sapien quam vitae nulla. Nulla facilisi. Maecenas gravida, quam in elementum
|
|
162
|
+
sodales, justo magna rutrum mi, et ultricies arcu diam et dolor. Sed sit amet odio vitae urna eleifend sodales et congue
|
|
163
|
+
libero. Etiam sit amet dolor tincidunt, porttitor mi et, ullamcorper tortor. Aenean tellus odio, posuere in condimentum
|
|
164
|
+
vitae, eleifend vel ligula. Donec luctus dolor eu massa luctus pellentesque.
|
|
165
|
+
</p>
|
|
166
|
+
<p>
|
|
167
|
+
Aenean dictum urna at lorem accumsan aliquam. Donec molestie, ante ac consectetur pharetra, nulla dolor tristique odio,
|
|
168
|
+
sit amet dictum elit libero eu velit. Etiam non accumsan lacus, sit amet condimentum eros. Aliquam erat volutpat. Etiam
|
|
169
|
+
rutrum porttitor tortor, ac convallis libero maximus interdum. Nullam ut tellus pulvinar, viverra ex vitae, sodales dui.
|
|
170
|
+
Nulla molestie, lacus eget condimentum vulputate, odio metus feugiat ex, ac consectetur nibh odio in erat. Nunc eget
|
|
171
|
+
justo ac ante euismod tempor. Integer nisi urna, bibendum vitae quam sit amet, laoreet vulputate nisi. Suspendisse
|
|
172
|
+
aliquam ex sit amet turpis iaculis, quis venenatis nibh dignissim.
|
|
173
|
+
</p>
|
|
174
|
+
<p>
|
|
175
|
+
Aliquam semper metus dolor, ac luctus dui posuere id. In facilisis tincidunt rutrum. Nam fermentum purus ante, vel
|
|
176
|
+
elementum lectus vehicula vel. Mauris eu dignissim lectus, quis ultrices turpis. Pellentesque eget accumsan sem, sit
|
|
177
|
+
amet porta tellus. Curabitur sit amet neque et mi porttitor ultricies. Duis eu ipsum finibus, fringilla diam eget,
|
|
178
|
+
fermentum nisl. Ut augue tortor, cursus a congue vel, maximus facilisis massa. Ut gravida, velit eu feugiat faucibus,
|
|
179
|
+
eros magna tincidunt ipsum, sed pellentesque elit diam a ligula. Cras vel viverra massa, laoreet auctor justo. Quisque
|
|
180
|
+
gravida bibendum lorem, at laoreet nisi fermentum a.
|
|
181
|
+
</p>
|
|
182
|
+
</section>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
3
|
+
<g clip-path="url(#clip0_7495_1339)">
|
|
4
|
+
<path d="M13.144 6.14002H3.16257C3.13304 6.13973 3.10429 6.13053 3.08009 6.11361C3.05589 6.09669 3.03737 6.07285 3.02696 6.04522C3.01655 6.01759 3.01474 5.98746 3.02176 5.95878C3.02879 5.9301 3.04432 5.90421 3.06632 5.88452L8.10457 1.45118C8.26296 1.30184 8.35719 1.09685 8.3674 0.879393C8.37761 0.66194 8.303 0.449016 8.15929 0.285496C8.01559 0.121975 7.81402 0.0206296 7.59705 0.00281822C7.38009 -0.0149932 7.16469 0.0521206 6.99624 0.190015L0.473987 5.92943C0.325018 6.06081 0.205709 6.22238 0.123987 6.40342C0.0422649 6.58445 0 6.78081 0 6.97943C0 7.17806 0.0422649 7.37441 0.123987 7.55544C0.205709 7.73648 0.325018 7.89805 0.473987 8.02943L6.99507 13.7677C7.16365 13.9038 7.37834 13.9696 7.59426 13.9512C7.81018 13.9328 8.01063 13.8316 8.15372 13.6689C8.29681 13.5061 8.37147 13.2944 8.36209 13.0779C8.35272 12.8614 8.26003 12.6569 8.1034 12.5071L3.06515 8.07377C3.04315 8.05407 3.02762 8.02818 3.0206 7.9995C3.01357 7.97082 3.01539 7.94069 3.0258 7.91306C3.0362 7.88543 3.05473 7.86159 3.07893 7.84467C3.10313 7.82775 3.13188 7.81855 3.1614 7.81827H13.144C13.3629 7.8125 13.5708 7.7215 13.7236 7.56465C13.8764 7.4078 13.9619 7.19751 13.9619 6.97856C13.9619 6.75961 13.8764 6.54931 13.7236 6.39246C13.5708 6.23562 13.3629 6.14462 13.144 6.13885V6.14002Z" fill="currentColor"/>
|
|
5
|
+
</g>
|
|
6
|
+
<defs>
|
|
7
|
+
<clipPath id="clip0_7495_1339">
|
|
8
|
+
<rect width="14" height="14" fill="currentColor"/>
|
|
9
|
+
</clipPath>
|
|
10
|
+
</defs>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
3
|
+
<g clip-path="url(#clip0_7495_4915)">
|
|
4
|
+
<path d="M13.9854 6.97943C13.9849 6.78081 13.9423 6.58455 13.8605 6.40357C13.7786 6.22259 13.6594 6.061 13.5106 5.92943L6.9895 0.190015C6.82105 0.0521206 6.60565 -0.0149932 6.38868 0.00281822C6.17172 0.0206296 5.97015 0.121975 5.82644 0.285496C5.68274 0.449016 5.60813 0.66194 5.61834 0.879393C5.62854 1.09685 5.72277 1.30184 5.88117 1.45118L10.9194 5.88452C10.9415 5.90412 10.9571 5.92997 10.9642 5.95865C10.9713 5.98733 10.9695 6.01748 10.9591 6.04511C10.9486 6.07275 10.93 6.09656 10.9058 6.1134C10.8815 6.13025 10.8527 6.13932 10.8232 6.13943H0.84C0.617218 6.13943 0.403561 6.22793 0.24603 6.38546C0.0884998 6.54299 0 6.75665 0 6.97943C0 7.20221 0.0884998 7.41587 0.24603 7.5734C0.403561 7.73093 0.617218 7.81943 0.84 7.81943H10.8214C10.8509 7.81972 10.8797 7.82892 10.9039 7.84584C10.9281 7.86276 10.9466 7.88659 10.957 7.91423C10.9674 7.94186 10.9692 7.97199 10.9622 8.00067C10.9552 8.02935 10.9397 8.05524 10.9177 8.07493L5.87942 12.5083C5.7913 12.5794 5.71849 12.6677 5.66537 12.7677C5.61224 12.8678 5.57989 12.9775 5.57026 13.0904C5.56064 13.2032 5.57394 13.3168 5.60936 13.4244C5.64478 13.532 5.70159 13.6313 5.77638 13.7164C5.85117 13.8014 5.94239 13.8705 6.04456 13.9194C6.14672 13.9683 6.25772 13.996 6.37088 14.0009C6.48404 14.0058 6.59702 13.9878 6.70303 13.9479C6.80904 13.908 6.90589 13.8471 6.98775 13.7688L13.51 8.02943C13.659 7.89797 13.7784 7.73641 13.8603 7.55542C13.9423 7.37442 13.9849 7.17811 13.9854 6.97943V6.97943Z" fill="currentColor"/>
|
|
5
|
+
</g>
|
|
6
|
+
<defs>
|
|
7
|
+
<clipPath id="clip0_7495_4915">
|
|
8
|
+
<rect width="14" height="14" fill="currentColor"/>
|
|
9
|
+
</clipPath>
|
|
10
|
+
</defs>
|
|
11
|
+
</svg>
|