UNPKG

@postnord/web-components

Version:
428 lines (427 loc) 18 kB
/*! * Built with Stencil * By PostNord. */ import { h, Host, forceUpdate, } from "@stencil/core"; import { uuidv4 } from "../../../../globals/helpers"; import { chevron_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 { mo; navContainer; navWrapper; navItems = []; dropdownButton; dropdownEl; dropdownItems; dropdownEls; eventHandler = this.keyboardEvents.bind(this); globalEventHandler = this.globalEvents.bind(this); activeBg; hoverBg; scrollRegistered = false; hostElement; currentSelection; showScrollArrows = false; showLeftArrow = false; showRightArrow = false; dropdownOpen = false; dropdownLinks = []; dropdownActive = false; /** Currently active menu item value */ value; /** Pass a string which will be the text on the dropdown button. */ dropdown = false; /** Set a unique HTML ID. */ navid = `pn-page-nav-${uuidv4()}`; /** 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) { /** @ts-ignore This component will be removed soon, no need to deal with this. */ 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: 'f76a115ccea11e238d2aee5288b06675abc80b8a' }, h("div", { key: '453102a432daf9b567be4f5dc415bab34b48bc84', class: "pn-page-nav-wrapper" }, h("nav", { key: '9207ade4b4d6cea4c22622c91988eef2b3c812bc', class: "pn-page-nav" }, this.dropdownActive && (h("button", { key: '3365178312cfe2b9d48fccbc794d2a8a857ed6f0', type: "button", class: this.dropdownButtonClasses(), onClick: () => this.toggleDropdown(), "aria-controls": "page-nav-dropdown", "aria-expanded": `${this.dropdownOpen}` }, this.dropdown, h("pn-icon", { key: '460172b12b334e96920d185dcbc868acafc0cdcc', icon: chevron_down, color: "white", small: true }), h("div", { key: 'f16219e3070e00ebd1ab143b8803a94c44e242bc', class: "pn-page-nav-divider" }))), h("ul", { key: '560ef88c95269f29b1f76ee65e3a2541fc2482ce', class: "pn-page-nav-items" }, h("slot", { key: '7b1cfc89976421963cc19f958f21e29bfc796621' }), h("li", { key: '7df00d0a42fc3f48f82afdb0bbbf943229604293', class: "pn-pn-bg pn-pn-active", role: "presentation" }), h("li", { key: '68fd6cc6f9dfcb834c62105f81ff2e21b332be94', class: "pn-pn-bg pn-pn-hover", role: "presentation" }))), this.showScrollArrows && (h("div", { key: 'ba26546a6fa3f750a5314dd80f80e1487b4962ec', class: this.scrollArrowClasses() }, h("button", { key: '57b43598cb3288b068c3583c822be35418b28e91', class: "pn-pn-arrow-left", onClick: () => this.scroll(-120), tabindex: "-1" }, h("pn-icon", { key: 'b812b5b471ba0982be6cb3bcacc84bb2f52fa0e3', icon: arrow_left, color: "white" })), h("button", { key: '70d5bffed71999aaf154f88590378496d79e7f55', class: "pn-pn-arrow-right", onClick: () => this.scroll(120), tabindex: "-1" }, h("pn-icon", { key: 'a2d4319bab3a80b092321873ff66e4b421f14743', icon: arrow_right, color: "blue700" }))))), this.dropdownActive && (h("ul", { key: '1e2a73e7b4d523f3feba0dc18ccc14871b551862', id: this.navid, class: this.dropdownClasses() }, h("slot", { key: '20f647483c10a84de4bf2f40126b83b65b61b99f', 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" }, "getter": false, "setter": false, "reflect": false, "attribute": "value" }, "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." }, "getter": false, "setter": false, "reflect": false, "attribute": "dropdown", "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." }, "getter": false, "setter": false, "reflect": false, "attribute": "navid", "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 }]; } }