UNPKG

@postnord/web-components

Version:

PostNord Web Components

421 lines (420 loc) 17.7 kB
/*! * Built with Stencil * By PostNord. */ import { h, Host, forceUpdate, } from "@stencil/core"; import { uuidv4 } from "../../../../globals/helpers"; import { angle_down, arrow_left, arrow_right } from "pn-design-assets/pn-assets/icons.js"; /** * @deprecated This component will be removed in v8. Please use the `pn-tablist` instead. * @see {@link https://portal.postnord.com/web-components/?path=/docs/components-navigation-tablist--docs} Use `pn-tablist` instead. * * @slot dropdown-item - Place your `pn-page-nav-dropdown-item` components here. */ export class PnPageNav { constructor() { this.currentSelection = undefined; this.showScrollArrows = false; this.showLeftArrow = false; this.showRightArrow = false; this.dropdownOpen = false; this.dropdownLinks = []; this.dropdownActive = false; this.value = undefined; this.dropdown = false; this.navid = `pn-page-nav-${uuidv4()}`; } mo; navContainer; navWrapper; navItems = []; dropdownButton; dropdownEl; dropdownItems; dropdownEls; eventHandler = this.keyboardEvents.bind(this); globalEventHandler = this.globalEvents.bind(this); activeBg; hoverBg; scrollRegistered = false; hostElement; /** Emits the value of the selected item. */ navchange; changeHandler({ target }) { this.currentSelection = target.closest('pn-page-nav-item'); if (target.value) this.value = target.value; if (this.dropdownOpen) this.dropdownOpen = false; } handleResize() { this.rerender(); } valueHandler() { if (!this.value) this.currentSelection = null; this.calcHighlight(this.currentSelection, this.activeBg); this.navchange.emit(this.value); if (!this.dropdownActive) return; this.isDropdownItemActive(); } dropdownHandler() { if (this.dropdownOpen) { requestAnimationFrame(() => { this.addGlobalEventListeners(); }); return; } this.removeGlobalEventListeners(); } /* ---------------------------------------LIFECYCLE--------------------------------------- */ componentWillLoad() { if (!this.dropdown) return; this.dropdownEls = Array.from(this.hostElement.querySelectorAll('pn-page-nav-dropdown-item')); if (this.dropdownEls.length) { this.dropdownActive = true; this.initiateDropdown(); } } componentDidLoad() { if (this.mo) this.mo.disconnect(); this.mo = new MutationObserver(() => { forceUpdate(this.hostElement); this.setActiveNavItem(); this.rerender(); }); this.mo.observe(this.hostElement, { childList: true, subtree: true }); this.navWrapper = this.hostElement.querySelector('.pn-page-nav'); this.navContainer = this.hostElement.querySelector('.pn-page-nav-items'); this.activeBg = this.hostElement.querySelector('.pn-pn-active'); this.hoverBg = this.hostElement.querySelector('.pn-pn-hover'); this.hostElement.addEventListener('mouseover', ({ target }) => this.calcHighlight(target, this.hoverBg)); this.setActiveNavItem(); this.rerender(); } /* ---------------------------------------/LIFECYCLE--------------------------------------- */ setActiveNavItem() { this.navItems = Array.from(this.hostElement.querySelectorAll('pn-page-nav-item')); this.navItems.forEach(navItemEl => { if (this.value === navItemEl.value) { this.currentSelection = navItemEl; } else { navItemEl.removeAttribute('selected'); } navItemEl .querySelector('a') .addEventListener('focus', ({ target }) => this.calcHighlight(target, this.hoverBg)); }); /* -----------------dropdown------------------ */ if (!this.dropdownActive) return; this.dropdownItems = Array.from(this.hostElement.querySelectorAll('.pn-page-nav-dropdown-item')); //Check active state on each item this.isDropdownItemActive(); //Store all values to check if dropdown button should be active this.dropdownLinks = this.dropdownItems.map((el, i) => { el.setAttribute('data-index', `${i}`); return el.closest('pn-page-nav-dropdown-item').value; }); /* -----------------/dropdown------------------ */ } rerender() { requestAnimationFrame(() => { this.calcHighlight(this.currentSelection, this.activeBg); this.scrollArrowRender(); }); } /*---------------------------------------HIGHLIGHT LOGIC-------------------------------------------*/ calcHighlight(el, bgEl) { if (!el?.closest('pn-page-nav-item')) { bgEl?.classList.add('hidden'); return; } if (bgEl) bgEl.classList.remove('hidden'); const elRect = el.closest('pn-page-nav-item').getBoundingClientRect(); const { left: hostLeft } = this.navContainer.getBoundingClientRect(); const { left: navLeft, height: navHeight, width: navWidth } = elRect; const offset = navLeft - hostLeft + this.navContainer.scrollLeft; bgEl.style.setProperty('transform', `translate(${offset}px, -50%`); bgEl.style.setProperty('width', `${navWidth}px`); bgEl.style.setProperty('height', `${navHeight}px`); } /*---------------------------------------/HIGHLIGHT LOGIC-------------------------------------------*/ /*---------------------------------------SCROLL ARROW LOGIC-------------------------------------------*/ scrollArrowRender() { if (!this.navWrapper) return; if (this.navWrapper.scrollWidth > this.navWrapper.clientWidth) { this.showScrollArrows = true; if (!this.scrollRegistered) { this.navWrapper.addEventListener('scroll', this.scrollArrowRender.bind(this)); this.scrollRegistered = true; } const amountScrolled = Math.round(this.navWrapper.scrollWidth - this.navWrapper.scrollLeft); const distanceToEnd = amountScrolled - this.navWrapper.clientWidth; const distanceToStart = this.navWrapper.scrollLeft; this.showLeftArrow = distanceToStart > 0; this.showRightArrow = distanceToEnd > 0; return; } this.showLeftArrow = false; this.showRightArrow = false; this.showScrollArrows = false; } scroll(val) { let amount = this.navWrapper.scrollLeft + val; this.navWrapper.scroll({ left: amount, behavior: 'smooth', }); } scrollArrowClasses() { let classNames = 'pn-pn-arrows '; if (this.showLeftArrow) classNames += 'pn-pn-left-visible '; if (this.showRightArrow) classNames += 'pn-pn-right-visible '; return classNames; } /*---------------------------------------/SCROLL ARROW LOGIC-------------------------------------------*/ /* ---------------------------------------DROPDOWN LOGIC--------------------------------------- */ initiateDropdown() { requestAnimationFrame(() => { this.dropdownButton = this.hostElement.querySelector('.pn-page-nav-dropdown-button'); this.dropdownEl = this.hostElement.querySelector('.pn-page-nav-dropdown'); this.addDropdownEventListeners(); }); } toggleDropdown() { this.dropdownOpen = !this.dropdownOpen; } isDropdownItemActive() { this.dropdownEls.forEach(el => { if (el.value && this.value === el.value) { el.setAttribute('active', 'true'); return; } el.removeAttribute('active'); }); } /* -----------------events------------------ */ /* -----------------temporary events------------------ */ addDropdownEventListeners() { this.hostElement.addEventListener('keydown', this.eventHandler); this.hostElement.addEventListener('click', this.eventHandler); } addGlobalEventListeners() { const root = this.hostElement.getRootNode(); root.addEventListener('focusin', this.globalEventHandler); root.addEventListener('keydown', this.globalEventHandler); root.addEventListener('click', this.globalEventHandler); } removeGlobalEventListeners() { const root = this.hostElement.getRootNode(); root.removeEventListener('focusin', this.globalEventHandler); root.removeEventListener('keydown', this.globalEventHandler); root.removeEventListener('click', this.globalEventHandler); } /* -----------------/temporary events------------------ */ /* -----------------Open dropdown with keyboard------------------ */ keyboardEvents(e) { const target = e.composedPath()[0]; // As long as the dropdown is closed, we only want it to react to keyboard input // is the user has focus on the button if (e.type === 'keydown') { if (!this.dropdownOpen && target === this.dropdownButton && ['ArrowUp', 'ArrowDown'].includes(e.code)) { this.dropdownOpen = true; requestAnimationFrame(() => { this.focusNextDropdownItem(); }); } } } /* -----------------/Open dropdown with keyboard------------------ */ globalEvents(e) { const target = e.composedPath()[0]; if (e.type === 'keydown' && e.code === 'Escape') { this.dropdownOpen = false; this.dropdownButton.focus(); } if (e.code === 'ArrowDown') this.focusNextDropdownItem(); if (e.code === 'ArrowUp') this.focusPrevDropdownItem(); if ((e.type === 'click' || e.type === 'focusin') && !this.dropdownEl.contains(target)) { this.dropdownOpen = false; } } /* -----------------/events------------------ */ /* -----------------focusing------------------ */ focusNextDropdownItem() { const { activeElement } = this.hostElement.getRootNode(); if (!activeElement.classList.contains('pn-page-nav-dropdown-item')) { this.dropdownItems[0].focus(); return; } // focus next item const index = parseInt(activeElement.getAttribute('data-index')); if (index < this.dropdownItems.length - 1) { this.dropdownItems[index + 1].focus(); } } focusPrevDropdownItem() { const { activeElement } = this.hostElement.getRootNode(); if (!activeElement.classList.contains('dropdown-item')) { this.dropdownItems[this.dropdownItems.length - 1].focus(); } // focus previous item const index = parseInt(activeElement.getAttribute('data-index')); if (index > 0) { this.dropdownItems[index - 1].focus(); return; } this.dropdownButton.focus(); } /* -----------------/focusing------------------ */ dropdownButtonClasses() { let classList = 'pn-page-nav-dropdown-button '; if (this.dropdownLinks.includes(this.value)) classList += 'pn-page-nav-dropdown-active '; return classList; } dropdownClasses() { let classList = 'pn-page-nav-dropdown '; if (this.dropdownOpen) classList += 'pn-page-nav-dropdown-open '; return classList; } /* ---------------------------------------/DROPDOWN LOGIC--------------------------------------- */ render() { return (h(Host, { key: '78363e8727ce2d9b1525572a8d909b65141c4932' }, h("div", { key: '1cdb635b439f40cf77df3a77432d542f3055e952', class: "pn-page-nav-wrapper" }, h("nav", { key: '13ae19bf916612a04990edade8d7325665a91060', class: "pn-page-nav" }, this.dropdownActive && (h("button", { key: '802fbae4d73a7b26a34ed4e41e5b5a6efd3ba101', type: "button", class: this.dropdownButtonClasses(), onClick: () => this.toggleDropdown(), "aria-controls": "page-nav-dropdown", "aria-expanded": `${this.dropdownOpen}` }, this.dropdown, h("pn-icon", { key: 'ac26e1ec6b9feeb52a898d7f80d2c4356423e93d', icon: angle_down, color: "white", small: true }), h("div", { key: 'c040e7c7ac1db10687b53ad6f59f6c3ded1d8e8a', class: "pn-page-nav-divider" }))), h("ul", { key: '4bf5dc223bd55b0fb81fedebfa09fbe5db42569d', class: "pn-page-nav-items" }, h("slot", { key: '3b77f7bd7a09700b860df10927b3889072e3bc34' }), h("li", { key: '71f85348e913043ca037efbe523f25a00bf8d211', class: "pn-pn-bg pn-pn-active", role: "presentation" }), h("li", { key: '462f8b0e8e1c788143d4ffaf5011b030ff100f5f', class: "pn-pn-bg pn-pn-hover", role: "presentation" }))), this.showScrollArrows && (h("div", { key: 'c69db4a2202a5b5f9da49f72466bfa2d68eec0c8', class: this.scrollArrowClasses() }, h("button", { key: 'eb809646459a703b8ea984ef6f9ec8b23ea34029', class: "pn-pn-arrow-left", onClick: () => this.scroll(-120), tabindex: "-1" }, h("pn-icon", { key: '4d797edf6525213134b3acc0e36876ade74952a8', icon: arrow_left, color: "white" })), h("button", { key: '612286fcac6b7608a1162aa5b8ad3d6ff401bf87', class: "pn-pn-arrow-right", onClick: () => this.scroll(120), tabindex: "-1" }, h("pn-icon", { key: '5a336276c0b4256fb976fc2bdc9037ec25cbc382', icon: arrow_right, color: "blue700" }))))), this.dropdownActive && (h("ul", { key: '518b79ad7c1a7ff8652a5fb909f8285f94405a4a', id: this.navid, class: this.dropdownClasses() }, h("slot", { key: '7d03ba12daffea62b0b0a7c0f1a1c1c006a125c4', name: "dropdown-item" }))))); } static get is() { return "pn-page-nav"; } static get originalStyleUrls() { return { "$": ["pn-page-nav.scss"] }; } static get styleUrls() { return { "$": ["pn-page-nav.css"] }; } static get properties() { return { "value": { "type": "string", "mutable": true, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Currently active menu item value" }, "attribute": "value", "reflect": false }, "dropdown": { "type": "any", "mutable": false, "complexType": { "original": "string | boolean", "resolved": "boolean | string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Pass a string which will be the text on the dropdown button." }, "attribute": "dropdown", "reflect": false, "defaultValue": "false" }, "navid": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Set a unique HTML ID." }, "attribute": "navid", "reflect": false, "defaultValue": "`pn-page-nav-${uuidv4()}`" } }; } static get states() { return { "currentSelection": {}, "showScrollArrows": {}, "showLeftArrow": {}, "showRightArrow": {}, "dropdownOpen": {}, "dropdownLinks": {}, "dropdownActive": {} }; } static get events() { return [{ "method": "navchange", "name": "navchange", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emits the value of the selected item." }, "complexType": { "original": "string", "resolved": "string", "references": {} } }]; } static get elementRef() { return "hostElement"; } static get watchers() { return [{ "propName": "value", "methodName": "valueHandler" }, { "propName": "dropdownOpen", "methodName": "dropdownHandler" }]; } static get listeners() { return [{ "name": "itemselection", "method": "changeHandler", "target": undefined, "capture": false, "passive": false }, { "name": "resize", "method": "handleResize", "target": "window", "capture": false, "passive": true }]; } } //# sourceMappingURL=pn-page-nav.js.map