From e3787363d2ce364bfe4817c91582703479a0a04f Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Thu, 5 Mar 2026 11:34:07 +0100 Subject: [PATCH] feat: add focus-visible rings and refactor accordion chevron Add global :focus-visible outline style and migrate form components from :focus box-shadow to :focus-visible outline. Refactor accordion chevron from CSS ::after pseudo-element to explicit span element. --- src/ui/accordion.cljc | 12 +++++++++--- src/ui/accordion.css | 24 ++++++++++++++++-------- src/ui/css/gen.clj | 11 +++++++++++ src/ui/form.css | 30 ++++++++++++++---------------- 4 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/ui/accordion.cljc b/src/ui/accordion.cljc index 0987d97..ac85a28 100644 --- a/src/ui/accordion.cljc +++ b/src/ui/accordion.cljc @@ -26,7 +26,9 @@ base-attrs (cond-> (merge {:class classes} attrs) open (assoc :open true))] [:details base-attrs - [:summary {:class "accordion-trigger"} title] + [:summary {:class "accordion-trigger"} + [:span {:class "accordion-trigger-text"} title] + [:span {:class "accordion-chevron" :aria-hidden "true"}]] (into [:div {:class "accordion-content"}] children)]) :cljs @@ -35,7 +37,9 @@ base-attrs (cond-> (merge {:class classes} attrs) open (assoc :open true))] [:details base-attrs - [:summary {:class ["accordion-trigger"]} title] + [:summary {:class ["accordion-trigger"]} + [:span {:class ["accordion-trigger-text"]} title] + [:span {:class ["accordion-chevron"] :aria-hidden "true"}]] (into [:div {:class ["accordion-content"]}] children)]) :clj @@ -44,5 +48,7 @@ base-attrs (cond-> (merge {:class classes} attrs) open (assoc :open true))] [:details base-attrs - [:summary {:class "accordion-trigger"} title] + [:summary {:class "accordion-trigger"} + [:span {:class "accordion-trigger-text"} title] + [:span {:class "accordion-chevron" :aria-hidden "true"}]] (into [:div {:class "accordion-content"}] children)]))) diff --git a/src/ui/accordion.css b/src/ui/accordion.css index 269a2d9..a3aa1df 100644 --- a/src/ui/accordion.css +++ b/src/ui/accordion.css @@ -9,6 +9,10 @@ overflow: hidden; } +.accordion-trigger:focus-visible { + outline-offset: -2px; +} + .accordion + .accordion { margin-top: -1px; border-top-left-radius: 0; @@ -25,7 +29,6 @@ align-items: center; justify-content: space-between; gap: var(--size-2); - width: 100%; padding: var(--size-4); font-weight: 500; font-size: inherit; @@ -50,15 +53,20 @@ background: var(--bg-1); } -.accordion-trigger::after { - content: ""; +.accordion-trigger-text { + flex: 1; + text-align: left; +} + +.accordion-chevron { + display: block; width: 1em; height: 1em; flex-shrink: 0; - background-color: currentColor; - mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); - mask-size: contain; - mask-repeat: no-repeat; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2371717a' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; transition: transform 150ms ease; } @@ -66,7 +74,7 @@ border-bottom: var(--border-0); } -.accordion[open] > .accordion-trigger::after { +.accordion[open] > .accordion-trigger .accordion-chevron { transform: rotate(180deg); } diff --git a/src/ui/css/gen.clj b/src/ui/css/gen.clj index 9414f6f..b713f7a 100644 --- a/src/ui/css/gen.clj +++ b/src/ui/css/gen.clj @@ -63,6 +63,17 @@ margin: 0; background: var(--bg-0); color: var(--fg-0); +} + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + box-shadow: none; +} + +:focus:not(:focus-visible) { + outline: none; + box-shadow: none; }") (defn collect-component-css diff --git a/src/ui/form.css b/src/ui/form.css index 5bb0d3d..abae017 100644 --- a/src/ui/form.css +++ b/src/ui/form.css @@ -23,10 +23,10 @@ border-color: var(--danger); } -.form-field--error .form-input:focus, -.form-field--error .form-textarea:focus, -.form-field--error .form-select:focus { - box-shadow: 0 0 0 2px color-mix(in srgb, var(--danger) 20%, transparent); +.form-field--error .form-input:focus-visible, +.form-field--error .form-textarea:focus-visible, +.form-field--error .form-select:focus-visible { + outline-color: var(--danger); } /* ── Label ─────────────────────────────────────────────────────── */ @@ -60,12 +60,11 @@ color: var(--fg-2); } -.form-input:focus, -.form-textarea:focus, -.form-select:focus { - outline: none; +.form-input:focus-visible, +.form-textarea:focus-visible, +.form-select:focus-visible { border-color: var(--accent); - box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent); + outline-offset: -1px; } .form-input:disabled, @@ -81,9 +80,9 @@ border-color: var(--danger); } -.form-input--error:focus, -.form-textarea--error:focus { - box-shadow: 0 0 0 2px color-mix(in srgb, var(--danger) 20%, transparent); +.form-input--error:focus-visible, +.form-textarea--error:focus-visible { + outline-color: var(--danger); } /* ── Textarea ──────────────────────────────────────────────────── */ @@ -148,10 +147,9 @@ background-repeat: no-repeat; } -.form-checkbox:focus, -.form-radio:focus { - outline: none; - box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent); +.form-checkbox:focus-visible, +.form-radio:focus-visible { + outline-offset: 0; } .form-checkbox:disabled,