fix(ui): refine responsive catalog navigation
This commit is contained in:
@@ -688,7 +688,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.8.42",
|
||||
"version": "0.8.43",
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*",
|
||||
},
|
||||
|
||||
@@ -193,11 +193,12 @@ component MegaMenu {
|
||||
class="wrn-mega__trigger"
|
||||
aria-haspopup="true"
|
||||
aria-expanded='{visible}'
|
||||
aria-label='{label}'
|
||||
@click='activatePanel(event)'
|
||||
@mouseenter='hoverPanel(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__trigger-label" data-label='{label}' aria-hidden="true"></span>
|
||||
<span class="wrn-mega__arrow" aria-hidden="true">›</span>
|
||||
</button>
|
||||
|
||||
@@ -242,7 +243,13 @@ component MegaMenu {
|
||||
<li>
|
||||
<a class="wrn-mega__link" href='{item.href || "#"}' data-wrn-roving-item="true" aria-disabled='{item.disabled ? "true" : "false"}' @click='choose(item)'>
|
||||
{#if item.image}<img class="wrn-mega__link-image" src='{item.image}' alt="" loading="lazy" />{/if}
|
||||
{#if item.icon}<span class='wrn-mega__link-icon {item.icon}' aria-hidden="true"></span>{/if}
|
||||
{#if item.icon}
|
||||
<span class='wrn-mega__link-icon {item.icon}' aria-hidden="true"></span>
|
||||
{:else if variant === "catalog"}
|
||||
<span class="wrn-mega__link-icon wrn-mega__link-icon--fallback" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"></rect><path d="M3 9h18"></path><path d="M9 21V9"></path></svg>
|
||||
</span>
|
||||
{/if}
|
||||
<span class="wrn-mega__link-body">
|
||||
{#if item.eyebrow}<span class="wrn-mega__link-eyebrow">{item.eyebrow}</span>{/if}
|
||||
<span class="wrn-mega__link-label">{item.label}{#if item.badge}<span class="wrn-mega__badge">{item.badge}</span>{/if}</span>
|
||||
@@ -328,6 +335,10 @@ component MegaMenu {
|
||||
color: var(--wrn-color-text);
|
||||
}
|
||||
|
||||
.wrn-mega__trigger-label::before {
|
||||
content: attr(data-label);
|
||||
}
|
||||
|
||||
.wrn-mega__trigger:focus-visible {
|
||||
outline: 2px solid var(--mega-accent);
|
||||
outline-offset: 2px;
|
||||
@@ -489,9 +500,9 @@ component MegaMenu {
|
||||
.wrn-mega[data-variant="catalog"] .wrn-mega__column-header { width: 100%; min-width: 0; margin: 0 0 0.85rem; padding: 0 0 0.65rem; border: 0; border-bottom: 1px solid var(--wrn-color-border); border-radius: 0; background: transparent; }
|
||||
.wrn-mega[data-variant="catalog"] .wrn-mega__heading { margin: 0; color: var(--wrn-color-text-muted); font-size: 0.75rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; }
|
||||
.wrn-mega[data-variant="catalog"] .wrn-mega__column-description { margin: 0.3rem 0 0; }
|
||||
.wrn-mega[data-variant="catalog"] .wrn-mega__list { grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0.35rem; }
|
||||
.wrn-mega[data-variant="catalog"] .wrn-mega__link { min-height: 3.25rem; padding: 0.7rem 0.8rem; align-items: center; border: 1px solid var(--wrn-color-border); background: color-mix(in srgb, var(--wrn-color-surface-soft) 55%, transparent); }
|
||||
.wrn-mega[data-variant="catalog"] .wrn-mega__link:hover { border-color: color-mix(in srgb, var(--mega-accent) 45%, var(--wrn-color-border)); background: color-mix(in srgb, var(--mega-accent) 7%, var(--wrn-color-surface)); transform: translateY(-1px); }
|
||||
.wrn-mega[data-variant="catalog"] .wrn-mega__list { grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: 0.3rem; }
|
||||
.wrn-mega[data-variant="catalog"] .wrn-mega__link { min-height: 2.7rem; padding: 0.45rem 0.55rem; align-items: center; gap: 0.55rem; border: 1px solid transparent; background: transparent; }
|
||||
.wrn-mega[data-variant="catalog"] .wrn-mega__link:hover { border-color: color-mix(in srgb, var(--mega-accent) 28%, var(--wrn-color-border)); background: color-mix(in srgb, var(--mega-accent) 7%, var(--wrn-color-surface)); transform: translateY(-1px); }
|
||||
.wrn-mega[data-variant="catalog"] .wrn-mega__column-action { margin-top: auto; padding-top: 1rem; }
|
||||
.wrn-mega[data-variant="dark"] .wrn-mega__panel { color: #f8fafc; border-color: #2b2b2b; background: #151515; }
|
||||
.wrn-mega[data-variant="dark"] :is(.wrn-mega__heading, .wrn-mega__link, .wrn-mega__featured strong) { color: #f8fafc; }
|
||||
@@ -518,7 +529,10 @@ component MegaMenu {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wrn-mega[data-variant="catalog"] .wrn-mega__link-icon { width: 1.75rem; height: 1.75rem; flex: 0 0 1.75rem; }
|
||||
.wrn-mega[data-variant="catalog"] .wrn-mega__link-icon { width: 1.65rem; height: 1.65rem; flex: 0 0 1.65rem; border: 1px solid color-mix(in srgb, var(--mega-accent) 22%, var(--wrn-color-border)); border-radius: 0.45rem; font-size: 0.9rem; }
|
||||
.wrn-mega[data-variant="catalog"] .wrn-mega__link-icon--fallback svg { width: 0.9rem; height: 0.9rem; }
|
||||
.wrn-mega[data-variant="catalog"] .wrn-mega__link-label { font-size: 0.78rem; line-height: 1.25; }
|
||||
.wrn-mega[data-variant="catalog"] .wrn-mega__link-description { font-size: 0.7rem; line-height: 1.35; }
|
||||
|
||||
.wrn-mega__link-label {
|
||||
font-size: 0.88rem;
|
||||
|
||||
@@ -100,19 +100,29 @@ size: string = "default"
|
||||
}
|
||||
return [{ heading: item.description || "", items: legacy }]
|
||||
}
|
||||
client function toggleNavigation() {
|
||||
mobileOpen = !mobileOpen
|
||||
output.toggle({ open: mobileOpen })
|
||||
output[mobileOpen ? "open" : "close"]({ source: "mobile" })
|
||||
client function setMobileNavigation(open, trigger) {
|
||||
const root = trigger && trigger.closest ? trigger.closest("[data-wrn-navbar]") : null
|
||||
if (!root) return
|
||||
const collapse = root.querySelector(".wrn-navbar__collapse")
|
||||
const toggle = root.querySelector(".wrn-navbar__toggle")
|
||||
if (collapse) collapse.classList.toggle("is-open", open)
|
||||
if (toggle) toggle.setAttribute("aria-expanded", open ? "true" : "false")
|
||||
}
|
||||
|
||||
client function selectItem(item, level) {
|
||||
mobileOpen = false
|
||||
client function toggleNavigation(event) {
|
||||
const open = event.currentTarget.getAttribute("aria-expanded") !== "true"
|
||||
setMobileNavigation(open, event.currentTarget)
|
||||
output.toggle({ open: open })
|
||||
output[open ? "open" : "close"]({ source: "mobile" })
|
||||
}
|
||||
|
||||
client function selectItem(item, level, event) {
|
||||
setMobileNavigation(false, event.currentTarget)
|
||||
output.select({ item: item, value: item.value || "", level: level })
|
||||
}
|
||||
|
||||
client function selectAction(item) {
|
||||
mobileOpen = false
|
||||
client function selectAction(item, event) {
|
||||
setMobileNavigation(false, event.currentTarget)
|
||||
output.action({ item: item, value: item.value || "" })
|
||||
}
|
||||
|
||||
@@ -161,6 +171,7 @@ size: string = "default"
|
||||
</div>
|
||||
|
||||
<div class="wrn-navbar__main">
|
||||
<slot name="brand">
|
||||
{#if brand.logo || brand.icon || brand.label || brand.description}
|
||||
<a class="wrn-navbar__brand" href="{brand.href || '/'}" aria-label="{brand.ariaLabel || brand.label || label}">
|
||||
{#if brand.logo}
|
||||
@@ -177,6 +188,7 @@ size: string = "default"
|
||||
{/if}
|
||||
</a>
|
||||
{/if}
|
||||
</slot>
|
||||
|
||||
<div class="wrn-navbar__collapse {mobileOpen ? 'is-open' : ''}">
|
||||
<nav class="wrn-navbar__menus" aria-label="{label}" data-wrn-roving="horizontal">
|
||||
@@ -187,22 +199,26 @@ size: string = "default"
|
||||
data-active="{isItemActive(item) ? 'true' : 'false'}"
|
||||
label="{item.label}"
|
||||
icon="{item.icon || ''}"
|
||||
columns="{megaColumns(item)}"
|
||||
rail="{megaData(item).rail || []}"
|
||||
featured="{megaData(item).featured || {}}"
|
||||
columns={megaColumns(item)}
|
||||
rail={megaData(item).rail || []}
|
||||
featured={megaData(item).featured || {}}
|
||||
variant="{megaData(item).variant || 'default'}"
|
||||
density="{megaData(item).density || 'default'}"
|
||||
panelWidth="{megaData(item).panelWidth || 'xl'}"
|
||||
panelAlign="{megaData(item).panelAlign || 'start'}"
|
||||
columnCount="{megaData(item).columnCount || 0}"
|
||||
fullWidth="{megaData(item).fullWidth || false}"
|
||||
openOnHover="{openOnHover}"
|
||||
columnCount={megaData(item).columnCount || 0}
|
||||
fullWidth={megaData(item).fullWidth || false}
|
||||
openOnHover={openOnHover}
|
||||
activeCategory="{megaData(item).activeCategory || ''}"
|
||||
actionLabel="{megaData(item).actionLabel || ''}"
|
||||
actionHref="{megaData(item).actionHref || ''}"
|
||||
actionIcon="{megaData(item).actionIcon || 'icon-[lucide--arrow-right]'}"
|
||||
footer="{megaData(item).footer || ''}"
|
||||
/>
|
||||
<a class="wrn-navbar__menu-link wrn-navbar__mobile-mega-link" data-wrn-roving-item="true" href="{item.href || megaData(item).actionHref || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{isItemActive(item) ? 'page' : 'false'}" @click="selectItem(item, 1, event)">
|
||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||
<span data-t="{translationKey(item, 'label')}">{item.label}</span>
|
||||
</a>
|
||||
{:else if itemType(item) === "dropdown"}
|
||||
<details class="wrn-navbar__dropdown wrn-navbar__dropdown--{item.type || 'dropdown'}" name="wrn-navbar-menu" @toggle="toggleDropdown(event, item)">
|
||||
<summary data-wrn-roving-item="true" aria-current="{isItemActive(item) ? 'page' : 'false'}">
|
||||
@@ -218,13 +234,13 @@ size: string = "default"
|
||||
{#if child.label}<strong class="wrn-navbar__group-title" data-t="{translationKey(child, 'label')}">{child.label}</strong>{/if}
|
||||
{#if child.description}<small data-t="{translationKey(child, 'description')}">{child.description}</small>{/if}
|
||||
{#each dropdownGroupItems(child) as nested}
|
||||
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : 'false'}" @click="selectItem(nested, 3)">
|
||||
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : 'false'}" @click="selectItem(nested, 3, event)">
|
||||
{#if nested.icon}<span class="{nested.icon}" aria-hidden="true"></span>{/if}
|
||||
<span><strong data-t="{translationKey(nested, 'label')}">{nested.label}</strong>{#if nested.description}<small data-t="{translationKey(nested, 'description')}">{nested.description}</small>{/if}</span>
|
||||
</a>
|
||||
{/each}
|
||||
{:else}
|
||||
<a href="{child.href || '#'}" target="{child.target || ''}" rel="{child.rel || ''}" aria-current="{child.value === active ? 'page' : 'false'}" @click="selectItem(child, 2)">
|
||||
<a href="{child.href || '#'}" target="{child.target || ''}" rel="{child.rel || ''}" aria-current="{child.value === active ? 'page' : 'false'}" @click="selectItem(child, 2, event)">
|
||||
{#if child.icon}<span class="{child.icon}" aria-hidden="true"></span>{/if}
|
||||
<span><strong data-t="{translationKey(child, 'label')}">{child.label}</strong>{#if child.description}<small data-t="{translationKey(child, 'description')}">{child.description}</small>{/if}</span>
|
||||
</a>
|
||||
@@ -234,7 +250,7 @@ size: string = "default"
|
||||
</div>
|
||||
</details>
|
||||
{:else}
|
||||
<a class="wrn-navbar__menu-link" data-wrn-roving-item="true" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{item.value === active ? 'page' : 'false'}" @click="selectItem(item, 1)">
|
||||
<a class="wrn-navbar__menu-link" data-wrn-roving-item="true" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{item.value === active ? 'page' : 'false'}" @click="selectItem(item, 1, event)">
|
||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||
<span data-t="{translationKey(item, 'label')}">{item.label}</span>
|
||||
</a>
|
||||
@@ -243,19 +259,19 @@ size: string = "default"
|
||||
</nav>
|
||||
|
||||
<div class="wrn-navbar__actions">
|
||||
<slot name="actions" />
|
||||
{#each actions as item}
|
||||
<a class="wrn-navbar__action wrn-navbar__action--{item.variant || 'link'}" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" @click="selectAction(item)">
|
||||
<a class="wrn-navbar__action wrn-navbar__action--{item.variant || 'link'}" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" @click="selectAction(item, event)">
|
||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||
<span data-t="{translationKey(item, 'label')}">{item.label}</span>
|
||||
</a>
|
||||
{/each}
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrn-navbar__utilities"><slot name="utilities" /></div>
|
||||
|
||||
<button type="button" class="wrn-navbar__toggle" aria-label="{mobileLabel}" aria-expanded="{mobileOpen}" @click="toggleNavigation()">
|
||||
<button type="button" class="wrn-navbar__toggle" aria-label="{mobileLabel}" aria-expanded="{mobileOpen}" @click="toggleNavigation(event)">
|
||||
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -672,6 +688,8 @@ size: string = "default"
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.wrn-navbar__mobile-mega-link { display: none; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.wrn-navbar__main {
|
||||
min-height: 3.5rem;
|
||||
@@ -724,6 +742,8 @@ size: string = "default"
|
||||
.wrn-navbar__mega .wrn-mega__trigger {
|
||||
width: 100%;
|
||||
}
|
||||
.wrn-navbar__mega { display: none; }
|
||||
.wrn-navbar__mobile-mega-link { display: flex; }
|
||||
.wrn-navbar__menu-link,
|
||||
.wrn-navbar__dropdown > summary,
|
||||
.wrn-navbar__action {
|
||||
@@ -785,6 +805,8 @@ size: string = "default"
|
||||
}
|
||||
.wrn-navbar[data-collapse-at="1100px"] :is(.wrn-navbar__menu-link, .wrn-navbar__dropdown > summary, .wrn-navbar__action) { width: 100%; }
|
||||
.wrn-navbar[data-collapse-at="1100px"] :is(.wrn-navbar__mega, .wrn-navbar__mega .wrn-mega__trigger) { width: 100%; }
|
||||
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__mega { display: none; }
|
||||
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__mobile-mega-link { display: flex; }
|
||||
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__panel { position: static; width: 100%; max-width: none; transform: none; }
|
||||
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__mega[data-full-width="true"] .wrn-mega__panel { position: static; width: 100%; max-width: 100%; margin-top: 0.25rem; }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.8.42",
|
||||
"version": "0.8.43",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -126,6 +126,42 @@ describe("@wrnexus/ui redesign component pack", () => {
|
||||
expect(source).not.toContain(".wrn-navbar__topbar:not(:empty)");
|
||||
});
|
||||
|
||||
test("Navbar exposes a brand slot while retaining the brand prop fallback", async () => {
|
||||
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
|
||||
|
||||
expect(source).toContain('<slot name="brand">');
|
||||
expect(source).toContain("brand.logo || brand.icon || brand.label || brand.description");
|
||||
});
|
||||
|
||||
test("Navbar renders slotted utilities before its built-in actions", async () => {
|
||||
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
|
||||
const actionsStart = source.indexOf('<div class="wrn-navbar__actions">');
|
||||
const slotIndex = source.indexOf('<slot name="actions" />', actionsStart);
|
||||
const actionsLoop = source.indexOf("{#each actions as item}", actionsStart);
|
||||
|
||||
expect(slotIndex).toBeGreaterThan(actionsStart);
|
||||
expect(slotIndex).toBeLessThan(actionsLoop);
|
||||
});
|
||||
|
||||
test("Navbar passes structured MegaMenu props as expressions", async () => {
|
||||
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
|
||||
|
||||
expect(source).toContain("columns={megaColumns(item)}");
|
||||
expect(source).toContain("rail={megaData(item).rail || []}");
|
||||
expect(source).toContain("featured={megaData(item).featured || {}}");
|
||||
expect(source).toContain("fullWidth={megaData(item).fullWidth || false}");
|
||||
expect(source).not.toContain('columns="{megaColumns(item)}"');
|
||||
});
|
||||
|
||||
test("Navbar replaces mega panels with direct links in collapsed navigation", async () => {
|
||||
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
|
||||
|
||||
expect(source).toContain("wrn-navbar__mobile-mega-link");
|
||||
expect(source).toContain("item.href || megaData(item).actionHref");
|
||||
expect(source).toContain('.wrn-navbar__mega { display: none; }');
|
||||
expect(source).toContain('.wrn-navbar__mobile-mega-link { display: flex; }');
|
||||
});
|
||||
|
||||
test("Navbar supports in-flow floating and elevated menu panels", async () => {
|
||||
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
|
||||
const ast = parse(source);
|
||||
|
||||
@@ -2928,6 +2928,8 @@ test("mega menu renders column groups in an anchored panel", async () => {
|
||||
const trigger = dom.querySelector(".wrn-mega__trigger") as HTMLElement;
|
||||
expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
||||
expect(trigger.getAttribute("aria-haspopup")).toBe("true");
|
||||
expect(trigger.getAttribute("aria-label")).toBe("Products");
|
||||
expect(dom.querySelector(".wrn-mega__trigger-label")?.getAttribute("data-label")).toBe("Products");
|
||||
|
||||
const panel = dom.querySelector(".wrn-mega__panel") as HTMLElement;
|
||||
expect(panel.getAttribute("data-wrn-anchored")).toBe("true");
|
||||
@@ -2980,7 +2982,8 @@ test("catalog mega menu renders only the active rail category", async () => {
|
||||
expect(source).toContain("overflow-y: auto");
|
||||
expect(source).not.toContain("@mouseenter='chooseCategory(item, index)'");
|
||||
expect(dom.querySelector(".wrn-mega__column-header")?.textContent).toContain("Sales");
|
||||
expect(dom.querySelectorAll(".wrn-mega__link-icon")).toHaveLength(0);
|
||||
expect(dom.querySelectorAll(".wrn-mega__link-icon")).toHaveLength(1);
|
||||
expect(dom.querySelector(".wrn-mega__link-icon--fallback svg")).not.toBeNull();
|
||||
expect(dom.querySelectorAll(".wrn-mega__link-description")).toHaveLength(0);
|
||||
expect(source).toContain("border-bottom: 1px solid var(--wrn-color-border)");
|
||||
expect(source).toContain('.wrn-mega__rail-icon > span');
|
||||
|
||||
Reference in New Issue
Block a user