345 lines
8.9 KiB
Plaintext
345 lines
8.9 KiB
Plaintext
// MegaMenu -- a trigger and a wide panel of grouped links.
|
|
//
|
|
// <MegaMenu label="Products" columns='[{"heading":"Platform",
|
|
// "items":[{"label":"Runtime","href":"/runtime"}]}]' />
|
|
//
|
|
// Deliberately one level deep. A mega menu exists to show breadth flat, so
|
|
// everything is one click away; nesting inside the panel buries content behind
|
|
// hover-within-hover and is close to unusable by keyboard and touch. Use Nav
|
|
// when you actually want cascading submenus.
|
|
//
|
|
// The panel carries data-wrn-anchored so the runtime clamp keeps it inside the
|
|
// viewport instead of hanging off the edge of a wide layout.
|
|
//
|
|
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
|
|
// and silently swallows the rule that follows it.
|
|
component MegaMenu {
|
|
outputs {
|
|
open(payload: { sourceEvent: Event })
|
|
close(payload: { reason: string })
|
|
select(payload: { item: object; value: string })
|
|
}
|
|
|
|
props {
|
|
color: string = "primary"
|
|
size: string = "default"
|
|
label: string = "Menu"
|
|
icon: string = ""
|
|
columns: unknown[] = []
|
|
footer: string = ""
|
|
defaultOpen: boolean = false
|
|
class: string = ""
|
|
}
|
|
|
|
state visible = defaultOpen
|
|
|
|
functions {
|
|
shared function columnList() {
|
|
return Array.isArray(columns) ? columns : []
|
|
}
|
|
|
|
shared function itemsOf(column) {
|
|
return column && Array.isArray(column.items) ? column.items : []
|
|
}
|
|
|
|
client function showPanel(sourceEvent) {
|
|
visible = true
|
|
output.open({ sourceEvent: sourceEvent })
|
|
}
|
|
|
|
client function hidePanel(reason) {
|
|
visible = false
|
|
output.close({ reason: reason })
|
|
}
|
|
|
|
client function togglePanel(sourceEvent) {
|
|
if (visible) {
|
|
hidePanel("toggle")
|
|
} else {
|
|
showPanel(sourceEvent)
|
|
}
|
|
}
|
|
|
|
client function handleKeydown(sourceEvent) {
|
|
if (sourceEvent.key === "Escape" && visible) {
|
|
sourceEvent.preventDefault()
|
|
hidePanel("escape")
|
|
}
|
|
}
|
|
|
|
client function choose(item) {
|
|
if (item.disabled) {
|
|
return
|
|
}
|
|
output.select({ item: item, value: item.value || item.label || "" })
|
|
hidePanel("select")
|
|
}
|
|
}
|
|
|
|
view {
|
|
<div
|
|
{...attrs}
|
|
data-ui-component="MegaMenu"
|
|
class='wrn-mega {class}'
|
|
data-color='{color}'
|
|
data-size='{size}'
|
|
data-open='{visible}'
|
|
@keydown='handleKeydown(event)'
|
|
@mouseleave='hidePanel("pointer-leave")'
|
|
>
|
|
<button
|
|
type="button"
|
|
class="wrn-mega__trigger"
|
|
aria-haspopup="true"
|
|
aria-expanded='{visible}'
|
|
@click='togglePanel(event)'
|
|
@mouseenter='showPanel(event)'
|
|
@focus='showPanel(event)'
|
|
>
|
|
<span class='wrn-mega__trigger-icon {icon}' data-show="icon" aria-hidden="true"></span>
|
|
<span class="wrn-mega__trigger-label">{label}</span>
|
|
<span class="wrn-mega__arrow" aria-hidden="true">›</span>
|
|
</button>
|
|
|
|
<div
|
|
class="wrn-mega__panel"
|
|
data-wrn-anchored="true"
|
|
data-show="visible"
|
|
data-wrn-roving="both"
|
|
>
|
|
<div class="wrn-mega__columns">
|
|
{#each columnList() as column}
|
|
<section class="wrn-mega__column">
|
|
<h3 class="wrn-mega__heading" data-show="column.heading">{column.heading}</h3>
|
|
<ul class="wrn-mega__list">
|
|
{#each itemsOf(column) as item}
|
|
<li>
|
|
<a
|
|
class="wrn-mega__link"
|
|
href='{item.href || "#"}'
|
|
data-wrn-roving-item="true"
|
|
aria-disabled='{item.disabled ? "true" : "false"}'
|
|
@click='choose(item)'
|
|
>
|
|
<span
|
|
class='wrn-mega__link-icon {item.icon}'
|
|
data-show="item.icon"
|
|
aria-hidden="true"
|
|
></span>
|
|
<span class="wrn-mega__link-body">
|
|
<span class="wrn-mega__link-label">{item.label}</span>
|
|
<span class="wrn-mega__link-description" data-show="item.description">
|
|
{item.description}
|
|
</span>
|
|
</span>
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</section>
|
|
{/each}
|
|
</div>
|
|
|
|
<p class="wrn-mega__footer" data-show="footer">{footer}</p>
|
|
<slot />
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
style {
|
|
.wrn-mega {
|
|
--mega-accent: var(--wrn-color-primary);
|
|
position: relative;
|
|
display: inline-block;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.wrn-mega[data-color="secondary"] {
|
|
--mega-accent: var(--wrn-color-secondary);
|
|
}
|
|
|
|
.wrn-mega[data-color="success"] {
|
|
--mega-accent: var(--wrn-color-success);
|
|
}
|
|
|
|
.wrn-mega[data-color="danger"] {
|
|
--mega-accent: var(--wrn-color-danger);
|
|
}
|
|
|
|
.wrn-mega[data-color="info"] {
|
|
--mega-accent: var(--wrn-color-info);
|
|
}
|
|
|
|
.wrn-mega[data-size="sm"] {
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.wrn-mega[data-size="lg"] {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.wrn-mega__trigger {
|
|
appearance: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
padding: 0.45rem 0.7rem;
|
|
border: 1px solid transparent;
|
|
border-radius: var(--wrn-radius-sm);
|
|
background: transparent;
|
|
color: var(--wrn-color-text-muted);
|
|
font: inherit;
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.wrn-mega__trigger:hover,
|
|
.wrn-mega[data-open="true"] .wrn-mega__trigger {
|
|
background: var(--wrn-color-surface-soft);
|
|
color: var(--wrn-color-text);
|
|
}
|
|
|
|
.wrn-mega__trigger:focus-visible {
|
|
outline: 2px solid var(--mega-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.wrn-mega__arrow {
|
|
display: inline-block;
|
|
transition: transform 160ms ease;
|
|
}
|
|
|
|
.wrn-mega[data-open="true"] .wrn-mega__arrow {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
/*
|
|
* The panel is offset below the trigger for looks, which used to close the
|
|
* menu on the way to it: that offset is dead space belonging to neither
|
|
* element, so crossing it fired mouseleave on the root. The bridge below
|
|
* covers the gap with a descendant, so the pointer never actually leaves.
|
|
*/
|
|
.wrn-mega__panel::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 100%;
|
|
height: 0.6rem;
|
|
}
|
|
|
|
.wrn-mega__panel {
|
|
position: absolute;
|
|
z-index: 40;
|
|
top: calc(100% + 0.4rem);
|
|
left: 0;
|
|
width: max-content;
|
|
max-width: min(60rem, calc(100vw - 2rem));
|
|
padding: 1rem;
|
|
border: 1px solid var(--wrn-color-border);
|
|
border-radius: var(--wrn-radius-lg);
|
|
background: var(--wrn-color-surface);
|
|
box-shadow: var(--wrn-shadow-1);
|
|
}
|
|
|
|
.wrn-mega__columns {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
|
|
gap: 1.25rem;
|
|
}
|
|
|
|
.wrn-mega__column {
|
|
min-width: 0;
|
|
}
|
|
|
|
.wrn-mega__heading {
|
|
margin: 0 0 0.5rem;
|
|
color: var(--wrn-color-text-muted);
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.wrn-mega__list {
|
|
display: grid;
|
|
gap: 0.15rem;
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
.wrn-mega__link {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0.5rem;
|
|
padding: 0.45rem 0.5rem;
|
|
border-radius: var(--wrn-radius-sm);
|
|
color: var(--wrn-color-text);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.wrn-mega__link:hover {
|
|
background: var(--wrn-color-surface-soft);
|
|
}
|
|
|
|
.wrn-mega__link:focus-visible {
|
|
outline: 2px solid var(--mega-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.wrn-mega__link[aria-disabled="true"] {
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.wrn-mega__link-body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
}
|
|
|
|
.wrn-mega__link-label {
|
|
font-size: 0.88rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.wrn-mega__link-description {
|
|
color: var(--wrn-color-text-muted);
|
|
font-size: 0.78rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.wrn-mega__footer {
|
|
margin: 1rem 0 0;
|
|
padding-top: 0.75rem;
|
|
border-top: 1px solid var(--wrn-color-border);
|
|
color: var(--wrn-color-text-muted);
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
/*
|
|
* On a phone the panel stops floating and becomes part of the flow. A
|
|
* pointer-opened overlay pinned to a trigger is unreachable on touch, and
|
|
* a 60rem grid has nowhere to go on a 360px screen.
|
|
*/
|
|
@media (max-width: 767px) {
|
|
.wrn-mega {
|
|
display: block;
|
|
}
|
|
|
|
.wrn-mega__panel {
|
|
position: static;
|
|
width: auto;
|
|
max-width: 100%;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.wrn-mega__columns {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
}
|
|
}
|