@postnord/web-components
Version:
PostNord Web Components
421 lines (420 loc) • 17.7 kB
JavaScript
/*!
* 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: '9c4bd4aeecdc22a7c1d10816d70fbb514ab613f3' }, h("div", { key: '0902e5f87e7eb1a3fa1efa06441f63634722bdd2', class: "pn-page-nav-wrapper" }, h("nav", { key: 'c3bc0a339f0339d22c637866ba153f87f5c06420', class: "pn-page-nav" }, this.dropdownActive && (h("button", { key: 'e1bc1da3bdf6b279d2550124af6908ba338d2a8a', type: "button", class: this.dropdownButtonClasses(), onClick: () => this.toggleDropdown(), "aria-controls": "page-nav-dropdown", "aria-expanded": `${this.dropdownOpen}` }, this.dropdown, h("pn-icon", { key: 'f913dd0b5abb7815f889029c597c26ec92d26dfa', icon: angle_down, color: "white", small: true }), h("div", { key: '42e5cbd460c914cbb763a2552159a4d07d4005f7', class: "pn-page-nav-divider" }))), h("ul", { key: 'f4ef8ae4b301e0dc6da25d6be0f5c3c3e93c5c75', class: "pn-page-nav-items" }, h("slot", { key: '4aaa8d2f47534f95ea40ea96ce48d278aca3c139' }), h("li", { key: 'b18fd3fe98b2890a26a19cc7384b5a41dec7ea79', class: "pn-pn-bg pn-pn-active", role: "presentation" }), h("li", { key: '5cf37103941a756c161d95ced420b2b21d58461d', class: "pn-pn-bg pn-pn-hover", role: "presentation" }))), this.showScrollArrows && (h("div", { key: '1a851e18abc8b3093b2e70b4efb549443a5feccf', class: this.scrollArrowClasses() }, h("button", { key: '6a0639c1d254c46fe4d446094eff00896693c977', class: "pn-pn-arrow-left", onClick: () => this.scroll(-120), tabindex: "-1" }, h("pn-icon", { key: '26d489b47136dd30e7ce764b508fa5104e652588', icon: arrow_left, color: "white" })), h("button", { key: 'dc835f73f754ea6acabd8596e924c461207c4387', class: "pn-pn-arrow-right", onClick: () => this.scroll(120), tabindex: "-1" }, h("pn-icon", { key: '2df638385b40a1c0eaa01d795a1d0b796c50e7b5', icon: arrow_right, color: "blue700" }))))), this.dropdownActive && (h("ul", { key: 'b9088ed407e8952ee95ba7a2239bea98c49066b0', id: this.navid, class: this.dropdownClasses() }, h("slot", { key: '0bf168ec966e4bb68e5a4e18bcee76807e7c96d7', 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