feat: Enhance MegaMenu with localization, caching, and new properties

- Added localization support for loading states, empty navigation, and submenu actions in both German and English.
- Introduced new properties in IMenuItem for description, openInNewWindow, and external links.
- Updated SPTermStorePickerService to handle cache versioning and language-specific caching.
- Implemented a new MegaMenuCore service for handling term properties, cache management, and navigation URL resolution.
- Refactored TaxonomyNavigationService to build menu hierarchy and filter hidden terms.
- Added automated tests for MegaMenu core functionalities and static asset validation.
- Removed unused ItemDictionary service.
- Created a comprehensive ToDo document outlining the implementation status and future tasks.
This commit is contained in:
Torsten Brendgen
2026-07-20 21:34:24 +02:00
parent bc61166267
commit 50b85468f1
30 changed files with 1394 additions and 294 deletions
+136 -13
View File
@@ -70,6 +70,10 @@ $menu-layer: var(--megaMenuZIndex, 6000);
padding: 0;
}
.mega-menu-mobile-toggle {
display: none;
}
.mega-menu-mode-flyout .mega-menu-top-item {
position: relative;
}
@@ -94,20 +98,40 @@ $menu-layer: var(--megaMenuZIndex, 6000);
outline: none;
}
.menu-item-has-children::after {
.menu-item-toggle {
position: relative;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 32px;
min-height: var(--megaMenuBarHeight, 44px);
margin: 0 8px 0 -10px;
padding: 0;
border: 0;
background: transparent;
color: $neutral-primary;
cursor: pointer;
}
.menu-item-toggle > span::after {
content: '';
display: block;
width: 6px;
height: 6px;
margin: -3px 0 0 10px;
border-right: 1px solid currentColor;
border-bottom: 1px solid currentColor;
transform: rotate(45deg);
}
.menu-item-toggle[aria-expanded='true'] > span::after {
transform: rotate(225deg);
}
.menu-item-link:hover,
.menu-item-text:hover,
.menu-item-link[aria-expanded='true'],
.menu-item-text[aria-expanded='true'] {
.menu-item-toggle:hover,
.menu-item-toggle[aria-expanded='true'] {
background: $neutral-lighter;
color: $neutral-primary;
text-decoration: none;
@@ -134,6 +158,8 @@ $menu-layer: var(--megaMenuZIndex, 6000);
.menu-item-link:focus,
.menu-item-text:focus,
.menu-item-toggle:focus,
.mega-menu-mobile-toggle:focus,
.mega-menu-category a:focus,
.flyout-link:focus {
outline: 2px solid $theme-primary;
@@ -180,6 +206,13 @@ $menu-layer: var(--megaMenuZIndex, 6000);
padding: 0 0 8px;
}
.mega-menu-description {
margin: -4px 0 12px;
color: $neutral-secondary;
font-size: 12px;
line-height: 18px;
}
.mega-menu-category-title {
margin: 0 0 10px;
padding: 0 0 8px;
@@ -289,19 +322,37 @@ $menu-layer: var(--megaMenuZIndex, 6000);
font-weight: 600;
}
.flyout-link-has-children::after {
content: '';
.flyout-toggle {
position: absolute;
top: 50%;
right: 18px;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
padding: 0;
border: 0;
background: transparent;
color: $neutral-primary;
cursor: pointer;
}
.flyout-toggle > span::after {
content: '';
display: block;
width: 6px;
height: 6px;
margin-top: -4px;
border-top: 1px solid currentColor;
border-right: 1px solid currentColor;
transform: rotate(45deg);
}
.flyout-toggle:focus {
outline: 2px solid $theme-primary;
outline-offset: -2px;
}
.flyout-level-3 {
position: absolute;
top: -9px;
@@ -314,7 +365,8 @@ $menu-layer: var(--megaMenuZIndex, 6000);
}
.flyout-item:hover > .flyout-level-3,
.flyout-item:focus-within > .flyout-level-3 {
.flyout-item:focus-within > .flyout-level-3,
.flyout-item.is-open > .flyout-level-3 {
display: block;
}
@@ -342,9 +394,61 @@ $menu-layer: var(--megaMenuZIndex, 6000);
}
@media (max-width: 767px) {
.mega-menu-mobile-toggle {
display: flex;
align-items: center;
width: 100%;
min-height: var(--megaMenuBarHeight, 44px);
padding: 0 16px;
border: 0;
background: $white;
color: $neutral-primary;
cursor: pointer;
font: inherit;
font-weight: 600;
}
.mega-menu-mobile-icon,
.mega-menu-mobile-icon::before,
.mega-menu-mobile-icon::after {
display: block;
width: 18px;
height: 2px;
background: currentColor;
}
.mega-menu-mobile-icon {
position: relative;
margin-right: 12px;
}
.mega-menu-mobile-icon::before,
.mega-menu-mobile-icon::after {
content: '';
position: absolute;
left: 0;
}
.mega-menu-mobile-icon::before { top: -6px; }
.mega-menu-mobile-icon::after { top: 6px; }
.mega-menu-top-level {
flex-wrap: wrap;
padding: 0 8px;
display: none;
flex-direction: column;
padding: 0;
border-top: 1px solid $neutral-light;
}
.mega-menu-main.is-mobile-open .mega-menu-top-level {
display: flex;
}
.mega-menu-top-item,
.mega-menu-mode-flyout .mega-menu-top-item {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) 44px;
border-bottom: 1px solid $neutral-light;
}
.menu-item-link,
@@ -354,6 +458,21 @@ $menu-layer: var(--megaMenuZIndex, 6000);
font-size: 13px;
}
.menu-item-toggle {
width: 44px;
margin: 0;
border-left: 1px solid $neutral-light;
}
.mega-menu {
position: static;
grid-column: 1 / -1;
width: 100%;
border-right: 0;
border-left: 0;
box-shadow: none;
}
.mega-menu-grid {
grid-template-columns: 1fr;
padding: 16px;
@@ -368,12 +487,16 @@ $menu-layer: var(--megaMenuZIndex, 6000);
.flyout-level-3 {
position: static;
display: block;
display: none;
width: 100%;
border: 0;
box-shadow: none;
padding: 0 0 4px 16px;
}
.flyout-item.is-open > .flyout-level-3 {
display: block;
}
}
@media (prefers-reduced-motion: reduce) {
@@ -28,6 +28,7 @@ export interface IMegaMenuApplicationCustomizerProperties {
cacheMinutes?: number;
debug?: boolean;
menuMode?: MegaMenuMode;
cacheVersion?: string;
}
export default class MegaMenuApplicationCustomizer
@@ -42,7 +43,7 @@ export default class MegaMenuApplicationCustomizer
const debug: boolean = this._isDebugEnabled();
debugLog(debug, LOG_SOURCE, 'onInit started.', {
componentId: UserCustomActionMegaMenuId,
version: '2.1.0',
version: '2.2.0',
properties: this.properties || {},
pageUrl: window.location.href,
webUrl: this.context.pageContext.web.absoluteUrl,
@@ -108,13 +109,16 @@ export default class MegaMenuApplicationCustomizer
return;
}
this._renderStatus(strings.Loading);
try {
debugLog(debug, LOG_SOURCE, 'Creating TaxonomyNavigationService.');
const taxonomyService: TaxonomyNavigationService = new TaxonomyNavigationService(
this.context,
termSetIdentifier,
debug,
this._getCacheMinutes()
this._getCacheMinutes(),
this._getCacheVersion(),
this._getLanguageLcid()
);
debugLog(debug, LOG_SOURCE, 'Requesting navigation items from taxonomy.');
@@ -123,6 +127,11 @@ export default class MegaMenuApplicationCustomizer
topLevelItemCount: menuItems.length
});
if (!menuItems || menuItems.length === 0) {
this._renderStatus(strings.EmptyNavigation);
return;
}
if (this._renderer) {
debugLog(debug, LOG_SOURCE, 'Disposing the previous renderer instance.');
this._renderer.dispose();
@@ -131,7 +140,18 @@ export default class MegaMenuApplicationCustomizer
this._renderer = new MegaMenuRenderer(
menuItems,
debug,
this._getMenuMode()
this._getMenuMode(),
{
navigationLabel: strings.NavigationLabel,
openNavigation: strings.OpenNavigation,
closeNavigation: strings.CloseNavigation,
openSubmenu: strings.OpenSubmenu,
closeSubmenu: strings.CloseSubmenu,
submenuLabel: strings.SubmenuLabel,
externalLink: strings.ExternalLink,
opened: strings.Opened,
closed: strings.Closed
}
);
this._menuContainer = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
@@ -213,6 +233,19 @@ export default class MegaMenuApplicationCustomizer
return normalizeMegaMenuMode(this.properties && this.properties.menuMode);
}
private _getCacheVersion(): string {
const value: string = this.properties && this.properties.cacheVersion
? String(this.properties.cacheVersion).trim()
: '1';
return value || '1';
}
private _getLanguageLcid(): number {
const legacyContext: any = (this.context.pageContext as any).legacyPageContext; // tslint:disable-line:no-any
const value: number = legacyContext ? Number(legacyContext.currentLanguage) : 0;
return isFinite(value) && value > 0 ? value : 1033;
}
private _renderStatus(message: string): void {
if (!this._topPlaceholder) {
return;
+198 -69
View File
@@ -6,9 +6,22 @@
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 = 140;
private static readonly hoverCloseDelayMs: number = 180;
@@ -16,11 +29,13 @@ export class MegaMenuRenderer {
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 menuMode: MegaMenuMode = 'megaMenu',
private strings: IMegaMenuRendererStrings
) { }
public render(container: HTMLElement): void {
@@ -38,10 +53,22 @@ export class MegaMenuRenderer {
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', 'Hauptnavigation');
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 = '<span class="mega-menu-mobile-icon" aria-hidden="true"></span><span class="mega-menu-mobile-label"></span>';
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);
@@ -76,6 +103,7 @@ export class MegaMenuRenderer {
this._announcer.parentElement.removeChild(this._announcer);
}
this._announcer = undefined;
this._mobileToggle = undefined;
if (this._container) {
this._container.innerHTML = '';
}
@@ -100,9 +128,20 @@ export class MegaMenuRenderer {
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 = '<span aria-hidden="true"></span>';
li.appendChild(toggle);
const childMenu = this.menuMode === 'flyout'
? this.createFlyoutMenu(item)
: this.createMegaMenu(item);
childMenu.id = menuId;
li.appendChild(childMenu);
}
@@ -119,7 +158,9 @@ export class MegaMenuRenderer {
} else {
element = document.createElement('span');
element.className = 'menu-item-text';
element.setAttribute('tabindex', '0');
if (!hasChildren) {
element.setAttribute('tabindex', '0');
}
}
element.textContent = item.label;
@@ -130,11 +171,6 @@ export class MegaMenuRenderer {
isCurrent ? 'is-current' : undefined
);
if (hasChildren) {
element.setAttribute('aria-haspopup', 'true');
element.setAttribute('aria-expanded', 'false');
}
if (isCurrent && item.url) {
element.setAttribute('aria-current', 'page');
}
@@ -143,6 +179,10 @@ export class MegaMenuRenderer {
element.title = item.hoverText;
}
if (item.url) {
this.applyLinkBehavior(element as HTMLAnchorElement, item);
}
return element;
}
@@ -150,7 +190,7 @@ export class MegaMenuRenderer {
const megaMenuDiv = document.createElement('div');
megaMenuDiv.className = 'mega-menu';
megaMenuDiv.setAttribute('aria-expanded', 'false');
megaMenuDiv.setAttribute('aria-label', parentItem.label + ' Unterkategorien');
megaMenuDiv.setAttribute('aria-label', parentItem.label + ' ' + this.strings.submenuLabel);
const gridDiv = document.createElement('div');
gridDiv.className = 'mega-menu-grid';
@@ -184,6 +224,7 @@ export class MegaMenuRenderer {
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
@@ -207,6 +248,13 @@ export class MegaMenuRenderer {
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) {
@@ -226,6 +274,7 @@ export class MegaMenuRenderer {
link.href = thirdLevelItem.url || '#';
link.textContent = thirdLevelItem.label;
this.applyLinkBehavior(link, thirdLevelItem);
link.className = this.joinClasses(
isLinkActive ? 'is-active' : undefined,
isLinkCurrent ? 'is-current' : undefined
@@ -253,7 +302,7 @@ export class MegaMenuRenderer {
const flyout = document.createElement('div');
flyout.className = 'mega-menu mega-menu-flyout';
flyout.setAttribute('aria-expanded', 'false');
flyout.setAttribute('aria-label', parentItem.label + ' Untermenü');
flyout.setAttribute('aria-label', parentItem.label + ' ' + this.strings.submenuLabel);
const list = document.createElement('ul');
list.className = 'flyout-list flyout-level-2';
@@ -285,9 +334,19 @@ export class MegaMenuRenderer {
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 = '<span aria-hidden="true"></span>';
li.appendChild(toggle);
const childList = document.createElement('ul');
childList.className = 'flyout-list flyout-level-3';
childList.setAttribute('aria-label', item.label + ' Untermenü');
childList.id = childId;
childList.setAttribute('aria-label', item.label + ' ' + this.strings.submenuLabel);
item.items.forEach(thirdLevelItem => {
childList.appendChild(this.createFlyoutItem(thirdLevelItem));
@@ -306,6 +365,7 @@ export class MegaMenuRenderer {
if (item.url) {
(element as HTMLAnchorElement).href = item.url;
this.applyLinkBehavior(element as HTMLAnchorElement, item);
} else {
element.setAttribute('tabindex', '0');
}
@@ -318,9 +378,6 @@ export class MegaMenuRenderer {
isCurrent ? 'is-current' : undefined
);
if (hasChildren) {
element.setAttribute('aria-haspopup', 'true');
}
if (isCurrent && item.url) {
element.setAttribute('aria-current', 'page');
}
@@ -332,7 +389,7 @@ export class MegaMenuRenderer {
}
private attachEventListeners(): void {
const headings = document.querySelectorAll('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > .menu-item-text');
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;
@@ -347,21 +404,17 @@ export class MegaMenuRenderer {
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 === 'Enter') {
if (heading.tagName === 'A') {
return;
}
e.preventDefault();
this.toggleMegaMenu(heading, megaMenu);
} else if (e.key === ' ') {
e.preventDefault();
this.toggleMegaMenu(heading, megaMenu);
} else if (e.key === 'ArrowDown') {
if (e.key === 'ArrowDown') {
e.preventDefault();
this.openMegaMenu(heading, megaMenu);
this.focusFirstLink(megaMenu);
@@ -375,12 +428,6 @@ export class MegaMenuRenderer {
}
});
if (heading.tagName === 'A') {
heading.addEventListener('click', () => {
debugLog(this.debug, LOG_SOURCE, 'Link clicked:', (heading as HTMLAnchorElement).href);
});
}
heading.addEventListener('focus', () => {
debugLog(this.debug, LOG_SOURCE, 'Focus moved to:', heading.textContent);
});
@@ -444,7 +491,7 @@ export class MegaMenuRenderer {
const isInsideThisMenu = megaMenu.contains(focusedElement);
const isOnThisTrigger = focusedElement === heading;
const isInAnyMegaMenu = focusedElement.closest('.mega-menu');
const isOnAnyTopLevel = focusedElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span');
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.');
@@ -467,9 +514,30 @@ export class MegaMenuRenderer {
}
}
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('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span') as HTMLElement;
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;
@@ -523,21 +591,86 @@ export class MegaMenuRenderer {
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);
menu.setAttribute('aria-expanded', 'true');
menu.classList.add('js-open');
this.announce((trigger.textContent || '') + ' geöffnet');
debugLog(this.debug, LOG_SOURCE, 'Menu opened:', trigger.textContent);
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);
menu.setAttribute('aria-expanded', 'false');
menu.classList.remove('js-open');
this.announce((trigger.textContent || '') + ' geschlossen');
debugLog(this.debug, LOG_SOURCE, 'Menu closed:', trigger.textContent);
this.announce(label + ' ' + this.strings.closed);
debugLog(this.debug, LOG_SOURCE, 'Menu closed:', label);
}
private toggleMegaMenu(trigger: HTMLElement, menu: HTMLElement): void {
@@ -550,7 +683,7 @@ export class MegaMenuRenderer {
}
private closeAllMegaMenus(exceptMenu?: HTMLElement): void {
const allTriggers = document.querySelectorAll('#Mega-Menu > ul > li > a[aria-expanded="true"], #Mega-Menu > ul > li > span[aria-expanded="true"]');
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;
@@ -586,36 +719,7 @@ export class MegaMenuRenderer {
}
private isCurrentUrl(url?: string): boolean {
const currentPath = this.normalizePath(window.location.href);
const itemPath = this.normalizePath(url);
if (!currentPath || !itemPath) {
return false;
}
if (itemPath === '/') {
return currentPath === '/';
}
if (currentPath === itemPath) {
return true;
}
return currentPath.indexOf(itemPath + '/') === 0;
}
private normalizePath(url?: string): string {
if (!url) {
return '';
}
try {
const parsedUrl = new URL(url, window.location.origin);
const pathname = (parsedUrl.pathname || '/').replace(/\/+$/, '');
return (pathname || '/').toLowerCase();
} catch (error) {
return '';
}
return isNavigationUrlActive(window.location.href, url || '', window.location.origin);
}
private getColumnCount(items?: IMenuItem[]): string {
@@ -624,6 +728,31 @@ export class MegaMenuRenderer {
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 | undefined>): string {
const filteredClassNames: string[] = [];
+12 -1
View File
@@ -2,6 +2,17 @@ define([], function() {
return {
"Title": "MegaMenuApplicationCustomizer",
"ConfigurationMissing": "Die Navigation ist nicht konfiguriert.",
"LoadError": "Die Navigation konnte nicht geladen werden."
"LoadError": "Die Navigation konnte nicht geladen werden.",
"Loading": "Navigation wird geladen…",
"EmptyNavigation": "Die Navigation enthält keine sichtbaren Einträge.",
"NavigationLabel": "Hauptnavigation",
"OpenNavigation": "Hauptnavigation öffnen",
"CloseNavigation": "Hauptnavigation schließen",
"OpenSubmenu": "Untermenü öffnen für",
"CloseSubmenu": "Untermenü schließen für",
"SubmenuLabel": "Untermenü",
"ExternalLink": "externer Link",
"Opened": "geöffnet",
"Closed": "geschlossen"
};
});
+12 -1
View File
@@ -2,6 +2,17 @@ define([], function() {
return {
"Title": "MegaMenuApplicationCustomizer",
"ConfigurationMissing": "Navigation is not configured.",
"LoadError": "Navigation could not be loaded."
"LoadError": "Navigation could not be loaded.",
"Loading": "Loading navigation…",
"EmptyNavigation": "The navigation does not contain any visible entries.",
"NavigationLabel": "Main navigation",
"OpenNavigation": "Open main navigation",
"CloseNavigation": "Close main navigation",
"OpenSubmenu": "Open submenu for",
"CloseSubmenu": "Close submenu for",
"SubmenuLabel": "submenu",
"ExternalLink": "external link",
"Opened": "opened",
"Closed": "closed"
}
});
+11
View File
@@ -2,6 +2,17 @@ declare interface IMegaMenuApplicationCustomizerStrings {
Title: string;
ConfigurationMissing: string;
LoadError: string;
Loading: string;
EmptyNavigation: string;
NavigationLabel: string;
OpenNavigation: string;
CloseNavigation: string;
OpenSubmenu: string;
CloseSubmenu: string;
SubmenuLabel: string;
ExternalLink: string;
Opened: string;
Closed: string;
}
declare module 'MegaMenuApplicationCustomizerStrings' {