admin-lte 4.0.0-beta3 → 4.0.0-rc2
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/.browserslistrc +9 -4
- package/ACCESSIBILITY-COMPLIANCE.md +369 -0
- package/CHANGELOG.md +314 -0
- package/README.md +43 -7
- package/dist/css/adminlte.css +1412 -1264
- package/dist/css/adminlte.css.map +1 -1
- package/dist/css/adminlte.min.css +1 -1
- package/dist/css/adminlte.min.css.map +1 -1
- package/dist/css/adminlte.rtl.css +1430 -1256
- package/dist/css/adminlte.rtl.css.map +1 -1
- package/dist/css/adminlte.rtl.min.css +1 -1
- package/dist/css/adminlte.rtl.min.css.map +1 -1
- package/dist/js/adminlte.js +786 -345
- package/dist/js/adminlte.js.map +1 -1
- package/dist/js/adminlte.min.js +3 -3
- package/dist/js/adminlte.min.js.map +1 -1
- package/dist/pages/UI/general.html +41 -9
- package/dist/pages/UI/icons.html +38 -6
- package/dist/pages/UI/timeline.html +38 -6
- package/dist/pages/docs/browser-support.html +39 -25
- package/dist/pages/docs/color-mode.html +44 -31
- package/dist/pages/docs/components/main-header.html +39 -25
- package/dist/pages/docs/components/main-sidebar.html +40 -27
- package/dist/pages/docs/faq.html +38 -23
- package/dist/pages/docs/how-to-contribute.html +39 -25
- package/dist/pages/docs/introduction.html +44 -35
- package/dist/pages/docs/javascript/treeview.html +39 -25
- package/dist/pages/docs/layout.html +38 -6
- package/dist/pages/docs/license.html +38 -23
- package/dist/pages/examples/lockscreen.html +17 -5
- package/dist/pages/examples/login-v2.html +16 -4
- package/dist/pages/examples/login.html +16 -4
- package/dist/pages/examples/register-v2.html +16 -4
- package/dist/pages/examples/register.html +16 -4
- package/dist/pages/forms/general.html +38 -6
- package/dist/pages/generate/theme.html +155 -5
- package/dist/pages/index.html +42 -27
- package/dist/pages/index2.html +39 -7
- package/dist/pages/index3.html +38 -6
- package/dist/pages/layout/collapsed-sidebar.html +38 -6
- package/dist/pages/layout/fixed-complete.html +862 -0
- package/dist/pages/layout/fixed-footer.html +861 -0
- package/dist/pages/layout/fixed-header.html +861 -0
- package/dist/pages/layout/fixed-sidebar.html +38 -6
- package/dist/pages/layout/layout-custom-area.html +38 -6
- package/dist/pages/layout/layout-rtl.html +38 -6
- package/dist/pages/layout/logo-switch.html +17 -5
- package/dist/pages/layout/sidebar-mini.html +38 -6
- package/dist/pages/layout/unfixed-sidebar.html +38 -6
- package/dist/pages/tables/simple.html +38 -6
- package/dist/pages/widgets/cards.html +38 -6
- package/dist/pages/widgets/info-box.html +38 -6
- package/dist/pages/widgets/small-box.html +38 -6
- package/eslint.config.js +81 -0
- package/package.json +34 -31
- package/src/scss/_accessibility.scss +292 -0
- package/src/scss/_app-footer.scss +8 -0
- package/src/scss/_app-header.scss +8 -0
- package/src/scss/_table.scss +1 -9
- package/src/scss/_variables.scss +2 -1
- package/src/scss/adminlte.scss +3 -0
- package/src/ts/accessibility.ts +533 -0
- package/src/ts/adminlte.ts +48 -9
- package/src/ts/card-widget.ts +1 -2
- package/src/ts/push-menu.ts +1 -2
- package/src/ts/util/index.ts +39 -7
- package/.astro/content-assets.mjs +0 -1
- package/.astro/content-modules.mjs +0 -1
- package/.astro/content.d.ts +0 -166
- package/.astro/data-store.json +0 -1
- package/.astro/settings.json +0 -5
- package/.astro/types.d.ts +0 -2
- package/adminlte-v4.0.0-beta1-with-dist.zip +0 -0
- package/adminlte-v4.0.0-beta3.zip +0 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdminLTE Accessibility Module
|
|
3
|
+
* WCAG 2.1 AA Compliance Features
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface AccessibilityConfig {
|
|
7
|
+
announcements: boolean
|
|
8
|
+
skipLinks: boolean
|
|
9
|
+
focusManagement: boolean
|
|
10
|
+
keyboardNavigation: boolean
|
|
11
|
+
reducedMotion: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class AccessibilityManager {
|
|
15
|
+
private config: AccessibilityConfig
|
|
16
|
+
private liveRegion: HTMLElement | null = null
|
|
17
|
+
private focusHistory: HTMLElement[] = []
|
|
18
|
+
|
|
19
|
+
constructor(config: Partial<AccessibilityConfig> = {}) {
|
|
20
|
+
this.config = {
|
|
21
|
+
announcements: true,
|
|
22
|
+
skipLinks: true,
|
|
23
|
+
focusManagement: true,
|
|
24
|
+
keyboardNavigation: true,
|
|
25
|
+
reducedMotion: true,
|
|
26
|
+
...config
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.init()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private init(): void {
|
|
33
|
+
if (this.config.announcements) {
|
|
34
|
+
this.createLiveRegion()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (this.config.skipLinks) {
|
|
38
|
+
this.addSkipLinks()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (this.config.focusManagement) {
|
|
42
|
+
this.initFocusManagement()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (this.config.keyboardNavigation) {
|
|
46
|
+
this.initKeyboardNavigation()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (this.config.reducedMotion) {
|
|
50
|
+
this.respectReducedMotion()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.initErrorAnnouncements()
|
|
54
|
+
this.initTableAccessibility()
|
|
55
|
+
this.initFormAccessibility()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// WCAG 4.1.3: Status Messages
|
|
59
|
+
private createLiveRegion(): void {
|
|
60
|
+
if (this.liveRegion) return
|
|
61
|
+
|
|
62
|
+
this.liveRegion = document.createElement('div')
|
|
63
|
+
this.liveRegion.id = 'live-region'
|
|
64
|
+
this.liveRegion.className = 'live-region'
|
|
65
|
+
this.liveRegion.setAttribute('aria-live', 'polite')
|
|
66
|
+
this.liveRegion.setAttribute('aria-atomic', 'true')
|
|
67
|
+
this.liveRegion.setAttribute('role', 'status')
|
|
68
|
+
|
|
69
|
+
document.body.append(this.liveRegion)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// WCAG 2.4.1: Bypass Blocks
|
|
73
|
+
private addSkipLinks(): void {
|
|
74
|
+
const skipLinksContainer = document.createElement('div')
|
|
75
|
+
skipLinksContainer.className = 'skip-links'
|
|
76
|
+
|
|
77
|
+
const skipToMain = document.createElement('a')
|
|
78
|
+
skipToMain.href = '#main'
|
|
79
|
+
skipToMain.className = 'skip-link'
|
|
80
|
+
skipToMain.textContent = 'Skip to main content'
|
|
81
|
+
|
|
82
|
+
const skipToNav = document.createElement('a')
|
|
83
|
+
skipToNav.href = '#navigation'
|
|
84
|
+
skipToNav.className = 'skip-link'
|
|
85
|
+
skipToNav.textContent = 'Skip to navigation'
|
|
86
|
+
|
|
87
|
+
skipLinksContainer.append(skipToMain)
|
|
88
|
+
skipLinksContainer.append(skipToNav)
|
|
89
|
+
|
|
90
|
+
document.body.insertBefore(skipLinksContainer, document.body.firstChild)
|
|
91
|
+
|
|
92
|
+
// Ensure targets exist and are focusable
|
|
93
|
+
this.ensureSkipTargets()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private ensureSkipTargets(): void {
|
|
97
|
+
const main = document.querySelector('#main, main, [role="main"]')
|
|
98
|
+
if (main && !main.id) {
|
|
99
|
+
main.id = 'main'
|
|
100
|
+
}
|
|
101
|
+
if (main && !main.hasAttribute('tabindex')) {
|
|
102
|
+
main.setAttribute('tabindex', '-1')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const nav = document.querySelector('#navigation, nav, [role="navigation"]')
|
|
106
|
+
if (nav && !nav.id) {
|
|
107
|
+
nav.id = 'navigation'
|
|
108
|
+
}
|
|
109
|
+
if (nav && !nav.hasAttribute('tabindex')) {
|
|
110
|
+
nav.setAttribute('tabindex', '-1')
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// WCAG 2.4.3: Focus Order & 2.4.7: Focus Visible
|
|
115
|
+
private initFocusManagement(): void {
|
|
116
|
+
document.addEventListener('keydown', (event) => {
|
|
117
|
+
if (event.key === 'Tab') {
|
|
118
|
+
this.handleTabNavigation(event)
|
|
119
|
+
}
|
|
120
|
+
if (event.key === 'Escape') {
|
|
121
|
+
this.handleEscapeKey(event)
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// Focus management for modals and dropdowns
|
|
126
|
+
this.initModalFocusManagement()
|
|
127
|
+
this.initDropdownFocusManagement()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private handleTabNavigation(event: KeyboardEvent): void {
|
|
131
|
+
const focusableElements = this.getFocusableElements()
|
|
132
|
+
const currentIndex = focusableElements.indexOf(document.activeElement as HTMLElement)
|
|
133
|
+
|
|
134
|
+
if (event.shiftKey) {
|
|
135
|
+
// Shift+Tab (backward)
|
|
136
|
+
if (currentIndex <= 0) {
|
|
137
|
+
event.preventDefault()
|
|
138
|
+
focusableElements.at(-1)?.focus()
|
|
139
|
+
}
|
|
140
|
+
} else if (currentIndex >= focusableElements.length - 1) {
|
|
141
|
+
// Tab (forward)
|
|
142
|
+
event.preventDefault()
|
|
143
|
+
focusableElements[0]?.focus()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private getFocusableElements(): HTMLElement[] {
|
|
148
|
+
const selector = [
|
|
149
|
+
'a[href]',
|
|
150
|
+
'button:not([disabled])',
|
|
151
|
+
'input:not([disabled])',
|
|
152
|
+
'select:not([disabled])',
|
|
153
|
+
'textarea:not([disabled])',
|
|
154
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
155
|
+
'[contenteditable="true"]'
|
|
156
|
+
].join(', ')
|
|
157
|
+
|
|
158
|
+
return Array.from(document.querySelectorAll(selector)) as HTMLElement[]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private handleEscapeKey(event: KeyboardEvent): void {
|
|
162
|
+
// Close modals, dropdowns, etc.
|
|
163
|
+
const activeModal = document.querySelector('.modal.show')
|
|
164
|
+
const activeDropdown = document.querySelector('.dropdown-menu.show')
|
|
165
|
+
|
|
166
|
+
if (activeModal) {
|
|
167
|
+
const closeButton = activeModal.querySelector('[data-bs-dismiss="modal"]') as HTMLElement
|
|
168
|
+
closeButton?.click()
|
|
169
|
+
event.preventDefault()
|
|
170
|
+
} else if (activeDropdown) {
|
|
171
|
+
const toggleButton = document.querySelector('[data-bs-toggle="dropdown"][aria-expanded="true"]') as HTMLElement
|
|
172
|
+
toggleButton?.click()
|
|
173
|
+
event.preventDefault()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// WCAG 2.1.1: Keyboard Access
|
|
178
|
+
private initKeyboardNavigation(): void {
|
|
179
|
+
// Add keyboard support for custom components
|
|
180
|
+
document.addEventListener('keydown', (event) => {
|
|
181
|
+
const target = event.target as HTMLElement
|
|
182
|
+
|
|
183
|
+
// Handle arrow key navigation for menus
|
|
184
|
+
if (target.closest('.nav, .navbar-nav, .dropdown-menu')) {
|
|
185
|
+
this.handleMenuNavigation(event)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Handle Enter and Space for custom buttons
|
|
189
|
+
if ((event.key === 'Enter' || event.key === ' ') && target.hasAttribute('role') && target.getAttribute('role') === 'button' && !target.matches('button, input[type="button"], input[type="submit"]')) {
|
|
190
|
+
event.preventDefault()
|
|
191
|
+
target.click()
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private handleMenuNavigation(event: KeyboardEvent): void {
|
|
197
|
+
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const currentElement = event.target as HTMLElement
|
|
202
|
+
const menuItems = Array.from(currentElement.closest('.nav, .navbar-nav, .dropdown-menu')?.querySelectorAll('a, button') || []) as HTMLElement[]
|
|
203
|
+
const currentIndex = menuItems.indexOf(currentElement)
|
|
204
|
+
|
|
205
|
+
let nextIndex: number
|
|
206
|
+
|
|
207
|
+
switch (event.key) {
|
|
208
|
+
case 'ArrowDown':
|
|
209
|
+
case 'ArrowRight': {
|
|
210
|
+
nextIndex = currentIndex < menuItems.length - 1 ? currentIndex + 1 : 0
|
|
211
|
+
break
|
|
212
|
+
}
|
|
213
|
+
case 'ArrowUp':
|
|
214
|
+
case 'ArrowLeft': {
|
|
215
|
+
nextIndex = currentIndex > 0 ? currentIndex - 1 : menuItems.length - 1
|
|
216
|
+
break
|
|
217
|
+
}
|
|
218
|
+
case 'Home': {
|
|
219
|
+
nextIndex = 0
|
|
220
|
+
break
|
|
221
|
+
}
|
|
222
|
+
case 'End': {
|
|
223
|
+
nextIndex = menuItems.length - 1
|
|
224
|
+
break
|
|
225
|
+
}
|
|
226
|
+
default: {
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
event.preventDefault()
|
|
232
|
+
menuItems[nextIndex]?.focus()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// WCAG 2.3.3: Animation from Interactions
|
|
236
|
+
private respectReducedMotion(): void {
|
|
237
|
+
const prefersReducedMotion = globalThis.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
238
|
+
|
|
239
|
+
if (prefersReducedMotion) {
|
|
240
|
+
document.body.classList.add('reduce-motion')
|
|
241
|
+
|
|
242
|
+
// Disable smooth scrolling
|
|
243
|
+
document.documentElement.style.scrollBehavior = 'auto'
|
|
244
|
+
|
|
245
|
+
// Reduce animation duration
|
|
246
|
+
const style = document.createElement('style')
|
|
247
|
+
style.textContent = `
|
|
248
|
+
*, *::before, *::after {
|
|
249
|
+
animation-duration: 0.01ms !important;
|
|
250
|
+
animation-iteration-count: 1 !important;
|
|
251
|
+
transition-duration: 0.01ms !important;
|
|
252
|
+
}
|
|
253
|
+
`
|
|
254
|
+
document.head.append(style)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// WCAG 3.3.1: Error Identification
|
|
259
|
+
private initErrorAnnouncements(): void {
|
|
260
|
+
const observer = new MutationObserver((mutations) => {
|
|
261
|
+
mutations.forEach((mutation) => {
|
|
262
|
+
mutation.addedNodes.forEach((node) => {
|
|
263
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
264
|
+
const element = node as Element
|
|
265
|
+
|
|
266
|
+
// Check for error messages
|
|
267
|
+
if (element.matches('.alert-danger, .invalid-feedback, .error')) {
|
|
268
|
+
this.announce(element.textContent || 'Error occurred', 'assertive')
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Check for success messages
|
|
272
|
+
if (element.matches('.alert-success, .success')) {
|
|
273
|
+
this.announce(element.textContent || 'Success', 'polite')
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
observer.observe(document.body, {
|
|
281
|
+
childList: true,
|
|
282
|
+
subtree: true
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// WCAG 1.3.1: Info and Relationships
|
|
287
|
+
private initTableAccessibility(): void {
|
|
288
|
+
document.querySelectorAll('table').forEach((table) => {
|
|
289
|
+
// Add table role if missing
|
|
290
|
+
if (!table.hasAttribute('role')) {
|
|
291
|
+
table.setAttribute('role', 'table')
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Ensure headers have proper scope
|
|
295
|
+
table.querySelectorAll('th').forEach((th) => {
|
|
296
|
+
if (!th.hasAttribute('scope')) {
|
|
297
|
+
const isInThead = th.closest('thead')
|
|
298
|
+
const isFirstColumn = th.cellIndex === 0
|
|
299
|
+
|
|
300
|
+
if (isInThead) {
|
|
301
|
+
th.setAttribute('scope', 'col')
|
|
302
|
+
} else if (isFirstColumn) {
|
|
303
|
+
th.setAttribute('scope', 'row')
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// Add caption if missing but title exists
|
|
309
|
+
if (!table.querySelector('caption') && table.hasAttribute('title')) {
|
|
310
|
+
const caption = document.createElement('caption')
|
|
311
|
+
caption.textContent = table.getAttribute('title') || ''
|
|
312
|
+
table.insertBefore(caption, table.firstChild)
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// WCAG 3.3.2: Labels or Instructions
|
|
318
|
+
private initFormAccessibility(): void {
|
|
319
|
+
document.querySelectorAll('input, select, textarea').forEach((input) => {
|
|
320
|
+
const htmlInput = input as HTMLInputElement
|
|
321
|
+
|
|
322
|
+
// Ensure all inputs have labels
|
|
323
|
+
if (!htmlInput.labels?.length && !htmlInput.hasAttribute('aria-label') && !htmlInput.hasAttribute('aria-labelledby')) {
|
|
324
|
+
const placeholder = htmlInput.getAttribute('placeholder')
|
|
325
|
+
if (placeholder) {
|
|
326
|
+
htmlInput.setAttribute('aria-label', placeholder)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Add required indicators
|
|
331
|
+
if (htmlInput.hasAttribute('required')) {
|
|
332
|
+
const label = htmlInput.labels?.[0]
|
|
333
|
+
if (label && !label.querySelector('.required-indicator')) {
|
|
334
|
+
const indicator = document.createElement('span')
|
|
335
|
+
indicator.className = 'required-indicator sr-only'
|
|
336
|
+
indicator.textContent = ' (required)'
|
|
337
|
+
label.append(indicator)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Handle invalid states
|
|
342
|
+
htmlInput.addEventListener('invalid', () => {
|
|
343
|
+
this.handleFormError(htmlInput)
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private handleFormError(input: HTMLInputElement): void {
|
|
349
|
+
const errorId = `${input.id || input.name}-error`
|
|
350
|
+
let errorElement = document.getElementById(errorId)
|
|
351
|
+
|
|
352
|
+
if (!errorElement) {
|
|
353
|
+
errorElement = document.createElement('div')
|
|
354
|
+
errorElement.id = errorId
|
|
355
|
+
errorElement.className = 'invalid-feedback'
|
|
356
|
+
errorElement.setAttribute('role', 'alert')
|
|
357
|
+
input.parentNode?.insertBefore(errorElement, input.nextSibling)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
errorElement.textContent = input.validationMessage
|
|
361
|
+
input.setAttribute('aria-describedby', errorId)
|
|
362
|
+
input.classList.add('is-invalid')
|
|
363
|
+
|
|
364
|
+
this.announce(`Error in ${input.labels?.[0]?.textContent || input.name}: ${input.validationMessage}`, 'assertive')
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Modal focus management
|
|
368
|
+
private initModalFocusManagement(): void {
|
|
369
|
+
document.addEventListener('shown.bs.modal', (event) => {
|
|
370
|
+
const modal = event.target as HTMLElement
|
|
371
|
+
const focusableElements = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
|
|
372
|
+
|
|
373
|
+
if (focusableElements.length > 0) {
|
|
374
|
+
(focusableElements[0] as HTMLElement).focus()
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Store previous focus
|
|
378
|
+
this.focusHistory.push(document.activeElement as HTMLElement)
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
document.addEventListener('hidden.bs.modal', () => {
|
|
382
|
+
// Restore previous focus
|
|
383
|
+
const previousElement = this.focusHistory.pop()
|
|
384
|
+
if (previousElement) {
|
|
385
|
+
previousElement.focus()
|
|
386
|
+
}
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Dropdown focus management
|
|
391
|
+
private initDropdownFocusManagement(): void {
|
|
392
|
+
document.addEventListener('shown.bs.dropdown', (event) => {
|
|
393
|
+
const dropdown = event.target as HTMLElement
|
|
394
|
+
const menu = dropdown.querySelector('.dropdown-menu')
|
|
395
|
+
const firstItem = menu?.querySelector('a, button') as HTMLElement
|
|
396
|
+
|
|
397
|
+
if (firstItem) {
|
|
398
|
+
firstItem.focus()
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Public API methods
|
|
404
|
+
public announce(message: string, priority: 'polite' | 'assertive' = 'polite'): void {
|
|
405
|
+
if (!this.liveRegion) {
|
|
406
|
+
this.createLiveRegion()
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (this.liveRegion) {
|
|
410
|
+
this.liveRegion.setAttribute('aria-live', priority)
|
|
411
|
+
this.liveRegion.textContent = message
|
|
412
|
+
|
|
413
|
+
// Clear after announcement
|
|
414
|
+
setTimeout(() => {
|
|
415
|
+
if (this.liveRegion) {
|
|
416
|
+
this.liveRegion.textContent = ''
|
|
417
|
+
}
|
|
418
|
+
}, 1000)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
public focusElement(selector: string): void {
|
|
423
|
+
const element = document.querySelector(selector) as HTMLElement
|
|
424
|
+
if (element) {
|
|
425
|
+
element.focus()
|
|
426
|
+
|
|
427
|
+
// Ensure element is visible
|
|
428
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
public trapFocus(container: HTMLElement): void {
|
|
433
|
+
const focusableElements = container.querySelectorAll(
|
|
434
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
435
|
+
) as NodeListOf<HTMLElement>
|
|
436
|
+
|
|
437
|
+
const focusableArray = Array.from(focusableElements)
|
|
438
|
+
const firstElement = focusableArray[0]
|
|
439
|
+
const lastElement = focusableArray.at(-1)
|
|
440
|
+
|
|
441
|
+
container.addEventListener('keydown', (event) => {
|
|
442
|
+
if (event.key === 'Tab') {
|
|
443
|
+
if (event.shiftKey) {
|
|
444
|
+
if (document.activeElement === firstElement) {
|
|
445
|
+
lastElement?.focus()
|
|
446
|
+
event.preventDefault()
|
|
447
|
+
}
|
|
448
|
+
} else if (document.activeElement === lastElement) {
|
|
449
|
+
firstElement.focus()
|
|
450
|
+
event.preventDefault()
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
public addLandmarks(): void {
|
|
457
|
+
// Add main landmark if missing
|
|
458
|
+
const main = document.querySelector('main')
|
|
459
|
+
if (!main) {
|
|
460
|
+
const appMain = document.querySelector('.app-main')
|
|
461
|
+
if (appMain) {
|
|
462
|
+
appMain.setAttribute('role', 'main')
|
|
463
|
+
appMain.id = 'main'
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Add navigation landmarks
|
|
468
|
+
document.querySelectorAll('.navbar-nav, .nav').forEach((nav, index) => {
|
|
469
|
+
if (!nav.hasAttribute('role')) {
|
|
470
|
+
nav.setAttribute('role', 'navigation')
|
|
471
|
+
}
|
|
472
|
+
if (!nav.hasAttribute('aria-label')) {
|
|
473
|
+
nav.setAttribute('aria-label', `Navigation ${index + 1}`)
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
// Add search landmark
|
|
478
|
+
const searchForm = document.querySelector('form[role="search"], .navbar-search')
|
|
479
|
+
if (searchForm && !searchForm.hasAttribute('role')) {
|
|
480
|
+
searchForm.setAttribute('role', 'search')
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Initialize accessibility when DOM is ready
|
|
486
|
+
export const initAccessibility = (config?: Partial<AccessibilityConfig>): AccessibilityManager => {
|
|
487
|
+
return new AccessibilityManager(config)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Utility function for luminance calculation
|
|
491
|
+
const getLuminance = (color: string): number => {
|
|
492
|
+
const rgb = color.match(/\d+/g)?.map(Number) || [0, 0, 0]
|
|
493
|
+
const [r, g, b] = rgb.map(c => {
|
|
494
|
+
c = c / 255
|
|
495
|
+
return c <= 0.039_28 ? c / 12.92 : (c + 0.055) ** 2.4 / (1.055 ** 2.4)
|
|
496
|
+
})
|
|
497
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Export utility functions
|
|
501
|
+
export const accessibilityUtils = {
|
|
502
|
+
// WCAG 1.4.3: Contrast checking utility
|
|
503
|
+
checkColorContrast: (foreground: string, background: string): { ratio: number; passes: boolean } => {
|
|
504
|
+
const l1 = getLuminance(foreground)
|
|
505
|
+
const l2 = getLuminance(background)
|
|
506
|
+
const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05)
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
ratio: Math.round(ratio * 100) / 100,
|
|
510
|
+
passes: ratio >= 4.5
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
// Generate unique IDs for accessibility
|
|
515
|
+
generateId: (prefix: string = 'a11y'): string => {
|
|
516
|
+
return `${prefix}-${Math.random().toString(36).slice(2, 11)}`
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
// Check if element is focusable
|
|
520
|
+
isFocusable: (element: HTMLElement): boolean => {
|
|
521
|
+
const focusableSelectors = [
|
|
522
|
+
'a[href]',
|
|
523
|
+
'button:not([disabled])',
|
|
524
|
+
'input:not([disabled])',
|
|
525
|
+
'select:not([disabled])',
|
|
526
|
+
'textarea:not([disabled])',
|
|
527
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
528
|
+
'[contenteditable="true"]'
|
|
529
|
+
]
|
|
530
|
+
|
|
531
|
+
return focusableSelectors.some(selector => element.matches(selector))
|
|
532
|
+
}
|
|
533
|
+
}
|
package/src/ts/adminlte.ts
CHANGED
|
@@ -1,15 +1,54 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import FullScreen from './fullscreen'
|
|
1
|
+
import { onDOMContentLoaded } from './util/index.js'
|
|
2
|
+
import Layout from './layout.js'
|
|
3
|
+
import CardWidget from './card-widget.js'
|
|
4
|
+
import Treeview from './treeview.js'
|
|
5
|
+
import DirectChat from './direct-chat.js'
|
|
6
|
+
import FullScreen from './fullscreen.js'
|
|
7
|
+
import PushMenu from './push-menu.js'
|
|
8
|
+
import { initAccessibility } from './accessibility.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* AdminLTE v4.0.0-rc1
|
|
12
|
+
* Author: Colorlib
|
|
13
|
+
* Website: AdminLTE.io <https://adminlte.io>
|
|
14
|
+
* License: Open source - MIT <https://opensource.org/licenses/MIT>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
onDOMContentLoaded(() => {
|
|
18
|
+
/**
|
|
19
|
+
* Initialize AdminLTE Core Components
|
|
20
|
+
* -------------------------------
|
|
21
|
+
*/
|
|
22
|
+
const layout = new Layout(document.body)
|
|
23
|
+
layout.holdTransition()
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initialize Accessibility Features - WCAG 2.1 AA Compliance
|
|
27
|
+
* --------------------------------------------------------
|
|
28
|
+
*/
|
|
29
|
+
const accessibilityManager = initAccessibility({
|
|
30
|
+
announcements: true,
|
|
31
|
+
skipLinks: true,
|
|
32
|
+
focusManagement: true,
|
|
33
|
+
keyboardNavigation: true,
|
|
34
|
+
reducedMotion: true
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Add semantic landmarks
|
|
38
|
+
accessibilityManager.addLandmarks()
|
|
39
|
+
|
|
40
|
+
// Mark app as loaded after initialization
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
document.body.classList.add('app-loaded')
|
|
43
|
+
}, 400)
|
|
44
|
+
})
|
|
7
45
|
|
|
8
46
|
export {
|
|
9
47
|
Layout,
|
|
10
|
-
|
|
48
|
+
CardWidget,
|
|
11
49
|
Treeview,
|
|
12
50
|
DirectChat,
|
|
13
|
-
|
|
14
|
-
|
|
51
|
+
FullScreen,
|
|
52
|
+
PushMenu,
|
|
53
|
+
initAccessibility
|
|
15
54
|
}
|
package/src/ts/card-widget.ts
CHANGED
|
@@ -111,8 +111,7 @@ class CardWidget {
|
|
|
111
111
|
|
|
112
112
|
setTimeout(() => {
|
|
113
113
|
if (this._parent) {
|
|
114
|
-
this._parent.classList.remove(CLASS_NAME_COLLAPSED)
|
|
115
|
-
this._parent.classList.remove(CLASS_NAME_EXPANDING)
|
|
114
|
+
this._parent.classList.remove(CLASS_NAME_COLLAPSED, CLASS_NAME_EXPANDING)
|
|
116
115
|
}
|
|
117
116
|
}, this._config.animationSpeed)
|
|
118
117
|
}
|
package/src/ts/push-menu.ts
CHANGED
|
@@ -59,7 +59,6 @@ class PushMenu {
|
|
|
59
59
|
this._config = { ...Defaults, ...config }
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
// TODO
|
|
63
62
|
menusClose() {
|
|
64
63
|
const navTreeview = document.querySelectorAll<HTMLElement>(SELECTOR_NAV_TREEVIEW)
|
|
65
64
|
|
|
@@ -100,7 +99,7 @@ class PushMenu {
|
|
|
100
99
|
const sidebarExpandList = document.querySelector(SELECTOR_SIDEBAR_EXPAND)?.classList ?? []
|
|
101
100
|
const sidebarExpand = Array.from(sidebarExpandList).find(className => className.startsWith(CLASS_NAME_SIDEBAR_EXPAND)) ?? ''
|
|
102
101
|
const sidebar = document.getElementsByClassName(sidebarExpand)[0]
|
|
103
|
-
const sidebarContent =
|
|
102
|
+
const sidebarContent = globalThis.getComputedStyle(sidebar, '::before').getPropertyValue('content')
|
|
104
103
|
this._config = { ...this._config, sidebarBreakpoint: Number(sidebarContent.replace(/[^\d.-]/g, '')) }
|
|
105
104
|
|
|
106
105
|
if (window.innerWidth <= this._config.sidebarBreakpoint) {
|