<nav class="gov-navigation" aria-label="Hlavní navigace">
    <ul>
        <li>
            <gov-button type="base" color="primary" size="l" aria-expanded="false" aria-controls="subnav0" aria-label="Úřad">
                Úřad
                <gov-icon slot="icon-end" type="components" name="chevron-up" size="l"></gov-icon>
            </gov-button>
            <ul class="gov-mega-menu" id="megamenu0">
                <li class="gov-mega-menu__heading">
                    <a href="#">
                        <span>
                            <gov-icon type="components" name="briefcase" size="m"></gov-icon>
                            Heading item
                        </span>
                    </a>
                    <ul>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li class="gov-mega-menu__more"><a href="#">+ Další</a></li>
                    </ul>
                </li>
                <li class="gov-mega-menu__heading">
                    <a href="#">
                        <span>
                            <gov-icon type="components" name="briefcase" size="m"></gov-icon>
                            Heading item
                        </span>
                    </a>
                    <ul>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li class="gov-mega-menu__more"><a href="#">+ Další</a></li>
                    </ul>
                </li>
                <li class="gov-mega-menu__heading">
                    <a href="#">
                        <span>
                            <gov-icon type="components" name="briefcase" size="m"></gov-icon>
                            Heading item
                        </span>
                    </a>
                    <ul>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                    </ul>
                </li>
                <li class="gov-mega-menu__heading">
                    <a href="#">
                        <span>
                            <gov-icon type="components" name="briefcase" size="m"></gov-icon>
                            Heading item
                        </span>
                    </a>
                    <ul>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li class="gov-mega-menu__more"><a href="#">+ Další</a></li>
                    </ul>
                </li>
                <li class="gov-mega-menu__heading">
                    <a href="#">
                        <span>
                            <gov-icon type="components" name="briefcase" size="m"></gov-icon>
                            Heading item
                        </span>
                    </a>
                    <ul>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li class="gov-mega-menu__more"><a href="#">+ Další</a></li>
                    </ul>
                </li>
                <li class="gov-mega-menu__heading">
                    <a href="#">
                        <span>
                            <gov-icon type="components" name="briefcase" size="m"></gov-icon>
                            Heading item
                        </span>
                    </a>
                    <ul>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                    </ul>
                </li>
                <li class="gov-mega-menu__heading">
                    <a href="#">
                        <span>
                            <gov-icon type="components" name="briefcase" size="m"></gov-icon>
                            Heading item
                        </span>
                    </a>
                    <ul>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                    </ul>
                </li>
                <li class="gov-mega-menu__heading">
                    <a href="#">
                        <span>
                            <gov-icon type="components" name="briefcase" size="m"></gov-icon>
                            Heading item
                        </span>
                    </a>
                    <ul>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                        <li><a href="#">Menu item</a></li>
                    </ul>
                </li>
            </ul>
        </li>
        <li>
            <a href="#">Menu item</a>
        </li>
        <li>
            <a href="#">Menu item</a>
        </li>
        <li>
            <a href="#">Menu item</a>
        </li>
        <li>
            <gov-button type="base" color="primary" size="l" aria-expanded="false" aria-controls="subnav4" aria-label="Tisk a média">
                Tisk a média
                <gov-icon slot="icon-end" type="components" name="chevron-down" size="l"></gov-icon>
            </gov-button>
        </li>
        <li>
            <a href="#">Kontakty</a>
        </li>
        <li class="gov-mobile-only">
            <a href="#">[object Object]</a>
        </li>
        <li class="gov-navigation__item gov-mobile-only">
            <gov-theme-switch size="m" display-label="true"></gov-theme-switch>
        </li>
    </ul>
</nav>
<nav class="gov-navigation"{{#if aria-label}} aria-label="{{{ aria-label }}}"{{/if}}>
    <ul>
        {{#each navigation }}
        <li>
            {{#if href}}
                <a href="{{ href }}">{{{ text }}}</a>
            {{else}}
                <gov-button type="base" color="primary" size="l"
                            aria-expanded="false" aria-controls="subnav{{@index}}" aria-label="{{{ label/otevrit }}}{{{ button }}}">
                    {{{ button }}}
                    <gov-icon slot="icon-end" type="components" name="{{#if megamenu}}chevron-up{{else}}chevron-down{{/if}}" size="l"></gov-icon>
                </gov-button>
            {{/if}}
            {{#if megamenu}}
                <ul class="gov-mega-menu" id="megamenu{{@index}}">
                    {{#each megamenu }}
                        <li class="gov-mega-menu__heading">
                            <a href="#">
                                <span>
                                    <gov-icon type="components" name="briefcase" size="m"></gov-icon>
                                    {{{ heading }}}
                                </span>
                            </a>
                            <ul>
                                {{#each link }}
                                    <li><a href="#">{{{ name }}}</a></li>
                                {{/each}}
                                {{#if more }}
                                    <li class="gov-mega-menu__more"><a href="#">{{ more }}</a></li>
                                {{/if}}
                            </ul>
                        </li>
                    {{/each}}
                </ul>
            {{/if}}
        </li>
        {{/each}}
        <li class="gov-mobile-only">
            <a href="#">{{{ lang }}}</a>
        </li>
        <li class="gov-navigation__item gov-mobile-only">
            <gov-theme-switch size="m" display-label="true"></gov-theme-switch>
        </li>
    </ul>
</nav>
{
  "aria-label": "Hlavní navigace",
  "lang": {
    "text": "English",
    "color": "primary",
    "type": "base",
    "size": "l",
    "aria-label": "Click to change language to English"
  },
  "label": {
    "otevrit": "Otevřít menu"
  },
  "navigation": [
    {
      "button": "Úřad",
      "megamenu": [
        {
          "heading": "Heading item",
          "link": [
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            }
          ],
          "more": "+ Další"
        },
        {
          "heading": "Heading item",
          "link": [
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            }
          ],
          "more": "+ Další"
        },
        {
          "heading": "Heading item",
          "link": [
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            }
          ]
        },
        {
          "heading": "Heading item",
          "link": [
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            }
          ],
          "more": "+ Další"
        },
        {
          "heading": "Heading item",
          "link": [
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            }
          ],
          "more": "+ Další"
        },
        {
          "heading": "Heading item",
          "link": [
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            }
          ]
        },
        {
          "heading": "Heading item",
          "link": [
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            }
          ]
        },
        {
          "heading": "Heading item",
          "link": [
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            },
            {
              "name": "Menu item"
            }
          ]
        }
      ]
    },
    {
      "text": "Menu item",
      "href": "#"
    },
    {
      "text": "Menu item",
      "href": "#"
    },
    {
      "text": "Menu item",
      "href": "#"
    },
    {
      "button": "Tisk a média",
      "subnav": [
        {
          "name": "Menu item"
        },
        {
          "name": "Menu item"
        }
      ]
    },
    {
      "text": "Kontakty",
      "href": "#"
    }
  ]
}
  • Content:
    @import "../../../styles/mixins";
    
    $class: ".gov-navigation";
    $subnavigation: ".gov-subnavigation";
    $megamenu: ".gov-mega-menu";
    $deferred-navigation: ".gov-deferred-navigation";
    $last-visible-item: ".gov-navigation__last-visible-item";
    
    
    // MIXINS
    @mixin list-li() {
        li {
            margin-bottom: 0;
            a {
                @include flex($align: center);
                @include typography("body-s");
                min-height: var(--height-2xl);
                padding: var(--spacing-xs) 0 var(--spacing-xs) var(--spacing-xs);
                color: var(--text-secondary);
                text-decoration: none;
                &:hover {
                    color: var(--text-primary);
                    text-decoration: underline;
                }
            }
        }
    }
    @mixin a-like-button() {
        @include flex($align: center);
        height: var(--height-component-l);
        padding: 0 var(--spacing-m-nudge);
        text-decoration: none;
        @include typography("body-l");
        font-weight: 500;
        color: var(--text-primary-color);
        white-space: nowrap;
        border-radius: var(--corner-radius-s);
        &:hover {
            background-color: var(--button-outlined-primary-hover);
            color: var(--text-primary-color);
            text-decoration: none;
        }
        &:active {
            background-color: var(--button-outlined-primary-active);
        }
    }
    /**
     * The one definition every dropdown opened from the navigation row shares:
     * `.gov-subnavigation`, `.gov-mega-menu` and `.gov-deferred-navigation`. They
     * used to be styled independently and had drifted apart — different background
     * tokens (the deferred menu rendered grey, the other two white), a megamenu
     * hanging 8px below the row while the others sat flush against it, and
     * different paddings. Only what genuinely belongs to a panel's own layout —
     * the megamenu grid, the deferred accordion — stays local to it.
     */
    @mixin dropdown-panel() {
        padding: var(--spacing-s) var(--spacing-m);
        background-color: var(--background-page);
        @include mq($from: lg) {
            position: absolute;
            top: 100%;
            left: 0;
            min-width: 280px;
            background-color: var(--background-block-primary);
            border-bottom-left-radius: var(--corner-radius-m);
            border-bottom-right-radius: var(--corner-radius-m);
        }
    }
    
    // MAIN NAVIGATION
    #{$class} {
        background-color: var(--component-nav-background);
        ul {
            margin: 0;
            li {
                margin: 0;
                &:before {
                    content: none;
                }
            }
        }
        > ul {
            @include flex($direction: column, $justify: center, $gap: var(--templates-margin-s));
            margin: 0;
            padding: 0;
        }
        > ul > li {
            width: 100%;
        }
        > ul > li > a {
            @include a-like-button();
        }
        > ul > li > gov-button,
        > ul > li > .gov-button {
            width: 100%;
            .element {
                @include flex($align: center, $justify: space-between, $gap: var(--spacing-m));
                width: calc(100% + (var(--spacing-m-nudge) + var(--border-width, 0.0625rem)) * 2);
                font-weight: 500 !important;
            }
    
        }
        &__item {
            width: calc(100% - (var(--spacing-m-nudge) * 2));
            height: var(--height-component-l);
            padding: 0 var(--spacing-m-nudge);
        }
        &--has-megamenu {
            position: static !important;
        }
    
        @include mq($from: lg) {
            background-color: transparent;
            > ul {
                @include flex($direction: row, $justify: space-between, $align: center, $gap: var(--spacing-m));
            }
            > ul > li {
                position: relative;
                display: flex;
                width: auto;
                // Dropdowns are anchored with `left: 0`, so the last item in the row has
                // to flip to `right: 0` or it overflows the right edge of the page.
                // Three anchors are needed because the trailing items differ per markup:
                // `:last-child` covers plain markup (and the JS-generated "Další" item),
                // `:nth-last-child(3)` the header template with its two `.gov-mobile-only`
                // items, and `--last-visible-item` is set by main-navigation.ts once the
                // deferred item is appended and shifts the positional indexes.
                &:last-child #{$subnavigation},
                &:last-child #{$deferred-navigation},
                &:nth-last-child(3) #{$subnavigation},
                &#{$last-visible-item} #{$subnavigation},
                &#{$last-visible-item} #{$deferred-navigation} {
                    left: initial;
                    right: 0;
                }
            }
            > ul > li > a {
                justify-content: center;
                width: 100%;
            }
        }
        @include mq($until: lg) {
            gov-button, .gov-button {
                width: 100%;
                max-width: 100%;
    
                .element {
                    width: calc(100% + (var(--spacing-m-nudge) + var(--border-width, 0.0625rem)) * 2);
                    justify-content: space-between;
                }
            }
        }
    }
    
    // SUB NAVIGATION
    #{$subnavigation} {
        @include flex($direction: column, $gap: var(--spacing-xs));
        @include list-li();
        @include dropdown-panel();
    
        @include mq($from: lg) {
            > li a {
                min-height: auto;
            }
        }
    }
    
    // MEGA MENU
    #{$megamenu} {
        @include dropdown-panel();
        max-width: 100%;
        width: 100%;
        > li ul {
            @include flex($direction: column, $gap: var(--spacing-xs));
            @include list-li();
        }
        &__heading {
            margin: 0;
            span {
                @include flex($align: center, $gap: var(--spacing-s-nudge));
                padding: var(--spacing-xs) 0;
                @include typography("body-m");
                font-weight: 700;
                line-height: var(--height-line-m);
                color: var(--text-primary-color);
            }
            a {
                display: block;
                text-decoration: none;
                &:hover {
                    color: var(--interactive-timer-primary-solid);
                    text-decoration: underline;
                }
            }
        }
        li a {
            min-height: auto;
        }
        &__more a {
            color: var(--text-tertiary) !important;
        }
    
        @include mq($from: lg) {
            top: calc(100% - var(--templates-margin-s));
            left: var(--templates-margin-l);
            right: var(--templates-margin-l);
            width: auto;
            max-width: none;
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
            gap: var(--templates-margin-m) var(--templates-margin-l);
            > li {
                ul {
                    padding-left: var(--spacing-2xl);
                    > li a {
                        min-height: auto;
                    }
                }
            }
        }
    }
    
    // DEFERRED NAVIGATION
    #{$deferred-navigation} {
        @include mq($from: lg) {
            @include flex($direction: column, $gap: var(--spacing-xs));
            @include list-li();
            @include dropdown-panel();
            > li a {
                min-height: auto;
            }
            > li > ul {
                @include flex($direction: column, $gap: var(--spacing-xs));
                @include list-li();
                position: static;
                min-width: 0;
                padding: 0 0 0 var(--spacing-m);
                background-color: transparent;
                border-radius: 0;
            }
            gov-button, .gov-button {
                width: 100%;
                max-width: 100%;
                .element {
                    width: calc(100% + (var(--spacing-m-nudge) + var(--border-width, 0.0625rem)) * 2);
                    justify-content: space-between;
                    font-weight: 500;
                }
            }
        }
    }
  • URL: /components/raw/main-navigation/main-navigation.scss
  • Filesystem Path: src/components/molecules/main-navigation/main-navigation.scss
  • Size: 7.6 KB
  • Content:
    import {
        blockRapidClicks,
        createID,
        debounce,
        delay,
        firstFocusableElement,
        getDocument,
        getWindow,
        toBool
    } from "@gov-design-system-ce/utils";
    
    // Matches the `lg` breakpoint (64em / 1024px) the header stylesheet switches
    // its mobile layout at — NOT `md`. A first-level navigation row does not
    // reliably fit into a tablet width, so the hamburger runs up to 1023px.
    // Kept in px because it is compared against `window.innerWidth`; with a
    // zoomed root font size this drifts from the em-based media query.
    const TABLET_SIZE = 1024;
    const LAST_VISIBLE_ITEM_CLASS = 'gov-navigation__last-visible-item';
    let windowWidth = window.innerWidth;
    
    class MainNavigation {
        rootElement: Element;
    
        constructor(rootElement: Element) {
            this.rootElement = rootElement;
            this.verifyAndFixAccessibility(this.rootUlElement);
    
            this.calculateSizeOfElements();
            this.registerListeners();
            this.accessibilityMobileNavigation().catch();
    
            if (getWindow().innerWidth >= TABLET_SIZE) {
                this.moveDeferredItems().catch()
                this.controlHeaderNavigation();
            } else {
                this.controlHeaderNavigation(false);
            }
            this.markLastVisibleItem();
        }
    
        private registerListeners() {
            this.registerClickTriggers(this.rootUlElement);
    
            getWindow().addEventListener('resize', debounce(() => {
                const isRealResize = windowWidth !== window.innerWidth;
                if (isRealResize) {
                    windowWidth = window.innerWidth;
                } else {
                    return;
                }
                this.resetDeferredMenu();
                if (getWindow().innerWidth >= TABLET_SIZE) {
                    this.controlHeaderNavigation();
                    this.moveDeferredItems().catch();
                } else if (!this.isMobileMenuOpen()) {
                    this.controlHeaderNavigation(false);
                }
                this.markLastVisibleItem();
            }, 50))
        }
    
        private isMobileMenuOpen(): boolean {
            const trigger = getDocument().querySelector<HTMLElement>('.js-gov-header__navigation-trigger');
            if (!trigger) return false;
            return toBool(this.resolveTriggerType(trigger).getAttribute('aria-expanded'));
        }
    
        private async moveDeferredItems() {
            const gap = 16;
            let size = this.rootElement.getBoundingClientRect().width - 120;
    
            this.firstLevelLiElements.forEach((liElement) => {
                const itemSize = liElement.getBoundingClientRect().width + gap;
    
                if ((size - itemSize) >= 0) {
                    size -= itemSize;
                } else {
                    size -= itemSize;
                    this.createDeferredContainer();
                    this.deferredItemsContainer.removeAttribute('hidden');
                    this.deferredItemsContainer.setAttribute('aria-hidden', 'false');
                    this.collapseItem(liElement);
                    this.temporaryListForDeferredItems.appendChild(liElement);
                }
            })
        }
    
        /**
         * Flags the last item that is still rendered in the navigation row so its
         * dropdown can be right-aligned instead of overflowing the page. Positional
         * CSS selectors cannot express this on their own: the row is trailed by two
         * `.gov-mobile-only` items and, once items overflow, by the generated
         * deferred ("Další") item, so the index of the last visible item shifts.
         */
        private markLastVisibleItem() {
            const items = Array.from(this.firstLevelLiElements).filter(
                (liElement) => liElement !== this.deferredItemsContainer
            );
    
            items.forEach((liElement) => liElement.classList.remove(LAST_VISIBLE_ITEM_CLASS));
            items[items.length - 1]?.classList.add(LAST_VISIBLE_ITEM_CLASS);
        }
    
        private get temporaryListForDeferredItems() {
            return this.deferredItemsContainer.querySelector('ul');
        }
    
        private createDeferredContainer() {
            if (this.deferredItemsContainer) {
                return;
            }
    
            const controlId = createID('MenuDeferred');
            const ul = getDocument().createElement('ul');
            const li = getDocument().createElement('li');
            const trigger = getDocument().createElement('gov-button');
            const icon = getDocument().createElement('gov-icon');
            const deferredName = this.rootElement.getAttribute('data-deferred-item-name');
    
            li.classList.add('js-deferred-items-container');
    
            trigger.setAttribute('type', 'base');
            trigger.setAttribute('color', 'primary');
            trigger.setAttribute('size', 'l');
            trigger.setAttribute('aria-expanded', 'false');
            trigger.setAttribute('aria-controls', controlId);
            trigger.setAttribute('aria-label', 'Zobrazit další položky');
            trigger.innerHTML = deferredName ?? 'Další';
    
            icon.setAttribute('type', 'templates');
            icon.setAttribute('name', 'chevron-down');
            icon.setAttribute('size', 'l');
            icon.setAttribute('slot', 'icon-end');
    
            ul.setAttribute('id', controlId);
            ul.setAttribute('hidden', 'hidden');
            ul.setAttribute('aria-hidden', 'true');
            ul.classList.add('gov-deferred-navigation');
    
            trigger.appendChild(icon);
            trigger.addEventListener('gov-click', blockRapidClicks(() => this.toggleMenu(trigger)));
    
            li.appendChild(trigger);
            li.appendChild(ul);
    
            this.rootUlElement.appendChild(li);
        }
    
        private resetDeferredMenu() {
            this.deferredItemsContainer?.querySelectorAll<HTMLLIElement>(':scope > ul > li').forEach((liElement) => {
                this.collapseItem(liElement);
                this.rootUlElement.appendChild(liElement);
            });
            this.rootUlElement?.querySelectorAll(':scope > .gov-mobile-only').forEach((liElement) => this.rootUlElement.appendChild(liElement));
            this.deferredItemsContainer?.remove();
        }
    
        private calculateSizeOfElements() {
            this.firstLevelLiElements.forEach((liElement, i) => {
                liElement.setAttribute('data-item-width', liElement.getBoundingClientRect().width.toString());
                liElement.setAttribute('data-item-index', i.toString());
            })
        }
    
        /**
         * Walks every level, not just the first. Once the row overflows, items with
         * their own submenu are relocated into the generated "Další" menu, where they
         * become second-level triggers. Binding only the first level left them inert
         * while `verifyAndFixAccessibility` still advertised them as expandable.
         */
        private registerClickTriggers(ulElement: HTMLUListElement) {
            this.getLiElementsInUlElement(ulElement).forEach(liElement => {
                const trigger = this.getTriggerInLiElement(liElement);
                const innerList = this.getInnerListInLiElement(liElement);
    
                if (!trigger || !innerList) {
                    return;
                }
    
                trigger.addEventListener('gov-click', blockRapidClicks(() => this.toggleMenu(trigger)));
                trigger.addEventListener('click', blockRapidClicks(() => this.toggleMenu(trigger)));
    
                this.registerClickTriggers(innerList);
            });
        }
    
        private applyMenuState(trigger: HTMLElement, menuLevel: Element, expanded: boolean, icons: string[]) {
            const iconElement = trigger.querySelector('gov-icon');
            const value = expanded ? 'true' : 'false';
    
            trigger.setAttribute('aria-expanded', value);
            this.resolveTriggerType(trigger).setAttribute('aria-expanded', value);
    
            if (expanded) {
                menuLevel.removeAttribute('hidden');
                menuLevel.setAttribute('aria-hidden', 'false');
            } else {
                menuLevel.setAttribute('hidden', 'hidden');
                menuLevel.setAttribute('aria-hidden', 'true');
            }
    
            iconElement && iconElement.setAttribute('name', icons[expanded ? 1 : 0]);
        }
    
        private collapseItem(liElement: HTMLLIElement, icons = ['chevron-down', 'chevron-up']) {
            const trigger = this.getTriggerInLiElement(liElement);
            const innerList = this.getInnerListInLiElement(liElement);
    
            if (trigger && innerList) {
                this.applyMenuState(trigger, innerList, false, icons);
            }
        }
    
        /**
         * Closes every open menu except the branch the toggled trigger lives in.
         * Collapsing ancestors is what made nested menus unusable: expanding an item
         * inside the deferred "Další" panel closed the panel containing it.
         */
        private collapseMenus(icons: string[], keepOpenFor?: HTMLElement, ulElement = this.rootUlElement) {
            this.getLiElementsInUlElement(ulElement).forEach(liElement => {
                const trigger = this.getTriggerInLiElement(liElement);
                const innerList = this.getInnerListInLiElement(liElement);
    
                if (!trigger || !innerList) {
                    return;
                }
    
                this.collapseMenus(icons, keepOpenFor, innerList);
    
                if (keepOpenFor && liElement.contains(keepOpenFor)) {
                    return;
                }
    
                if (toBool(this.resolveTriggerType(trigger).getAttribute('aria-expanded'))) {
                    this.applyMenuState(trigger, innerList, false, icons);
                }
            });
        }
    
        private toggleMenu(trigger: HTMLElement, icons = ['chevron-down', 'chevron-up']) {
            // Read the trigger's own state before collapsing anything, so the new
            // state never depends on what `collapseMenus` did in the meantime.
            const controlId = this.resolveTriggerType(trigger).getAttribute('aria-controls');
            const isExpanded = toBool(this.resolveTriggerType(trigger).getAttribute('aria-expanded'));
            const menuLevel = getDocument().querySelector('[id="' + controlId + '"]');
    
            this.collapseMenus(icons, trigger);
    
            if (!menuLevel) {
                return;
            }
    
            this.applyMenuState(trigger, menuLevel, !isExpanded, icons);
    
            const elementToFocus = firstFocusableElement(menuLevel);
            if (elementToFocus) {
                elementToFocus.focus && elementToFocus.focus();
            }
        }
    
        private verifyAndFixAccessibility(ul: HTMLUListElement) {
            this.getLiElementsInUlElement(ul).forEach((liElement) => {
                const trigger = this.getTriggerInLiElement(liElement);
                const innerList = this.getInnerListInLiElement(liElement);
    
                if (!trigger || !innerList) {
                    return;
                }
    
                const controlsId = createID('MainMenu');
    
                if (trigger.getAttribute('aria-controls') !== innerList.getAttribute('id')) {
                    trigger.setAttribute('aria-controls', controlsId);
                    innerList.setAttribute('id', controlsId);
                }
    
                if (!trigger.getAttribute('aria-expanded')) {
                    trigger.setAttribute('aria-expanded', 'false')
                }
    
                if (!trigger.getAttribute('aria-label')) {
                    trigger.setAttribute('aria-label', 'Zobrazit / Skrýt položky sekce ' + trigger.textContent)
                }
    
                this.verifyAndFixAccessibility(innerList);
            })
        }
    
        private async accessibilityMobileNavigation() {
            const trigger = getDocument().querySelector<HTMLElement>('.js-gov-header__navigation-trigger');
            const menu = this.headerNavigationElement;
    
            if (!trigger || !menu) {
                return
            }
    
            await delay(500);
    
            const controller = this.resolveTriggerType(trigger);
            const controlsId = createID('MainMobileMenu');
    
            if (controller.getAttribute('aria-controls') !== menu.getAttribute('id') || menu.getAttribute('id') === null) {
                trigger.setAttribute('aria-controls', controlsId);
                menu.setAttribute('id', controlsId);
            }
    
            if (!controller.getAttribute('aria-expanded')) {
                trigger.setAttribute('aria-expanded', 'false')
            }
    
            if (!controller.getAttribute('aria-label')) {
                trigger.setAttribute('aria-label', 'Zobrazit / Skrýt menu')
            }
    
            trigger.addEventListener('gov-click', blockRapidClicks(() => this.toggleMenu(trigger, ['list', 'x-lg'])));
            trigger.addEventListener('click', blockRapidClicks(() => this.toggleMenu(trigger, ['list', 'x-lg'])));
        }
    
        private controlHeaderNavigation(show = true) {
            if (show) {
                this.headerNavigationElement?.removeAttribute('hidden')
                this.headerNavigationElement?.removeAttribute('aria-hidden')
            } else {
                this.headerNavigationElement?.setAttribute('hidden', 'hidden')
                this.headerNavigationElement?.setAttribute('aria-hidden', 'true')
            }
        }
    
        /**
         * gov-button moves the host's aria-* onto its inner `<button>`, so that is
         * where the state lives — but only once the component has rendered. Before
         * hydration the query returns `null` and the host still carries the
         * attributes itself, hence the fallback.
         */
        private resolveTriggerType(trigger: HTMLElement): HTMLElement {
            const inner = trigger && trigger.nodeName === 'GOV-BUTTON' ? trigger.querySelector('button') : trigger;
            return inner ?? trigger;
        }
    
        private getTriggerInLiElement(liElement: HTMLLIElement): HTMLLIElement {
            return liElement.querySelector(':scope > gov-button, :scope > button');
        }
    
        private getInnerListInLiElement(liElement: HTMLLIElement): HTMLUListElement {
            return liElement.querySelector(':scope > ul');
        }
    
        private getLiElementsInUlElement(ulElement: HTMLUListElement): NodeListOf<HTMLLIElement> {
            return ulElement.querySelectorAll(':scope > li');
        }
    
        private get firstLevelLiElements(): NodeListOf<HTMLLIElement> {
            return this.rootUlElement.querySelectorAll(':scope > li:not(.gov-mobile-only)')
        }
    
        private get deferredItemsContainer() {
            return this.rootElement.querySelector('.js-deferred-items-container')
        }
    
        private get rootUlElement(): HTMLUListElement {
            return this.rootElement.querySelector(':scope > ul')
        }
    
        private get headerNavigationElement() {
            return getDocument().querySelector<HTMLElement>('.js-gov-header__navigation');
        }
    }
    
    export default MainNavigation;
    
  • URL: /components/raw/main-navigation/main-navigation.ts
  • Filesystem Path: src/components/molecules/main-navigation/main-navigation.ts
  • Size: 14.3 KB

No notes defined.