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