355 lines
9.6 KiB
Plaintext
355 lines
9.6 KiB
Plaintext
// Nav -- a navigation link list, flat or with submenus.
|
|
//
|
|
// <Nav items='[{"label":"Home","href":"/","value":"home"}]' active="home" />
|
|
//
|
|
// An item carrying its own items array becomes a submenu. Depth is capped at
|
|
// three levels: this template language has no component recursion, so each
|
|
// level is written out, and three covers any realistic navigation.
|
|
//
|
|
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
|
|
// and silently swallows the rule that follows it.
|
|
component Nav {
|
|
outputs {
|
|
select(payload: { item: object; value: string })
|
|
}
|
|
|
|
props {
|
|
color: string = "primary"
|
|
size: string = "default"
|
|
items: unknown[] = []
|
|
active: string = ""
|
|
orientation: string = "horizontal"
|
|
label: string = "Main"
|
|
collapsible: boolean = true
|
|
toggleLabel: string = "Menu"
|
|
class: string = ""
|
|
}
|
|
|
|
state expanded = false
|
|
|
|
functions {
|
|
shared function itemList() {
|
|
return Array.isArray(items) ? items : []
|
|
}
|
|
|
|
shared function childrenOf(item) {
|
|
return item && Array.isArray(item.items) ? item.items : []
|
|
}
|
|
|
|
shared function isActive(item) {
|
|
return Boolean(item.value) && item.value === active
|
|
}
|
|
|
|
shared function rovingAxis() {
|
|
return orientation === "vertical" ? "vertical" : "horizontal"
|
|
}
|
|
|
|
client function choose(item) {
|
|
if (item.disabled) {
|
|
return
|
|
}
|
|
output.select({ item: item, value: item.value || "" })
|
|
}
|
|
|
|
client function toggleMenu() {
|
|
expanded = !expanded
|
|
}
|
|
}
|
|
|
|
view {
|
|
<nav
|
|
{...attrs}
|
|
data-ui-component="Nav"
|
|
class='wrn-nav {class}'
|
|
data-orientation='{orientation}'
|
|
data-color='{color}'
|
|
data-size='{size}'
|
|
data-expanded='{expanded}'
|
|
role="navigation"
|
|
aria-label='{label}'
|
|
>
|
|
<button
|
|
type="button"
|
|
class="wrn-nav__toggle"
|
|
data-show="collapsible"
|
|
aria-expanded='{expanded}'
|
|
aria-label='{toggleLabel}'
|
|
@click='toggleMenu()'
|
|
>
|
|
<span class="wrn-nav__toggle-bar" aria-hidden="true"></span>
|
|
<span class="wrn-nav__toggle-text">{toggleLabel}</span>
|
|
</button>
|
|
|
|
<ul class="wrn-nav__list" data-wrn-roving='{rovingAxis()}'>
|
|
{#each itemList() as item}
|
|
<li class="wrn-nav__item" data-has-children='{childrenOf(item).length > 0}'>
|
|
<a
|
|
class="wrn-nav__link"
|
|
href='{item.href || "#"}'
|
|
data-wrn-roving-item="true"
|
|
data-active='{isActive(item)}'
|
|
aria-current='{isActive(item) ? "page" : "false"}'
|
|
aria-disabled='{item.disabled ? "true" : "false"}'
|
|
@click='choose(item)'
|
|
>
|
|
<span
|
|
class='wrn-nav__icon {item.icon}'
|
|
data-show="item.icon"
|
|
aria-hidden="true"
|
|
></span>
|
|
<span class="wrn-nav__label">{item.label}</span>
|
|
<span class="wrn-nav__badge" data-show="item.badge">{item.badge}</span>
|
|
<span
|
|
class="wrn-nav__arrow"
|
|
data-show="childrenOf(item).length > 0"
|
|
aria-hidden="true"
|
|
>›</span>
|
|
</a>
|
|
|
|
<ul
|
|
class="wrn-nav__submenu"
|
|
data-wrn-anchored="true"
|
|
data-show="childrenOf(item).length > 0"
|
|
>
|
|
{#each childrenOf(item) as child}
|
|
<li class="wrn-nav__item" data-has-children='{childrenOf(child).length > 0}'>
|
|
<a
|
|
class="wrn-nav__link"
|
|
href='{child.href || "#"}'
|
|
data-active='{isActive(child)}'
|
|
aria-current='{isActive(child) ? "page" : "false"}'
|
|
aria-disabled='{child.disabled ? "true" : "false"}'
|
|
@click='choose(child)'
|
|
>
|
|
<span
|
|
class='wrn-nav__icon {child.icon}'
|
|
data-show="child.icon"
|
|
aria-hidden="true"
|
|
></span>
|
|
<span class="wrn-nav__label">{child.label}</span>
|
|
<span class="wrn-nav__badge" data-show="child.badge">{child.badge}</span>
|
|
<span
|
|
class="wrn-nav__arrow"
|
|
data-show="childrenOf(child).length > 0"
|
|
aria-hidden="true"
|
|
>›</span>
|
|
</a>
|
|
|
|
<ul
|
|
class="wrn-nav__submenu wrn-nav__submenu--level3"
|
|
data-wrn-anchored="true"
|
|
data-show="childrenOf(child).length > 0"
|
|
>
|
|
{#each childrenOf(child) as leaf}
|
|
<li class="wrn-nav__item">
|
|
<a
|
|
class="wrn-nav__link"
|
|
href='{leaf.href || "#"}'
|
|
data-active='{isActive(leaf)}'
|
|
aria-current='{isActive(leaf) ? "page" : "false"}'
|
|
aria-disabled='{leaf.disabled ? "true" : "false"}'
|
|
@click='choose(leaf)'
|
|
>
|
|
<span
|
|
class='wrn-nav__icon {leaf.icon}'
|
|
data-show="leaf.icon"
|
|
aria-hidden="true"
|
|
></span>
|
|
<span class="wrn-nav__label">{leaf.label}</span>
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
|
|
<slot />
|
|
</nav>
|
|
}
|
|
|
|
style {
|
|
.wrn-nav {
|
|
--nav-accent: var(--wrn-color-primary);
|
|
max-width: 100%;
|
|
}
|
|
|
|
.wrn-nav[data-color="secondary"] {
|
|
--nav-accent: var(--wrn-color-secondary);
|
|
}
|
|
|
|
.wrn-nav[data-color="success"] {
|
|
--nav-accent: var(--wrn-color-success);
|
|
}
|
|
|
|
.wrn-nav[data-color="danger"] {
|
|
--nav-accent: var(--wrn-color-danger);
|
|
}
|
|
|
|
.wrn-nav[data-color="info"] {
|
|
--nav-accent: var(--wrn-color-info);
|
|
}
|
|
|
|
.wrn-nav[data-size="sm"] {
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.wrn-nav[data-size="lg"] {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.wrn-nav__list {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
.wrn-nav[data-orientation="vertical"] .wrn-nav__list {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.wrn-nav__item {
|
|
position: relative;
|
|
}
|
|
|
|
.wrn-nav__link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.45rem;
|
|
padding: 0.45rem 0.7rem;
|
|
border-radius: var(--wrn-radius-sm);
|
|
color: var(--wrn-color-text-muted);
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.wrn-nav__link:hover {
|
|
background: var(--wrn-color-surface-soft);
|
|
color: var(--wrn-color-text);
|
|
}
|
|
|
|
.wrn-nav__link:focus-visible {
|
|
outline: 2px solid var(--nav-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.wrn-nav__link[data-active="true"] {
|
|
background: color-mix(in srgb, var(--nav-accent) 16%, transparent);
|
|
color: var(--nav-accent);
|
|
}
|
|
|
|
.wrn-nav__link[aria-disabled="true"] {
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.wrn-nav__badge {
|
|
padding: 0.05rem 0.4rem;
|
|
border-radius: 999px;
|
|
background: color-mix(in srgb, var(--nav-accent) 16%, transparent);
|
|
color: var(--nav-accent);
|
|
font-size: 0.72rem;
|
|
}
|
|
|
|
.wrn-nav__arrow {
|
|
display: inline-block;
|
|
transition: transform 160ms ease;
|
|
}
|
|
|
|
.wrn-nav__item:hover > .wrn-nav__link > .wrn-nav__arrow,
|
|
.wrn-nav__item:focus-within > .wrn-nav__link > .wrn-nav__arrow {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
.wrn-nav__submenu {
|
|
position: absolute;
|
|
z-index: 30;
|
|
top: 100%;
|
|
left: 0;
|
|
min-width: 12rem;
|
|
margin: 0;
|
|
padding: 0.35rem;
|
|
border: 1px solid var(--wrn-color-border);
|
|
border-radius: var(--wrn-radius-md);
|
|
background: var(--wrn-color-surface);
|
|
box-shadow: var(--wrn-shadow-1);
|
|
list-style: none;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: opacity 160ms ease;
|
|
}
|
|
|
|
.wrn-nav__item:hover > .wrn-nav__submenu,
|
|
.wrn-nav__item:focus-within > .wrn-nav__submenu {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
}
|
|
|
|
.wrn-nav__submenu--level3 {
|
|
top: 0;
|
|
left: 100%;
|
|
}
|
|
|
|
.wrn-nav__toggle {
|
|
display: none;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.45rem 0.7rem;
|
|
border: 1px solid var(--wrn-color-border);
|
|
border-radius: var(--wrn-radius-sm);
|
|
background: var(--wrn-color-surface);
|
|
color: var(--wrn-color-text);
|
|
font: inherit;
|
|
font-size: 0.9rem;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.wrn-nav__toggle-bar {
|
|
width: 1rem;
|
|
height: 2px;
|
|
background: currentColor;
|
|
box-shadow:
|
|
0 -5px 0 currentColor,
|
|
0 5px 0 currentColor;
|
|
}
|
|
|
|
/*
|
|
* On a phone the bar becomes a disclosure: submenus stop being floating
|
|
* overlays and stack inline, because a hover-opened overlay is
|
|
* unreachable on touch.
|
|
*/
|
|
@media (max-width: 767px) {
|
|
.wrn-nav__toggle {
|
|
display: inline-flex;
|
|
}
|
|
|
|
.wrn-nav__list {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.wrn-nav[data-expanded="false"] .wrn-nav__list {
|
|
display: none;
|
|
}
|
|
|
|
.wrn-nav__submenu,
|
|
.wrn-nav__submenu--level3 {
|
|
position: static;
|
|
opacity: 1;
|
|
visibility: visible;
|
|
border: 0;
|
|
box-shadow: none;
|
|
padding-left: 1rem;
|
|
}
|
|
}
|
|
}
|
|
}
|