@sc4rfurryx/proteusjs 1.1.0 → 1.1.1
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.md +5 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/react.d.ts +1 -0
- package/dist/adapters/react.esm.js +2 -1
- package/dist/adapters/react.esm.js.map +1 -1
- package/dist/adapters/svelte.esm.js +2 -1
- package/dist/adapters/svelte.esm.js.map +1 -1
- package/dist/adapters/vue.esm.js +2 -1
- package/dist/adapters/vue.esm.js.map +1 -1
- package/dist/modules/a11y-audit.d.ts +1 -9
- package/dist/modules/a11y-audit.esm.js +30 -475
- package/dist/modules/a11y-audit.esm.js.map +1 -1
- package/dist/modules/a11y-primitives.d.ts +8 -41
- package/dist/modules/a11y-primitives.esm.js +69 -400
- package/dist/modules/a11y-primitives.esm.js.map +1 -1
- package/dist/modules/anchor.d.ts +1 -0
- package/dist/modules/anchor.esm.js +2 -1
- package/dist/modules/anchor.esm.js.map +1 -1
- package/dist/modules/container.esm.js +1 -1
- package/dist/modules/perf.esm.js +1 -1
- package/dist/modules/popover.esm.js +1 -1
- package/dist/modules/scroll.esm.js +1 -1
- package/dist/modules/transitions.esm.js +1 -1
- package/dist/modules/typography.esm.js +1 -1
- package/dist/proteus.cjs.js +97 -875
- package/dist/proteus.cjs.js.map +1 -1
- package/dist/proteus.d.ts +11 -56
- package/dist/proteus.esm.js +97 -875
- package/dist/proteus.esm.js.map +1 -1
- package/dist/proteus.esm.min.js +2 -2
- package/dist/proteus.esm.min.js.map +1 -1
- package/dist/proteus.js +97 -875
- package/dist/proteus.js.map +1 -1
- package/dist/proteus.min.js +2 -2
- package/dist/proteus.min.js.map +1 -1
- package/package.json +9 -4
- package/src/index.ts +1 -1
- package/src/modules/a11y-audit/index.ts +29 -553
- package/src/modules/a11y-primitives/index.ts +73 -475
- package/src/modules/anchor/index.ts +2 -0
package/dist/proteus.cjs.js
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
/*!
|
2
|
-
* ProteusJS v1.1.
|
2
|
+
* ProteusJS v1.1.1
|
3
3
|
* Shape-shifting responsive design that adapts like the sea god himself
|
4
4
|
* (c) 2025 sc4rfurry
|
5
5
|
* Released under the MIT License
|
@@ -16509,6 +16509,7 @@ function tether(floating, opts) {
|
|
16509
16509
|
setupJSFallback();
|
16510
16510
|
}
|
16511
16511
|
return {
|
16512
|
+
update: updatePosition,
|
16512
16513
|
destroy
|
16513
16514
|
};
|
16514
16515
|
}
|
@@ -17087,504 +17088,59 @@ var index$7 = /*#__PURE__*/Object.freeze({
|
|
17087
17088
|
|
17088
17089
|
/**
|
17089
17090
|
* @sc4rfurryx/proteusjs/a11y-audit
|
17090
|
-
*
|
17091
|
+
* Lightweight accessibility audits for development
|
17091
17092
|
*
|
17092
17093
|
* @version 1.1.0
|
17093
17094
|
* @author sc4rfurry
|
17094
17095
|
* @license MIT
|
17095
17096
|
*/
|
17096
|
-
/**
|
17097
|
-
* Run accessibility audits with actionable output
|
17098
|
-
* DEV-ONLY: This module should be tree-shaken in production
|
17099
|
-
*/
|
17100
17097
|
async function audit(target = document, options = {}) {
|
17101
|
-
|
17102
|
-
|
17103
|
-
console.warn('a11y-audit should not be used in production');
|
17104
|
-
return {
|
17105
|
-
violations: [],
|
17106
|
-
passes: 0,
|
17107
|
-
incomplete: 0,
|
17108
|
-
timestamp: Date.now(),
|
17109
|
-
url: window.location.href
|
17110
|
-
};
|
17098
|
+
if (typeof window === 'undefined' || process.env['NODE_ENV'] === 'production') {
|
17099
|
+
return { violations: [], passes: 0, timestamp: Date.now(), url: '' };
|
17111
17100
|
}
|
17112
|
-
const { rules = ['
|
17101
|
+
const { rules = ['images', 'headings', 'forms'], format = 'console' } = options;
|
17113
17102
|
const violations = [];
|
17114
17103
|
let passes = 0;
|
17115
|
-
|
17116
|
-
|
17117
|
-
|
17118
|
-
'color-contrast': checkColorContrast,
|
17119
|
-
'heading-order': checkHeadingOrder,
|
17120
|
-
'image-alt': checkImageAlt,
|
17121
|
-
'label': checkFormLabels,
|
17122
|
-
'link-name': checkLinkNames,
|
17123
|
-
'button-name': checkButtonNames,
|
17124
|
-
'focus-visible': checkFocusVisible,
|
17125
|
-
'aria-labels': checkAriaLabels,
|
17126
|
-
'landmark-roles': checkLandmarkRoles,
|
17127
|
-
'skip-links': checkSkipLinks
|
17128
|
-
};
|
17129
|
-
// Run selected checks
|
17130
|
-
for (const ruleId of rules) {
|
17131
|
-
if (checks[ruleId]) {
|
17132
|
-
try {
|
17133
|
-
const result = await checks[ruleId](target);
|
17134
|
-
if (result.violations.length > 0) {
|
17135
|
-
violations.push(...result.violations);
|
17136
|
-
}
|
17137
|
-
else {
|
17138
|
-
passes++;
|
17139
|
-
}
|
17140
|
-
}
|
17141
|
-
catch (error) {
|
17142
|
-
incomplete++;
|
17143
|
-
console.warn(`Failed to run accessibility check: ${ruleId}`, error);
|
17144
|
-
}
|
17145
|
-
}
|
17146
|
-
}
|
17147
|
-
const report = {
|
17148
|
-
violations,
|
17149
|
-
passes,
|
17150
|
-
incomplete,
|
17151
|
-
timestamp: Date.now(),
|
17152
|
-
url: window.location.href
|
17153
|
-
};
|
17154
|
-
// Output results
|
17155
|
-
if (format === 'console') {
|
17156
|
-
outputToConsole(report);
|
17157
|
-
}
|
17158
|
-
if (openInBrowser) {
|
17159
|
-
openReportInBrowser(report);
|
17160
|
-
}
|
17161
|
-
return report;
|
17162
|
-
}
|
17163
|
-
// Individual check functions
|
17164
|
-
async function checkColorContrast(target) {
|
17165
|
-
const violations = [];
|
17166
|
-
const elements = target.querySelectorAll('*');
|
17167
|
-
elements.forEach(element => {
|
17168
|
-
const style = getComputedStyle(element);
|
17169
|
-
const color = style.color;
|
17170
|
-
const backgroundColor = style.backgroundColor;
|
17171
|
-
// Simple contrast check (would need more sophisticated implementation)
|
17172
|
-
if (color && backgroundColor && color !== 'rgba(0, 0, 0, 0)' && backgroundColor !== 'rgba(0, 0, 0, 0)') {
|
17173
|
-
const contrast = calculateContrast(color, backgroundColor);
|
17174
|
-
if (contrast < 4.5) {
|
17175
|
-
violations.push({
|
17176
|
-
id: 'color-contrast',
|
17177
|
-
impact: 'serious',
|
17178
|
-
nodes: 1,
|
17179
|
-
help: 'Elements must have sufficient color contrast',
|
17180
|
-
fix: `Increase contrast ratio to at least 4.5:1. Current: ${contrast.toFixed(2)}:1`,
|
17181
|
-
elements: [element]
|
17182
|
-
});
|
17183
|
-
}
|
17184
|
-
}
|
17185
|
-
});
|
17186
|
-
return { violations };
|
17187
|
-
}
|
17188
|
-
async function checkHeadingOrder(target) {
|
17189
|
-
const violations = [];
|
17190
|
-
const headings = target.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
17191
|
-
let lastLevel = 0;
|
17192
|
-
headings.forEach(heading => {
|
17193
|
-
const level = parseInt(heading.tagName.charAt(1));
|
17194
|
-
if (level > lastLevel + 1) {
|
17104
|
+
if (rules.includes('images')) {
|
17105
|
+
const imgs = target.querySelectorAll('img:not([alt])');
|
17106
|
+
if (imgs.length > 0) {
|
17195
17107
|
violations.push({
|
17196
|
-
id: '
|
17197
|
-
impact: 'moderate',
|
17198
|
-
nodes: 1,
|
17199
|
-
help: 'Heading levels should only increase by one',
|
17200
|
-
fix: `Change ${heading.tagName} to H${lastLevel + 1} or add intermediate headings`,
|
17201
|
-
elements: [heading]
|
17108
|
+
id: 'image-alt', impact: 'critical', nodes: imgs.length, help: 'Images need alt text'
|
17202
17109
|
});
|
17203
17110
|
}
|
17204
|
-
|
17205
|
-
});
|
17206
|
-
return { violations };
|
17207
|
-
}
|
17208
|
-
async function checkImageAlt(target) {
|
17209
|
-
const violations = [];
|
17210
|
-
const images = target.querySelectorAll('img');
|
17211
|
-
images.forEach(img => {
|
17212
|
-
if (!img.hasAttribute('alt')) {
|
17213
|
-
violations.push({
|
17214
|
-
id: 'image-alt',
|
17215
|
-
impact: 'critical',
|
17216
|
-
nodes: 1,
|
17217
|
-
help: 'Images must have alternative text',
|
17218
|
-
fix: 'Add alt attribute with descriptive text or alt="" for decorative images',
|
17219
|
-
elements: [img]
|
17220
|
-
});
|
17221
|
-
}
|
17222
|
-
});
|
17223
|
-
return { violations };
|
17224
|
-
}
|
17225
|
-
async function checkFormLabels(target) {
|
17226
|
-
const violations = [];
|
17227
|
-
const inputs = target.querySelectorAll('input, select, textarea');
|
17228
|
-
inputs.forEach(input => {
|
17229
|
-
const hasLabel = input.hasAttribute('aria-label') ||
|
17230
|
-
input.hasAttribute('aria-labelledby') ||
|
17231
|
-
target.querySelector(`label[for="${input.id}"]`) ||
|
17232
|
-
input.closest('label');
|
17233
|
-
if (!hasLabel) {
|
17234
|
-
violations.push({
|
17235
|
-
id: 'label',
|
17236
|
-
impact: 'critical',
|
17237
|
-
nodes: 1,
|
17238
|
-
help: 'Form elements must have labels',
|
17239
|
-
fix: 'Add a label element, aria-label, or aria-labelledby attribute',
|
17240
|
-
elements: [input]
|
17241
|
-
});
|
17242
|
-
}
|
17243
|
-
});
|
17244
|
-
return { violations };
|
17245
|
-
}
|
17246
|
-
async function checkLinkNames(target) {
|
17247
|
-
const violations = [];
|
17248
|
-
const links = target.querySelectorAll('a[href]');
|
17249
|
-
links.forEach(link => {
|
17250
|
-
const text = link.textContent?.trim();
|
17251
|
-
const ariaLabel = link.getAttribute('aria-label');
|
17252
|
-
if (!text && !ariaLabel) {
|
17253
|
-
violations.push({
|
17254
|
-
id: 'link-name',
|
17255
|
-
impact: 'serious',
|
17256
|
-
nodes: 1,
|
17257
|
-
help: 'Links must have discernible text',
|
17258
|
-
fix: 'Add descriptive text content or aria-label attribute',
|
17259
|
-
elements: [link]
|
17260
|
-
});
|
17261
|
-
}
|
17262
|
-
});
|
17263
|
-
return { violations };
|
17264
|
-
}
|
17265
|
-
async function checkButtonNames(target) {
|
17266
|
-
const violations = [];
|
17267
|
-
const buttons = target.querySelectorAll('button, input[type="button"], input[type="submit"]');
|
17268
|
-
buttons.forEach(button => {
|
17269
|
-
const text = button.textContent?.trim();
|
17270
|
-
const ariaLabel = button.getAttribute('aria-label');
|
17271
|
-
const value = button.getAttribute('value');
|
17272
|
-
if (!text && !ariaLabel && !value) {
|
17273
|
-
violations.push({
|
17274
|
-
id: 'button-name',
|
17275
|
-
impact: 'serious',
|
17276
|
-
nodes: 1,
|
17277
|
-
help: 'Buttons must have discernible text',
|
17278
|
-
fix: 'Add text content, aria-label, or value attribute',
|
17279
|
-
elements: [button]
|
17280
|
-
});
|
17281
|
-
}
|
17282
|
-
});
|
17283
|
-
return { violations };
|
17284
|
-
}
|
17285
|
-
async function checkFocusVisible(target) {
|
17286
|
-
const violations = [];
|
17287
|
-
const focusableElements = target.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
17288
|
-
focusableElements.forEach(element => {
|
17289
|
-
const styles = getComputedStyle(element);
|
17290
|
-
const hasVisibleFocus = styles.outline !== 'none' && styles.outline !== '0px' &&
|
17291
|
-
styles.outline !== '0' && styles.outlineWidth !== '0px';
|
17292
|
-
// Check if element has custom focus styles
|
17293
|
-
const hasCustomFocus = styles.boxShadow.includes('inset') ||
|
17294
|
-
styles.border !== styles.borderColor ||
|
17295
|
-
element.hasAttribute('data-focus-visible');
|
17296
|
-
if (!hasVisibleFocus && !hasCustomFocus) {
|
17297
|
-
violations.push({
|
17298
|
-
id: 'focus-visible',
|
17299
|
-
impact: 'serious',
|
17300
|
-
nodes: 1,
|
17301
|
-
help: 'Interactive elements must have visible focus indicators',
|
17302
|
-
fix: 'Add outline, box-shadow, or other visible focus styles',
|
17303
|
-
elements: [element]
|
17304
|
-
});
|
17305
|
-
}
|
17306
|
-
});
|
17307
|
-
return { violations };
|
17308
|
-
}
|
17309
|
-
async function checkAriaLabels(target) {
|
17310
|
-
const violations = [];
|
17311
|
-
// Check for aria-labelledby pointing to non-existent elements
|
17312
|
-
const elementsWithLabelledBy = target.querySelectorAll('[aria-labelledby]');
|
17313
|
-
elementsWithLabelledBy.forEach(element => {
|
17314
|
-
const labelledBy = element.getAttribute('aria-labelledby');
|
17315
|
-
if (labelledBy) {
|
17316
|
-
const labelIds = labelledBy.split(' ');
|
17317
|
-
const missingIds = labelIds.filter(id => !target.querySelector(`#${id}`));
|
17318
|
-
if (missingIds.length > 0) {
|
17319
|
-
violations.push({
|
17320
|
-
id: 'aria-labelledby-invalid',
|
17321
|
-
impact: 'serious',
|
17322
|
-
nodes: 1,
|
17323
|
-
help: 'aria-labelledby must reference existing elements',
|
17324
|
-
fix: `Fix or remove references to missing IDs: ${missingIds.join(', ')}`,
|
17325
|
-
elements: [element]
|
17326
|
-
});
|
17327
|
-
}
|
17328
|
-
}
|
17329
|
-
});
|
17330
|
-
// Check for aria-describedby pointing to non-existent elements
|
17331
|
-
const elementsWithDescribedBy = target.querySelectorAll('[aria-describedby]');
|
17332
|
-
elementsWithDescribedBy.forEach(element => {
|
17333
|
-
const describedBy = element.getAttribute('aria-describedby');
|
17334
|
-
if (describedBy) {
|
17335
|
-
const descriptionIds = describedBy.split(' ');
|
17336
|
-
const missingIds = descriptionIds.filter(id => !target.querySelector(`#${id}`));
|
17337
|
-
if (missingIds.length > 0) {
|
17338
|
-
violations.push({
|
17339
|
-
id: 'aria-describedby-invalid',
|
17340
|
-
impact: 'moderate',
|
17341
|
-
nodes: 1,
|
17342
|
-
help: 'aria-describedby must reference existing elements',
|
17343
|
-
fix: `Fix or remove references to missing IDs: ${missingIds.join(', ')}`,
|
17344
|
-
elements: [element]
|
17345
|
-
});
|
17346
|
-
}
|
17347
|
-
}
|
17348
|
-
});
|
17349
|
-
return { violations };
|
17350
|
-
}
|
17351
|
-
async function checkLandmarkRoles(target) {
|
17352
|
-
const violations = [];
|
17353
|
-
// Check for missing main landmark
|
17354
|
-
const mainElements = target.querySelectorAll('main, [role="main"]');
|
17355
|
-
if (mainElements.length === 0) {
|
17356
|
-
violations.push({
|
17357
|
-
id: 'landmark-main-missing',
|
17358
|
-
impact: 'moderate',
|
17359
|
-
nodes: 0,
|
17360
|
-
help: 'Page should have a main landmark',
|
17361
|
-
fix: 'Add a <main> element or role="main" to identify the main content area'
|
17362
|
-
});
|
17363
|
-
}
|
17364
|
-
else if (mainElements.length > 1) {
|
17365
|
-
violations.push({
|
17366
|
-
id: 'landmark-main-multiple',
|
17367
|
-
impact: 'moderate',
|
17368
|
-
nodes: mainElements.length,
|
17369
|
-
help: 'Page should have only one main landmark',
|
17370
|
-
fix: 'Ensure only one main element or role="main" exists per page',
|
17371
|
-
elements: Array.from(mainElements)
|
17372
|
-
});
|
17373
|
-
}
|
17374
|
-
// Check for navigation landmarks without labels when multiple exist
|
17375
|
-
const navElements = target.querySelectorAll('nav, [role="navigation"]');
|
17376
|
-
if (navElements.length > 1) {
|
17377
|
-
navElements.forEach(nav => {
|
17378
|
-
const hasLabel = nav.hasAttribute('aria-label') ||
|
17379
|
-
nav.hasAttribute('aria-labelledby') ||
|
17380
|
-
nav.querySelector('h1, h2, h3, h4, h5, h6');
|
17381
|
-
if (!hasLabel) {
|
17382
|
-
violations.push({
|
17383
|
-
id: 'landmark-nav-unlabeled',
|
17384
|
-
impact: 'moderate',
|
17385
|
-
nodes: 1,
|
17386
|
-
help: 'Multiple navigation landmarks should be labeled',
|
17387
|
-
fix: 'Add aria-label or aria-labelledby to distinguish navigation areas',
|
17388
|
-
elements: [nav]
|
17389
|
-
});
|
17390
|
-
}
|
17391
|
-
});
|
17111
|
+
passes += target.querySelectorAll('img[alt]').length;
|
17392
17112
|
}
|
17393
|
-
|
17394
|
-
|
17395
|
-
|
17396
|
-
const violations = [];
|
17397
|
-
// Check for skip links in documents with navigation
|
17398
|
-
const navElements = target.querySelectorAll('nav, [role="navigation"]');
|
17399
|
-
const mainElement = target.querySelector('main, [role="main"]');
|
17400
|
-
if (navElements.length > 0 && mainElement) {
|
17401
|
-
const skipLinks = target.querySelectorAll('a[href^="#"]');
|
17402
|
-
const hasSkipToMain = Array.from(skipLinks).some(link => {
|
17403
|
-
const href = link.getAttribute('href');
|
17404
|
-
return href && (href === '#main' ||
|
17405
|
-
href === `#${mainElement.id}` ||
|
17406
|
-
link.textContent?.toLowerCase().includes('skip to main') ||
|
17407
|
-
link.textContent?.toLowerCase().includes('skip to content'));
|
17408
|
-
});
|
17409
|
-
if (!hasSkipToMain) {
|
17113
|
+
if (rules.includes('headings')) {
|
17114
|
+
const h1s = target.querySelectorAll('h1');
|
17115
|
+
if (h1s.length !== 1) {
|
17410
17116
|
violations.push({
|
17411
|
-
id: '
|
17412
|
-
impact: 'moderate',
|
17413
|
-
nodes: 0,
|
17414
|
-
help: 'Page with navigation should have skip links',
|
17415
|
-
fix: 'Add a skip link to the main content area for keyboard users'
|
17117
|
+
id: 'heading-structure', impact: 'moderate', nodes: h1s.length, help: 'Page should have exactly one h1'
|
17416
17118
|
});
|
17417
17119
|
}
|
17120
|
+
else
|
17121
|
+
passes++;
|
17418
17122
|
}
|
17419
|
-
|
17420
|
-
|
17421
|
-
|
17422
|
-
const href = firstFocusable.getAttribute('href');
|
17423
|
-
if (!href?.startsWith('#')) {
|
17123
|
+
if (rules.includes('forms')) {
|
17124
|
+
const unlabeled = target.querySelectorAll('input:not([aria-label]):not([aria-labelledby])');
|
17125
|
+
if (unlabeled.length > 0) {
|
17424
17126
|
violations.push({
|
17425
|
-
id: '
|
17426
|
-
impact: 'minor',
|
17427
|
-
nodes: 1,
|
17428
|
-
help: 'Skip links should be the first focusable elements',
|
17429
|
-
fix: 'Move skip links to the beginning of the document',
|
17430
|
-
elements: [firstFocusable]
|
17127
|
+
id: 'form-labels', impact: 'critical', nodes: unlabeled.length, help: 'Form inputs need labels'
|
17431
17128
|
});
|
17432
17129
|
}
|
17130
|
+
passes += target.querySelectorAll('input[aria-label], input[aria-labelledby]').length;
|
17433
17131
|
}
|
17434
|
-
|
17435
|
-
|
17436
|
-
|
17437
|
-
function calculateContrast(color1, color2) {
|
17438
|
-
// Convert colors to RGB values
|
17439
|
-
const rgb1 = parseColor(color1);
|
17440
|
-
const rgb2 = parseColor(color2);
|
17441
|
-
if (!rgb1 || !rgb2)
|
17442
|
-
return 4.5; // Fallback if parsing fails
|
17443
|
-
// Calculate relative luminance
|
17444
|
-
const l1 = getRelativeLuminance(rgb1);
|
17445
|
-
const l2 = getRelativeLuminance(rgb2);
|
17446
|
-
// Calculate contrast ratio
|
17447
|
-
const lighter = Math.max(l1, l2);
|
17448
|
-
const darker = Math.min(l1, l2);
|
17449
|
-
return (lighter + 0.05) / (darker + 0.05);
|
17450
|
-
}
|
17451
|
-
function parseColor(color) {
|
17452
|
-
// Handle rgb() format
|
17453
|
-
const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
17454
|
-
if (rgbMatch && rgbMatch[1] && rgbMatch[2] && rgbMatch[3]) {
|
17455
|
-
return {
|
17456
|
-
r: parseInt(rgbMatch[1], 10),
|
17457
|
-
g: parseInt(rgbMatch[2], 10),
|
17458
|
-
b: parseInt(rgbMatch[3], 10)
|
17459
|
-
};
|
17460
|
-
}
|
17461
|
-
// Handle hex format
|
17462
|
-
const hexMatch = color.match(/^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
|
17463
|
-
if (hexMatch && hexMatch[1] && hexMatch[2] && hexMatch[3]) {
|
17464
|
-
return {
|
17465
|
-
r: parseInt(hexMatch[1], 16),
|
17466
|
-
g: parseInt(hexMatch[2], 16),
|
17467
|
-
b: parseInt(hexMatch[3], 16)
|
17468
|
-
};
|
17469
|
-
}
|
17470
|
-
// Handle named colors (basic set)
|
17471
|
-
const namedColors = {
|
17472
|
-
'black': { r: 0, g: 0, b: 0 },
|
17473
|
-
'white': { r: 255, g: 255, b: 255 },
|
17474
|
-
'red': { r: 255, g: 0, b: 0 },
|
17475
|
-
'green': { r: 0, g: 128, b: 0 },
|
17476
|
-
'blue': { r: 0, g: 0, b: 255 }
|
17132
|
+
const report = {
|
17133
|
+
violations, passes, timestamp: Date.now(),
|
17134
|
+
url: typeof window !== 'undefined' ? window.location.href : ''
|
17477
17135
|
};
|
17478
|
-
|
17479
|
-
|
17480
|
-
|
17481
|
-
|
17482
|
-
// Convert to sRGB
|
17483
|
-
const rsRGB = r / 255;
|
17484
|
-
const gsRGB = g / 255;
|
17485
|
-
const bsRGB = b / 255;
|
17486
|
-
// Apply gamma correction
|
17487
|
-
const rLinear = rsRGB <= 0.03928 ? rsRGB / 12.92 : Math.pow((rsRGB + 0.055) / 1.055, 2.4);
|
17488
|
-
const gLinear = gsRGB <= 0.03928 ? gsRGB / 12.92 : Math.pow((gsRGB + 0.055) / 1.055, 2.4);
|
17489
|
-
const bLinear = bsRGB <= 0.03928 ? bsRGB / 12.92 : Math.pow((bsRGB + 0.055) / 1.055, 2.4);
|
17490
|
-
// Calculate relative luminance
|
17491
|
-
return 0.2126 * rLinear + 0.7152 * gLinear + 0.0722 * bLinear;
|
17492
|
-
}
|
17493
|
-
function outputToConsole(report) {
|
17494
|
-
console.group('🔍 Accessibility Audit Results');
|
17495
|
-
if (report.violations.length === 0) {
|
17496
|
-
console.log('✅ No accessibility violations found!');
|
17497
|
-
}
|
17498
|
-
else {
|
17499
|
-
console.log(`❌ Found ${report.violations.length} accessibility violations:`);
|
17500
|
-
report.violations.forEach(violation => {
|
17501
|
-
const emoji = violation.impact === 'critical' ? '🚨' :
|
17502
|
-
violation.impact === 'serious' ? '⚠️' :
|
17503
|
-
violation.impact === 'moderate' ? '⚡' : 'ℹ️';
|
17504
|
-
console.group(`${emoji} ${violation.help}`);
|
17505
|
-
console.log(`Impact: ${violation.impact}`);
|
17506
|
-
console.log(`Fix: ${violation.fix}`);
|
17507
|
-
if (violation.elements) {
|
17508
|
-
console.log('Elements:', violation.elements);
|
17509
|
-
}
|
17510
|
-
console.groupEnd();
|
17511
|
-
});
|
17512
|
-
}
|
17513
|
-
console.log(`✅ ${report.passes} checks passed`);
|
17514
|
-
if (report.incomplete > 0) {
|
17515
|
-
console.log(`⚠️ ${report.incomplete} checks incomplete`);
|
17136
|
+
if (format === 'console' && violations.length > 0) {
|
17137
|
+
console.group('🔍 A11y Audit Results');
|
17138
|
+
violations.forEach(v => console.warn(`${v.impact}: ${v.help}`));
|
17139
|
+
console.groupEnd();
|
17516
17140
|
}
|
17517
|
-
|
17518
|
-
}
|
17519
|
-
function openReportInBrowser(report) {
|
17520
|
-
const html = generateHTMLReport(report);
|
17521
|
-
const blob = new Blob([html], { type: 'text/html' });
|
17522
|
-
const url = URL.createObjectURL(blob);
|
17523
|
-
const newWindow = window.open(url, '_blank');
|
17524
|
-
if (!newWindow) {
|
17525
|
-
console.warn('Could not open report in new window. Please check popup blocker settings.');
|
17526
|
-
// Fallback: download the report
|
17527
|
-
const link = document.createElement('a');
|
17528
|
-
link.href = url;
|
17529
|
-
link.download = `proteus-a11y-report-${Date.now()}.html`;
|
17530
|
-
link.click();
|
17531
|
-
}
|
17532
|
-
// Clean up the blob URL after a delay
|
17533
|
-
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
17534
|
-
}
|
17535
|
-
function generateHTMLReport(report) {
|
17536
|
-
const violationsList = report.violations.map(violation => `
|
17537
|
-
<div class="violation violation--${violation.impact}">
|
17538
|
-
<h3>${violation.help}</h3>
|
17539
|
-
<p><strong>Impact:</strong> ${violation.impact}</p>
|
17540
|
-
<p><strong>Fix:</strong> ${violation.fix}</p>
|
17541
|
-
<p><strong>Affected elements:</strong> ${violation.nodes}</p>
|
17542
|
-
</div>
|
17543
|
-
`).join('');
|
17544
|
-
return `
|
17545
|
-
<!DOCTYPE html>
|
17546
|
-
<html lang="en">
|
17547
|
-
<head>
|
17548
|
-
<meta charset="UTF-8">
|
17549
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
17550
|
-
<title>ProteusJS Accessibility Report</title>
|
17551
|
-
<style>
|
17552
|
-
body { font-family: system-ui, sans-serif; margin: 2rem; line-height: 1.6; }
|
17553
|
-
.header { border-bottom: 2px solid #e5e7eb; padding-bottom: 1rem; margin-bottom: 2rem; }
|
17554
|
-
.summary { background: #f3f4f6; padding: 1rem; border-radius: 8px; margin-bottom: 2rem; }
|
17555
|
-
.violation { border-left: 4px solid #ef4444; padding: 1rem; margin-bottom: 1rem; background: #fef2f2; }
|
17556
|
-
.violation--critical { border-color: #dc2626; background: #fef2f2; }
|
17557
|
-
.violation--serious { border-color: #ea580c; background: #fff7ed; }
|
17558
|
-
.violation--moderate { border-color: #d97706; background: #fffbeb; }
|
17559
|
-
.violation--minor { border-color: #65a30d; background: #f7fee7; }
|
17560
|
-
.violation h3 { margin-top: 0; color: #374151; }
|
17561
|
-
.no-violations { text-align: center; color: #059669; font-size: 1.25rem; padding: 2rem; }
|
17562
|
-
</style>
|
17563
|
-
</head>
|
17564
|
-
<body>
|
17565
|
-
<div class="header">
|
17566
|
-
<h1>🌊 ProteusJS Accessibility Report</h1>
|
17567
|
-
<p>Generated on ${new Date(report.timestamp).toLocaleString()}</p>
|
17568
|
-
<p>URL: ${report.url}</p>
|
17569
|
-
</div>
|
17570
|
-
|
17571
|
-
<div class="summary">
|
17572
|
-
<h2>Summary</h2>
|
17573
|
-
<p><strong>Violations:</strong> ${report.violations.length}</p>
|
17574
|
-
<p><strong>Checks passed:</strong> ${report.passes}</p>
|
17575
|
-
<p><strong>Incomplete checks:</strong> ${report.incomplete}</p>
|
17576
|
-
</div>
|
17577
|
-
|
17578
|
-
${report.violations.length === 0 ?
|
17579
|
-
'<div class="no-violations">✅ No accessibility violations found!</div>' :
|
17580
|
-
`<h2>Violations</h2>${violationsList}`}
|
17581
|
-
</body>
|
17582
|
-
</html>`;
|
17141
|
+
return report;
|
17583
17142
|
}
|
17584
|
-
|
17585
|
-
var index$4 = {
|
17586
|
-
audit
|
17587
|
-
};
|
17143
|
+
var index$4 = { audit };
|
17588
17144
|
|
17589
17145
|
var index$5 = /*#__PURE__*/Object.freeze({
|
17590
17146
|
__proto__: null,
|
@@ -17594,450 +17150,116 @@ var index$5 = /*#__PURE__*/Object.freeze({
|
|
17594
17150
|
|
17595
17151
|
/**
|
17596
17152
|
* @sc4rfurryx/proteusjs/a11y-primitives
|
17597
|
-
*
|
17153
|
+
* Lightweight accessibility patterns
|
17598
17154
|
*
|
17599
17155
|
* @version 1.1.0
|
17600
17156
|
* @author sc4rfurry
|
17601
17157
|
* @license MIT
|
17602
17158
|
*/
|
17603
|
-
/**
|
17604
|
-
* Dialog primitive with proper ARIA and focus management
|
17605
|
-
*/
|
17606
17159
|
function dialog(root, opts = {}) {
|
17607
|
-
const
|
17608
|
-
if (!
|
17609
|
-
throw new Error('Dialog
|
17160
|
+
const el = typeof root === 'string' ? document.querySelector(root) : root;
|
17161
|
+
if (!el)
|
17162
|
+
throw new Error('Dialog element not found');
|
17610
17163
|
const { modal = true, restoreFocus = true } = opts;
|
17611
|
-
|
17612
|
-
const setup = () => {
|
17613
|
-
rootEl.setAttribute('role', 'dialog');
|
17614
|
-
if (modal) {
|
17615
|
-
rootEl.setAttribute('aria-modal', 'true');
|
17616
|
-
}
|
17617
|
-
// Ensure dialog is initially hidden
|
17618
|
-
if (!rootEl.hasAttribute('hidden')) {
|
17619
|
-
rootEl.setAttribute('hidden', '');
|
17620
|
-
}
|
17621
|
-
};
|
17622
|
-
const handleKeyDown = (e) => {
|
17623
|
-
if (e.key === 'Escape' && isOpen) ;
|
17624
|
-
};
|
17625
|
-
setup();
|
17626
|
-
document.addEventListener('keydown', handleKeyDown);
|
17627
|
-
return {
|
17628
|
-
destroy: () => {
|
17629
|
-
document.removeEventListener('keydown', handleKeyDown);
|
17630
|
-
}
|
17164
|
+
const close = () => {
|
17631
17165
|
};
|
17166
|
+
return { destroy: () => close() };
|
17632
17167
|
}
|
17633
|
-
|
17634
|
-
|
17635
|
-
|
17636
|
-
function tooltip(trigger, panel, opts = {}) {
|
17637
|
-
const triggerEl = typeof trigger === 'string' ? document.querySelector(trigger) : trigger;
|
17638
|
-
const panelEl = typeof panel === 'string' ? document.querySelector(panel) : panel;
|
17639
|
-
if (!triggerEl || !panelEl) {
|
17640
|
-
throw new Error('Both trigger and panel elements must exist');
|
17641
|
-
}
|
17642
|
-
const { delay = 500 } = opts;
|
17643
|
-
let timeoutId = null;
|
17644
|
-
let isVisible = false;
|
17645
|
-
const setup = () => {
|
17646
|
-
const tooltipId = panelEl.id || `tooltip-${Math.random().toString(36).substring(2, 11)}`;
|
17647
|
-
panelEl.id = tooltipId;
|
17648
|
-
panelEl.setAttribute('role', 'tooltip');
|
17649
|
-
triggerEl.setAttribute('aria-describedby', tooltipId);
|
17650
|
-
// Initially hidden
|
17651
|
-
panelEl.style.display = 'none';
|
17652
|
-
};
|
17168
|
+
function tooltip(trigger, content, opts = {}) {
|
17169
|
+
const { delay = 300 } = opts;
|
17170
|
+
let timeout;
|
17653
17171
|
const show = () => {
|
17654
|
-
|
17655
|
-
|
17656
|
-
|
17657
|
-
|
17172
|
+
clearTimeout(timeout);
|
17173
|
+
timeout = window.setTimeout(() => {
|
17174
|
+
content.setAttribute('role', 'tooltip');
|
17175
|
+
trigger.setAttribute('aria-describedby', content.id || 'tooltip');
|
17176
|
+
content.style.display = 'block';
|
17177
|
+
}, delay);
|
17658
17178
|
};
|
17659
17179
|
const hide = () => {
|
17660
|
-
|
17661
|
-
|
17662
|
-
|
17663
|
-
isVisible = false;
|
17180
|
+
clearTimeout(timeout);
|
17181
|
+
content.style.display = 'none';
|
17182
|
+
trigger.removeAttribute('aria-describedby');
|
17664
17183
|
};
|
17665
|
-
|
17666
|
-
|
17667
|
-
|
17668
|
-
|
17669
|
-
};
|
17670
|
-
const handleMouseLeave = () => {
|
17671
|
-
if (timeoutId) {
|
17672
|
-
clearTimeout(timeoutId);
|
17673
|
-
timeoutId = null;
|
17674
|
-
}
|
17675
|
-
hide();
|
17676
|
-
};
|
17677
|
-
const handleFocus = () => {
|
17678
|
-
show();
|
17679
|
-
};
|
17680
|
-
const handleBlur = () => {
|
17681
|
-
hide();
|
17682
|
-
};
|
17683
|
-
setup();
|
17684
|
-
triggerEl.addEventListener('mouseenter', handleMouseEnter);
|
17685
|
-
triggerEl.addEventListener('mouseleave', handleMouseLeave);
|
17686
|
-
triggerEl.addEventListener('focus', handleFocus);
|
17687
|
-
triggerEl.addEventListener('blur', handleBlur);
|
17184
|
+
trigger.addEventListener('mouseenter', show);
|
17185
|
+
trigger.addEventListener('mouseleave', hide);
|
17186
|
+
trigger.addEventListener('focus', show);
|
17187
|
+
trigger.addEventListener('blur', hide);
|
17688
17188
|
return {
|
17689
17189
|
destroy: () => {
|
17690
|
-
|
17691
|
-
|
17692
|
-
|
17693
|
-
|
17694
|
-
|
17695
|
-
triggerEl.removeEventListener('blur', handleBlur);
|
17696
|
-
hide();
|
17190
|
+
clearTimeout(timeout);
|
17191
|
+
trigger.removeEventListener('mouseenter', show);
|
17192
|
+
trigger.removeEventListener('mouseleave', hide);
|
17193
|
+
trigger.removeEventListener('focus', show);
|
17194
|
+
trigger.removeEventListener('blur', hide);
|
17697
17195
|
}
|
17698
17196
|
};
|
17699
17197
|
}
|
17700
|
-
|
17701
|
-
|
17702
|
-
|
17703
|
-
|
17704
|
-
|
17705
|
-
if (!rootEl)
|
17706
|
-
throw new Error('Listbox root element not found');
|
17707
|
-
const { multiselect = false } = opts;
|
17708
|
-
let currentIndex = -1;
|
17709
|
-
const setup = () => {
|
17710
|
-
rootEl.setAttribute('role', 'listbox');
|
17711
|
-
if (multiselect) {
|
17712
|
-
rootEl.setAttribute('aria-multiselectable', 'true');
|
17713
|
-
}
|
17714
|
-
// Set up options
|
17715
|
-
const options = rootEl.querySelectorAll('[role="option"]');
|
17716
|
-
options.forEach((option, _index) => {
|
17717
|
-
option.setAttribute('aria-selected', 'false');
|
17718
|
-
option.setAttribute('tabindex', '-1');
|
17719
|
-
});
|
17720
|
-
if (options.length > 0) {
|
17721
|
-
options[0]?.setAttribute('tabindex', '0');
|
17722
|
-
currentIndex = 0;
|
17723
|
-
}
|
17724
|
-
};
|
17725
|
-
const getOptions = () => rootEl.querySelectorAll('[role="option"]');
|
17726
|
-
const setCurrentIndex = (index) => {
|
17727
|
-
const options = getOptions();
|
17728
|
-
if (index < 0 || index >= options.length)
|
17729
|
-
return;
|
17730
|
-
// Remove tabindex from all options
|
17731
|
-
options.forEach(option => option.setAttribute('tabindex', '-1'));
|
17732
|
-
// Set current option
|
17733
|
-
currentIndex = index;
|
17734
|
-
options[currentIndex]?.setAttribute('tabindex', '0');
|
17735
|
-
options[currentIndex]?.focus();
|
17736
|
-
};
|
17737
|
-
const selectOption = (index) => {
|
17738
|
-
const options = getOptions();
|
17739
|
-
if (index < 0 || index >= options.length)
|
17740
|
-
return;
|
17741
|
-
if (multiselect) {
|
17742
|
-
const isSelected = options[index]?.getAttribute('aria-selected') === 'true';
|
17743
|
-
options[index]?.setAttribute('aria-selected', (!isSelected).toString());
|
17744
|
-
}
|
17745
|
-
else {
|
17746
|
-
// Single select - clear all others
|
17747
|
-
options.forEach(option => option.setAttribute('aria-selected', 'false'));
|
17748
|
-
options[index]?.setAttribute('aria-selected', 'true');
|
17749
|
-
}
|
17750
|
-
};
|
17751
|
-
const handleKeyDown = (e) => {
|
17752
|
-
const keyEvent = e;
|
17753
|
-
const options = getOptions();
|
17754
|
-
switch (keyEvent.key) {
|
17755
|
-
case 'ArrowDown':
|
17756
|
-
keyEvent.preventDefault();
|
17757
|
-
setCurrentIndex(Math.min(currentIndex + 1, options.length - 1));
|
17758
|
-
break;
|
17759
|
-
case 'ArrowUp':
|
17760
|
-
keyEvent.preventDefault();
|
17761
|
-
setCurrentIndex(Math.max(currentIndex - 1, 0));
|
17762
|
-
break;
|
17763
|
-
case 'Home':
|
17764
|
-
keyEvent.preventDefault();
|
17765
|
-
setCurrentIndex(0);
|
17766
|
-
break;
|
17767
|
-
case 'End':
|
17768
|
-
keyEvent.preventDefault();
|
17769
|
-
setCurrentIndex(options.length - 1);
|
17770
|
-
break;
|
17771
|
-
case 'Enter':
|
17772
|
-
case ' ':
|
17773
|
-
keyEvent.preventDefault();
|
17774
|
-
selectOption(currentIndex);
|
17775
|
-
break;
|
17776
|
-
}
|
17777
|
-
};
|
17778
|
-
const handleClick = (e) => {
|
17779
|
-
const target = e.target;
|
17780
|
-
const option = target.closest('[role="option"]');
|
17781
|
-
if (!option)
|
17198
|
+
function focusTrap(container) {
|
17199
|
+
const focusable = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
17200
|
+
const activate = () => {
|
17201
|
+
const elements = container.querySelectorAll(focusable);
|
17202
|
+
if (elements.length === 0)
|
17782
17203
|
return;
|
17783
|
-
const
|
17784
|
-
const
|
17785
|
-
|
17786
|
-
|
17787
|
-
|
17788
|
-
|
17789
|
-
|
17790
|
-
|
17791
|
-
rootEl.addEventListener('keydown', handleKeyDown);
|
17792
|
-
rootEl.addEventListener('click', handleClick);
|
17793
|
-
return {
|
17794
|
-
destroy: () => {
|
17795
|
-
rootEl.removeEventListener('keydown', handleKeyDown);
|
17796
|
-
rootEl.removeEventListener('click', handleClick);
|
17797
|
-
}
|
17798
|
-
};
|
17799
|
-
}
|
17800
|
-
/**
|
17801
|
-
* Combobox primitive with filtering and multiselect
|
17802
|
-
*/
|
17803
|
-
function combobox(root, opts = {}) {
|
17804
|
-
const rootEl = typeof root === 'string' ? document.querySelector(root) : root;
|
17805
|
-
if (!rootEl)
|
17806
|
-
throw new Error('Combobox root element not found');
|
17807
|
-
const { multiselect = false, filtering: _filtering } = opts;
|
17808
|
-
let isOpen = false;
|
17809
|
-
const setup = () => {
|
17810
|
-
rootEl.setAttribute('role', 'combobox');
|
17811
|
-
rootEl.setAttribute('aria-expanded', 'false');
|
17812
|
-
if (multiselect) {
|
17813
|
-
rootEl.setAttribute('aria-multiselectable', 'true');
|
17814
|
-
}
|
17815
|
-
};
|
17816
|
-
const handleKeyDown = (e) => {
|
17817
|
-
const keyEvent = e;
|
17818
|
-
switch (keyEvent.key) {
|
17819
|
-
case 'ArrowDown':
|
17820
|
-
keyEvent.preventDefault();
|
17821
|
-
if (!isOpen) {
|
17822
|
-
isOpen = true;
|
17823
|
-
rootEl.setAttribute('aria-expanded', 'true');
|
17824
|
-
}
|
17825
|
-
// Navigate options logic would go here
|
17826
|
-
break;
|
17827
|
-
case 'Escape':
|
17828
|
-
keyEvent.preventDefault();
|
17829
|
-
isOpen = false;
|
17830
|
-
rootEl.setAttribute('aria-expanded', 'false');
|
17831
|
-
break;
|
17832
|
-
}
|
17833
|
-
};
|
17834
|
-
setup();
|
17835
|
-
rootEl.addEventListener('keydown', handleKeyDown);
|
17836
|
-
return {
|
17837
|
-
destroy: () => {
|
17838
|
-
rootEl.removeEventListener('keydown', handleKeyDown);
|
17839
|
-
}
|
17840
|
-
};
|
17841
|
-
}
|
17842
|
-
/**
|
17843
|
-
* Tabs primitive with keyboard navigation
|
17844
|
-
*/
|
17845
|
-
function tabs(root) {
|
17846
|
-
const rootEl = typeof root === 'string' ? document.querySelector(root) : root;
|
17847
|
-
if (!rootEl)
|
17848
|
-
throw new Error('Tabs root element not found');
|
17849
|
-
let currentIndex = 0;
|
17850
|
-
const setup = () => {
|
17851
|
-
const tabList = rootEl.querySelector('[role="tablist"]');
|
17852
|
-
const tabs = rootEl.querySelectorAll('[role="tab"]');
|
17853
|
-
const panels = rootEl.querySelectorAll('[role="tabpanel"]');
|
17854
|
-
if (!tabList) {
|
17855
|
-
rootEl.setAttribute('role', 'tablist');
|
17856
|
-
}
|
17857
|
-
tabs.forEach((tab, index) => {
|
17858
|
-
tab.setAttribute('tabindex', index === 0 ? '0' : '-1');
|
17859
|
-
tab.setAttribute('aria-selected', index === 0 ? 'true' : 'false');
|
17860
|
-
});
|
17861
|
-
panels.forEach((panel, index) => {
|
17862
|
-
panel.setAttribute('hidden', index === 0 ? '' : 'true');
|
17863
|
-
});
|
17864
|
-
};
|
17865
|
-
const handleKeyDown = (e) => {
|
17866
|
-
const keyEvent = e;
|
17867
|
-
const tabs = Array.from(rootEl.querySelectorAll('[role="tab"]'));
|
17868
|
-
switch (keyEvent.key) {
|
17869
|
-
case 'ArrowRight':
|
17870
|
-
keyEvent.preventDefault();
|
17871
|
-
currentIndex = (currentIndex + 1) % tabs.length;
|
17872
|
-
activateTab(currentIndex);
|
17873
|
-
break;
|
17874
|
-
case 'ArrowLeft':
|
17875
|
-
keyEvent.preventDefault();
|
17876
|
-
currentIndex = currentIndex === 0 ? tabs.length - 1 : currentIndex - 1;
|
17877
|
-
activateTab(currentIndex);
|
17878
|
-
break;
|
17879
|
-
}
|
17880
|
-
};
|
17881
|
-
const activateTab = (index) => {
|
17882
|
-
const tabs = rootEl.querySelectorAll('[role="tab"]');
|
17883
|
-
const panels = rootEl.querySelectorAll('[role="tabpanel"]');
|
17884
|
-
tabs.forEach((tab, i) => {
|
17885
|
-
tab.setAttribute('tabindex', i === index ? '0' : '-1');
|
17886
|
-
tab.setAttribute('aria-selected', i === index ? 'true' : 'false');
|
17887
|
-
if (i === index) {
|
17888
|
-
tab.focus();
|
17889
|
-
}
|
17890
|
-
});
|
17891
|
-
panels.forEach((panel, i) => {
|
17892
|
-
if (i === index) {
|
17893
|
-
panel.removeAttribute('hidden');
|
17204
|
+
const first = elements[0];
|
17205
|
+
const last = elements[elements.length - 1];
|
17206
|
+
const handleTab = (e) => {
|
17207
|
+
if (e.key !== 'Tab')
|
17208
|
+
return;
|
17209
|
+
if (e.shiftKey && document.activeElement === first) {
|
17210
|
+
e.preventDefault();
|
17211
|
+
last.focus();
|
17894
17212
|
}
|
17895
|
-
else {
|
17896
|
-
|
17213
|
+
else if (!e.shiftKey && document.activeElement === last) {
|
17214
|
+
e.preventDefault();
|
17215
|
+
first.focus();
|
17897
17216
|
}
|
17898
|
-
}
|
17217
|
+
};
|
17218
|
+
container.addEventListener('keydown', handleTab);
|
17219
|
+
first.focus();
|
17220
|
+
return () => container.removeEventListener('keydown', handleTab);
|
17899
17221
|
};
|
17900
|
-
|
17901
|
-
rootEl.addEventListener('keydown', handleKeyDown);
|
17222
|
+
let deactivate = () => { };
|
17902
17223
|
return {
|
17903
|
-
|
17904
|
-
|
17905
|
-
}
|
17224
|
+
activate: () => { deactivate = activate() || (() => { }); },
|
17225
|
+
deactivate: () => deactivate()
|
17906
17226
|
};
|
17907
17227
|
}
|
17908
|
-
|
17909
|
-
|
17910
|
-
|
17911
|
-
|
17912
|
-
|
17913
|
-
if (!rootEl)
|
17914
|
-
throw new Error('Menu root element not found');
|
17915
|
-
let currentIndex = -1;
|
17916
|
-
const setup = () => {
|
17917
|
-
rootEl.setAttribute('role', 'menu');
|
17918
|
-
const items = rootEl.querySelectorAll('[role="menuitem"]');
|
17919
|
-
items.forEach((item, index) => {
|
17920
|
-
item.setAttribute('tabindex', index === 0 ? '0' : '-1');
|
17921
|
-
});
|
17922
|
-
if (items.length > 0) {
|
17923
|
-
currentIndex = 0;
|
17924
|
-
}
|
17925
|
-
};
|
17926
|
-
const handleKeyDown = (e) => {
|
17927
|
-
const keyEvent = e;
|
17928
|
-
const items = Array.from(rootEl.querySelectorAll('[role="menuitem"]'));
|
17929
|
-
switch (keyEvent.key) {
|
17228
|
+
function menu(container) {
|
17229
|
+
const items = container.querySelectorAll('[role="menuitem"]');
|
17230
|
+
let currentIndex = 0;
|
17231
|
+
const navigate = (e) => {
|
17232
|
+
switch (e.key) {
|
17930
17233
|
case 'ArrowDown':
|
17931
|
-
|
17234
|
+
e.preventDefault();
|
17932
17235
|
currentIndex = (currentIndex + 1) % items.length;
|
17933
|
-
|
17236
|
+
items[currentIndex].focus();
|
17934
17237
|
break;
|
17935
17238
|
case 'ArrowUp':
|
17936
|
-
|
17239
|
+
e.preventDefault();
|
17937
17240
|
currentIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1;
|
17938
|
-
|
17939
|
-
break;
|
17940
|
-
case 'Enter':
|
17941
|
-
case ' ':
|
17942
|
-
keyEvent.preventDefault();
|
17943
|
-
if (items[currentIndex]) {
|
17944
|
-
items[currentIndex].click();
|
17945
|
-
}
|
17241
|
+
items[currentIndex].focus();
|
17946
17242
|
break;
|
17947
|
-
|
17948
|
-
};
|
17949
|
-
const setCurrentItem = (index) => {
|
17950
|
-
const items = rootEl.querySelectorAll('[role="menuitem"]');
|
17951
|
-
items.forEach((item, i) => {
|
17952
|
-
item.setAttribute('tabindex', i === index ? '0' : '-1');
|
17953
|
-
if (i === index) {
|
17954
|
-
item.focus();
|
17955
|
-
}
|
17956
|
-
});
|
17957
|
-
};
|
17958
|
-
setup();
|
17959
|
-
rootEl.addEventListener('keydown', handleKeyDown);
|
17960
|
-
return {
|
17961
|
-
destroy: () => {
|
17962
|
-
rootEl.removeEventListener('keydown', handleKeyDown);
|
17963
|
-
}
|
17964
|
-
};
|
17965
|
-
}
|
17966
|
-
/**
|
17967
|
-
* Focus trap utility
|
17968
|
-
*/
|
17969
|
-
function focusTrap(root) {
|
17970
|
-
const rootEl = typeof root === 'string' ? document.querySelector(root) : root;
|
17971
|
-
if (!rootEl)
|
17972
|
-
throw new Error('Focus trap root element not found');
|
17973
|
-
let isActive = false;
|
17974
|
-
let focusableElements = [];
|
17975
|
-
const updateFocusableElements = () => {
|
17976
|
-
const selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
17977
|
-
focusableElements = Array.from(rootEl.querySelectorAll(selector));
|
17978
|
-
};
|
17979
|
-
const handleKeyDown = (e) => {
|
17980
|
-
if (!isActive || e.key !== 'Tab')
|
17981
|
-
return;
|
17982
|
-
updateFocusableElements();
|
17983
|
-
if (focusableElements.length === 0)
|
17984
|
-
return;
|
17985
|
-
const firstElement = focusableElements[0];
|
17986
|
-
const lastElement = focusableElements[focusableElements.length - 1];
|
17987
|
-
if (e.shiftKey) {
|
17988
|
-
if (document.activeElement === firstElement) {
|
17989
|
-
e.preventDefault();
|
17990
|
-
lastElement.focus();
|
17991
|
-
}
|
17992
|
-
}
|
17993
|
-
else {
|
17994
|
-
if (document.activeElement === lastElement) {
|
17243
|
+
case 'Escape':
|
17995
17244
|
e.preventDefault();
|
17996
|
-
|
17997
|
-
|
17998
|
-
}
|
17999
|
-
};
|
18000
|
-
const activate = () => {
|
18001
|
-
if (isActive)
|
18002
|
-
return;
|
18003
|
-
isActive = true;
|
18004
|
-
updateFocusableElements();
|
18005
|
-
if (focusableElements.length > 0) {
|
18006
|
-
focusableElements[0].focus();
|
17245
|
+
container.dispatchEvent(new CustomEvent('menu:close'));
|
17246
|
+
break;
|
18007
17247
|
}
|
18008
|
-
document.addEventListener('keydown', handleKeyDown);
|
18009
|
-
};
|
18010
|
-
const deactivate = () => {
|
18011
|
-
if (!isActive)
|
18012
|
-
return;
|
18013
|
-
isActive = false;
|
18014
|
-
document.removeEventListener('keydown', handleKeyDown);
|
18015
17248
|
};
|
17249
|
+
container.setAttribute('role', 'menu');
|
17250
|
+
container.addEventListener('keydown', navigate);
|
18016
17251
|
return {
|
18017
|
-
|
18018
|
-
deactivate
|
17252
|
+
destroy: () => container.removeEventListener('keydown', navigate)
|
18019
17253
|
};
|
18020
17254
|
}
|
18021
|
-
|
18022
|
-
var index$2 = {
|
18023
|
-
dialog,
|
18024
|
-
tooltip,
|
18025
|
-
combobox,
|
18026
|
-
listbox,
|
18027
|
-
tabs,
|
18028
|
-
menu,
|
18029
|
-
focusTrap
|
18030
|
-
};
|
17255
|
+
var index$2 = { dialog, tooltip, focusTrap, menu };
|
18031
17256
|
|
18032
17257
|
var index$3 = /*#__PURE__*/Object.freeze({
|
18033
17258
|
__proto__: null,
|
18034
|
-
combobox: combobox,
|
18035
17259
|
default: index$2,
|
18036
17260
|
dialog: dialog,
|
18037
17261
|
focusTrap: focusTrap,
|
18038
|
-
listbox: listbox,
|
18039
17262
|
menu: menu,
|
18040
|
-
tabs: tabs,
|
18041
17263
|
tooltip: tooltip
|
18042
17264
|
});
|
18043
17265
|
|