1/* eslint-disable */ 2this.BX = this.BX || {}; 3this.BX.UI = this.BX.UI || {}; 4(function (exports, main_core) { 5 'use strict'; 6 7 const focusableElements = ['a[href]', 'input:not([disabled]):not([type=hidden])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'area[href]', 'summary', 'iframe', 'object', 'embed', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable="false"])', '[tabindex]:not([disabled]):not([hidden])']; 8 const FOCUSABLE_SELECTOR = focusableElements.join(','); 9 10 const supportsCheckVisibility = !main_core.Type.isUndefined(window.Element) && 'checkVisibility' in window.Element.prototype; 11 class InteractivityChecker { 12 static isDisabled(element) { 13 return main_core.Type.isElementNode(element) && (element.hasAttribute('disabled') || element.getAttribute('aria-disabled') === 'true'); 14 } 15 static isVisible(element) { 16 if (!main_core.Type.isElementNode(element) || !element.isConnected) { 17 return false; 18 } 19 if (supportsCheckVisibility) { 20 return element.checkVisibility({ 21 visibilityProperty: true, 22 opacityProperty: true 23 }); 24 } 25 const hasGeometry = element.offsetWidth > 0 || element.offsetHeight > 0 || element.getClientRects().length > 0; 26 return hasGeometry && getComputedStyle(element).visibility === 'visible'; 27 } 28 static isTabbable(element) { 29 if (!this.isFocusable(element)) { 30 return false; 31 } 32 return !this.hasNegativeTabIndex(element); 33 } 34 static hasNegativeTabIndex(element) { 35 const tabindex = element?.getAttribute('tabindex'); 36 if (tabindex === null) { 37 return false; 38 } 39 return Number.parseInt(tabindex, 10) < 0; 40 } 41 static isFocusable(element) { 42 if (!main_core.Type.isElementNode(element) || element.closest('[inert]') || !element.isConnected) { 43 return false; 44 } 45 return element.matches(FOCUSABLE_SELECTOR) && this.isVisible(element); 46 } 47 } 48 49 const STORAGE_KEY = 'bx:a11y:debug'; 50 const ALL = '*'; 51 const CATEGORY_LABELS = { 52 'focus-monitor': 'FocusMonitor', 53 'focus-trap': 'FocusTrap', 54 'focus-zone': 'FocusZone', 55 'input-modality': 'InputModality', 56 'live-announcer': 'LiveAnnouncer' 57 }; 58 class AccessibilityLogger { 59 static #categories = new Set(); 60 static #all = false; 61 static #initialized = false; 62 static #init() { 63 if (this.#initialized) { 64 return; 65 } 66 this.#initialized = true; 67 try { 68 const raw = localStorage.getItem(STORAGE_KEY); 69 if (raw === ALL) { 70 this.#all = true; 71 } else if (raw) { 72 const parsed = JSON.parse(raw); 73 if (Array.isArray(parsed)) { 74 for (const category of parsed) { 75 this.#categories.add(category); 76 } 77 } 78 } 79 } catch { 80 } 81 } 82 static enable(category) { 83 this.#init(); 84 if (category) { 85 this.#categories.add(category); 86 } else { 87 this.#all = true; 88 } 89 this.#save(); 90 } 91 static disable(category) { 92 this.#init(); 93 if (category) { 94 this.#categories.delete(category); 95 } else { 96 this.#all = false; 97 this.#categories.clear(); 98 } 99 this.#save(); 100 } 101 static isEnabled(category) { 102 this.#init(); 103 return this.#all || this.#categories.has(category); 104 } 105 static log(category, message, ...args) { 106 if (!this.isEnabled(category)) { 107 return; 108 } 109 const label = CATEGORY_LABELS[category] || category; 110 console.log(`%c[${label}]%c ${message}`, 'color: #0075ff; font-weight: bold;', 'color: inherit; font-weight: normal;', ...args); 111 } 112 static logNode(category, message, node) { 113 if (!this.isEnabled(category) || !main_core.Type.isElementNode(node)) { 114 return; 115 } 116 const tag = node.tagName.toLowerCase(); 117 const classes = node.className ? `.${[...node.classList].join('.')}` : ''; 118 const id = node.id ? `#${node.id}` : ''; 119 const selector = `${tag}${classes}${id}`; 120 const role = node.getAttribute('role'); 121 const labelledBy = node.getAttribute('aria-labelledby'); 122 const labelledByText = labelledBy ? labelledBy.split(/\s+/).map(labelId => document.getElementById(labelId)?.textContent?.trim()).filter(Boolean).join(' ') : ''; 123 const ariaLabel = (node.getAttribute('aria-label') || labelledByText || node.textContent.trim().slice(0, 30
123) || 'â').replaceAll(/\s+/g, ' '); 124 const attrs = [role && `role="${role}"`, node.getAttribute('aria-expanded') !== null && `aria-expanded="${node.getAttribute('aria-expanded')}"`, node.getAttribute('href') && `href="${node.getAttribute('href')}"`, node.getAttribute('tabindex') && `tabindex="${node.getAttribute('tabindex')}"`].filter(Boolean).join(' '); 125 const label = CATEGORY_LABELS[category] || category; 126 const parts = [`%c[${label}]%c ${message}`, `%c${selector}%c`, `"%c${ariaLabel}%c"`, attrs && `%c${attrs}%c`].filter(Boolean).join(' '); 127 console.log(parts, 'color: #0075ff; font-weight: bold;', 128 'color: inherit; font-weight: normal;', 129 'color: #0075ff; font-weight: bold;', 130 'color: inherit; font-weight: normal;', 131 'color: #e36209; font-style: italic;', 132 'color: inherit; font-style: normal;', 133 ...(attrs ? ['color: #9b59b6;', 134 'color: inherit;' 135 ] : [])); 136 } 137 static warn(category, message, ...args) { 138 if (!this.isEnabled(category)) { 139 return; 140 } 141 const label = CATEGORY_LABELS[category] || category; 142 console.warn(`[${label}] ${message}`, ...args); 143 } 144 static #save() { 145 try { 146 if (this.#all) { 147 localStorage.setItem(STORAGE_KEY, ALL); 148 } else if (this.#categories.size > 0) { 149 localStorage.setItem(STORAGE_KEY, JSON.stringify([...this.#categories])); 150 } else { 151 localStorage.removeItem(STORAGE_KEY); 152 } 153 } catch { 154 } 155 } 156 } 157 158 const RESTORE_FOCUS_EVENT = 'a11y:restore-focus'; 159 class FocusNavigator { 160 static get FOCUSABLE_SELECTOR() { 161 return FOCUSABLE_SELECTOR; 162 } 163 static getFirst(container, options) { 164 return this.#traverse(container, 'first', options); 165 } 166 static getLast(container, options) { 167 return this.#traverse(container, 'last', options); 168 } 169 static getNext(container, options) { 170 return this.#traverse(container, 'next', options); 171 } 172 static getPrevious(container, options) { 173 return this.#traverse(container, 'previous', options); 174 } 175 static focusFirst(container, options) { 176 return this.focusTarget(this.#traverse(container, 'first', options), options); 177 } 178 static focusLast(container, options) { 179 return this.focusTarget(this.#traverse(container, 'last', options), options); 180 } 181 static focusNext(container, options) { 182 return this.focusTarget(this.#traverse(container, 'next', options), options); 183 } 184 static focusPrevious(container, options) { 185 return this.focusTarget(this.#traverse(container, 'previous', options), options); 186 } 187 static focusContainer(container, options) { 188 if (!container.hasAttribute('tabindex')) { 189 container.setAttribute('tabindex', '-1'); 190 } 191 this.focusTarget(container, options); 192 return container; 193 } 194 static getActiveElement(node) { 195 const { 196 activeElement 197 } = this.#getDocument(node); 198 if (activeElement?.tagName === 'IFRAME' && activeElement.contentDocument?.body) { 199 const iframe = activeElement; 200 return this.getActiveElement(iframe.contentDocument?.body); 201 } 202 if (main_core.Type.isElementNode(activeElement)) { 203 return activeElement; 204 } 205 return null; 206 } 207 static createWalker(container, options) { 208 const { 209 tabbableOnly = false, 210 accept 211 } = options ?? {}; 212 const ownerDocument = container.ownerDocument ?? document; 213 return ownerDocument.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, { 214 acceptNode(node) { 215 const el = node; 216 if (!InteractivityChecker.isFocusable(el)) { 217 return NodeFilter.FILTER_SKIP; 218 } 219 if (tabbableOnly && el.tabIndex < 0) { 220 return NodeFilter.FILTER_SKIP; 221 } 222 if (accept && !accept(el) && el.tagName !== 'IFRAME') { 223 return NodeFilter.FILTER_SKIP; 224 } 225 return NodeFilter.FILTER_ACCEPT; 226 } 227 }); 228 } 229 static *#walk(container, options) { 230 const walker = this.createWalker(container, options); 231 for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) { 232 const el = node; 233 if (el.tagName === 'IFRAME') { 234 try { 235 const iframe = el; 236 const body = iframe.contentDocument?.body; 237 if (body) { 238 yield* this.#walk(body, options); 239 continue; 240 } 241 } catch { 242 } 243 } 244 yield el; 245 } 246 } 247 static #traverse(container, direction, options) { 248 const walkerOptions = { 249 tabbableOnly: options?.tabbableOnly ?? true, 250 accept: options?.accept 251 }; 252 switch (direction) { 253 case 'first': 254 { 255 return this.#walk(container, walkerOptions).next().value ?? null; 256 } 257 case 'last': 258 { 259 let last = null; 260 for (const el of this.#walk(container, walkerOptions)) { 261 last = el; 262 } 263 return last; 264 } 265 case 'next': 266 { 267 const from = this.#resolveFrom(container, options); 268 let found = from === null; 269 for (const el of this.#walk(container, walkerOptions)) { 270 if (found) { 271 return el; 272 } 273 if (el === from) { 274 found = true; 275 } 276 } 277 if (options?.wrap) { 278 return this.#walk(container, walkerOptions).next().value ?? null; 279 } 280 return null; 281 } 282 case 'previous': 283 { 284 const from = this.#resolveFrom(container, options); 285 let prev = null; 286 for (const el of this.#walk(container, walkerOptions)) { 287 if (el === from) { 288 break; 289 } 290 prev = el; 291 } 292 if (prev) { 293 return prev; 294 } 295 if (options?.wrap) { 296 return this.#traverse(container, 'last', options); 297 } 298 return null; 299 } 300 } 301 return null; 302 } 303 static focusBySelector(container, selector, options) { 304 const { 305 accept: outerAccept, 306 ...rest 307 } = options ?? {}; 308 const accept = el => {
309 return el.matches(selector) && (outerAccept ? outerAccept(el) : true); 310 }; 311 const from = this.#resolveFrom(container, options); 312 let found = from === null; 313 for (const el of this.#walk(container, { 314 ...rest, 315 accept 316 })) { 317 if (found) { 318 return this.focusTarget(el, options); 319 } 320 if (el === from) { 321 found = true; 322 } 323 } 324 if (options?.wrap) { 325 const el = this.#walk(container, { 326 ...rest, 327 accept 328 }).next().value; 329 if (el) { 330 return this.focusTarget(el, options); 331 } 332 } 333 return null; 334 } 335 static #resolveFrom(container, options) { 336 const from = options?.from ?? this.getActiveElement(container); 337 if (!from || container === from) { 338 return null; 339 } 340 if (container.contains(from)) { 341 return from; 342 } 343 let doc = from.ownerDocument; 344 while (doc !== null) { 345 const frame = doc.defaultView?.frameElement; 346 if (!frame) { 347 return null; 348 } 349 if (container.contains(frame)) { 350 return from; 351 } 352 doc = frame.ownerDocument; 353 } 354 return null; 355 } 356 static focusTarget(target, options) { 357 if (!target) { 358 return null; 359 } 360 const { 361 preventScroll, 362 focusVisible 363 } = options ?? {}; 364 target.focus({ 365 preventScroll, 366 focusVisible 367 }); 368 return target; 369 } 370 static restoreFocus(target, options) { 371 if (!main_core.Type.isElementNode(target)) { 372 return null; 373 } 374 try { 375 const customEvent = new CustomEvent(RESTORE_FOCUS_EVENT, { 376 bubbles: true, 377 cancelable: true 378 }); 379 const dispatchResult = target.dispatchEvent(customEvent); 380 if (dispatchResult) { 381 this.focusTarget(target, options); 382 AccessibilityLogger.log('focus-monitor', 'restored focus to', target); 383 return target; 384 } 385 } catch { 386 AccessibilityLogger.warn('focus-monitor', 'failed to restore focus to', target); 387 } 388 return null; 389 } 390 static #getDocument(node) { 391 if (!node) { 392 return document; 393 } 394 if (node.window === node) { 395 return node.document; 396 } 397 return node.ownerDocument ?? document; 398 } 399 } 400 401 class FocusHistory { 402 #limit; 403 #stack = []; 404 constructor(limit = 25) { 405 this.#limit = limit; 406 } 407 record(el) { 408 if (!InteractivityChecker.isFocusable(el)) { 409 return; 410 } 411 if (this.#stack[this.#stack.length - 1]?.deref() === el) { 412 return; 413 } 414 for (let i = this.#stack.length - 1; i >= 0; i--) { 415 const current = this.#stack[i].deref(); 416 if (!current) { 417 this.#stack.splice(i, 1); 418 continue; 419 } 420 if (current === el) { 421 this.#stack.splice(i, 1); 422 break; 423 } 424 } 425 AccessibilityLogger.logNode('focus-monitor', 'recorded', el); 426 this.#stack.push(new WeakRef(el)); 427 this.#trimByWindow(el.ownerDocument); 428 } 429 getLastValid() { 430 for (let i = this.#stack.length - 1; i >= 0; i--) { 431 const el = this.#stack[i].deref(); 432 if (!el) { 433 this.#stack.splice(i, 1); 434 continue; 435 } 436 if (InteractivityChecker.isFocusable(el)) { 437 this.#stack.length = i + 1; 438 return el; 439 } 440 this.#stack.splice(i, 1); 441 } 442 return null; 443 } 444 #trimByWindow(doc) { 445 const docIndexes = []; 446 for (let i = this.#stack.length - 1; i >= 0; i--) { 447 const current = this.#stack[i].deref(); 448 if (!current) { 449 this.#stack.splice(i, 1); 450 continue; 451 } 452 if (current.ownerDocument === doc) { 453 docIndexes.push(i); 454 } 455 } 456 const excess = docIndexes.length - this.#limit; 457 if (excess <= 0) { 458 return; 459 } 460 for (let j = docIndexes.length - excess; j < docIndexes.length; j++) { 461 this.#stack.splice(docIndexes[j], 1); 462 } 463 } 464 } 465 466 const NAV_KEYS = new Set(['Tab', 'Escape', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End']); 467 const POINTER_TYPES = new Set(['mouse', 'pen', 'touch']); 468 class InputModalityTracker { 469 #modality = 'unknown'; 470 #pointerType = null; 471 #lastNavKey = null; 472 #lastNavShift = false; 473 #attachedDocs = []; 474 #detachHandlers = new WeakMap(); 475 attach(doc) { 476 if (this.#detachHandlers.has(doc)) { 477 return; 478 } 479 const onKeyDown = event => {
480 if (NAV_KEYS.has(event.key)) { 481 this.#lastNavKey = event.key; 482 this.#lastNavShift = event.shiftKey; 483 this.#setModality('keyboard'); 484 } 485 }; 486 const onPointer = event => { 487 this.#pointerType = POINTER_TYPES.has(event.pointerType) ? event.pointerType : 'mouse'; 488 this.#setModality('pointer'); 489 }; 490 main_core.Event.bind(doc, 'keydown', onKeyDown, true); 491 main_core.Event.bind(doc, 'pointerdown', onPointer, true); 492 const detach = () => { 493 main_core.Event.unbind(doc, 'keydown', onKeyDown, true); 494 main_core.Event.unbind(doc, 'pointerdown', onPointer, true); 495 }; 496 this.#attachedDocs.push(new WeakRef(doc)); 497 this.#detachHandlers.set(doc, detach); 498 doc.documentElement.dataset.inputModality = this.#modality; 499 } 500 detach(doc) { 501 const detach = this.#detachHandlers.get(doc); 502 if (detach) { 503 detach(); 504 this.#detachHandlers.delete(doc); 505 this.#cleanupWeakRefs(); 506 } 507 } 508 static enableDebug() { 509 AccessibilityLogger.enable('input-modality'); 510 } 511 static disableDebug() { 512 AccessibilityLogger.disable('input-modality'); 513 } 514 #setModality(modality) { 515 if (this.#modality !== modality) { 516 AccessibilityLogger.log('input-modality', `${this.#modality} -> ${modality}`); 517 this.#modality = modality; 518 this.#updateAllDocuments(); 519 } 520 } 521 #updateAllDocuments() { 522 const aliveRefs = []; 523 for (const weakRef of this.#attachedDocs) { 524 const doc = weakRef.deref(); 525 if (doc) { 526 doc.documentElement.dataset.inputModality = this.#modality; 527 aliveRefs.push(weakRef); 528 } 529 } 530 this.#attachedDocs = aliveRefs; 531 } 532 #cleanupWeakRefs() { 533 this.#attachedDocs = this.#attachedDocs.filter(weakRef => { 534 const doc = weakRef.deref(); 535 return doc && this.#detachHandlers.has(doc); 536 }); 537 } 538 getLastModality() { 539 return this.#modality; 540 } 541 getLastPointerType() { 542 return this.#modality === 'pointer' ? this.#pointerType : null; 543 } 544 getLastNavigationKey() { 545 return this.#modality === 'keyboard' ? this.#lastNavKey : null; 546 } 547 isLastNavigationReversed() { 548 return this.#modality === 'keyboard' && this.#lastNavKey === 'Tab' && this.#lastNavShift; 549 } 550 } 551 552 class AccessibilitySettings { 553 static useFocusTrapInDialogs() { 554 const settings = main_core.Extension.getSettings('ui.a11y'); 555 return settings.get('useFocusTrapInDialogs') === true; 556 } 557 static restoreLostFocus() { 558 const settings = main_core.Extension.getSettings('ui.a11y'); 559 return settings.get('restoreLostFocus') === true; 560 } 561 static suppressFocusOnRestore() { 562 const settings = main_core.Extension.getSettings('ui.a11y'); 563 return settings.get('suppressFocusOnRestore') === true; 564 } 565 } 566 567 class FocusMonitor { 568 static #instance = null; 569 #history; 570 #modality; 571 #documents = new WeakMap(); 572 constructor() { 573 this.#history = new FocusHistory(25); 574 this.#modality = new InputModalityTracker(); 575 this.#attachDocument(document); 576 } 577 static get Instance() { 578 return this.initialize(); 579 } 580 static initialize() { 581 const topWindow = main_core.Page.getRootWindow(); 582 if (topWindow !== window) { 583 const monitor = main_core.Reflection.getClass('top.BX.UI.Accessibility.FocusMonitor'); 584 if (monitor !== null) { 585 return monitor.Instance; 586 } 587 } 588 if (this.#instance === null) { 589 this.#instance = new FocusMonitor(); 590 } 591 return this.#instance; 592 } 593 static enableDebug() { 594 AccessibilityLogger.enable('focus-monitor'); 595 } 596 static disableDebug() { 597 AccessibilityLogger.disable('focus-monitor'); 598 } 599 static shouldRestoreLostFocus() { 600 return AccessibilitySettings.restoreLostFocus(); 601 } 602 #attachDocument(doc) { 603 if (this.#documents.has(doc)) { 604 return; 605 } 606 let pendingRestore = null; 607 const onFocusIn = event => { 608 if (pendingRestore !== null) { 609 clearTimeout(pendingRestore); 610 pendingRestore = null; 611 } 612 const target = event.target; 613 if (main_core.Type.isElementNode(target)) { 614 this.#history.record(target); 615 if (target.tagName === 'IFRAME') { 616 this.attachIframe(target); 617 } 618 } 619 }; 620 const onFocusOut = () => { 621 if (pendingRestore !== null) { 622 return; 623 } 624 pendingRestore = window.setTimeout(() => { 625 pendingRestore = null; 626 this.#tryRestoreLostFocus(doc); 627 }, 0); 628 }; 629 doc.addEventListener('focusin', onFocusIn, true); 630 doc.addEventListener('focusout', onFocusOut, true); 631 const observer = this.#attachObserver(doc); 632 this.#modality.attach(doc); 633 this.#documents.set(doc, { 634 detach: () => { 635 if (pendingRestore !== null) { 636 clearTimeout(pendingRestore); 637 } 638 doc.removeEventListener('focusin', onFocusIn, true); 639 doc.removeEventListener('focusout', onFocusOut, true); 640 this.#modality.detach(doc); 641 observer?.disconnect(); 642 } 643 }); 644 } 645 attachIframe(iframe) { 646 if (!main_core.Type.isElementNode(iframe) || iframe.tagName !== 'IFRAME') { 647 return; 648 } 649 try { 650 const doc = iframe.contentDocument; 651 if (!doc) { 652 return; 653 } 654 const state = this.#documents.get(doc); 655 if (state) { 656 return; 657 } 658 this.#attachDocument(doc); 659 } catch { 660 } 661 } 662 detachIframe(iframe) { 663 try { 664 const doc = iframe.contentDocument; 665 if (!doc) { 666 return; 667 } 668 const state = this.#documents.get(doc); 669 if (state) { 670 state.detach();
671 this.#documents.delete(doc); 672 } 673 } catch { 674 } 675 } 676 #tryRestoreLostFocus(doc) { 677 if (!FocusMonitor.shouldRestoreLostFocus()) { 678 return; 679 } 680 const active = doc.activeElement; 681 if (active && active !== doc.body) { 682 return; 683 } 684 if (this.#modality.getLastModality() !== 'keyboard') { 685 return; 686 } 687 const target = this.#history.getLastValid(); 688 if (target) { 689 this.#restoreElementFocus(target); 690 return; 691 } 692 this.#restoreFocusToRoot(doc); 693 } 694 restoreFocus() { 695 const target = this.#history.getLastValid(); 696 if (target) { 697 this.#restoreElementFocus(target); 698 return; 699 } 700 this.#restoreFocusToRoot(document); 701 } 702 #restoreFocusToRoot(doc) { 703 AccessibilityLogger.log('focus-monitor', 'restoring focus to root'); 704 FocusNavigator.restoreFocus(this.getRoot(doc), { 705 preventScroll: true 706 }); 707 } 708 getRoot(doc = document) { 709 return doc.querySelector('[data-focus-root]') || doc.querySelector('main') || doc.body; 710 } 711 #restoreElementFocus(el) { 712 if (!el.isConnected) { 713 return; 714 } 715 const ownerWindow = el.ownerDocument.defaultView; 716 if (!ownerWindow) { 717 return; 718 } 719 const activeElement = FocusNavigator.getActiveElement(el); 720 if (activeElement === el) { 721 return; 722 } 723 if (ownerWindow === window) { 724 FocusNavigator.restoreFocus(el, { 725 preventScroll: true 726 }); 727 return; 728 } 729 ownerWindow.requestAnimationFrame(() => { 730 FocusNavigator.restoreFocus(el, { 731 preventScroll: true 732 }); 733 }); 734 } 735 getLastInputModality() { 736 return this.#modality.getLastModality(); 737 } 738 getModalityTracker() { 739 return this.#modality; 740 } 741 #attachObserver(doc) { 742 if (!FocusMonitor.shouldRestoreLostFocus()) { 743 return null; 744 } 745 if (main_core.Browser.isFirefox() || main_core.Browser.isSafari()) { 746 const observer = new MutationObserver(mutations => { 747 this.#tryRestoreLostFocus(doc); 748 }); 749 const observe = () => observer.observe(doc.body, { 750 childList: true, 751 subtree: true, 752 characterData: false 753 }); 754 if (doc.readyState === 'loading') { 755 doc.addEventListener('DOMContentLoaded', observe); 756 } else { 757 observe(); 758 } 759 return observer; 760 } 761 return null; 762 } 763 } 764 765 const PRECEDING = Node.DOCUMENT_POSITION_PRECEDING | Node.DOCUMENT_POSITION_CONTAINS; 766 const FOLLOWING = Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_CONTAINED_BY; 767 class FocusTrap { 768 #id = `focus-trap-${main_core.Text.getRandom()}`; 769 #container; 770 #options; 771 #active = false; 772 #looped = true; 773 #initialized = false; 774 #restoreFocus = null; 775 #preventScroll = true; 776 #startFocusBoundary = null; 777 #endFocusBoundary = null; 778 #lastFocusedElement = null; 779 #alreadyInert = new WeakSet(); 780 #startFocusHandler = this.#handleStartBoundaryFocus.bind(this); 781 #endFocusHandler = this.#handleEndBoundaryFocus.bind(this); 782 constructor(container, options = {}) { 783 this.#container = container; 784 this.#options = main_core.Type.isPlainObject(options) ? options : {}; 785 this.setRestoreFocus(this.#options.restoreFocus ?? null); 786 if (this.#options.looped !== undefined) { 787 this.setLooped(this.#options.looped); 788 } 789 if (this.#options.preventScroll !== undefined) { 790 this.setPreventScroll(this.#options.preventScroll); 791 } 792 } 793 static enableDebug() { 794 AccessibilityLogger.enable('focus-trap'); 795 } 796 static disableDebug() { 797 AccessibilityLogger.disable('focus-trap'); 798 } 799 activate(options = {}) { 800 if (this.#active) { 801 return; 802 } 803 if (!this.#initialized) { 804 this.#init(); 805 } 806 if (this.#lastFocusedElement === null) { 807 this.captureActiveElement(); 808 } 809 this.#active = true; 810 if (this.#looped) { 811 this.#setBoundariesFocusable(true); 812 } 813 const { 814 initialFocus 815 } = { 816 initialFocus: true, 817 ...options 818 }; 819 if (initialFocus) { 820 this.applyInitialFocus(); 821 } 822 this.#setOutsideIsolation(true); 823 } 824 deactivate() { 825 if (!this.#active) { 826 return; 827 } 828 this.#active = false; 829 this.#setBoundariesFocusable(false); 830 this.#setOutsideIsolation(false); 831 this.restoreFocus(); 832 } 833 destroy() { 834 this.deactivate(); 835 if (!this.#initialized) { 836 return; 837 }
838 main_core.Event.unbind(this.#startFocusBoundary, 'focus', this.#startFocusHandler); 839 main_core.Event.unbind(this.#endFocusBoundary, 'focus', this.#endFocusHandler); 840 main_core.Dom.remove(this.#startFocusBoundary); 841 main_core.Dom.remove(this.#endFocusBoundary); 842 this.#initialized = false; 843 } 844 setLooped(flag) { 845 if (!main_core.Type.isBoolean(flag)) { 846 return; 847 } 848 this.#looped = flag; 849 if (this.#active) { 850 this.#setBoundariesFocusable(flag); 851 } 852 } 853 setPreventScroll(flag) { 854 this.#preventScroll = main_core.Type.isBoolean(flag) ? flag : true; 855 } 856 isLooped() { 857 return this.#looped; 858 } 859 isActive() { 860 return this.#active; 861 } 862 setLastFocusedElement(el) { 863 if (main_core.Type.isElementNode(el) && el.tagName !== 'BODY' && !this.contains(el)) { 864 this.#lastFocusedElement = el; 865 AccessibilityLogger.logNode('focus-trap', 'set last focus', el); 866 } 867 } 868 captureActiveElement() { 869 const activeElement = FocusNavigator.getActiveElement(); 870 if (activeElement !== null) { 871 this.setLastFocusedElement(activeElement); 872 } 873 return activeElement; 874 } 875 contains(el) { 876 return this.#container.contains(el); 877 } 878 focusFirst(options) { 879 return FocusNavigator.focusFirst(this.#container, this.#prepareFocusOptions(options)); 880 } 881 focusLast(options) { 882 return FocusNavigator.focusLast(this.#container, this.#prepareFocusOptions(options)); 883 } 884 focusNext(options) { 885 return FocusNavigator.focusNext(this.#container, this.#prepareFocusOptions(options)); 886 } 887 focusPrevious(options) { 888 return FocusNavigator.focusPrevious(this.#container, this.#prepareFocusOptions(options)); 889 } 890 focusContainer(options) { 891 return FocusNavigator.focusContainer(this.#container, this.#prepareFocusOptions(options)); 892 } 893 focusBySelector(selector, options) { 894 return FocusNavigator.focusBySelector(this.#container, selector, this.#prepareFocusOptions(options)); 895 } 896 #prepareFocusOptions(options) { 897 const preparedOptions = main_core.Type.isPlainObject(options) ? { 898 ...options 899 } : {}; 900 if (main_core.Type.isNil(preparedOptions.preventScroll)) { 901 preparedOptions.preventScroll = this.#preventScroll; 902 } 903 return preparedOptions; 904 } 905 getId() { 906 return this.#id; 907 } 908 #init() { 909 this.#startFocusBoundary = this.#createFocusBoundary(); 910 this.#endFocusBoundary = this.#createFocusBoundary(); 911 main_core.Event.bind(this.#startFocusBoundary, 'focus', this.#startFocusHandler); 912 main_core.Event.bind(this.#endFocusBoundary, 'focus', this.#endFocusHandler); 913 this.#container.insertAdjacentElement('beforebegin', this.#startFocusBoundary); 914 this.#container.insertAdjacentElement('afterend', this.#endFocusBoundary); 915 this.#initialized = true; 916 } 917 #createFocusBoundary() { 918 const el = document.createElement('div'); 919 el.setAttribute('aria-hidden', 'true'); 920 el.setAttribute('data-focus-trap', this.getId()); 921 main_core.Dom.style(el, { 922 position: 'fixed', 923 width: 0, 924 height: 0, 925 opacity: '0', 926 pointerEvents: 'none', 927 outline: 'none' 928 }); 929 return el; 930 } 931 #handleStartBoundaryFocus(event) { 932 if (!this.#active || !this.#startFocusBoundary) { 933 return; 934 } 935 const position = event.relatedTarget ? this.#startFocusBoundary.compareDocumentPosition(event.relatedTarget) : 0; 936 const isBackward = position & FOLLOWING; 937 if (!isBackward) { 938 if (!this.focusFirst()) { 939 this.focusContainer(); 940 } 941 return; 942 } 943 const target = this.#options.startBoundary ? this.#resolveBoundary(this.#options.startBoundary) : null; 944 if (target) { 945 FocusNavigator.focusTarget(target, { 946 preventScroll: this.#preventScroll 947 }); 948 return; 949 } 950 if (!this.focusLast()) { 951 this.focusContainer(); 952 } 953 } 954 #handleEndBoundaryFocus(event) { 955 if (!this.#active || !this.#endFocusBoundary) { 956 return; 957 } 958 const position = event.relatedTarget ? this.#endFocusBoundary.compareDocumentPosition(event.relatedTarget) : 0; 959 const isBackward = position & PRECEDING; 960 if (!isBackward) { 961 if (!this.focusLast()) { 962 this.focusContainer(); 963 } 964 return; 965 } 966 const target = this.#options.endBoundary ? this.#resolveBoundary(this.#options.endBoundary) : null; 967 if (target) { 968 FocusNavigator.focusTarget(target, { 969 preventScroll: this.#preventScroll 970 }); 971 return; 972 } 973 if (!this.focusFirst()) { 974 this.focusContainer(); 975 } 976 } 977 #resolveBoundary(boundary) { 978 if (main_core.Type.isStringFilled(boundary)) { 979 const el = document.querySelector(boundary); 980 return el !== null && main_core.Type.isElementNode(el) && InteractivityChecker.isFocusable(el) ? el : null; 981 } 982 if (main_core.Type.isElementNode(boundary) && InteractivityChecker.isFocusable(boundary)) { 983 return boundary; 984 }
985 if (main_core.Type.isFunction(boundary)) { 986 const el = boundary(); 987 return main_core.Type.isElementNode(el) && InteractivityChecker.isFocusable(el) ? el : null; 988 } 989 return null; 990 } 991 #setBoundariesFocusable(enabled) { 992 const tabindex = enabled ? '0' : '-1'; 993 this.#startFocusBoundary?.setAttribute('tabindex', tabindex); 994 this.#endFocusBoundary?.setAttribute('tabindex', tabindex); 995 } 996 applyInitialFocus() { 997 const initialFocus = this.#options.initialFocus ?? true; 998 if (initialFocus === false) { 999 return; 1000 } 1001 if (!this.#options.forceInitialFocus) { 1002 const activeElement = FocusNavigator.getActiveElement(this.#container); 1003 if (activeElement && this.#container.contains(activeElement) && activeElement !== this.#container) { 1004 return; 1005 } 1006 } 1007 const candidates = Array.isArray(initialFocus) ? initialFocus : [initialFocus]; 1008 for (const candidate of candidates) { 1009 if (this.#tryInitialFocus(candidate)) { 1010 return; 1011 } 1012 } 1013 this.focusContainer(); 1014 } 1015 #tryInitialFocus(initialFocus) { 1016 if (initialFocus === false) { 1017 return false; 1018 } 1019 if (initialFocus === 'container') { 1020 this.focusContainer(); 1021 return true; 1022 } 1023 if (main_core.Type.isStringFilled(initialFocus)) { 1024 return this.focusBySelector(initialFocus) !== null; 1025 } 1026 if (main_core.Type.isFunction(initialFocus)) { 1027 const el = initialFocus(); 1028 if (InteractivityChecker.isFocusable(el)) { 1029 FocusNavigator.focusTarget(el, { 1030 preventScroll: this.#preventScroll 1031 }); 1032 return true; 1033 } 1034 return false; 1035 } 1036 if (initialFocus === true || initialFocus === 'first-tabbable' || main_core.Type.isNil(initialFocus)) { 1037 return Boolean(this.focusFirst()); 1038 } 1039 return false; 1040 } 1041 setRestoreFocus(restore) { 1042 if (main_core.Type.isBoolean(restore) || main_core.Type.isNull(restore) || main_core.Type.isStringFilled(restore) || main_core.Type.isFunction(restore) || main_core.Type.isElementNode(restore)) { 1043 this.#restoreFocus = restore; 1044 } 1045 } 1046 restoreFocus() { 1047 try { 1048 const restoreFocus = this.#restoreFocus ?? true; 1049 if (restoreFocus === false) { 1050 return; 1051 } 1052 if (main_core.Type.isStringFilled(restoreFocus)) { 1053 const doc = this.#container.ownerDocument ?? document; 1054 let el = null; 1055 try { 1056 el = doc.querySelector(restoreFocus); 1057 } catch { 1058 console.error('FocusTrap: invalid restoreFocus selector provided'); 1059 } 1060 if (el !== null && InteractivityChecker.isFocusable(el)) { 1061 FocusNavigator.restoreFocus(el, { 1062 preventScroll: true 1063 }); 1064 return; 1065 } 1066 } 1067 if (main_core.Type.isElementNode(restoreFocus) && InteractivityChecker.isFocusable(restoreFocus)) { 1068 FocusNavigator.restoreFocus(restoreFocus, { 1069 preventScroll: true 1070 }); 1071 return; 1072 } 1073 if (main_core.Type.isFunction(restoreFocus)) { 1074 const el = restoreFocus(); 1075 if (InteractivityChecker.isFocusable(el)) { 1076 FocusNavigator.restoreFocus(el, { 1077 preventScroll: true 1078 }); 1079 return; 1080 } 1081 } 1082 if (this.#lastFocusedElement !== null && InteractivityChecker.isFocusable(this.#lastFocusedElement)) { 1083 let eventListeners = []; 1084 const suppressFocusOnRestore = main_core.Type.isBoolean(this.#options.suppressFocusOnRestore) ? this.#options.suppressFocusOnRestore : AccessibilitySettings.suppressFocusOnRestore(); 1085 try { 1086 if (suppressFocusOnRestore) { 1087 eventListeners = main_core.Event.getEventListeners(this.#lastFocusedElement, 'focus'); 1088 if (eventListeners.length > 0) { 1089 main_core.Event.unbindAll(this.#lastFocusedElement, 'focus'); 1090 } 1091 } 1092 AccessibilityLogger.logNode('focus-trap', 'back to last focus', this.#lastFocusedElement); 1093 FocusNavigator.restoreFocus(this.#lastFocusedElement, { 1094 preventScroll: true 1095 }); 1096 } finally { 1097 if (suppressFocusOnRestore) { 1098 for (const eventListener of eventListeners) { 1099 main_core.Event.bind(this.#lastFocusedElement, 'focus', eventListener.listener); 1100 } 1101 } 1102 } 1103 return; 1104 } 1105 FocusMonitor.Instance.restoreFocus(); 1106 } finally { 1107 this.#lastFocusedElement = null; 1108 } 1109 } 1110 #setOutsideIsolation(enable) { 1111 if (this.#options.isolateOutside !== true) { 1112 return; 1113 } 1114 const containers = [this.#container, ...this.#getOutsideExceptionElements()]; 1115 const topLevelContainers = containers.filter(el => { 1116 return !containers.some(other => other !== el && other.contains(el)); 1117 }); 1118 const adjacentElements = this.#getAdjacentElements(topLevelContainers
1118); 1119 if (enable) { 1120 this.#alreadyInert = new WeakSet(); 1121 for (const el of adjacentElements) { 1122 if (el === this.#container || this.#container.contains(el) || el.getAttribute('data-focus-trap') === this.getId()) { 1123 continue; 1124 } 1125 if (el.parentNode?.nodeName === 'BODY' && el.offsetWidth === 0 && el.offsetHeight === 0) { 1126 continue; 1127 } 1128 if (el.inert || el.hasAttribute('inert')) { 1129 this.#alreadyInert.add(el); 1130 } 1131 el.setAttribute('inert', 'true'); 1132 } 1133 } else { 1134 for (const el of adjacentElements) { 1135 if (this.#alreadyInert.has(el)) { 1136 continue; 1137 } 1138 el.removeAttribute('inert'); 1139 } 1140 this.#alreadyInert = new WeakSet(); 1141 } 1142 } 1143 #getAdjacentElements(containers) { 1144 const adjacentElements = new Set(); 1145 const containerAncestors = new Set(); 1146 for (const container of containers) { 1147 let currentElement = container; 1148 while (currentElement && currentElement.tagName !== 'BODY') { 1149 containerAncestors.add(currentElement); 1150 const parent = currentElement.parentElement; 1151 const siblings = parent ? parent.children : []; 1152 for (const sibling of siblings) { 1153 if (sibling.tagName !== 'SCRIPT' && sibling.tagName !== 'STYLE' && !sibling.getAttribute('data-a11y-ignore-inert') && main_core.Type.isElementNode(sibling)) { 1154 adjacentElements.add(sibling); 1155 } 1156 } 1157 currentElement = parent; 1158 } 1159 } 1160 containerAncestors.forEach(el => { 1161 adjacentElements.delete(el); 1162 }); 1163 return adjacentElements; 1164 } 1165 #getOutsideExceptionElements() { 1166 if (!main_core.Type.isArrayFilled(this.#options.outsideExceptionSelectors)) { 1167 return []; 1168 } 1169 try { 1170 const selectors = this.#options.outsideExceptionSelectors.join(','); 1171 return [...document.querySelectorAll(selectors)]; 1172 } catch { 1173 console.error('FocusTrap: invalid outsideExceptionSelectors provided'); 1174 return []; 1175 } 1176 } 1177 } 1178 1179 const FocusTrapDirective = { 1180 mounted(el, binding) { 1181 const { 1182 active, 1183 options 1184 } = normalizeValue(binding.value); 1185 el.__focusTrap = new FocusTrap(el, options); 1186 if (active) { 1187 el.__focusTrap.activate(); 1188 } 1189 }, 1190 updated(el, binding) { 1191 const { 1192 active, 1193 options 1194 } = normalizeValue(binding.value); 1195 const { 1196 active: prevActive, 1197 options: prevOptions 1198 } = normalizeValue(binding.oldValue); 1199 if (options !== prevOptions && options) { 1200 el.__focusTrap?.destroy(); 1201 el.__focusTrap = new FocusTrap(el, options); 1202 } 1203 const trap = el.__focusTrap; 1204 if (!trap) { 1205 return; 1206 }
1207 if (active) { 1208 trap.activate(); 1209 } else { 1210 trap.deactivate(); 1211 } 1212 }, 1213 unmounted(el) { 1214 el.__focusTrap?.destroy(); 1215 delete el.__focusTrap; 1216 } 1217 }; 1218 function normalizeValue(value) { 1219 if (main_core.Type.isBoolean(value)) { 1220 return { 1221 active: value 1222 }; 1223 } 1224 if (main_core.Type.isPlainObject(value)) { 1225 return { 1226 active: value.active !== false, 1227 options: value.options 1228 }; 1229 } 1230 return { 1231 active: false 1232 }; 1233 } 1234 1235 class LiveAnnouncer { 1236 static #instance = null; 1237 #el; 1238 #queue = []; 1239 #isSpeaking = false; 1240 #frameId = null; 1241 #timerId = null; 1242 #ready = false; 1243 #readyFrameId = null; 1244 #baseDelay = 500; 1245 #charDelay = 30; 1246 #maxDelay = 4000; 1247 #maxMessageLength = 160; 1248 static enableDebug() { 1249 AccessibilityLogger.enable('live-announcer'); 1250 } 1251 static disableDebug() { 1252 AccessibilityLogger.disable('live-announcer'); 1253 } 1254 static announce(message, politeness = 'polite') { 1255 if (!LiveAnnouncer.#instance) { 1256 LiveAnnouncer.#instance = new LiveAnnouncer(); 1257 } 1258 LiveAnnouncer.#instance.announce(message, politeness); 1259 } 1260 static destroy() { 1261 LiveAnnouncer.#instance?.destroy(); 1262 LiveAnnouncer.#instance = null; 1263 } 1264 constructor(options) { 1265 const { 1266 politeness = 'polite', 1267 container = document.body || document.documentElement, 1268 baseDelay, 1269 charDelay, 1270 maxDelay, 1271 maxMessageLength 1272 } = options ?? {}; 1273 if (main_core.Type.isNumber(baseDelay)) { 1274 this.#baseDelay = baseDelay; 1275 } 1276 if (main_core.Type.isNumber(charDelay)) { 1277 this.#charDelay = charDelay; 1278 } 1279 if (main_core.Type.isNumber(maxDelay)) { 1280 this.#maxDelay = maxDelay; 1281 } 1282 if (main_core.Type.isNumber(maxMessageLength)) { 1283 this.#maxMessageLength = maxMessageLength; 1284 } 1285 this.#el = this.#createLiveRegion(politeness); 1286 main_core.Dom.append(this.#el, container); 1287 this.#readyFrameId = requestAnimationFrame(() => { 1288 this.#readyFrameId = null; 1289 this.#ready = true; 1290 this.#process(); 1291 }); 1292 } 1293 announce(message, politeness = 'polite') { 1294 const normalized = this.#normalizeMessage(message); 1295 if (!normalized) { 1296 return; 1297 } 1298 AccessibilityLogger.log('live-announcer', `[${politeness}] ${normalized}`); 1299 const last = this.#queue[this.#queue.length - 1]; 1300 if (last && last.message === normalized && last.politeness === politeness) { 1301 return; 1302 } 1303 if (politeness === 'assertive') { 1304 this.#clearPendingAnnouncement(); 1305 this.#queue.unshift({ 1306 message: normalized, 1307 politeness 1308 }); 1309 } else { 1310 this.#queue.push({ 1311 message: normalized, 1312 politeness 1313 }); 1314 } 1315 this.#process(); 1316 } 1317 destroy() { 1318 if (this.#readyFrameId !== null) { 1319 cancelAnimationFrame(this.#readyFrameId); 1320 this.#readyFrameId = null; 1321 } 1322 this.#queue.length = 0; 1323 this.#clearPendingAnnouncement(); 1324 this.#el.remove(); 1325 } 1326 #process() { 1327 if (!this.#ready || this.#isSpeaking || this.#queue.length === 0) { 1328 return; 1329 } 1330 const item = this.#queue.shift(); 1331 if (!item) { 1332 return; 1333 } 1334 this.#isSpeaking = true; 1335 const { 1336 message, 1337 politeness 1338 } = item; 1339 const delay = this.#getDelay(message); 1340 this.#el.setAttribute('aria-live', politeness); 1341 this.#el.textContent = ''; 1342 this.#frameId = requestAnimationFrame(() => { 1343 this.#frameId = null; 1344 this.#el.textContent = message; 1345 this.#timerId = setTimeout(() => { 1346 this.#timerId = null; 1347 this.#isSpeaking = false; 1348 if (this.#queue.length === 0) { 1349 this.#el.textContent = ''; 1350 } 1351 this.#process(); 1352 }, delay); 1353 }); 1354 } 1355 #clearPendingAnnouncement() { 1356 this.#isSpeaking = false; 1357 if (this.#frameId !== null) { 1358 cancelAnimationFrame(this.#frameId); 1359 this.#frameId = null; 1360 } 1361 if (this.#timerId !== null) { 1362 clearTimeout(this.#timerId); 1363 this.#timerId = null; 1364 } 1365 this.#el.textContent = ''; 1366 } 1367 #getDelay(message) { 1368 const delay = this.#baseDelay + message.length * this.#charDelay; 1369 return Math.min(delay, this.#maxDelay); 1370 } 1371 #normalizeMessage(message) { 1372 if (!main_core.Type.isStringFilled(message)) { 1373 return ''; 1374 } 1375 const text = message.trim(); 1376 if (!text) { 1377 return ''; 1378 } 1379 if (text.length <= this.#maxMessageLength) { 1380 return text; 1381 } 1382 const truncated = text.slice(0, this.#maxMessageLength - 1); 1383 const trimmedToWord = truncated.replace(/\s+\S*$/, ''); 1384 return `${trimmedToWord || truncated}\u2026`; 1385 } 1386 #createLiveRegion(politeness) { 1387 const el = document.createElement('div'); 1388 el.setAttribute('aria-live', politeness); 1389 el.setAttribute('aria-atomic', 'true'); 1390 el.setAttribute('data-a11y-ignore-inert', 'true'); 1391 main_core.Dom.style(el, { 1392 position: 'absolute', 1393 width: '1px', 1394 height: '1px', 1395 margin: '-1px', 1396 padding: '0', 1397 overflow: 'hidden', 1398 clip: 'rect(0 0 0 0)', 1399 whiteSpace: 'nowrap', 1400 border: '0' 1401 }); 1402 return el; 1403 } 1404 } 1405 1406 const ARROW_HORIZONTAL = 0b0000000001; 1407 const ARROW_VERTICAL = 0b0000000010; 1408 const JK_BITS = 0b0000000100; 1409 const HL_BITS = 0b0000001000; 1410 const HOME_AND_END = 0b0000010000; 1411 const WS_BITS = 0b0000100000; 1412 const AD_BITS = 0b0001000000; 1413 const TAB_BIT = 0b0010000000; 1414 const PAGE_UP_DOWN = 0b0100000000; 1415 const BACKSPACE_BIT = 0b1000000000; 1416 const FocusKeys = { 1417 ArrowHorizontal: ARROW_HORIZONTAL, 1418 ArrowVertical: ARROW_VERTICAL, 1419 JK: JK_BITS, 1420 HL: HL_BITS, 1421 HomeAndEnd: HOME_AND_END, 1422 WS: WS_BITS, 1423 AD: AD_BITS, 1424 Tab: TAB_BIT, 1425 PageUpDown: PAGE_UP_DOWN, 1426 Backspace: BACKSPACE_BIT, 1427 ArrowAll: ARROW_HORIZONTAL | ARROW_VERTICAL, 1428 HJKL: HL_BITS | JK_BITS, 1429 WASD: WS_BITS | AD_BITS, 1430 All: ARROW_HORIZONTAL | ARROW_VERTICAL | JK_BITS | HL_BITS | HOME_AND_END | WS_BITS | AD_BITS | TAB_BIT | PAGE_UP_DOWN | BACKSPACE_BIT 1431 }; 1432 1433 const ActiveDescendant = { 1434 ELEMENT_ATTR: 'data-active-descendant', 1435 CONTAINER_ATTR: 'data-has-active-descendant', 1436 EXPLICIT: 'activated-by-keyboard', 1437 IMPLICIT: 'activated-by-other' 1438 }; 1439 const KEY_TO_BIT = { 1440 ArrowLeft: FocusKeys.ArrowHorizontal, 1441 ArrowDown: FocusKeys.ArrowVertical, 1442 ArrowUp: FocusKeys.ArrowVertical, 1443 ArrowRight: FocusKeys.ArrowHorizontal, 1444 h: FocusKeys.HL, 1445 j: FocusKeys.JK, 1446 k: FocusKeys.JK, 1447 l: FocusKeys.HL, 1448 a: FocusKeys.AD, 1449 s: FocusKeys.WS, 1450 w: FocusKeys.WS, 1451 d: FocusKeys.AD, 1452 Tab: FocusKeys.Tab, 1453 Home: FocusKeys.HomeAndEnd, 1454 End: FocusKeys.HomeAndEnd, 1455 PageUp: FocusKeys.PageUpDown, 1456 PageDown: FocusKeys.PageUpDown, 1457 Backspace: FocusKeys.Backspace 1458 }; 1459 const KEY_TO_DIRECTION = { 1460 ArrowLeft: 'previous', 1461 ArrowDown: 'next', 1462 ArrowUp: 'previous', 1463 ArrowRight: 'next', 1464 h: 'previous', 1465 j: 'next', 1466 k: 'previous', 1467 l: 'next', 1468 a: 'previous', 1469 s: 'next', 1470 w: 'previous', 1471 d: 'next', 1472 Tab: 'next', 1473 Home: 'start', 1474 End: 'end', 1475 PageUp: 'start', 1476 PageDown: 'end', 1477 Backspace: 'previous' 1478 }; 1479 const NON_EDITABLE_INPUT_TYPES = new Set(['button', 'checkbox', 'file', 'hidden', 'image', 'radio', 'range', 'reset', 'submit']); 1480 class FocusZone { 1481 #container; 1482 #options; 1483 #active = false;
1484 #bindKeys; 1485 #focusOutBehavior; 1486 #focusInStrategy; 1487 #preventScroll; 1488 #ignoreHoverEvents; 1489 #tabbableOnly; 1490 #elementSet = new Set(); 1491 #savedTabIndex = new WeakMap(); 1492 #currentFocusedElement = null; 1493 #activeDescendantControl = null; 1494 #observer = null; 1495 #syncFrameId = null; 1496 #bindings = []; 1497 #generatedContainerId = false; 1498 #generatedElementIds = new WeakSet(); 1499 #keyDownHandler = this.#handleKeyDown.bind(this); 1500 #focusInHandler = this.#handleFocusIn.bind(this); 1501 #mouseMoveHandler = this.#handleMouseMove.bind(this); 1502 #controlFocusInHandler = this.#handleControlFocusIn.bind(this); 1503 #controlFocusOutHandler = this.#handleControlFocusOut.bind(this); 1504 constructor(container, options = {}) { 1505 this.#container = container; 1506 this.#options = main_core.Type.isPlainObject(options) ? options : {}; 1507 this.#activeDescendantControl = this.#options.activeDescendantControl ?? null; 1508 this.#focusOutBehavior = this.#options.focusOutBehavior ?? 'stop'; 1509 this.#focusInStrategy = this.#options.focusInStrategy ?? 'previous'; 1510 this.#preventScroll = this.#options.preventScroll ?? false; 1511 this.#ignoreHoverEvents = this.#options.ignoreHoverEvents ?? false; 1512 this.#tabbableOnly = this.#options.tabbableOnly ?? false; 1513 this.#bindKeys = this.#options.bindKeys ?? (this.#options.getNextFocusable ? FocusKeys.ArrowAll : FocusKeys.ArrowVertical) | FocusKeys.HomeAndEnd; 1514 } 1515 static enableDebug() { 1516 AccessibilityLogger.enable('focus-zone'); 1517 } 1518 static disableDebug() { 1519 AccessibilityLogger.disable('focus-zone'); 1520 } 1521 activate() { 1522 if (this.#active) { 1523 return; 1524 } 1525 this.#active = true; 1526 this.#elementSet = this.#collectFocusableSet(); 1527 for (const element of this.#elementSet) { 1528 this.#saveAndSetTabIndex(element); 1529 } 1530 if (this.#activeDescendantControl) { 1531 if (!this.#container.id) { 1532 this.#container.setAttribute('id', `fz-${main_core.Text.getRandom()}`); 1533 this.#generatedContainerId = true; 1534 } 1535 this.#activeDescendantControl.setAttribute('aria-controls', this.#container.id); 1536 } 1537 const preventInitialFocus = this.#focusInStrategy === 'initial' && this.#activeDescendantControl; 1538 if (!preventInitialFocus) { 1539 this.#updateFocusedElement(this.#resolveInitialFocusedElement()); 1540 } 1541 this.#attachEventListeners(); 1542 this.#attachObserver(); 1543 AccessibilityLogger.log('focus-zone', `activated with ${this.#elementSet.size} elements`); 1544 } 1545 deactivate() { 1546 if (!this.#active) { 1547 return; 1548 } 1549 this.#active = false; 1550 if (this.#observer) { 1551 this.#observer.disconnect(); 1552 this.#observer = null; 1553 } 1554 if (this.#syncFrameId !== null) { 1555 cancelAnimationFrame(this.#syncFrameId); 1556 this.#syncFrameId = null; 1557 } 1558 this.#detachEventListeners(); 1559 for (const element of this.#elementSet) { 1560 this.#restoreTabIndex(element); 1561 if (this.#generatedElementIds.has(element)) { 1562 element.removeAttribute('id'); 1563 this.#generatedElementIds.delete(element); 1564 } 1565 } 1566 if (this.#activeDescendantControl) { 1567 this.#clearActiveDescendant(); 1568 this.#activeDescendantControl.removeAttribute('aria-controls'); 1569 } 1570 if (this.#generatedContainerId) { 1571 this.#container.removeAttribute('id'); 1572 this.#generatedContainerId = false; 1573 } 1574 this.#elementSet.clear(); 1575 this.#currentFocusedElement = null; 1576 AccessibilityLogger.log('focus-zone', 'deactivated'); 1577 } 1578 isActive() { 1579 return this.#active; 1580 } 1581 getCurrentFocusedElement() { 1582 return this.#currentFocusedElement; 1583 } 1584 refreshElements() { 1585 if (this.#active) { 1586 this.#syncFocusableElements(); 1587 } 1588 } 1589 #collectFocusableSet() { 1590 const elements = new Set(); 1591 const walker = FocusNavigator.createWalker(this.#container, { 1592 tabbableOnly: this.#tabbableOnly, 1593 accept: this.#options.focusableElementFilter 1594 }); 1595 for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) { 1596 elements.add(node); 1597 } 1598 return elements; 1599 } 1600 #getNavigatorOptions(from = null, wrap = false) { 1601 return { 1602 from: from ?? undefined, 1603 tabbableOnly: false, 1604 accept: el => this.#elementSet.has(el), 1605 wrap 1606 }; 1607 } 1608 #findManagedAncestor(node) { 1609 let el = main_core.Type.isElementNode(node) ? node : node.parentElement; 1610 while (el && el !== this.#container) { 1611 if (this.#elementSet.has(el)) { 1612 return el; 1613 } 1614 el = el.parentElement; 1615 } 1616 return null; 1617 } 1618 #saveAndSetTabIndex(element) { 1619 if (!this.#savedTabIndex.has(element)) { 1620 this.#savedTabIndex.set(element, element.getAttribute('tabindex')); 1621 } 1622 element.setAttribute('tabindex', '-1'); 1623 } 1624 #restoreTabIndex(element) { 1625 const savedIndex = this.#savedTabIndex.get(element); 1626 if (savedIndex !== undefined) { 1627 if (savedIndex === null) { 1628 element.removeAttribute('tabindex'); 1629 } else { 1630 element.setAttribute('tabindex', savedIndex); 1631 } 1632 this.#savedTabIndex.delete(element); 1633 } 1634 } 1635 #updateFocusedElement(to = null, directlyActivated = false) { 1636 const from = this.#currentFocusedElement; 1637 this.#currentFocusedElement = to; 1638 if (this.#activeDescendantControl) { 1639 if (to && FocusNavigator.getActiveElement() === this.#activeDescendantControl) { 1640 this.#setActiveDescendant(from, to, directlyActivated); 1641 } else { 1642 this.#clearActiveDescendant(); 1643 } 1644 return; 1645 } 1646 if (from && from !== to && this.#savedTabIndex.has(from)) { 1647 from.setAttribute('tabindex', '-1'); 1648 } 1649 if (to) { 1650 to.setAttribute('tabindex', '0'); 1651 } 1652 } 1653 #setActiveDescendant(from, to, directlyActivated = false) { 1654 if (!to.id) { 1655 to.setAttribute('id', `fz-${main_core.Text.getRandom()}`); 1656 this.#generatedElementIds.add(to); 1657 } 1658 if (from && from !== to) { 1659 from.removeAttribute(ActiveDescendant.ELEMENT_ATTR); 1660 } 1661 if (!this.#activeDescendantControl) { 1662 return; 1663 } 1664 const previousId = this.#activeDescendantControl.getAttribute('aria-activedescendant'); 1665 const nextActivationMode = directlyActivated ? ActiveDescendant.EXPLICIT : ActiveDescendant.IMPLICIT; 1666 const currentActivationMode = to.getAttribute(ActiveDescendant.ELEMENT_ATTR); 1667 if (previousId === to.id && currentActivationMode === nextActivationMode) { 1668 return; 1669 } 1670 this.#activeDescendantControl.setAttribute('aria-activedescendant', to.id); 1671 this.#container.setAttribute(ActiveDescendant.CONTAINER_ATTR, to.id); 1672 to.setAttribute(ActiveDescendant.ELEMENT_ATTR, nextActivationMode); 1673 AccessibilityLogger.logNode('focus-zone', 'active descendant ->', to); 1674 this.#options.onActiveDescendantChanged?.(to, from, directlyActivated); 1675 } 1676 #clearActiveDescendant(previousElement = this.#currentFocusedElement) { 1677 if (this.#focusInStrategy === 'first') { 1678 this.#currentFocusedElement = null; 1679 } 1680 this.#activeDescendantControl?.removeAttribute('aria-activedescendant'); 1681 this.#container.removeAttribute(ActiveDescendant.CONTAINER_ATTR); 1682 if (previousElement) { 1683 previousElement.removeAttribute(ActiveDescendant.ELEMENT_ATTR); 1684 } 1685 const stale = this.#container.querySelectorAll(`[${ActiveDescendant.ELEMENT_ATTR}]`); 1686 for (const element of stale) { 1687 element.removeAttribute(ActiveDescendant.ELEMENT_ATTR); 1688 } 1689 this.#options.onActiveDescendantChanged?.(null, previousElement, false); 1690 } 1691 #handleKeyDown(event) {
1692 const keyBit = KEY_TO_BIT[event.key]; 1693 if (keyBit === undefined || this.#elementSet.size === 0) { 1694 return; 1695 } 1696 if (event.defaultPrevented || (keyBit & this.#bindKeys) === 0 || FocusZone.#isNativeInputKey(event, FocusNavigator.getActiveElement())) { 1697 return; 1698 } 1699 const direction = FocusZone.#getDirection(event); 1700 let nextElement = null; 1701 if (this.#options.getNextFocusable) { 1702 nextElement = this.#options.getNextFocusable(direction, FocusNavigator.getActiveElement() ?? null, event); 1703 } 1704 if (!nextElement) { 1705 const wrap = this.#focusOutBehavior === 'wrap' && event.key !== 'Tab'; 1706 const navOptions = this.#getNavigatorOptions(this.#currentFocusedElement, wrap); 1707 switch (direction) { 1708 case 'previous': 1709 { 1710 nextElement = FocusNavigator.getPrevious(this.#container, navOptions); 1711 break; 1712 } 1713 case 'next': 1714 { 1715 nextElement = FocusNavigator.getNext(this.#container, navOptions); 1716 break; 1717 } 1718 case 'start': 1719 { 1720 nextElement = FocusNavigator.getFirst(this.#container, navOptions); 1721 break; 1722 } 1723 default: 1724 { 1725 nextElement = FocusNavigator.getLast(this.#container, navOptions); 1726 } 1727 } 1728 } 1729 if (this.#activeDescendantControl) { 1730 this.#updateFocusedElement(nextElement || this.#currentFocusedElement, true); 1731 } else if (nextElement) { 1732 nextElement.focus({ 1733 preventScroll: this.#preventScroll 1734 }); 1735 } 1736 if (event.key !== 'Tab' || nextElement) { 1737 event.preventDefault(); 1738 } 1739 } 1740 #handleFocusIn(event) { 1741 if (!main_core.Type.isElementNode(event.target)) { 1742 return; 1743 } 1744 if (this.#activeDescendantControl) { 1745 this.#handleFocusInActiveDescendant(event); 1746 } else { 1747 this.#handleFocusInRovingTabindex(event); 1748 } 1749 } 1750 #handleFocusInActiveDescendant(event) { 1751 const target = event.target; 1752 if (this.#elementSet.has(target)) { 1753 this.#activeDescendantControl?.focus({ 1754 preventScroll: this.#preventScroll 1755 }); 1756 this.#updateFocusedElement(target); 1757 } 1758 } 1759 #handleFocusInRovingTabindex(event) { 1760 const target = event.target; 1761 const tracker = FocusMonitor.Instance.getModalityTracker(); 1762 const isPointerFocus = tracker.getLastModality() === 'pointer'; 1763 if (isPointerFocus) { 1764 if (this.#elementSet.has(target)) { 1765 this.#updateFocusedElement(target); 1766 } 1767 return; 1768 } 1769 if (!this.#elementSet.has(target)) { 1770 return; 1771 } 1772 if (this.#focusInStrategy === 'previous') { 1773 this.#updateFocusedElement(target); 1774 } else if (this.#focusInStrategy === 'closest' || this.#focusInStrategy === 'first') { 1775 if (this.#isFocusEnteringFromOutside(event.relatedTarget)) { 1776 const targetElement = this.#getEntryFocusedElement(); 1777 if (targetElement) { 1778 targetElement.focus({ 1779 preventScroll: this.#preventScroll 1780 }); 1781 } 1782 return; 1783 } 1784 this.#updateFocusedElement(target); 1785 } else if (main_core.Type.isFunction(this.#focusInStrategy)) { 1786 if (this.#isFocusEnteringFromOutside(event.relatedTarget)) { 1787 const elementToFocus = this.#focusInStrategy(event.relatedTarget); 1788 if (elementToFocus && this.#elementSet.has(elementToFocus)) { 1789 elementToFocus.focus({ 1790 preventScroll: this.#preventScroll 1791 }); 1792 return; 1793 } 1794 AccessibilityLogger.warn('focus-zone', 'focusInStrategy returned an element outside the managed focus set.'); 1795 } 1796 this.#updateFocusedElement(target); 1797 } 1798 } 1799 #resolveInitialFocusedElement() { 1800 if (main_core.Type.isFunction(this.#focusInStrategy)) { 1801 const element = this.#focusInStrategy(this.#currentFocusedElement); 1802 return element && this.#elementSet.has(element) ? element : null; 1803 } 1804 return FocusNavigator.getFirst(this.#container, this.#getNavigatorOptions()); 1805 } 1806 #getEntryFocusedElement() { 1807 const navOptions = this.#getNavigatorOptions(); 1808 return FocusMonitor.Instance.getModalityTracker().isLastNavigationReversed() ? FocusNavigator.getLast(this.#container, navOptions) : FocusNavigator.getFirst(this.#container, navOptions); 1809 } 1810 #isFocusEnteringFromOutside(relatedTarget) { 1811 return main_core.Type.isElementNode(relatedTarget) && !this.#container.contains(relatedTarget); 1812 } 1813 #handleMouseMove(event) { 1814 const { 1815 target 1816 } = event; 1817 if (!main_core.Type.isElementNode(target)) { 1818 return; 1819 } 1820 const managedElement = this.#findManagedAncestor(target); 1821 if (managedElement) { 1822 this.#updateFocusedElement(managedElement); 1823 } 1824 } 1825 #handleControlFocusIn() { 1826 const preventInitialFocus = this.#focusInStrategy === 'initial' && this.#activeDescendantControl; 1827 if (this.#currentFocusedElement) { 1828 this.#setActiveDescendant(null, this.#currentFocusedElement); 1829 } else if (!preventInitialFocus) { 1830 this.#updateFocusedElement(FocusNavigator.getFirst(this.#container, this.#getNavigatorOptions())); 1831 } 1832 } 1833 #handleControlFocusOut() { 1834 this.#clearActiveDescendant(); 1835 } 1836 #listen(target, event, handler, capture = false) { 1837 main_core.Event.bind(target, event, handler, capture); 1838 this.#bindings.push([target, event, handler, capture]); 1839 } 1840 #attachEventListeners() {
1841 const keyboardTarget = this.#activeDescendantControl ?? this.#container; 1842 this.#listen(keyboardTarget, 'keydown', this.#keyDownHandler); 1843 this.#listen(this.#container, 'focusin', this.#focusInHandler); 1844 if (this.#activeDescendantControl) { 1845 if (!this.#ignoreHoverEvents) { 1846 this.#listen(this.#container, 'mousemove', this.#mouseMoveHandler, true); 1847 } 1848 this.#listen(this.#activeDescendantControl, 'focusin', this.#controlFocusInHandler); 1849 this.#listen(this.#activeDescendantControl, 'focusout', this.#controlFocusOutHandler); 1850 } 1851 } 1852 #detachEventListeners() { 1853 for (const [target, event, handler, capture] of this.#bindings) { 1854 main_core.Event.unbind(target, event, handler, capture); 1855 } 1856 this.#bindings = []; 1857 } 1858 #attachObserver() { 1859 this.#observer = new MutationObserver(() => { 1860 this.#scheduleSyncFocusableElements(); 1861 }); 1862 this.#observer.observe(this.#container, { 1863 attributes: true, 1864 subtree: true, 1865 childList: true, 1866 attributeFilter: ['hidden', 'disabled', 'tabindex', 'inert', 'contenteditable', 'aria-disabled'] 1867 }); 1868 } 1869 #scheduleSyncFocusableElements() { 1870 if (!this.#active || this.#syncFrameId !== null) { 1871 return; 1872 } 1873 this.#syncFrameId = requestAnimationFrame(() => { 1874 this.#syncFrameId = null; 1875 this.#syncFocusableElements(); 1876 }); 1877 } 1878 #syncFocusableElements() { 1879 if (!this.#active) { 1880 return; 1881 } 1882 const newSet = this.#collectFocusableSet(); 1883 const oldSet = this.#elementSet; 1884 for (const element of oldSet) { 1885 if (!newSet.has(element)) { 1886 this.#restoreTabIndex(element); 1887 } 1888 } 1889 for (const element of newSet) { 1890 if (!oldSet.has(element)) { 1891 this.#saveAndSetTabIndex(element); 1892 } 1893 } 1894 this.#elementSet = newSet; 1895 if (this.#currentFocusedElement && !newSet.has(this.#currentFocusedElement)) { 1896 this.#updateFocusedElement(FocusNavigator.getFirst(this.#container, this.#getNavigatorOptions())); 1897 } else if (!this.#currentFocusedElement && newSet.size > 0) { 1898 const preventInitialFocus = this.#focusInStrategy === 'initial' && this.#activeDescendantControl; 1899 if (!preventInitialFocus) { 1900 this.#updateFocusedElement(FocusNavigator.getFirst(this.#container, this.#getNavigatorOptions())); 1901 } 1902 } 1903 } 1904 static #getDirection(event) { 1905 const direction = KEY_TO_DIRECTION[event.key]; 1906 if (event.key === 'Tab' && event.shiftKey) { 1907 return 'previous'; 1908 } 1909 const isMac = main_core.Browser.isMac(); 1910 if (isMac && event.metaKey || !isMac && event.ctrlKey) { 1911 if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') { 1912 return 'start'; 1913 } 1914 if (event.key === 'ArrowRight' || event.key === 'ArrowDown') { 1915 return 'end'; 1916 } 1917 } 1918 return direction; 1919 } 1920 static #isEditableElement(element) { 1921 if (!main_core.Type.isElementNode(element)) { 1922 return false; 1923 } 1924 const el = element; 1925 if (el.tagName === 'INPUT') { 1926 return !NON_EDITABLE_INPUT_TYPES.has(element.type); 1927 } 1928 if (el.tagName === 'TEXTAREA' || el.tagName === 'SELECT') { 1929 return true; 1930 } 1931 return el.isContentEditable; 1932 } 1933 static #isNativeInputKey(event, activeElement) { 1934 const { 1935 key, 1936 metaKey, 1937 altKey 1938 } = event; 1939 const codePoint = key.codePointAt(0) ?? 0; 1940 const isSingleChar = key.length === 1 || key.length === 2 && codePoint >= 0xD800 && codePoint <= 0xDBFF; 1941 const isEditable = FocusZone.#isEditableElement(activeElement); 1942 const isSelect = activeElement?.tagName === 'SELECT'; 1943 if (isEditable && (isSingleChar || key === 'Home' || key === 'End')) { 1944 return true; 1945 } 1946 if (isSelect) { 1947 const isMac = main_core.Browser.isMac(); 1948 if (key === 'ArrowDown' && isMac && !metaKey) { 1949 return true; 1950 } 1951 return key === 'ArrowDown' && !isMac && altKey; 1952 } 1953 if (isEditable) { 1954 const isInputElement = activeElement?.tagName === 'TEXTAREA' || activeElement?.tagName === 'INPUT'; 1955 const inputEl = activeElement; 1956 const cursorAtStart = isInputElement && inputEl.selectionStart === 0 && inputEl.selectionEnd === 0; 1957 const cursorAtEnd = isInputElement && inputEl.selectionStart === inputEl.value.length && inputEl.selectionEnd === inputEl.value.length; 1958 if (key === 'ArrowLeft' && !cursorAtStart) { 1959 return true; 1960 } 1961 if (key === 'ArrowRight' && !cursorAtEnd) { 1962 return true; 1963 } 1964 const isContentEditable = main_core.Type.isElementNode(activeElement) && activeElement.isContentEditable; 1965 if (activeElement?.tagName === 'TEXTAREA' || isContentEditable) { 1966 if (key === 'PageUp' || key === 'PageDown') { 1967 return true; 1968 } 1969 if (key === 'ArrowUp' && !cursorAtStart) { 1970 return true; 1971 } 1972 if (key === 'ArrowDown' && !cursorAtEnd) { 1973 return true; 1974 } 1975 } 1976 } 1977 return false;
1978 } 1979 } 1980 1981 class VisuallyHidden extends HTMLElement { 1982 connectedCallback() { 1983 Object.assign(this.style, { 1984 position: 'absolute', 1985 width: '1px', 1986 height: '1px', 1987 padding: '0', 1988 margin: '-1px', 1989 overflow: 'hidden', 1990 clip: 'rect(0, 0, 0, 0)', 1991 whiteSpace: 'nowrap', 1992 border: '0' 1993 }); 1994 } 1995 } 1996 if (!customElements.get('visually-hidden')) 1997 { 1998 customElements.define('visually-hidden', VisuallyHidden); 1999 } 2000 2001 FocusMonitor.initialize(); 2002 2003 exports.AccessibilityLogger = AccessibilityLogger; 2004 exports.AccessibilitySettings = AccessibilitySettings; 2005 exports.ActiveDescendant = ActiveDescendant; 2006 exports.FocusKeys = FocusKeys; 2007 exports.FocusMonitor = FocusMonitor; 2008 exports.FocusNavigator = FocusNavigator; 2009 exports.FocusTrap = FocusTrap; 2010 exports.FocusTrapDirective = FocusTrapDirective; 2011 exports.FocusZone = FocusZone; 2012 exports.InputModalityTracker = InputModalityTracker; 2013 exports.InteractivityChecker = InteractivityChecker; 2014 exports.LiveAnnouncer = LiveAnnouncer; 2015 exports.RESTORE_FOCUS_EVENT = RESTORE_FOCUS_EVENT; 2016 exports.VisuallyHidden = VisuallyHidden; 2017 2018})(this.BX.UI.Accessibility = this.BX.UI.Accessibility || {}, BX); 2019//# sourceMappingURL=a11y.bundle.js.map
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.