Pārlūkot izejas kodu

fix(stats): correct breadcrumb behavior

Adam 1 mēnesi atpakaļ
vecāks
revīzija
92cc8f553a

+ 12 - 5
packages/stats/app/src/routes/[lab]/[model].tsx

@@ -39,6 +39,7 @@ import {
   uniqueComparisonPairs,
   type ComparisonModelRef,
 } from "../compare-cards"
+import { createBreadcrumbMenuRoot } from "../breadcrumb-menu"
 import {
   applyThemePreference,
   Footer,
@@ -284,9 +285,10 @@ function ModelHero(props: {
   const labModels = () =>
     props.catalogData?.labs.find((lab) => lab.id === providerSlug(labId()))?.models ??
     (props.catalog ? [props.catalog] : [])
+  const menuRoot = createBreadcrumbMenuRoot()
   return (
     <section id="overview" data-section="model-hero">
-      <nav data-component="model-hero-breadcrumb" aria-label="Data breadcrumb">
+      <nav ref={menuRoot} data-component="model-hero-breadcrumb" aria-label="Data breadcrumb">
         <a data-slot="model-hero-crumb" href={language.route(import.meta.env.BASE_URL)}>
           Data
         </a>
@@ -294,14 +296,14 @@ function ModelHero(props: {
         <Show
           when={labs().length > 0}
           fallback={
-            <span data-slot="model-hero-crumb" data-current="true">
+            <span data-slot="model-hero-crumb" data-menu="true">
               <span>{props.labName}</span>
               <ChevronDownIcon />
             </span>
           }
         >
           <details data-component="model-hero-menu">
-            <summary data-slot="model-hero-crumb" data-current="true">
+            <summary data-slot="model-hero-crumb" data-menu="true">
               <span>{props.labName}</span>
               <ChevronDownIcon />
             </summary>
@@ -324,14 +326,19 @@ function ModelHero(props: {
         <Show
           when={labModels().length > 0}
           fallback={
-            <span data-slot="model-hero-crumb" data-current="true" aria-current="page">
+            <span data-slot="model-hero-crumb" data-menu="true" data-current="true" aria-current="page">
               <span>{modelName()}</span>
               <ChevronDownIcon />
             </span>
           }
         >
           <details data-component="model-hero-menu">
-            <summary data-slot="model-hero-crumb" data-current="true" aria-current="page">
+            <summary
+              data-slot="model-hero-crumb"
+              data-menu="true"
+              data-current="true"
+              aria-current="page"
+            >
               <span>{modelName()}</span>
               <ChevronDownIcon />
             </summary>

+ 3 - 1
packages/stats/app/src/routes/[lab]/index.tsx

@@ -29,6 +29,7 @@ import { SectionHeading } from "../section-heading"
 import { runStatsEffect } from "../../stats-runtime"
 import { setStatsPageCacheHeaders } from "../stats-cache"
 import { ComparisonCardsSection, modelRefFromCatalog, uniqueComparisonPairs } from "../compare-cards"
+import { createBreadcrumbMenuRoot } from "../breadcrumb-menu"
 import {
   applyThemePreference,
   Footer,
@@ -205,8 +206,9 @@ function LabHero(props: { lab: ModelCatalogLab; labs: ModelCatalogLab[] }) {
 function LabHeroBreadcrumb(props: { label: string; labs?: ModelCatalogLab[] }) {
   const language = useLanguage()
   const labs = () => props.labs ?? []
+  const menuRoot = createBreadcrumbMenuRoot()
   return (
-    <nav data-component="lab-hero-breadcrumb" aria-label="Data breadcrumb">
+    <nav ref={menuRoot} data-component="lab-hero-breadcrumb" aria-label="Data breadcrumb">
       <a data-slot="lab-hero-crumb" href={language.route(import.meta.env.BASE_URL)}>
         Data
       </a>

+ 52 - 0
packages/stats/app/src/routes/breadcrumb-menu.ts

@@ -0,0 +1,52 @@
+import { onCleanup, onMount } from "solid-js"
+
+export function createBreadcrumbMenuRoot() {
+  let root: HTMLElement | undefined
+
+  onMount(() => {
+    const closeMenus = (except?: HTMLDetailsElement) => {
+      root?.querySelectorAll<HTMLDetailsElement>("details[open]").forEach((menu) => {
+        if (menu !== except) menu.open = false
+      })
+    }
+    const onPointerDown = (event: PointerEvent) => {
+      const path = event.composedPath()
+      root?.querySelectorAll<HTMLDetailsElement>("details[open]").forEach((menu) => {
+        if (!path.includes(menu)) menu.open = false
+      })
+    }
+    const onKeyDown = (event: KeyboardEvent) => {
+      if (event.key !== "Escape") return
+      const menu = root?.querySelector<HTMLDetailsElement>("details[open]")
+      if (!menu) return
+      event.preventDefault()
+      menu.open = false
+      menu.querySelector<HTMLElement>("summary")?.focus()
+    }
+    const onToggle = (event: Event) => {
+      if (!(event.target instanceof HTMLDetailsElement) || !event.target.open) return
+      closeMenus(event.target)
+    }
+    const onFocusOut = () => {
+      queueMicrotask(() => {
+        if (root?.contains(document.activeElement)) return
+        closeMenus()
+      })
+    }
+
+    document.addEventListener("pointerdown", onPointerDown)
+    document.addEventListener("keydown", onKeyDown)
+    root?.addEventListener("toggle", onToggle, true)
+    root?.addEventListener("focusout", onFocusOut)
+    onCleanup(() => {
+      document.removeEventListener("pointerdown", onPointerDown)
+      document.removeEventListener("keydown", onKeyDown)
+      root?.removeEventListener("toggle", onToggle, true)
+      root?.removeEventListener("focusout", onFocusOut)
+    })
+  })
+
+  return (element: HTMLElement) => {
+    root = element
+  }
+}

+ 18 - 2
packages/stats/app/src/routes/index.css

@@ -3113,10 +3113,18 @@
   white-space: nowrap;
 }
 
-[data-page="stats"] [data-slot="model-hero-crumb"][data-current="true"] {
+[data-page="stats"] a[data-slot="model-hero-crumb"] {
+  color: var(--stats-hero-muted);
+}
+
+[data-page="stats"] [data-slot="model-hero-crumb"][data-menu="true"] {
   padding-right: 4px;
 }
 
+[data-page="stats"] [data-slot="model-hero-crumb"][data-current="true"] {
+  color: var(--stats-text);
+}
+
 [data-page="stats"] [data-component="model-hero-menu"] {
   position: relative;
   display: inline-block;
@@ -3151,7 +3159,7 @@
   flex: 0 0 auto;
   width: 16px;
   height: 16px;
-  color: var(--stats-faint);
+  color: currentColor;
 }
 
 [data-page="stats"] [data-component="model-hero-menu"][open] [data-slot="model-hero-crumb"] svg {
@@ -5533,6 +5541,14 @@
   white-space: nowrap;
 }
 
+[data-page="stats"] a[data-slot="compare-home-crumb"] {
+  color: var(--stats-hero-muted);
+}
+
+[data-page="stats"] [data-slot="compare-home-crumb"][data-current="true"] {
+  color: var(--stats-text);
+}
+
 [data-page="stats"] a[data-slot="compare-home-crumb"]:hover,
 [data-page="stats"] a[data-slot="compare-home-crumb"]:focus-visible {
   color: var(--stats-text);