fix(ui): make mega menus work under strict CSP
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-24 21:52:25 +05:30
parent 9067e68f69
commit 0618782355
12 changed files with 99 additions and 19 deletions
+1 -1
View File
@@ -808,7 +808,7 @@ Show responsive hierarchical navigation with home support, separators, current-p
Theme-aware, responsive mega menu component.
- Mount: `data-component="MegaMenu"`
- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Menu"`, `icon: string = ""`, `columns: unknown[] = []`, `rail: unknown[] = []`, `featured: Record<string, unknown> = {}`, `variant: string = "default"`, `density: string = "default"`, `panelWidth: string = "xl"`, `panelAlign: string = "start"`, `columnCount: number = 0`, `fullWidth: boolean = false`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = "icon-[lucide--arrow-right]"`, `footer: string = ""`, `defaultOpen: boolean = false`, `class: string = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Menu"`, `icon: string = ""`, `columns: unknown[] = []`, `rail: unknown[] = []`, `featured: Record<string, unknown> = {}`, `variant: string = "default"`, `density: string = "default"`, `panelWidth: string = "xl"`, `panelAlign: string = "start"`, `columnCount: number = 0`, `fullWidth: boolean = false`, `openOnHover: boolean = true`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = "icon-[lucide--arrow-right]"`, `footer: string = ""`, `defaultOpen: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: `open({ sourceEvent: Event })`, `close({ reason: string })`, `select({ item: object; value: string })`
+7
View File
@@ -8401,6 +8401,13 @@
"default": "false",
"options": []
},
{
"name": "openOnHover",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "actionLabel",
"type": "string",
+29 -7
View File
@@ -34,6 +34,7 @@ component MegaMenu {
panelAlign: string = "start"
columnCount: number = 0
fullWidth: boolean = false
openOnHover: boolean = true
actionLabel: string = ""
actionHref: string = ""
actionIcon: string = "icon-[lucide--arrow-right]"
@@ -75,6 +76,20 @@ component MegaMenu {
}
}
client function activatePanel(sourceEvent) {
if (openOnHover) {
showPanel(sourceEvent)
} else {
togglePanel(sourceEvent)
}
}
client function hoverPanel(sourceEvent) {
if (openOnHover) {
showPanel(sourceEvent)
}
}
client function handleKeydown(sourceEvent) {
if (sourceEvent.key === "Escape" && visible) {
sourceEvent.preventDefault()
@@ -113,9 +128,8 @@ component MegaMenu {
class="wrn-mega__trigger"
aria-haspopup="true"
aria-expanded='{visible}'
@click='togglePanel(event)'
@mouseenter='showPanel(event)'
@focus='showPanel(event)'
@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>
@@ -274,7 +288,9 @@ component MegaMenu {
left: 0;
width: max-content;
max-width: min(60rem, calc(100vw - 2rem));
max-height: calc(100dvh - 5.5rem);
padding: 1rem;
overflow: hidden;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-lg);
background: var(--wrn-color-surface);
@@ -284,16 +300,19 @@ component MegaMenu {
.wrn-mega[data-panel-width="lg"] .wrn-mega__panel { width: min(48rem, calc(100vw - 2rem)); }
.wrn-mega[data-panel-width="xl"] .wrn-mega__panel { width: min(72rem, calc(100vw - 2rem)); }
.wrn-mega[data-panel-width="2xl"] .wrn-mega__panel { width: min(88rem, calc(100vw - 2rem)); }
.wrn-mega[data-full-width="true"] .wrn-mega__panel { position: fixed; top: auto; right: 1rem; left: 1rem; width: auto; max-width: none; }
.wrn-mega[data-full-width="true"] .wrn-mega__panel { width: calc(100vw - 2rem); max-width: none; }
.wrn-mega[data-panel-align="center"] .wrn-mega__panel { left: 50%; transform: translateX(-50%); }
.wrn-mega[data-panel-align="end"] .wrn-mega__panel { right: 0; left: auto; }
.wrn-mega__layout { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 1.25rem; }
.wrn-mega__layout { display: grid; min-height: 0; grid-template-columns: auto minmax(0, 1fr) auto; gap: 1.25rem; overflow: hidden; }
.wrn-mega__columns {
display: grid;
min-height: 0;
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
gap: 1.25rem;
overflow-y: auto;
overscroll-behavior: contain;
}
.wrn-mega[data-column-count="2"] .wrn-mega__columns { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@@ -301,7 +320,7 @@ component MegaMenu {
.wrn-mega[data-column-count="4"] .wrn-mega__columns { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.wrn-mega[data-column-count="5"] .wrn-mega__columns { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.wrn-mega__rail { display: grid; min-width: 13rem; padding: 0.75rem; align-content: start; gap: 0.25rem; border-radius: var(--wrn-radius-lg); background: var(--wrn-color-surface-soft); }
.wrn-mega__rail { display: grid; min-width: 13rem; min-height: 0; padding: 0.75rem; align-content: start; gap: 0.25rem; overflow-y: auto; overscroll-behavior: contain; border-radius: var(--wrn-radius-lg); background: var(--wrn-color-surface-soft); }
.wrn-mega__rail-link { display: flex; padding: 0.65rem; align-items: flex-start; gap: 0.65rem; color: var(--wrn-color-text); border-radius: var(--wrn-radius-sm); text-decoration: none; }
.wrn-mega__rail-link:hover { background: var(--wrn-color-surface); }
.wrn-mega__rail-link > span:last-child { display: grid; gap: 0.2rem; }
@@ -421,13 +440,16 @@ component MegaMenu {
position: static;
width: auto;
max-width: 100%;
max-height: none;
overflow: visible;
box-shadow: none;
}
.wrn-mega__columns {
grid-template-columns: 1fr;
}
.wrn-mega__layout { grid-template-columns: 1fr; }
.wrn-mega__layout { grid-template-columns: 1fr; overflow: visible; }
.wrn-mega__rail, .wrn-mega__columns { overflow: visible; }
.wrn-mega__featured { width: auto; padding: 1rem 0 0; border-top: 1px solid var(--wrn-color-border); border-left: 0; }
.wrn-mega[data-variant="icon-grid"] .wrn-mega__list { grid-template-columns: 1fr; }
}
+1
View File
@@ -196,6 +196,7 @@ size: string = "default"
panelAlign="{megaData(item).panelAlign || 'start'}"
columnCount="{megaData(item).columnCount || 0}"
fullWidth="{megaData(item).fullWidth || false}"
openOnHover="{openOnHover}"
actionLabel="{megaData(item).actionLabel || ''}"
actionHref="{megaData(item).actionHref || ''}"
actionIcon="{megaData(item).actionIcon || 'icon-[lucide--arrow-right]'}"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ui",
"version": "0.8.29",
"version": "0.8.30",
"private": true,
"type": "module",
"main": "src/index.ts",
+9
View File
@@ -3124,6 +3124,15 @@ test("mega menu bridges the gap between its trigger and panel", async () => {
expect(source).toMatch(/\.wrn-mega__panel::before \{[^}]*bottom: 100%/s);
});
test("mega menu keeps hover activation and click activation from cancelling each other", () => {
const source = readFileSync(uiComponentPath("MegaMenu"), "utf8");
expect(source).toContain("openOnHover: boolean = true");
expect(source).toContain("@click='activatePanel(event)'");
expect(source).toContain("@mouseenter='hoverPanel(event)'");
expect(source).not.toContain("@focus='showPanel(event)'");
expect(source).toContain("max-height: calc(100dvh - 5.5rem)");
});
test("stepper shows only the active step content and offers back, next and skip", async () => {
const source = readFileSync(uiComponentPath("Stepper"), "utf8");
const html = await renderComponent(source, {