@birdapi/velinstyle 0.6.1 → 0.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/README.de.md +315 -316
- package/README.md +8 -9
- package/cli/index.js +4 -3
- package/cli/scanner.js +46 -0
- package/components/focus-manager.js +106 -80
- package/components/index.js +1 -1
- package/components/velin-accordion.js +112 -98
- package/components/velin-carousel.js +40 -5
- package/components/velin-collapse.js +95 -65
- package/components/velin-drawer.js +6 -3
- package/components/velin-dropdown.js +33 -2
- package/components/velin-modal.js +3 -1
- package/components/velin-popover.js +61 -21
- package/components/velin-tooltip-wc.js +26 -2
- package/dist/velinstyle-components.iife.js +220 -51
- package/dist/velinstyle-components.js +222 -51
- package/dist/velinstyle-components.min.js +67 -68
- package/dist/velinstyle.css +168 -41
- package/dist/velinstyle.min.css +1 -1
- package/package.json +6 -3
- package/src/a11y/focus-not-obscured.css +21 -0
- package/src/a11y/forced-colors.css +86 -38
- package/src/a11y/high-contrast-aaa.css +37 -0
- package/src/a11y/preferences.css +106 -85
- package/src/a11y/security.css +101 -104
- package/src/a11y/target-size.css +32 -0
- package/src/base/root.css +4 -4
- package/src/velinstyle.css +6 -1
package/README.md
CHANGED
|
@@ -8,15 +8,14 @@
|
|
|
8
8
|
<br>
|
|
9
9
|
|
|
10
10
|
[](LICENSE)
|
|
11
|
-
[](CHANGELOG.md)
|
|
12
12
|
[](docs/a11y.html)
|
|
13
13
|
[]()
|
|
14
14
|
[]()
|
|
15
|
-
[](https://www.npmjs.com/package/@birdapi/velinstyle)
|
|
16
|
+
[](https://skylitedesign.github.io/velinstyle/)
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
[Website](https://velinstyle.info) · [Docs](docs/index.html) · [Samples](samples/) · [Playground](tools/playground/index.html) · [Theme Builder](tools/theme-builder/index.html) · [Issues](https://github.com/SkyliteDesign/velinstyle/issues)
|
|
18
|
+
[Documentation](https://skylitedesign.github.io/velinstyle/) · [npm](https://www.npmjs.com/package/@birdapi/velinstyle) · [Samples](samples/) · [Playground](tools/playground/index.html) · [Theme Builder](tools/theme-builder/index.html) · [Issues](https://github.com/SkyliteDesign/velinstyle/issues)
|
|
20
19
|
|
|
21
20
|
**English** · **[Deutsch](README.de.md)**
|
|
22
21
|
|
|
@@ -114,16 +113,16 @@ npm install @birdapi/velinstyle
|
|
|
114
113
|
### <span>②</span> Link CSS + components (ES modules)
|
|
115
114
|
|
|
116
115
|
```html
|
|
117
|
-
<link rel="stylesheet" href="node_modules/velinstyle/dist/velinstyle.min.css">
|
|
118
|
-
<script type="module" src="node_modules/velinstyle/dist/velinstyle-components.min.js"></script>
|
|
116
|
+
<link rel="stylesheet" href="node_modules/@birdapi/velinstyle/dist/velinstyle.min.css">
|
|
117
|
+
<script type="module" src="node_modules/@birdapi/velinstyle/dist/velinstyle-components.min.js"></script>
|
|
119
118
|
```
|
|
120
119
|
|
|
121
120
|
<details>
|
|
122
121
|
<summary><strong>CDN alternative</strong> (skip npm install)</summary>
|
|
123
122
|
|
|
124
123
|
```html
|
|
125
|
-
<link rel="stylesheet" href="https://unpkg.com/velinstyle@latest/dist/velinstyle.min.css">
|
|
126
|
-
<script type="module" src="https://unpkg.com/velinstyle@latest/dist/velinstyle-components.min.js"></script>
|
|
124
|
+
<link rel="stylesheet" href="https://unpkg.com/@birdapi/velinstyle@latest/dist/velinstyle.min.css">
|
|
125
|
+
<script type="module" src="https://unpkg.com/@birdapi/velinstyle@latest/dist/velinstyle-components.min.js"></script>
|
|
127
126
|
```
|
|
128
127
|
|
|
129
128
|
</details>
|
package/cli/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const PKG_ROOT = join(__dirname, '..');
|
|
|
11
11
|
const args = process.argv.slice(2);
|
|
12
12
|
const command = args[0];
|
|
13
13
|
|
|
14
|
-
const LAYERS = ['tokens', 'reset', 'base', 'a11y', 'layout', 'components', 'utilities', 'helpers'];
|
|
14
|
+
const LAYERS = ['tokens', 'reset', 'base', 'a11y', 'layout', 'components', 'utilities', 'security', 'helpers'];
|
|
15
15
|
|
|
16
16
|
const LAYER_FILES = {
|
|
17
17
|
tokens: [
|
|
@@ -24,8 +24,9 @@ const LAYER_FILES = {
|
|
|
24
24
|
a11y: [
|
|
25
25
|
'a11y/sr-only.css', 'a11y/skip-link.css', 'a11y/reduced-motion.css',
|
|
26
26
|
'a11y/forced-colors.css', 'a11y/skeleton.css', 'a11y/preferences.css',
|
|
27
|
-
'a11y/
|
|
27
|
+
'a11y/focus-not-obscured.css', 'a11y/target-size.css', 'a11y/high-contrast-aaa.css',
|
|
28
28
|
],
|
|
29
|
+
security: ['a11y/security.css'],
|
|
29
30
|
layout: [
|
|
30
31
|
'layout/breakpoints.css', 'layout/container.css', 'layout/grid.css',
|
|
31
32
|
'layout/flex.css', 'layout/patterns.css',
|
|
@@ -78,7 +79,7 @@ function hasFlag(flag, alias) {
|
|
|
78
79
|
|
|
79
80
|
function help() {
|
|
80
81
|
console.log(`
|
|
81
|
-
${C.bold('VelinStyle CLI')} v0.
|
|
82
|
+
${C.bold('VelinStyle CLI')} v0.7.0
|
|
82
83
|
|
|
83
84
|
${C.bold('Usage:')}
|
|
84
85
|
velinstyle init Create velinstyle.config.js
|
package/cli/scanner.js
CHANGED
|
@@ -207,6 +207,52 @@ function scanA11yHTML(content, file) {
|
|
|
207
207
|
});
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
if (/<body/i.test(content) && !/<main\b/i.test(content) && !/id\s*=\s*["']main["']/i.test(content)) {
|
|
211
|
+
issues.push({
|
|
212
|
+
file, line: 1, severity: 1,
|
|
213
|
+
rule: 'a11y/landmark-main',
|
|
214
|
+
message: 'No <main> landmark or id="main" found. Add a main landmark for screen readers.',
|
|
215
|
+
fixable: false,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const headings = [...content.matchAll(/<h([1-6])\b/gi)].map((m) => parseInt(m[1], 10));
|
|
220
|
+
for (let i = 1; i < headings.length; i++) {
|
|
221
|
+
if (headings[i] - headings[i - 1] > 1) {
|
|
222
|
+
issues.push({
|
|
223
|
+
file, line: 1, severity: 1,
|
|
224
|
+
rule: 'a11y/heading-order',
|
|
225
|
+
message: `Heading level skips from h${headings[i - 1]} to h${headings[i]}. Use sequential heading levels.`,
|
|
226
|
+
fixable: false,
|
|
227
|
+
});
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
lines.forEach((line, idx) => {
|
|
233
|
+
const ln = idx + 1;
|
|
234
|
+
if (/aria-hidden\s*=\s*["']true["']/i.test(line) && /<(button|a|input|select|textarea)\b/i.test(line)) {
|
|
235
|
+
issues.push({
|
|
236
|
+
file, line: ln, severity: 0,
|
|
237
|
+
rule: 'a11y/interactive-aria-hidden',
|
|
238
|
+
message: 'Interactive element with aria-hidden="true" is not exposed to assistive tech.',
|
|
239
|
+
fixable: false,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const iframeMatches = line.matchAll(/<iframe\b[^>]*>/gi);
|
|
244
|
+
for (const m of iframeMatches) {
|
|
245
|
+
if (!/\btitle\s*=/i.test(m[0])) {
|
|
246
|
+
issues.push({
|
|
247
|
+
file, line: ln, severity: 0,
|
|
248
|
+
rule: 'a11y/iframe-title',
|
|
249
|
+
message: '<iframe> without title attribute.',
|
|
250
|
+
fixable: false,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
210
256
|
return issues;
|
|
211
257
|
}
|
|
212
258
|
|
|
@@ -1,80 +1,106 @@
|
|
|
1
|
-
const FOCUSABLE_SELECTOR = [
|
|
2
|
-
'a[href]',
|
|
3
|
-
'button:not([disabled])',
|
|
4
|
-
'input:not([disabled])',
|
|
5
|
-
'select:not([disabled])',
|
|
6
|
-
'textarea:not([disabled])',
|
|
7
|
-
'[tabindex]:not([tabindex="-1"])',
|
|
8
|
-
'summary',
|
|
9
|
-
'details',
|
|
10
|
-
].join(', ');
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
case '
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
1
|
+
const FOCUSABLE_SELECTOR = [
|
|
2
|
+
'a[href]',
|
|
3
|
+
'button:not([disabled])',
|
|
4
|
+
'input:not([disabled])',
|
|
5
|
+
'select:not([disabled])',
|
|
6
|
+
'textarea:not([disabled])',
|
|
7
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
8
|
+
'summary',
|
|
9
|
+
'details',
|
|
10
|
+
].join(', ');
|
|
11
|
+
|
|
12
|
+
function isFocusable(el) {
|
|
13
|
+
if (el.hasAttribute('disabled') || el.getAttribute('aria-hidden') === 'true') return false;
|
|
14
|
+
if (el.closest('[inert]')) return false;
|
|
15
|
+
const style = el.ownerDocument.defaultView?.getComputedStyle(el);
|
|
16
|
+
if (style && (style.visibility === 'hidden' || style.display === 'none')) return false;
|
|
17
|
+
return el.getClientRects().length > 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getFocusableElements(root) {
|
|
21
|
+
return [...root.querySelectorAll(FOCUSABLE_SELECTOR)].filter(isFocusable);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function trapFocus(root, event) {
|
|
25
|
+
if (event.key !== 'Tab') return;
|
|
26
|
+
|
|
27
|
+
const focusable = getFocusableElements(root);
|
|
28
|
+
if (focusable.length === 0) return;
|
|
29
|
+
|
|
30
|
+
const first = focusable[0];
|
|
31
|
+
const last = focusable[focusable.length - 1];
|
|
32
|
+
const active = root.getRootNode().activeElement;
|
|
33
|
+
|
|
34
|
+
if (event.shiftKey && active === first) {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
last.focus();
|
|
37
|
+
} else if (!event.shiftKey && active === last) {
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
first.focus();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function rovingTabindex(container, items, event) {
|
|
44
|
+
const currentIndex = items.indexOf(event.target);
|
|
45
|
+
if (currentIndex === -1) return;
|
|
46
|
+
|
|
47
|
+
let nextIndex;
|
|
48
|
+
|
|
49
|
+
switch (event.key) {
|
|
50
|
+
case 'ArrowDown':
|
|
51
|
+
case 'ArrowRight':
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
nextIndex = (currentIndex + 1) % items.length;
|
|
54
|
+
break;
|
|
55
|
+
case 'ArrowUp':
|
|
56
|
+
case 'ArrowLeft':
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
nextIndex = (currentIndex - 1 + items.length) % items.length;
|
|
59
|
+
break;
|
|
60
|
+
case 'Home':
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
nextIndex = 0;
|
|
63
|
+
break;
|
|
64
|
+
case 'End':
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
nextIndex = items.length - 1;
|
|
67
|
+
break;
|
|
68
|
+
default:
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
items.forEach((item, i) => {
|
|
73
|
+
item.setAttribute('tabindex', i === nextIndex ? '0' : '-1');
|
|
74
|
+
});
|
|
75
|
+
items[nextIndex].focus();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function saveFocus() {
|
|
79
|
+
return document.activeElement;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function restoreFocus(element) {
|
|
83
|
+
if (element && typeof element.focus === 'function') {
|
|
84
|
+
element.focus();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let _inertSiblings = [];
|
|
89
|
+
|
|
90
|
+
export function setBackgroundInert(except) {
|
|
91
|
+
_inertSiblings = [];
|
|
92
|
+
for (const child of document.body.children) {
|
|
93
|
+
if (child === except || child.contains(except)) continue;
|
|
94
|
+
if (!child.hasAttribute('inert')) {
|
|
95
|
+
child.setAttribute('inert', '');
|
|
96
|
+
_inertSiblings.push(child);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function clearBackgroundInert() {
|
|
102
|
+
for (const el of _inertSiblings) {
|
|
103
|
+
el.removeAttribute('inert');
|
|
104
|
+
}
|
|
105
|
+
_inertSiblings = [];
|
|
106
|
+
}
|
package/components/index.js
CHANGED
|
@@ -20,7 +20,7 @@ export { default as VelinCountdown } from './velin-countdown.js';
|
|
|
20
20
|
export { default as VelinProgressRing } from './velin-progress-ring.js';
|
|
21
21
|
export { default as VelinPersist } from './velin-persist.js';
|
|
22
22
|
export { VelinHapticObserver, vibrate, applyHaptic, PATTERNS as HapticPatterns } from './velin-haptic.js';
|
|
23
|
-
export { trapFocus, rovingTabindex, saveFocus, restoreFocus, getFocusableElements } from './focus-manager.js';
|
|
23
|
+
export { trapFocus, rovingTabindex, saveFocus, restoreFocus, getFocusableElements, setBackgroundInert, clearBackgroundInert } from './focus-manager.js';
|
|
24
24
|
|
|
25
25
|
import { VelinHapticObserver } from './velin-haptic.js';
|
|
26
26
|
if (typeof document !== 'undefined') {
|
|
@@ -1,98 +1,112 @@
|
|
|
1
|
-
const styles = `
|
|
2
|
-
:host {
|
|
3
|
-
display: block;
|
|
4
|
-
border: 1px solid var(--velin-color-border, #ddd);
|
|
5
|
-
border-radius: var(--velin-radius-md, 0.5rem);
|
|
6
|
-
overflow: hidden;
|
|
7
|
-
}
|
|
8
|
-
::slotted(details) {
|
|
9
|
-
border-bottom: 1px solid var(--velin-color-border, #ddd);
|
|
10
|
-
}
|
|
11
|
-
::slotted(details:last-child) {
|
|
12
|
-
border-bottom: none;
|
|
13
|
-
}
|
|
14
|
-
::slotted(details > summary) {
|
|
15
|
-
display: flex;
|
|
16
|
-
align-items: center;
|
|
17
|
-
justify-content: space-between;
|
|
18
|
-
padding: var(--velin-space-4, 1rem);
|
|
19
|
-
min-block-size: 2.75rem;
|
|
20
|
-
font-size: var(--velin-text-base, 1rem);
|
|
21
|
-
font-weight: var(--velin-weight-medium, 500);
|
|
22
|
-
cursor: pointer;
|
|
23
|
-
user-select: none;
|
|
24
|
-
list-style: none;
|
|
25
|
-
}
|
|
26
|
-
::slotted(details > summary::-webkit-details-marker) {
|
|
27
|
-
display: none;
|
|
28
|
-
}
|
|
29
|
-
`;
|
|
30
|
-
|
|
31
|
-
class VelinAccordion extends HTMLElement {
|
|
32
|
-
constructor() {
|
|
33
|
-
super();
|
|
34
|
-
this.attachShadow({ mode: 'open' });
|
|
35
|
-
this._onToggle = this._onToggle.bind(this);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
connectedCallback() {
|
|
39
|
-
this.shadowRoot.innerHTML = `
|
|
40
|
-
<style>${styles}</style>
|
|
41
|
-
<
|
|
42
|
-
`;
|
|
43
|
-
|
|
44
|
-
this._exclusive = this.hasAttribute('exclusive');
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
this.addEventListener('
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
1
|
+
const styles = `
|
|
2
|
+
:host {
|
|
3
|
+
display: block;
|
|
4
|
+
border: 1px solid var(--velin-color-border, #ddd);
|
|
5
|
+
border-radius: var(--velin-radius-md, 0.5rem);
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
}
|
|
8
|
+
::slotted(details) {
|
|
9
|
+
border-bottom: 1px solid var(--velin-color-border, #ddd);
|
|
10
|
+
}
|
|
11
|
+
::slotted(details:last-child) {
|
|
12
|
+
border-bottom: none;
|
|
13
|
+
}
|
|
14
|
+
::slotted(details > summary) {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: space-between;
|
|
18
|
+
padding: var(--velin-space-4, 1rem);
|
|
19
|
+
min-block-size: 2.75rem;
|
|
20
|
+
font-size: var(--velin-text-base, 1rem);
|
|
21
|
+
font-weight: var(--velin-weight-medium, 500);
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
user-select: none;
|
|
24
|
+
list-style: none;
|
|
25
|
+
}
|
|
26
|
+
::slotted(details > summary::-webkit-details-marker) {
|
|
27
|
+
display: none;
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
class VelinAccordion extends HTMLElement {
|
|
32
|
+
constructor() {
|
|
33
|
+
super();
|
|
34
|
+
this.attachShadow({ mode: 'open' });
|
|
35
|
+
this._onToggle = this._onToggle.bind(this);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
connectedCallback() {
|
|
39
|
+
this.shadowRoot.innerHTML = `
|
|
40
|
+
<style>${styles}</style>
|
|
41
|
+
<slot></slot>
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
this._exclusive = this.hasAttribute('exclusive');
|
|
45
|
+
this._wireDetails();
|
|
46
|
+
|
|
47
|
+
this.addEventListener('toggle', this._onToggle, true);
|
|
48
|
+
this.addEventListener('keydown', this._onKeydown.bind(this));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_wireDetails() {
|
|
52
|
+
let panelIndex = 0;
|
|
53
|
+
for (const details of this.querySelectorAll('details')) {
|
|
54
|
+
const summary = details.querySelector('summary');
|
|
55
|
+
const panel = details.querySelector(':scope > :not(summary)');
|
|
56
|
+
const panelId = panel?.id || `velin-accordion-panel-${++panelIndex}`;
|
|
57
|
+
if (panel && !panel.id) panel.id = panelId;
|
|
58
|
+
if (summary && panel) {
|
|
59
|
+
summary.setAttribute('aria-controls', panelId);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_onToggle(event) {
|
|
65
|
+
if (!this._exclusive) return;
|
|
66
|
+
const openedDetail = event.target;
|
|
67
|
+
if (!openedDetail.open) return;
|
|
68
|
+
|
|
69
|
+
const details = [...this.querySelectorAll('details')];
|
|
70
|
+
details.forEach((d) => {
|
|
71
|
+
if (d !== openedDetail && d.open) {
|
|
72
|
+
d.open = false;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_onKeydown(event) {
|
|
78
|
+
const summaries = [...this.querySelectorAll('summary')];
|
|
79
|
+
const currentIndex = summaries.indexOf(event.target);
|
|
80
|
+
if (currentIndex === -1) return;
|
|
81
|
+
|
|
82
|
+
let nextIndex;
|
|
83
|
+
switch (event.key) {
|
|
84
|
+
case 'ArrowDown':
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
nextIndex = (currentIndex + 1) % summaries.length;
|
|
87
|
+
break;
|
|
88
|
+
case 'ArrowUp':
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
nextIndex = (currentIndex - 1 + summaries.length) % summaries.length;
|
|
91
|
+
break;
|
|
92
|
+
case 'Home':
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
nextIndex = 0;
|
|
95
|
+
break;
|
|
96
|
+
case 'End':
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
nextIndex = summaries.length - 1;
|
|
99
|
+
break;
|
|
100
|
+
default:
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
summaries[nextIndex].focus();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
disconnectedCallback() {
|
|
107
|
+
this.removeEventListener('toggle', this._onToggle, true);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
customElements.define('velin-accordion', VelinAccordion);
|
|
112
|
+
export default VelinAccordion;
|
|
@@ -25,9 +25,15 @@ const styles = `
|
|
|
25
25
|
button:disabled { opacity: 0.3; cursor: not-allowed; }
|
|
26
26
|
svg { width: 1.25rem; height: 1.25rem; }
|
|
27
27
|
.indicators {
|
|
28
|
-
display: flex; justify-content: center; gap: var(--velin-space-2, 0.5rem);
|
|
28
|
+
display: flex; justify-content: center; align-items: center; gap: var(--velin-space-2, 0.5rem);
|
|
29
29
|
padding-block: var(--velin-space-3, 0.75rem);
|
|
30
30
|
}
|
|
31
|
+
.pause-btn {
|
|
32
|
+
font-size: var(--velin-text-xs, 0.75rem);
|
|
33
|
+
padding-inline: var(--velin-space-3, 0.75rem);
|
|
34
|
+
min-inline-size: auto;
|
|
35
|
+
border-radius: var(--velin-radius-md, 0.375rem);
|
|
36
|
+
}
|
|
31
37
|
.dot {
|
|
32
38
|
display: inline-flex; align-items: center; justify-content: center;
|
|
33
39
|
min-width: 2.75rem; min-height: 2.75rem;
|
|
@@ -54,6 +60,7 @@ class VelinCarousel extends HTMLElement {
|
|
|
54
60
|
this._index = 0;
|
|
55
61
|
this._timer = null;
|
|
56
62
|
this._startX = 0;
|
|
63
|
+
this._autoplayPaused = false;
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
connectedCallback() {
|
|
@@ -68,7 +75,7 @@ class VelinCarousel extends HTMLElement {
|
|
|
68
75
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 6 15 12 9 18"/></svg>
|
|
69
76
|
</button>
|
|
70
77
|
</div>
|
|
71
|
-
<div class="indicators" role="
|
|
78
|
+
<div class="indicators" role="group" aria-label="Slide indicators" part="indicators"></div>
|
|
72
79
|
`;
|
|
73
80
|
|
|
74
81
|
this.shadowRoot.querySelector('.prev').addEventListener('click', () => this.prev());
|
|
@@ -128,18 +135,46 @@ class VelinCarousel extends HTMLElement {
|
|
|
128
135
|
c.innerHTML = '';
|
|
129
136
|
this._slides.forEach((_, i) => {
|
|
130
137
|
const d = document.createElement('button');
|
|
138
|
+
d.type = 'button';
|
|
131
139
|
d.className = 'dot';
|
|
132
|
-
d.setAttribute('
|
|
133
|
-
d.setAttribute('aria-label', `Slide ${i + 1}`);
|
|
140
|
+
d.setAttribute('aria-label', `Go to slide ${i + 1}`);
|
|
134
141
|
d.addEventListener('click', () => this.goTo(i));
|
|
135
142
|
c.appendChild(d);
|
|
136
143
|
});
|
|
144
|
+
if (this.hasAttribute('autoplay')) {
|
|
145
|
+
const pause = document.createElement('button');
|
|
146
|
+
pause.type = 'button';
|
|
147
|
+
pause.className = 'pause-btn';
|
|
148
|
+
pause.setAttribute('aria-pressed', 'false');
|
|
149
|
+
pause.setAttribute('aria-label', 'Pause automatic slide show');
|
|
150
|
+
pause.textContent = 'Pause';
|
|
151
|
+
pause.addEventListener('click', () => this._toggleAutoplayPause(pause));
|
|
152
|
+
c.appendChild(pause);
|
|
153
|
+
}
|
|
154
|
+
this._update();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_toggleAutoplayPause(btn) {
|
|
158
|
+
this._autoplayPaused = !this._autoplayPaused;
|
|
159
|
+
if (this._autoplayPaused) {
|
|
160
|
+
this._pause();
|
|
161
|
+
btn.setAttribute('aria-pressed', 'true');
|
|
162
|
+
btn.setAttribute('aria-label', 'Resume automatic slide show');
|
|
163
|
+
btn.textContent = 'Play';
|
|
164
|
+
} else {
|
|
165
|
+
this._resume();
|
|
166
|
+
btn.setAttribute('aria-pressed', 'false');
|
|
167
|
+
btn.setAttribute('aria-label', 'Pause automatic slide show');
|
|
168
|
+
btn.textContent = 'Pause';
|
|
169
|
+
}
|
|
137
170
|
}
|
|
138
171
|
|
|
139
172
|
_emit() { this.dispatchEvent(new CustomEvent('velin-slide-change', { bubbles: true, detail: { index: this._index } })); }
|
|
140
173
|
_startAutoplay() { const ms = parseInt(this.getAttribute('interval') || '5000', 10); this._timer = setInterval(() => this.next(), ms); }
|
|
141
174
|
_pause() { if (this._timer) { clearInterval(this._timer); this._timer = null; } }
|
|
142
|
-
_resume() {
|
|
175
|
+
_resume() {
|
|
176
|
+
if (this.hasAttribute('autoplay') && !this._timer && !this._autoplayPaused) this._startAutoplay();
|
|
177
|
+
}
|
|
143
178
|
|
|
144
179
|
disconnectedCallback() { this._pause(); }
|
|
145
180
|
}
|