MegaMenu replaces a scaffold with a trigger and a wide panel of grouped link columns. One level deep on purpose: a mega menu exists to show breadth flat so everything is one click away, and nesting inside the panel buries content behind hover-within-hover. Nav is the component for cascading submenus. The panel is anchored so the runtime clamp keeps it inside the viewport. Sidebar now composes Drawer for its off-canvas presentation instead of a hand-rolled backdrop, inheriting the focus trap and scroll lock from one place. Single items, labelled groups and branches nested to three levels, with vertical roving focus. Sidebar classes move to the BEM naming the rest of the library uses, which is a breaking change; nesting via children still works, since that is what shipped in 0.8.5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
330 lines
8.4 KiB
Plaintext
330 lines
8.4 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='wire-mega {class}'
|
|
data-color='{color}'
|
|
data-size='{size}'
|
|
data-open='{visible}'
|
|
@keydown='handleKeydown(event)'
|
|
@mouseleave='hidePanel("pointer-leave")'
|
|
>
|
|
<button
|
|
type="button"
|
|
class="wire-mega__trigger"
|
|
aria-haspopup="true"
|
|
aria-expanded='{visible}'
|
|
@click='togglePanel(event)'
|
|
@mouseenter='showPanel(event)'
|
|
@focus='showPanel(event)'
|
|
>
|
|
<span class='wire-mega__trigger-icon {icon}' data-show="icon" aria-hidden="true"></span>
|
|
<span class="wire-mega__trigger-label">{label}</span>
|
|
<span class="wire-mega__arrow" aria-hidden="true">›</span>
|
|
</button>
|
|
|
|
<div
|
|
class="wire-mega__panel"
|
|
data-wrn-anchored="true"
|
|
data-show="visible"
|
|
data-wrn-roving="both"
|
|
>
|
|
<div class="wire-mega__columns">
|
|
{#each columnList() as column}
|
|
<section class="wire-mega__column">
|
|
<h3 class="wire-mega__heading" data-show="column.heading">{column.heading}</h3>
|
|
<ul class="wire-mega__list">
|
|
{#each itemsOf(column) as item}
|
|
<li>
|
|
<a
|
|
class="wire-mega__link"
|
|
href='{item.href || "#"}'
|
|
data-wrn-roving-item="true"
|
|
aria-disabled='{item.disabled ? "true" : "false"}'
|
|
@click='choose(item)'
|
|
>
|
|
<span
|
|
class='wire-mega__link-icon {item.icon}'
|
|
data-show="item.icon"
|
|
aria-hidden="true"
|
|
></span>
|
|
<span class="wire-mega__link-body">
|
|
<span class="wire-mega__link-label">{item.label}</span>
|
|
<span class="wire-mega__link-description" data-show="item.description">
|
|
{item.description}
|
|
</span>
|
|
</span>
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</section>
|
|
{/each}
|
|
</div>
|
|
|
|
<p class="wire-mega__footer" data-show="footer">{footer}</p>
|
|
<slot />
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
style {
|
|
.wire-mega {
|
|
--mega-accent: var(--wire-color-primary);
|
|
position: relative;
|
|
display: inline-block;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.wire-mega[data-color="secondary"] {
|
|
--mega-accent: var(--wire-color-secondary);
|
|
}
|
|
|
|
.wire-mega[data-color="success"] {
|
|
--mega-accent: var(--wire-color-success);
|
|
}
|
|
|
|
.wire-mega[data-color="danger"] {
|
|
--mega-accent: var(--wire-color-danger);
|
|
}
|
|
|
|
.wire-mega[data-color="info"] {
|
|
--mega-accent: var(--wire-color-info);
|
|
}
|
|
|
|
.wire-mega[data-size="sm"] {
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.wire-mega[data-size="lg"] {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.wire-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(--wire-radius-sm);
|
|
background: transparent;
|
|
color: var(--wire-color-text-muted);
|
|
font: inherit;
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.wire-mega__trigger:hover,
|
|
.wire-mega[data-open="true"] .wire-mega__trigger {
|
|
background: var(--wire-color-surface-soft);
|
|
color: var(--wire-color-text);
|
|
}
|
|
|
|
.wire-mega__trigger:focus-visible {
|
|
outline: 2px solid var(--mega-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.wire-mega__arrow {
|
|
display: inline-block;
|
|
transition: transform 160ms ease;
|
|
}
|
|
|
|
.wire-mega[data-open="true"] .wire-mega__arrow {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
.wire-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(--wire-color-border);
|
|
border-radius: var(--wire-radius-lg);
|
|
background: var(--wire-color-surface);
|
|
box-shadow: var(--wire-shadow-1);
|
|
}
|
|
|
|
.wire-mega__columns {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
|
|
gap: 1.25rem;
|
|
}
|
|
|
|
.wire-mega__column {
|
|
min-width: 0;
|
|
}
|
|
|
|
.wire-mega__heading {
|
|
margin: 0 0 0.5rem;
|
|
color: var(--wire-color-text-muted);
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.wire-mega__list {
|
|
display: grid;
|
|
gap: 0.15rem;
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
.wire-mega__link {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0.5rem;
|
|
padding: 0.45rem 0.5rem;
|
|
border-radius: var(--wire-radius-sm);
|
|
color: var(--wire-color-text);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.wire-mega__link:hover {
|
|
background: var(--wire-color-surface-soft);
|
|
}
|
|
|
|
.wire-mega__link:focus-visible {
|
|
outline: 2px solid var(--mega-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.wire-mega__link[aria-disabled="true"] {
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.wire-mega__link-body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
}
|
|
|
|
.wire-mega__link-label {
|
|
font-size: 0.88rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.wire-mega__link-description {
|
|
color: var(--wire-color-text-muted);
|
|
font-size: 0.78rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.wire-mega__footer {
|
|
margin: 1rem 0 0;
|
|
padding-top: 0.75rem;
|
|
border-top: 1px solid var(--wire-color-border);
|
|
color: var(--wire-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) {
|
|
.wire-mega {
|
|
display: block;
|
|
}
|
|
|
|
.wire-mega__panel {
|
|
position: static;
|
|
width: auto;
|
|
max-width: 100%;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.wire-mega__columns {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
}
|
|
}
|