diff --git a/src/main/frontend/components/icon.cljs b/src/main/frontend/components/icon.cljs index a7197da4f6..8437f24f63 100644 --- a/src/main/frontend/components/icon.cljs +++ b/src/main/frontend/components/icon.cljs @@ -3833,7 +3833,7 @@ [:span.absolute.hidden {:ref *el-ref}])) (rum/defc color-picker - [*color on-select! & {:keys [on-hover! on-hover-end!]}] + [*color on-select! & {:keys [on-hover! on-hover-end! button-attrs]}] (let [;; Defensive: never let the CSS sentinel "inherit" leak into React state. initial-color (let [v @*color] (when (and v (not= v "inherit")) v)) [color, set-color!] (rum/use-state initial-color) @@ -3900,8 +3900,9 @@ []) [:button.color-picker-trigger - {:ref *el - :on-click (fn [^js e] (shui/popup-show! (.-target e) content-fn {:content-props {:side "bottom" :side-offset 6}}))} + (merge button-attrs + {:ref *el + :on-click (fn [^js e] (shui/popup-show! (.-target e) content-fn {:content-props {:side "bottom" :side-offset 6}}))}) (if color [:span.color-picker-fill {:style {:background-color color}}] [:span.color-picker-empty @@ -4308,14 +4309,22 @@ :*input-ref *input-ref :flat-items flat-items :sections sections - :*virtuoso-ref *virtuoso-ref}) + :*virtuoso-ref *virtuoso-ref + :topbar-selector ".cp__emoji-icon-picker .tabs-section [data-topbar-stop]"}) (ui/tab-items {:tabs [[:all "All"] [:emoji "Emojis"] [:icon "Icons"] [:custom "Custom"]] :active @*tab - :on-change (fn [id _e] + :on-change (fn [id ^js e] (reset! *tab id) - (reset! *focus-region :search) - (reset! *highlighted-index nil))}) + (reset! *highlighted-index nil) + ;; Only return focus to search for genuine mouse + ;; clicks. Programmatic .click() from keyboard + ;; arrow-rove (handle-topbar-keys auto-activate) + ;; has e.detail = 0; real clicks are >= 1. Keeps + ;; arrow nav inside the topbar region. + (when (and e (pos? (.-detail e))) + (reset! *focus-region :search))) + :button-attrs {:data-topbar-stop "tab"}}) [:div.tab-actions ;; color picker (always visible) (color-picker *color (fn [c] @@ -4338,10 +4347,12 @@ :color c}))) :on-hover-end! (when preview-target-db-id (fn [] - (state/set-state! :ui/icon-hover-preview nil)))) + (state/set-state! :ui/icon-hover-preview nil))) + :button-attrs {:data-topbar-stop "color"}) ;; delete button (when del-btn? (shui/button {:variant :outline :size :sm :data-action "del" + :data-topbar-stop "trash" :on-click #(on-chosen nil)} (shui/tabler-icon "trash" {:size 17})))]] @@ -4368,11 +4379,11 @@ (shui/popup-hide!) (reset-q!))) - ;; Up Arrow / Shift+Tab: move to tab bar + ;; Up Arrow / Shift+Tab: move to topbar at the active tab (or (= code 38) (and (= code 9) (.-shiftKey e))) (do (util/stop e) - (reset! *focus-region :tabs) + (reset! *focus-region :topbar) (reset! *highlighted-index nil) (when-let [^js cnt (some-> (rum/deref *input-ref) (.closest ".cp__emoji-icon-picker"))] (when-let [active-tab (.querySelector cnt "[data-active='true'].tab-item")] diff --git a/src/main/frontend/components/icon.css b/src/main/frontend/components/icon.css index dce33c85ba..e14dbeab6a 100644 --- a/src/main/frontend/components/icon.css +++ b/src/main/frontend/components/icon.css @@ -124,7 +124,15 @@ > button { @apply flex items-center justify-center rounded-lg active:opacity-70; - transition: background-color 150ms, outline-color 150ms; + /* Animate width + offset so the ring grows/shrinks smoothly when + the highlight state changes. outline-color is intentionally NOT + transitioned — animating it from `currentColor` (the icon's + tint, e.g. red) would briefly tint the ring red before the + accent color landed. With color set instantly by the highlight + rules, the ring just grows in the right color from t=0. */ + transition: background-color 150ms, + outline-width 150ms, + outline-offset 150ms; &:hover { background-color: var(--rx-gray-05); @@ -162,6 +170,13 @@ &[data-action=del] { @apply !w-6 !h-6 overflow-hidden rounded-md opacity-60 hover:text-red-rx-09 hover:opacity-90; + + /* Force opacity 1 on keyboard focus so the focus ring paints + crisply; the base opacity-60 otherwise dims the outline into + a muted glow. */ + &:focus-visible { + opacity: 1; + } } } } @@ -244,8 +259,11 @@ background: transparent !important; } - /* Keyboard focus state */ + /* Keyboard focus state. Force opacity 1 so the focus ring on an + inactive tab paints fully (the base rule sets opacity-60, which + would otherwise dim the outline). */ &:focus-visible { + opacity: 1; outline: 2px solid var(--lx-accent-09); outline-offset: -2px; border-radius: 4px; @@ -708,9 +726,47 @@ .ui__button[data-action=del] { @apply !w-6 !h-6 overflow-hidden rounded-md opacity-60; @apply hover:text-red-rx-09 hover:opacity-90; + + /* Keyboard focus state. Force opacity 1 so the focus ring paints at + full alpha; otherwise the button's base opacity-60 dims the outline + and reads as a "muted glow" instead of a clear focus indicator. */ + &:focus-visible { + opacity: 1; + } } } +/* Unified focus ring across the asset-picker topbar. Without this, three + different mechanisms render three different blues: back button uses the + browser-default outline (system blue), segments use our custom outline + (--lx-accent-09), and the trash button uses shui's box-shadow ring + (Tailwind --ring token). This rule overrides all three to match. */ +.asset-picker-topbar :is( + .back-button, + .segmented-control .segment, + .ui__button[data-action=del] +):focus-visible { + outline: 2px solid var(--lx-accent-09); + outline-offset: 2px; + box-shadow: none; + border-radius: 4px; +} + +/* Same unified treatment for the icon-picker topbar. The color-picker + trigger and the trash button each had their own focus stylings (browser + default + shui's --ring respectively); pulling them onto --lx-accent-09 + so all topbar stops match. The .tab-item rule (above) already uses the + same token; keep it as-is. */ +.cp__emoji-icon-picker .tabs-section :is( + .color-picker-trigger, + .ui__button[data-action=del] +):focus-visible { + outline: 2px solid var(--lx-accent-09); + outline-offset: 2px; + box-shadow: none; + border-radius: 4px; +} + /* Search input matching icon picker */ .asset-picker-search { @apply py-1;