439 lines
12 KiB
JavaScript
439 lines
12 KiB
JavaScript
/* eslint-disable lit-a11y/anchor-is-valid */
|
|
import { css, html, nothing } from 'lit';
|
|
import '@lion/ui/define/lion-icon.js';
|
|
import { UIBaseElement } from './shared/UIBaseElement.js';
|
|
import { addIconResolverForPortal } from './iconset-portal/addIconResolverForPortal.js';
|
|
import uiPortalMainNavBurgerCss from './ui-portal-main-nav-burger.css.js';
|
|
|
|
try {
|
|
addIconResolverForPortal();
|
|
} catch (e) {
|
|
// do nothing
|
|
// icons can be registered by somebody else?
|
|
}
|
|
|
|
// TODO: apply https://web.dev/website-navigation/ (aria-current="page" etc.)
|
|
|
|
/**
|
|
* @typedef {{name: string; url: string; active?:boolean; iconId?: string; children?: NavItemData[]}} NavItem
|
|
*/
|
|
export class UIPortalMainNav extends UIBaseElement {
|
|
static properties = {
|
|
navData: { type: Array, attribute: 'nav-data' },
|
|
layoutWide: { type: Boolean, attribute: 'layout-wide' }, // true or false
|
|
};
|
|
|
|
constructor() {
|
|
super();
|
|
/**
|
|
* @type {NavItem[]}
|
|
*/
|
|
this.navData = [];
|
|
this.layoutWide = false;
|
|
this.getLink = item =>
|
|
html`<a href="${item.redirect || item.url}" aria-current=${item.active ? 'page' : ''}
|
|
>${item.name}</a
|
|
>`;
|
|
}
|
|
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
if (window) {
|
|
// only on the client
|
|
window.setTimeout(() => {
|
|
// remove the second navigation
|
|
// its rendered twice due to lack of lit/ssr
|
|
// https://github.com/lit/lit/issues/4472
|
|
const $navs = this.renderRoot.querySelectorAll('[data-part="nav"]');
|
|
if ($navs.length > 1) {
|
|
$navs[1].remove();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
get templateContext() {
|
|
return {
|
|
...super.templateContext,
|
|
data: { navData: this.navData },
|
|
};
|
|
}
|
|
|
|
static templates = {
|
|
main(context) {
|
|
const { data, templates } = context;
|
|
|
|
return html` <nav>${templates.navLevel(context, { children: data.navData })}</nav> `;
|
|
},
|
|
navLevel(context, { children }) {
|
|
const { templates } = context;
|
|
|
|
return html`<ul>
|
|
${children.map(
|
|
item =>
|
|
html`<li>
|
|
${templates.navItem(context, { item })}
|
|
${item.children?.length
|
|
? html`<ul>
|
|
<li>
|
|
${item.children.map(
|
|
child1 => html`
|
|
${this.getLink(child1)}
|
|
${child1.children?.length
|
|
? html` collapsible
|
|
<ul>
|
|
${item.children.map(
|
|
child2 => html`
|
|
<li>${templates.navItem(context, { item: child2 })}</li>
|
|
`,
|
|
)}
|
|
</ul>`
|
|
: nothing}
|
|
`,
|
|
)}
|
|
</li>
|
|
</ul>`
|
|
: nothing}
|
|
</li>`,
|
|
)}
|
|
</ul>`;
|
|
},
|
|
navItem(context, { item }) {
|
|
return this.getLink(item);
|
|
},
|
|
};
|
|
|
|
attributeChangedCallback(attrName, oldVal, newVal) {
|
|
super.attributeChangedCallback(attrName, oldVal, newVal);
|
|
if (attrName === 'layout-wide') {
|
|
if (newVal === true || newVal === 'true') {
|
|
this.setAttribute('data-wide', 'true');
|
|
} else {
|
|
this.removeAttribute('data-wide');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
export const tagName = 'ui-portal-main-nav';
|
|
|
|
const sharedGlobalStyles = css`
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
`;
|
|
|
|
/**
|
|
* Base UI Nav templates contains an accessible base html that can be used for all kinds of navigations,
|
|
* regardless of presentation: horizontally stacked, vertically stacked or a combination of both.
|
|
* With or without collapsible levels, with or without overlays.
|
|
* with any amount of nested levels.
|
|
* @returns
|
|
*/
|
|
const baseUINavMarkup = {
|
|
templates: () => ({
|
|
main(context) {
|
|
const { data, templates } = context;
|
|
|
|
return html`
|
|
<nav data-part="nav">
|
|
<input type="checkbox" id="burger-toggle" hidden />
|
|
<label for="burger-toggle" class="burger">
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</label>
|
|
|
|
<div id="l1-wrapper" data-part="l1-wrapper">
|
|
${templates.navLevel(context, { children: data.navData, level: 1 })}
|
|
</div>
|
|
</nav>
|
|
`;
|
|
},
|
|
navLevel(context, { children, level, hasActiveChild = false }) {
|
|
const { templates } = context;
|
|
|
|
return html`<div
|
|
data-part="level"
|
|
data-level="${level}"
|
|
data-has-active-child="${hasActiveChild}"
|
|
>
|
|
<ul data-part="list" data-level="${level}">
|
|
${children.map(
|
|
item =>
|
|
html`<li data-part="listitem" data-level="${level}" ?data-:active="${item.active}">
|
|
${templates.navItem(context, { item, level })}
|
|
${item.children?.length
|
|
? templates.navLevel(context, {
|
|
level: level + 1,
|
|
children: item.children,
|
|
hasActiveChild: item.hasActiveChild,
|
|
})
|
|
: nothing}
|
|
</li>`,
|
|
)}
|
|
</ul>
|
|
${level === 1
|
|
? html`
|
|
<div class="nav-item-last">
|
|
<a href="/search" data-part="anchor" data-level="${level}">
|
|
<lion-icon
|
|
data-part="icon"
|
|
data-level="${level}"
|
|
icon-id="lion-portal:portal:search"
|
|
></lion-icon>
|
|
<span>Search</span>
|
|
</a>
|
|
</div>
|
|
`
|
|
: nothing}
|
|
</div>`;
|
|
},
|
|
navLevel3(context, { children, level, item }) {
|
|
const { templates } = context;
|
|
|
|
return html`<div>
|
|
${this.getLink(item)}
|
|
<ul data-part="list" class="second-level-list">
|
|
${children.map(
|
|
child =>
|
|
html`<li ?data-:active="${child.active}">
|
|
${templates.navItem(context, { child, level })}
|
|
</li>`,
|
|
)}
|
|
</ul>
|
|
</div>`;
|
|
|
|
// accordion with all the links
|
|
// return html`<lion-accordion>
|
|
// <h3 slot="invoker"><button>${item.name}</button></h3>
|
|
// <div slot="content">
|
|
// <ul>
|
|
// ${children.map(
|
|
// item => html`<li ?data-:active="${item.isActive}">
|
|
// ${templates.navItem(context, { item, level })}
|
|
//
|
|
// </li>`)}
|
|
// </ul>
|
|
// </div>
|
|
// </div>`;
|
|
},
|
|
navItem(context, { item, level }) {
|
|
return html`<a
|
|
data-part="anchor"
|
|
data-level="${level}"
|
|
href="${item.redirect || item.url}"
|
|
aria-current=${item.active ? 'page' : ''}
|
|
>${level === 1
|
|
? html`<lion-icon
|
|
data-part="icon"
|
|
data-level="${level}"
|
|
icon-id="${item.iconId}${item.active ? 'Filled' : ''}"
|
|
></lion-icon>`
|
|
: nothing}<span>${item.name}</span></a
|
|
>`;
|
|
},
|
|
}),
|
|
// this is not working
|
|
// you need to use global elements definitions
|
|
scopedElements: () => ({}),
|
|
};
|
|
|
|
UIPortalMainNav.provideStylesAndMarkup({
|
|
markup: baseUINavMarkup,
|
|
/** 2 columns */
|
|
styles: () => [
|
|
sharedGlobalStyles,
|
|
uiPortalMainNavBurgerCss,
|
|
css`
|
|
:host {
|
|
--_width-l0: var(--size-12);
|
|
--_width-l1: var(--size-13);
|
|
height: 100vh;
|
|
/** Make this the positioning parent of l0 and l1 */
|
|
position: relative;
|
|
width: var(--_width-l0);
|
|
display: block;
|
|
position: sticky;
|
|
top: 0;
|
|
}
|
|
|
|
:host([data-layout='inline-columns'][data-wide='true']) {
|
|
width: calc(var(--_width-l0) + var(--_width-l1));
|
|
}
|
|
|
|
:host [data-part='nav'] {
|
|
height: 100%;
|
|
}
|
|
|
|
:host [data-part='level'][data-level='1'],
|
|
:host [data-part='level'][data-level='2'] {
|
|
padding-block-start: var(--size-6);
|
|
padding-inline: var(--size-2);
|
|
overflow-y: scroll;
|
|
}
|
|
|
|
:host [data-part='level'][data-level='1'] {
|
|
width: var(--_width-l0);
|
|
height: 100vh;
|
|
border-right: 1px solid #ccc;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
/**
|
|
* When a l0 child is active, or a l1 child => open correct l1
|
|
*/
|
|
:host
|
|
[data-part='listitem']:not([data-\:active])
|
|
[data-part='level'][data-level='2']:not([data-has-active-child]) {
|
|
/** TODO: sr-only, because we want to present all links to the screen reader */
|
|
display: none;
|
|
}
|
|
|
|
:host [data-part='level'][data-level='2'] {
|
|
width: var(--_width-l1);
|
|
position: absolute;
|
|
left: var(--_width-l0);
|
|
top: 0;
|
|
/* padding-inline: var(--size-6); */
|
|
border-right: 1px solid #ccc;
|
|
height: 100%;
|
|
}
|
|
|
|
:host [data-part='list'] {
|
|
list-style-type: none;
|
|
margin: 4px;
|
|
padding: 0;
|
|
}
|
|
|
|
:host [data-part='anchor'][data-level='1'] {
|
|
display: block;
|
|
padding-block: var(--size-6);
|
|
padding-inline: var(--size-6);
|
|
}
|
|
|
|
:host [data-part='anchor'][data-level='2'] {
|
|
display: block;
|
|
padding-block: var(--size-3);
|
|
padding-inline: var(--size-6);
|
|
}
|
|
|
|
:host [data-part='anchor'][data-level='2'][aria-current='page']:not(:last-child) {
|
|
padding-block: var(--size-2);
|
|
}
|
|
|
|
:host [data-part='icon'][data-level='1'] {
|
|
display: block;
|
|
width: var(--size-7);
|
|
height: var(--size-7);
|
|
margin-bottom: var(--size-1);
|
|
}
|
|
|
|
:host [data-part='anchor'][data-level='1'] {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
margin-block-end: 6px;
|
|
}
|
|
|
|
:host [data-part='anchor'] {
|
|
display: block;
|
|
color: var(--text-color);
|
|
text-decoration: inherit;
|
|
font-size: 1rem;
|
|
fill: var(--primary-icon-color);
|
|
margin-inline: var(--size-1);
|
|
border-radius: var(--radius-4);
|
|
}
|
|
|
|
:host [data-part='anchor'][aria-current='page'][data-level='1'],
|
|
:host [data-part='anchor'][aria-current='page'][data-level='3']:last-child,
|
|
:host [data-part='anchor'][aria-current='page'][data-level='4'] {
|
|
font-weight: bold;
|
|
background-color: var(--secondary-color);
|
|
}
|
|
|
|
:host [data-part='anchor']:hover {
|
|
text-decoration: underline;
|
|
text-underline-offset: 0.3em;
|
|
background-color: var(--secondary-color-lighter);
|
|
}
|
|
|
|
:host [data-part='anchor']:focus {
|
|
outline: 2px solid var(--contrast-color-dark);
|
|
}
|
|
|
|
:host [data-part='anchor'][data-level='2']:focus,
|
|
:host [data-part='anchor'][data-level='2']:focus {
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
:host [data-part='level'][data-level='2'] {
|
|
display: none;
|
|
}
|
|
|
|
:host [data-\\:active] [data-part='level'][data-level='2'] {
|
|
display: block;
|
|
}
|
|
|
|
:host [data-part='level'][data-level='2'] {
|
|
color: var(--text-color, #333);
|
|
|
|
/* 14px/Regular */
|
|
font-family: 'ING Me';
|
|
font-size: 0.875rem;
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
line-height: 20px; /* 142.857% */
|
|
text-decoration: none;
|
|
}
|
|
|
|
:host [data-part='listitem'][data-level='2'][data-\\:active] {
|
|
border-radius: var(--radius-4);
|
|
background: var(--neutral-color-lightest);
|
|
margin-block: 6px;
|
|
}
|
|
|
|
:host [data-part='level'][data-level='3'] {
|
|
overflow: hidden;
|
|
padding-block-end: 12px;
|
|
}
|
|
|
|
:host [data-part='anchor'][data-level='3'],
|
|
:host [data-part='anchor'][data-level='4'] {
|
|
/* 14px/Regular */
|
|
font-family: 'ING Me';
|
|
font-size: 0.875rem;
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
line-height: 20px; /* 142.857% */
|
|
text-decoration: none;
|
|
margin-left: var(--size-7);
|
|
padding-inline: var(--size-3);
|
|
}
|
|
|
|
:host [data-part='anchor'][data-level='3'][aria-current='page'],
|
|
:host [data-part='anchor'][data-level='4'][aria-current='page'] {
|
|
font-weight: bold;
|
|
}
|
|
|
|
:host [data-level='2'] > [aria-current='page'] {
|
|
background: transparent;
|
|
font-weight: bold;
|
|
}
|
|
|
|
:host [data-part='list'][data-level='4'] {
|
|
margin-left: var(--size-3);
|
|
}
|
|
`,
|
|
],
|
|
layouts: () => ({
|
|
'floating-toggle': 0,
|
|
'inline-columns': 900,
|
|
}),
|
|
layoutsContainer: () => globalThis,
|
|
});
|
|
|
|
customElements.define(tagName, UIPortalMainNav);
|