accessible-kit 1.0.2 → 1.0.4
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/CHANGELOG.md +47 -0
- package/package.json +1 -1
- package/src/css/a11y-modal.core.css +3 -0
- package/src/css/a11y-modal.theme.css +1 -6
- package/src/css/a11y-offcanvas.core.css +4 -0
- package/src/css/a11y-offcanvas.theme.css +8 -13
- package/src/js/a11y-modal.js +28 -6
- package/src/js/a11y-offcanvas.js +32 -6
- package/src/js/index.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,50 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.4] - 2025-12-21
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Focus Trap**: Fixed focus trap in Offcanvas and Modal components
|
|
12
|
+
- Focus trap now correctly excludes elements with `aria-hidden="true"` and their children
|
|
13
|
+
- Fixed timing issue where `updateFocusableElements()` was called before CSS visibility changes applied
|
|
14
|
+
- Focus trap now properly skips hidden elements in collapsed/nested components (e.g., collapse submenus in navigation)
|
|
15
|
+
- Added comprehensive filtering for hidden, invisible, and aria-hidden elements
|
|
16
|
+
- Removed `visibility: hidden` check from filter to prevent false positives during panel opening
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Added `:focus-visible` styles to Offcanvas theme for better keyboard navigation visibility
|
|
20
|
+
- Added navigation demo with nested collapse submenus to Offcanvas demo page
|
|
21
|
+
|
|
22
|
+
### Details
|
|
23
|
+
The focus trap improvements ensure that keyboard navigation works correctly in complex scenarios:
|
|
24
|
+
- When offcanvas/modal contains collapse components, Tab key properly skips hidden submenu items
|
|
25
|
+
- Focus is set after CSS transitions complete, preventing "no focusable elements" issue
|
|
26
|
+
- Users can now navigate nested menus with full accessibility support
|
|
27
|
+
|
|
28
|
+
## [1.0.3] - 2025-12-20
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **CSS Architecture**: Fixed inconsistent placement of animations in Offcanvas and Modal components
|
|
32
|
+
- Moved `transition` properties from `theme.css` to `core.css` for Offcanvas component
|
|
33
|
+
- Moved `transition` properties from `theme.css` to `core.css` for Modal component
|
|
34
|
+
- Animations now work out-of-the-box even when using custom themes without importing `theme.css`
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- Updated `package.json` exports to include `style` condition for better CSS import compatibility
|
|
38
|
+
- Improved CSS imports documentation for Tailwind CSS and other bundlers
|
|
39
|
+
|
|
40
|
+
### Details
|
|
41
|
+
All components now follow consistent CSS architecture:
|
|
42
|
+
- **Core CSS** (`*.core.css`): Contains layout, positioning, behavior, and **animations**
|
|
43
|
+
- **Theme CSS** (`*.theme.css`): Contains only visual styling (colors, spacing, borders, shadows)
|
|
44
|
+
|
|
45
|
+
This ensures animations work correctly even when developers create custom themes.
|
|
46
|
+
|
|
47
|
+
## [1.0.2] - 2025-12-20
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- Minor version bump for package.json exports improvements
|
|
51
|
+
|
|
8
52
|
## [1.0.1] - 2025-12-20
|
|
9
53
|
|
|
10
54
|
### Changed
|
|
@@ -65,5 +109,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
65
109
|
- Zero dependencies
|
|
66
110
|
- Full TypeScript-ready exports
|
|
67
111
|
|
|
112
|
+
[1.0.4]: https://github.com/5ulo/accessible-kit/compare/v1.0.3...v1.0.4
|
|
113
|
+
[1.0.3]: https://github.com/5ulo/accessible-kit/compare/v1.0.2...v1.0.3
|
|
114
|
+
[1.0.2]: https://github.com/5ulo/accessible-kit/compare/v1.0.1...v1.0.2
|
|
68
115
|
[1.0.1]: https://github.com/5ulo/accessible-kit/compare/v1.0.0...v1.0.1
|
|
69
116
|
[1.0.0]: https://github.com/5ulo/accessible-kit/releases/tag/v1.0.0
|
package/package.json
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
justify-content: center;
|
|
16
16
|
visibility: hidden;
|
|
17
17
|
opacity: 0;
|
|
18
|
+
transition: opacity var(--a11y-modal-duration, 0.2s) var(--a11y-modal-easing, ease),
|
|
19
|
+
visibility var(--a11y-modal-duration, 0.2s) var(--a11y-modal-easing, ease);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
[data-modal][aria-hidden="false"] {
|
|
@@ -31,6 +33,7 @@
|
|
|
31
33
|
max-width: 90vw;
|
|
32
34
|
overflow: hidden;
|
|
33
35
|
transform: scale(0.95);
|
|
36
|
+
transition: transform var(--a11y-modal-duration, 0.2s) var(--a11y-modal-easing, ease);
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
[data-modal][aria-hidden="false"] [data-modal-dialog] {
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
/* Modal wrapper */
|
|
6
6
|
[data-modal] {
|
|
7
7
|
padding: 1rem;
|
|
8
|
-
transition: opacity var(--a11y-modal-duration, 0.2s) var(--a11y-modal-easing, ease),
|
|
9
|
-
visibility var(--a11y-modal-duration, 0.2s) var(--a11y-modal-easing, ease);
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
/* Modal dialog */
|
|
@@ -16,7 +14,6 @@
|
|
|
16
14
|
border-radius: 0.5rem;
|
|
17
15
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
|
18
16
|
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
|
19
|
-
transition: transform var(--a11y-modal-duration, 0.2s) var(--a11y-modal-easing, ease);
|
|
20
17
|
}
|
|
21
18
|
|
|
22
19
|
/* Modal header */
|
|
@@ -208,10 +205,8 @@
|
|
|
208
205
|
}
|
|
209
206
|
}
|
|
210
207
|
|
|
211
|
-
/* Reduced motion support */
|
|
208
|
+
/* Reduced motion support for theme transitions */
|
|
212
209
|
@media (prefers-reduced-motion: reduce) {
|
|
213
|
-
[data-modal],
|
|
214
|
-
[data-modal-dialog],
|
|
215
210
|
[data-modal-close] {
|
|
216
211
|
transition: none !important;
|
|
217
212
|
}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
visibility: hidden;
|
|
10
10
|
overflow-y: auto;
|
|
11
11
|
-webkit-overflow-scrolling: touch;
|
|
12
|
+
transition: transform var(--a11y-offcanvas-duration, 0.3s) var(--a11y-offcanvas-easing, ease),
|
|
13
|
+
visibility var(--a11y-offcanvas-duration, 0.3s) var(--a11y-offcanvas-easing, ease);
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
[data-offcanvas-panel][aria-hidden="false"] {
|
|
@@ -96,6 +98,8 @@
|
|
|
96
98
|
z-index: 999;
|
|
97
99
|
visibility: hidden;
|
|
98
100
|
opacity: 0;
|
|
101
|
+
transition: opacity var(--a11y-offcanvas-duration, 0.3s) var(--a11y-offcanvas-easing, ease),
|
|
102
|
+
visibility var(--a11y-offcanvas-duration, 0.3s) var(--a11y-offcanvas-easing, ease);
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
[data-offcanvas-backdrop][aria-hidden="false"] {
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
/* Obsahuje vizualne nastavenia: farby, velkosti, bordery, radiusy, spacing */
|
|
3
3
|
/* Pre zmenu vzhladu upravte tento subor */
|
|
4
4
|
|
|
5
|
+
/* Focus visible for all focusable elements inside offcanvas */
|
|
6
|
+
[data-offcanvas-panel] :focus-visible {
|
|
7
|
+
outline: 2px solid #3b82f6;
|
|
8
|
+
outline-offset: 2px;
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
/* Offcanvas panel */
|
|
6
12
|
[data-offcanvas-panel] {
|
|
7
13
|
width: 320px;
|
|
8
14
|
max-width: 100%;
|
|
9
15
|
background: #fff;
|
|
10
16
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
|
|
11
|
-
transition: transform var(--a11y-offcanvas-duration, 0.3s) var(--a11y-offcanvas-easing, ease),
|
|
12
|
-
visibility var(--a11y-offcanvas-duration, 0.3s) var(--a11y-offcanvas-easing, ease);
|
|
13
17
|
display: flex;
|
|
14
18
|
flex-direction: column;
|
|
15
19
|
}
|
|
@@ -72,16 +76,9 @@
|
|
|
72
76
|
color: #111827;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
[data-offcanvas-close]:focus {
|
|
76
|
-
outline: 2px solid #3b82f6;
|
|
77
|
-
outline-offset: 2px;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
79
|
/* Backdrop */
|
|
81
80
|
[data-offcanvas-backdrop] {
|
|
82
81
|
background: rgba(0, 0, 0, 0.5);
|
|
83
|
-
transition: opacity var(--a11y-offcanvas-duration, 0.3s) var(--a11y-offcanvas-easing, ease),
|
|
84
|
-
visibility var(--a11y-offcanvas-duration, 0.3s) var(--a11y-offcanvas-easing, ease);
|
|
85
82
|
}
|
|
86
83
|
|
|
87
84
|
/* Wide variant */
|
|
@@ -155,15 +152,13 @@
|
|
|
155
152
|
border: 2px solid currentColor;
|
|
156
153
|
}
|
|
157
154
|
|
|
158
|
-
[data-offcanvas-
|
|
155
|
+
[data-offcanvas-panel] :focus-visible {
|
|
159
156
|
outline-width: 3px;
|
|
160
157
|
}
|
|
161
158
|
}
|
|
162
159
|
|
|
163
|
-
/* Reduced motion support */
|
|
160
|
+
/* Reduced motion support for theme transitions */
|
|
164
161
|
@media (prefers-reduced-motion: reduce) {
|
|
165
|
-
[data-offcanvas-panel],
|
|
166
|
-
[data-offcanvas-backdrop],
|
|
167
162
|
[data-offcanvas-close] {
|
|
168
163
|
transition: none !important;
|
|
169
164
|
}
|
package/src/js/a11y-modal.js
CHANGED
|
@@ -126,17 +126,16 @@ class AccessibleModal {
|
|
|
126
126
|
document.body.classList.add("modal-open");
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
// Update focusable elements
|
|
130
|
-
this.updateFocusableElements();
|
|
131
|
-
|
|
132
|
-
// Focus first element
|
|
129
|
+
// Update focusable elements and focus - needs to wait for CSS to apply
|
|
133
130
|
setTimeout(() => {
|
|
131
|
+
this.updateFocusableElements();
|
|
132
|
+
|
|
134
133
|
if (this.firstFocusable) {
|
|
135
134
|
this.firstFocusable.focus();
|
|
136
135
|
} else {
|
|
137
136
|
this.modal.focus();
|
|
138
137
|
}
|
|
139
|
-
},
|
|
138
|
+
}, 50);
|
|
140
139
|
|
|
141
140
|
// Callback
|
|
142
141
|
if (this.options.onOpen) {
|
|
@@ -188,11 +187,34 @@ class AccessibleModal {
|
|
|
188
187
|
this.focusableElements = Array.from(
|
|
189
188
|
this.dialog.querySelectorAll(focusableSelectors.join(","))
|
|
190
189
|
).filter((el) => {
|
|
191
|
-
|
|
190
|
+
// Check if element is visible
|
|
191
|
+
const isVisible = (
|
|
192
192
|
el.offsetWidth > 0 ||
|
|
193
193
|
el.offsetHeight > 0 ||
|
|
194
194
|
el.getClientRects().length > 0
|
|
195
195
|
);
|
|
196
|
+
|
|
197
|
+
// Check if element or any parent has aria-hidden="true"
|
|
198
|
+
let currentElement = el;
|
|
199
|
+
while (currentElement && currentElement !== this.dialog) {
|
|
200
|
+
if (currentElement.getAttribute('aria-hidden') === 'true') {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
currentElement = currentElement.parentElement;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check if element has hidden attribute
|
|
207
|
+
if (el.hasAttribute('hidden')) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check computed style for display (not visibility, as modal is opening)
|
|
212
|
+
const style = window.getComputedStyle(el);
|
|
213
|
+
if (style.display === 'none') {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return isVisible;
|
|
196
218
|
});
|
|
197
219
|
|
|
198
220
|
this.firstFocusable = this.focusableElements[0] || null;
|
package/src/js/a11y-offcanvas.js
CHANGED
|
@@ -131,17 +131,16 @@ class AccessibleOffcanvas {
|
|
|
131
131
|
document.body.classList.add("offcanvas-open");
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
// Update focusable elements
|
|
135
|
-
this.updateFocusableElements();
|
|
136
|
-
|
|
137
|
-
// Focus first element
|
|
134
|
+
// Update focusable elements and focus - needs to wait for CSS to apply
|
|
138
135
|
setTimeout(() => {
|
|
136
|
+
this.updateFocusableElements();
|
|
137
|
+
|
|
139
138
|
if (this.firstFocusable) {
|
|
140
139
|
this.firstFocusable.focus();
|
|
141
140
|
} else {
|
|
142
141
|
this.panel.focus();
|
|
143
142
|
}
|
|
144
|
-
},
|
|
143
|
+
}, 50);
|
|
145
144
|
|
|
146
145
|
// Callback
|
|
147
146
|
if (this.options.onOpen) {
|
|
@@ -196,11 +195,38 @@ class AccessibleOffcanvas {
|
|
|
196
195
|
this.focusableElements = Array.from(
|
|
197
196
|
this.panel.querySelectorAll(focusableSelectors.join(","))
|
|
198
197
|
).filter((el) => {
|
|
199
|
-
|
|
198
|
+
// Check if element is visible
|
|
199
|
+
const isVisible = (
|
|
200
200
|
el.offsetWidth > 0 ||
|
|
201
201
|
el.offsetHeight > 0 ||
|
|
202
202
|
el.getClientRects().length > 0
|
|
203
203
|
);
|
|
204
|
+
|
|
205
|
+
if (!isVisible) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check if element or any parent has aria-hidden="true"
|
|
210
|
+
let currentElement = el;
|
|
211
|
+
while (currentElement && currentElement !== this.panel) {
|
|
212
|
+
if (currentElement.getAttribute('aria-hidden') === 'true') {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
currentElement = currentElement.parentElement;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check if element has hidden attribute
|
|
219
|
+
if (el.hasAttribute('hidden')) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check computed style for display (not visibility, as panel is opening)
|
|
224
|
+
const style = window.getComputedStyle(el);
|
|
225
|
+
if (style.display === 'none') {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return true;
|
|
204
230
|
});
|
|
205
231
|
|
|
206
232
|
this.firstFocusable = this.focusableElements[0] || null;
|