// tslint:disable:max-line-length // tslint:disable:match-default-export-name // tslint:disable:typedef // tslint:disable:variable-name import { IMenuItem } from '../../services/IMenuItem'; import { debugLog } from '../../services/MegaMenuDebug'; import { MegaMenuMode } from './MegaMenuMode'; import { isNavigationUrlActive } from '../../services/MegaMenuCore'; const LOG_SOURCE: string = 'MegaMenuRenderer'; export interface IMegaMenuRendererStrings { navigationLabel: string; openNavigation: string; closeNavigation: string; openSubmenu: string; closeSubmenu: string; submenuLabel: string; externalLink: string; opened: string; closed: string; } export class MegaMenuRenderer { private static readonly hoverOpenDelayMs: number = 0; private static readonly hoverCloseDelayMs: number = 0; private _container: HTMLElement | undefined; private _documentKeydownHandler: ((event: KeyboardEvent) => void) | undefined; private _documentClickHandler: ((event: MouseEvent) => void) | undefined; private _announcer: HTMLElement | undefined; private _mobileToggle: HTMLButtonElement | undefined; constructor( private menuItems: IMenuItem[], private debug: boolean = false, private menuMode: MegaMenuMode = 'megaMenu', private strings: IMegaMenuRendererStrings ) { } public render(container: HTMLElement): void { debugLog(this.debug, LOG_SOURCE, 'Render started.', { menuItemCount: this.menuItems.length, containerId: container.id || '', containerClassName: container.className || '' }); this.dispose(); this._container = container; container.innerHTML = ''; container.id = 'CustomNavigation'; const nav = document.createElement('nav'); nav.id = 'Mega-Menu'; nav.className = this.joinClasses('mega-menu-main', 'mega-menu-mode-' + this.menuMode); nav.setAttribute('data-menu-mode', this.menuMode); nav.setAttribute('aria-label', this.strings.navigationLabel); this._mobileToggle = document.createElement('button'); this._mobileToggle.type = 'button'; this._mobileToggle.className = 'mega-menu-mobile-toggle'; this._mobileToggle.setAttribute('aria-expanded', 'false'); this._mobileToggle.setAttribute('aria-controls', 'Mega-Menu-Items'); this._mobileToggle.setAttribute('aria-label', this.strings.openNavigation); this._mobileToggle.innerHTML = ''; const mobileLabel = this._mobileToggle.querySelector('.mega-menu-mobile-label') as HTMLElement; mobileLabel.textContent = this.strings.navigationLabel; nav.appendChild(this._mobileToggle); const topLevelUl = document.createElement('ul'); topLevelUl.className = 'mega-menu-top-level'; topLevelUl.id = 'Mega-Menu-Items'; this.menuItems.forEach(topLevelItem => { const topLevelLi = this.createTopLevelItem(topLevelItem); topLevelUl.appendChild(topLevelLi); }); nav.appendChild(topLevelUl); container.appendChild(nav); this.attachEventListeners(); this.createScreenReaderAnnouncer(); debugLog(this.debug, LOG_SOURCE, 'Render completed.', { navigationId: nav.id, topLevelElements: topLevelUl.children.length }); } public dispose(): void { debugLog(this.debug, LOG_SOURCE, 'Dispose started.', { hasContainer: !!this._container, hasAnnouncer: !!this._announcer }); if (this._documentKeydownHandler) { document.removeEventListener('keydown', this._documentKeydownHandler); this._documentKeydownHandler = undefined; } if (this._documentClickHandler) { document.removeEventListener('click', this._documentClickHandler); this._documentClickHandler = undefined; } if (this._announcer && this._announcer.parentElement) { this._announcer.parentElement.removeChild(this._announcer); } this._announcer = undefined; this._mobileToggle = undefined; if (this._container) { this._container.innerHTML = ''; } this._container = undefined; debugLog(this.debug, LOG_SOURCE, 'Dispose completed.'); } private createTopLevelItem(item: IMenuItem): HTMLLIElement { const li = document.createElement('li'); const hasChildren = item.hasChildren() && item.items && item.items.length > 0; const isActive = this.isItemActive(item); const isCurrent = this.isCurrentUrl(item.url); li.className = this.joinClasses( 'mega-menu-top-item', hasChildren ? 'has-children' : undefined, isActive ? 'is-active' : undefined, isCurrent ? 'is-current' : undefined ); const topElement = this.createTopLevelElement(item, hasChildren, isActive, isCurrent); li.appendChild(topElement); if (hasChildren) { const toggle: HTMLButtonElement = document.createElement('button'); const menuId: string = 'Mega-Menu-Panel-' + item.id.replace(/[^a-z0-9_-]/gi, ''); toggle.type = 'button'; toggle.className = 'menu-item-toggle'; toggle.setAttribute('aria-haspopup', 'true'); toggle.setAttribute('aria-expanded', 'false'); toggle.setAttribute('aria-controls', menuId); toggle.setAttribute('aria-label', this.strings.openSubmenu + ' ' + item.label); toggle.innerHTML = ''; li.appendChild(toggle); const childMenu = this.menuMode === 'flyout' ? this.createFlyoutMenu(item) : this.createMegaMenu(item); childMenu.id = menuId; li.appendChild(childMenu); } return li; } private createTopLevelElement(item: IMenuItem, hasChildren: boolean, isActive: boolean, isCurrent: boolean): HTMLElement { let element: HTMLElement; if (item.url) { element = document.createElement('a'); (element as HTMLAnchorElement).href = item.url; element.className = 'menu-item-link'; } else { element = document.createElement('span'); element.className = 'menu-item-text'; if (!hasChildren) { element.setAttribute('tabindex', '0'); } } element.textContent = item.label; element.className = this.joinClasses( element.className, hasChildren ? 'menu-item-has-children' : undefined, isActive ? 'is-active' : undefined, isCurrent ? 'is-current' : undefined ); if (isCurrent && item.url) { element.setAttribute('aria-current', 'page'); } if (item.hoverText) { element.title = item.hoverText; } if (item.url) { this.applyLinkBehavior(element as HTMLAnchorElement, item); } return element; } private createMegaMenu(parentItem: IMenuItem): HTMLDivElement { const megaMenuDiv = document.createElement('div'); megaMenuDiv.className = 'mega-menu'; megaMenuDiv.setAttribute('aria-expanded', 'false'); megaMenuDiv.setAttribute('aria-label', parentItem.label + ' ' + this.strings.submenuLabel); const gridDiv = document.createElement('div'); gridDiv.className = 'mega-menu-grid'; gridDiv.style.setProperty('--megaMenuColumnCount', this.getColumnCount(parentItem.items)); if (parentItem.items) { parentItem.items.forEach(secondLevelItem => { const categoryDiv = this.createCategorySection(secondLevelItem); gridDiv.appendChild(categoryDiv); }); } megaMenuDiv.appendChild(gridDiv); return megaMenuDiv; } private createCategorySection(item: IMenuItem): HTMLDivElement { const categoryDiv = document.createElement('div'); const isActive = this.isItemActive(item); const isCurrent = this.isCurrentUrl(item.url); categoryDiv.className = this.joinClasses( 'mega-menu-category', isActive ? 'is-active' : undefined, isCurrent ? 'is-current' : undefined ); const h3 = document.createElement('h3'); h3.className = 'mega-menu-category-title'; if (item.url) { const link = document.createElement('a'); link.href = item.url; link.textContent = item.label; this.applyLinkBehavior(link, item); link.className = this.joinClasses( isActive ? 'is-active' : undefined, isCurrent ? 'is-current' : undefined ); if (isCurrent) { link.setAttribute('aria-current', 'page'); } if (item.hoverText) { link.title = item.hoverText; } h3.appendChild(link); } else { const span = document.createElement('span'); span.textContent = item.label; span.className = this.joinClasses( isActive ? 'is-active' : undefined ); if (item.hoverText) { span.title = item.hoverText; } h3.appendChild(span); } if (item.description) { const description: HTMLParagraphElement = document.createElement('p'); description.className = 'mega-menu-description'; description.textContent = item.description; categoryDiv.appendChild(description); } categoryDiv.appendChild(h3); if (item.hasChildren() && item.items && item.items.length > 0) { const ul = document.createElement('ul'); ul.className = 'mega-menu-links'; item.items.forEach(thirdLevelItem => { const li = document.createElement('li'); const link = document.createElement('a'); const isLinkActive = this.isItemActive(thirdLevelItem); const isLinkCurrent = this.isCurrentUrl(thirdLevelItem.url); li.className = this.joinClasses( isLinkActive ? 'is-active' : undefined, isLinkCurrent ? 'is-current' : undefined ); link.href = thirdLevelItem.url || '#'; link.textContent = thirdLevelItem.label; this.applyLinkBehavior(link, thirdLevelItem); link.className = this.joinClasses( isLinkActive ? 'is-active' : undefined, isLinkCurrent ? 'is-current' : undefined ); if (isLinkCurrent && thirdLevelItem.url) { link.setAttribute('aria-current', 'page'); } if (thirdLevelItem.hoverText) { link.title = thirdLevelItem.hoverText; } li.appendChild(link); ul.appendChild(li); }); categoryDiv.appendChild(ul); } return categoryDiv; } private createFlyoutMenu(parentItem: IMenuItem): HTMLDivElement { const flyout = document.createElement('div'); flyout.className = 'mega-menu mega-menu-flyout'; flyout.setAttribute('aria-expanded', 'false'); flyout.setAttribute('aria-label', parentItem.label + ' ' + this.strings.submenuLabel); const list = document.createElement('ul'); list.className = 'flyout-list flyout-level-2'; if (parentItem.items) { parentItem.items.forEach(secondLevelItem => { list.appendChild(this.createFlyoutItem(secondLevelItem)); }); } flyout.appendChild(list); return flyout; } private createFlyoutItem(item: IMenuItem): HTMLLIElement { const li = document.createElement('li'); const hasChildren = item.hasChildren() && item.items && item.items.length > 0; const isActive = this.isItemActive(item); const isCurrent = this.isCurrentUrl(item.url); li.className = this.joinClasses( 'flyout-item', hasChildren ? 'has-children' : undefined, isActive ? 'is-active' : undefined, isCurrent ? 'is-current' : undefined ); const element = this.createFlyoutElement(item, hasChildren, isActive, isCurrent); li.appendChild(element); if (hasChildren && item.items) { const toggle: HTMLButtonElement = document.createElement('button'); const childId: string = 'Mega-Menu-Flyout-' + item.id.replace(/[^a-z0-9_-]/gi, ''); toggle.type = 'button'; toggle.className = 'flyout-toggle'; toggle.setAttribute('aria-expanded', 'false'); toggle.setAttribute('aria-controls', childId); toggle.setAttribute('aria-label', this.strings.openSubmenu + ' ' + item.label); toggle.innerHTML = ''; li.appendChild(toggle); const childList = document.createElement('ul'); childList.className = 'flyout-list flyout-level-3'; childList.id = childId; childList.setAttribute('aria-label', item.label + ' ' + this.strings.submenuLabel); item.items.forEach(thirdLevelItem => { childList.appendChild(this.createFlyoutItem(thirdLevelItem)); }); li.appendChild(childList); } return li; } private createFlyoutElement(item: IMenuItem, hasChildren: boolean, isActive: boolean, isCurrent: boolean): HTMLElement { const element: HTMLElement = item.url ? document.createElement('a') : document.createElement('span'); if (item.url) { (element as HTMLAnchorElement).href = item.url; this.applyLinkBehavior(element as HTMLAnchorElement, item); } else { element.setAttribute('tabindex', '0'); } element.textContent = item.label; element.className = this.joinClasses( 'flyout-link', hasChildren ? 'flyout-link-has-children' : undefined, isActive ? 'is-active' : undefined, isCurrent ? 'is-current' : undefined ); if (isCurrent && item.url) { element.setAttribute('aria-current', 'page'); } if (item.hoverText) { element.title = item.hoverText; } return element; } private attachEventListeners(): void { const headings = document.querySelectorAll('#Mega-Menu > ul > li > .menu-item-toggle'); for (let i = 0; i < headings.length; i++) { const heading = headings[i] as HTMLElement; const megaMenu = heading.nextElementSibling as HTMLElement; if (megaMenu && megaMenu.classList.contains('mega-menu')) { this.attachKeyboardNavigation(heading, megaMenu); this.attachMouseEvents(heading, megaMenu); this.attachFocusManagement(heading, megaMenu); } } this.attachGlobalKeyboardNavigation(); this.attachGlobalClickOutsideHandler(); this.attachMobileToggle(); this.attachFlyoutToggles(); } private attachKeyboardNavigation(heading: HTMLElement, megaMenu: HTMLElement): void { heading.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); this.toggleMegaMenu(heading, megaMenu); }); heading.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key === 'ArrowDown') { e.preventDefault(); this.openMegaMenu(heading, megaMenu); this.focusFirstLink(megaMenu); } else if (e.key === 'ArrowUp') { e.preventDefault(); this.closeMegaMenu(heading, megaMenu); } else if (e.key === 'Escape') { e.preventDefault(); this.closeMegaMenu(heading, megaMenu); heading.focus(); } }); heading.addEventListener('focus', () => { debugLog(this.debug, LOG_SOURCE, 'Focus moved to:', heading.textContent); }); } private attachMouseEvents(heading: HTMLElement, megaMenu: HTMLElement): void { const parentLi = heading.parentElement as HTMLElement; let openTimeout: number | undefined; let closeTimeout: number | undefined; const clearOpenTimeout = () => { if (typeof openTimeout === 'number') { window.clearTimeout(openTimeout); openTimeout = undefined; } }; const clearCloseTimeout = () => { if (typeof closeTimeout === 'number') { window.clearTimeout(closeTimeout); closeTimeout = undefined; } }; parentLi.addEventListener('mouseenter', () => { clearCloseTimeout(); if (megaMenu.classList.contains('js-open')) { return; } clearOpenTimeout(); openTimeout = window.setTimeout(() => { this.openMegaMenu(heading, megaMenu); openTimeout = undefined; }, MegaMenuRenderer.hoverOpenDelayMs); }); parentLi.addEventListener('mouseleave', () => { clearOpenTimeout(); clearCloseTimeout(); closeTimeout = window.setTimeout(() => { this.closeMegaMenu(heading, megaMenu); closeTimeout = undefined; }, MegaMenuRenderer.hoverCloseDelayMs); }); heading.addEventListener('focus', () => { clearCloseTimeout(); }); megaMenu.addEventListener('mouseenter', () => { clearCloseTimeout(); }); } private attachFocusManagement(heading: HTMLElement, megaMenu: HTMLElement): void { megaMenu.addEventListener('focusout', () => { setTimeout(() => { const focusedElement = document.activeElement as HTMLElement; const isInsideThisMenu = megaMenu.contains(focusedElement); const isOnThisTrigger = focusedElement === heading; const isInAnyMegaMenu = focusedElement.closest('.mega-menu'); const isOnAnyTopLevel = focusedElement.closest('.mega-menu-top-item'); if (!isInsideThisMenu && !isOnThisTrigger && !isInAnyMegaMenu && !isOnAnyTopLevel) { debugLog(this.debug, LOG_SOURCE, 'Closing menu because focus left the navigation.'); this.closeMegaMenu(heading, megaMenu); } }, 150); }); } private attachGlobalKeyboardNavigation(): void { this._documentKeydownHandler = (e: KeyboardEvent): void => { const activeElement = document.activeElement as HTMLElement; if (e.key === 'Escape') { const openMenu = document.querySelector('.mega-menu[aria-expanded="true"]') as HTMLElement; if (openMenu) { const triggerLink = openMenu.previousElementSibling as HTMLElement; this.closeMegaMenu(triggerLink, openMenu); triggerLink.focus(); } } if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && activeElement.closest('.mega-menu-top-item')) { const currentItem = activeElement.closest('.mega-menu-top-item') as HTMLElement; const items = document.querySelectorAll('#Mega-Menu > ul > .mega-menu-top-item'); let currentIndex: number = -1; for (let index: number = 0; index < items.length; index++) { if (items[index] === currentItem) { currentIndex = index; break; } } if (currentIndex >= 0) { e.preventDefault(); const offset: number = e.key === 'ArrowRight' ? 1 : -1; const targetIndex: number = (currentIndex + offset + items.length) % items.length; const target = (items[targetIndex] as HTMLElement).querySelector('a, .menu-item-toggle') as HTMLElement; if (target) { target.focus(); } } } if (e.key === 'Tab') { if (!e.shiftKey) { const currentTopLevel = activeElement.closest('.menu-item-toggle') as HTMLElement; if (currentTopLevel) { const parentLi = currentTopLevel.closest('li') as HTMLElement; const megaMenu = parentLi.querySelector('.mega-menu.js-open') as HTMLElement; if (megaMenu) { e.preventDefault(); const firstLink = megaMenu.querySelector('a') as HTMLElement; if (firstLink) { firstLink.focus(); } return; } } } if (e.shiftKey) { const megaMenu = activeElement.closest('.mega-menu') as HTMLElement; if (megaMenu && megaMenu.classList.contains('js-open')) { const allLinksInMenu = megaMenu.querySelectorAll('a'); const firstLinkInMenu = allLinksInMenu[0] as HTMLElement; if (activeElement === firstLinkInMenu) { e.preventDefault(); const triggerElement = megaMenu.previousElementSibling as HTMLElement; triggerElement.focus(); return; } } } } }; document.addEventListener('keydown', this._documentKeydownHandler); } private attachGlobalClickOutsideHandler(): void { this._documentClickHandler = (e: MouseEvent): void => { const eventTarget = e.target as Element; const isInsideNavigation = !!(eventTarget && typeof eventTarget.closest === 'function' && eventTarget.closest('#CustomNavigation')); if (isInsideNavigation) { return; } const openMenu = document.querySelector('.mega-menu.js-open') as HTMLElement; if (openMenu) { debugLog(this.debug, LOG_SOURCE, 'Closing menu because of outside click.'); this.closeAllMegaMenus(); } }; document.addEventListener('click', this._documentClickHandler); } private attachMobileToggle(): void { if (!this._mobileToggle) { return; } this._mobileToggle.addEventListener('click', (): void => { const nav: HTMLElement = this._mobileToggle.closest('#Mega-Menu') as HTMLElement; const isOpen: boolean = this._mobileToggle.getAttribute('aria-expanded') === 'true'; this._mobileToggle.setAttribute('aria-expanded', String(!isOpen)); this._mobileToggle.setAttribute('aria-label', isOpen ? this.strings.openNavigation : this.strings.closeNavigation); nav.classList.toggle('is-mobile-open', !isOpen); if (isOpen) { this.closeAllMegaMenus(); } }); const nav: HTMLElement = this._mobileToggle.closest('#Mega-Menu') as HTMLElement; nav.addEventListener('click', (event: MouseEvent): void => { const target: Element = event.target as Element; if (target && target.closest('a') && window.innerWidth <= 767) { this._mobileToggle.setAttribute('aria-expanded', 'false'); this._mobileToggle.setAttribute('aria-label', this.strings.openNavigation); nav.classList.remove('is-mobile-open'); } }); } private attachFlyoutToggles(): void { const toggles = document.querySelectorAll('#Mega-Menu .flyout-toggle'); for (let i: number = 0; i < toggles.length; i++) { const toggle: HTMLButtonElement = toggles[i] as HTMLButtonElement; const parent: HTMLElement = toggle.parentElement as HTMLElement; const setOpen = (open: boolean): void => { toggle.setAttribute('aria-expanded', String(open)); toggle.setAttribute('aria-label', (open ? this.strings.closeSubmenu : this.strings.openSubmenu) + ' ' + this.getFlyoutToggleLabel(toggle)); parent.classList.toggle('is-open', open); }; toggle.addEventListener('click', (event: MouseEvent): void => { event.preventDefault(); setOpen(toggle.getAttribute('aria-expanded') !== 'true'); }); toggle.addEventListener('keydown', (event: KeyboardEvent): void => { if (event.key === 'Escape') { event.preventDefault(); setOpen(false); toggle.focus(); } }); parent.addEventListener('mouseenter', (): void => setOpen(true)); parent.addEventListener('mouseleave', (): void => setOpen(false)); parent.addEventListener('focusin', (): void => setOpen(true)); parent.addEventListener('focusout', (): void => { window.setTimeout((): void => { if (!parent.contains(document.activeElement)) { setOpen(false); } }, 0); }); } } private openMegaMenu(trigger: HTMLElement, menu: HTMLElement): void { this.closeAllMegaMenus(menu); const label: string = this.getTriggerLabel(trigger); trigger.setAttribute('aria-expanded', 'true'); trigger.setAttribute('aria-label', this.strings.closeSubmenu + ' ' + label); (trigger.parentElement as HTMLElement).classList.add('is-open'); menu.setAttribute('aria-expanded', 'true'); menu.classList.add('js-open'); this.announce(label + ' ' + this.strings.opened); debugLog(this.debug, LOG_SOURCE, 'Menu opened:', label); } private closeMegaMenu(trigger: HTMLElement, menu: HTMLElement): void { const label: string = this.getTriggerLabel(trigger); trigger.setAttribute('aria-expanded', 'false'); trigger.setAttribute('aria-label', this.strings.openSubmenu + ' ' + label); (trigger.parentElement as HTMLElement).classList.remove('is-open'); menu.setAttribute('aria-expanded', 'false'); menu.classList.remove('js-open'); this.announce(label + ' ' + this.strings.closed); debugLog(this.debug, LOG_SOURCE, 'Menu closed:', label); } private toggleMegaMenu(trigger: HTMLElement, menu: HTMLElement): void { const isOpen = trigger.getAttribute('aria-expanded') === 'true'; if (isOpen) { this.closeMegaMenu(trigger, menu); } else { this.openMegaMenu(trigger, menu); } } private closeAllMegaMenus(exceptMenu?: HTMLElement): void { const allTriggers = document.querySelectorAll('#Mega-Menu > ul > li > .menu-item-toggle[aria-expanded="true"]'); for (let i = 0; i < allTriggers.length; i++) { const trigger = allTriggers[i] as HTMLElement; const menu = trigger.nextElementSibling as HTMLElement; if (menu && menu !== exceptMenu) { this.closeMegaMenu(trigger, menu); } } } private focusFirstLink(megaMenu: HTMLElement): void { const firstLink = megaMenu.querySelector('a') as HTMLElement; if (firstLink) { firstLink.focus(); } } private isItemActive(item: IMenuItem): boolean { if (this.isCurrentUrl(item.url)) { return true; } if (!item.items || item.items.length === 0) { return false; } for (let i = 0; i < item.items.length; i++) { if (this.isItemActive(item.items[i])) { return true; } } return false; } private isCurrentUrl(url?: string): boolean { return isNavigationUrlActive(window.location.href, url || '', window.location.origin); } private getColumnCount(items?: IMenuItem[]): string { const itemCount = items ? items.length : 0; const columnCount = Math.min(Math.max(itemCount, 1), 6); return columnCount.toString(); } private applyLinkBehavior(link: HTMLAnchorElement, item: IMenuItem): void { if (item.openInNewWindow) { link.target = '_blank'; link.rel = 'noopener noreferrer'; } if (item.external) { const currentLabel: string = link.getAttribute('aria-label') || item.label; link.setAttribute('aria-label', currentLabel + ' (' + this.strings.externalLink + ')'); } } private getTriggerLabel(trigger: HTMLElement): string { const labelElement: HTMLElement = trigger.parentElement ? trigger.parentElement.querySelector('.menu-item-link, .menu-item-text') as HTMLElement : undefined; return labelElement ? labelElement.textContent || '' : ''; } private getFlyoutToggleLabel(toggle: HTMLElement): string { const labelElement: HTMLElement = toggle.parentElement ? toggle.parentElement.querySelector('.flyout-link') as HTMLElement : undefined; return labelElement ? labelElement.textContent || '' : ''; } private joinClasses(...classNames: Array): string { const filteredClassNames: string[] = []; for (let i = 0; i < classNames.length; i++) { if (classNames[i]) { filteredClassNames.push(classNames[i] as string); } } return filteredClassNames.join(' '); } private createScreenReaderAnnouncer(): void { const existingAnnouncer: HTMLElement = document.getElementById('mega-menu-sr-announcer'); if (existingAnnouncer) { this._announcer = existingAnnouncer; return; } const srAnnouncer = document.createElement('div'); srAnnouncer.id = 'mega-menu-sr-announcer'; srAnnouncer.setAttribute('aria-live', 'polite'); srAnnouncer.setAttribute('aria-atomic', 'true'); srAnnouncer.className = 'sr-only'; document.body.appendChild(srAnnouncer); this._announcer = srAnnouncer; debugLog(this.debug, LOG_SOURCE, 'Screen reader announcer is ready.'); } private announce(message: string): void { if (this._announcer) { this._announcer.textContent = ''; window.setTimeout((): void => { if (this._announcer) { this._announcer.textContent = message; } }, 20); } } }