From dcf0b197eb387279b56d629aa00a87b4c3b69956 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 7 Oct 2026 15:45:10 +0200 Subject: [PATCH] :bug: Fix inner shadow covering the shape in the SVG renderer (#12141) * :bug: Fix inner shadow covering the shape in the SVG renderer The svg filter components moved to the `*` syntax (#9448), but filter-entry* still built their props as a raw JS object with kebab-case keys. Raw JS props skip the kebab to camel conversion, so the components read nil for filterId and filterIn, and no filter primitive got its result or in2. With the chain broken, an inner shadow was blended over the empty flood instead of the shape, hiding the fill (and a board's children) under the shadow color. Exports using these filters broke the same way. Pass the props in camelCase. Closes #11967 AI-assisted-by: claude-opus-5-5 * :recycle: Build filter entry props with mf/props Use mf/props instead of a raw JS object so the props keep the kebab-case keys used across the codebase. The macro converts them to camelCase at compile time, so there is no runtime cost, and the comment about the manual conversion is no longer needed. AI-assisted-by: claude-opus-5-5 --- frontend/src/app/main/ui/shapes/filters.cljs | 6 +-- frontend/test/frontend_tests/runner.cljs | 2 + .../frontend_tests/ui/shape_filters_test.cljs | 47 +++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 frontend/test/frontend_tests/ui/shape_filters_test.cljs diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs index a4468d649d..bf82f08be0 100644 --- a/frontend/src/app/main/ui/shapes/filters.cljs +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -116,9 +116,9 @@ :result filter-id}]) (mf/defc filter-entry* [{:keys [entry]}] - (let [props #js {:filter-id (:id entry) - :filter-in (:filter-in entry) - :params (:params entry)}] + (let [props (mf/props {:filter-id (:id entry) + :filter-in (:filter-in entry) + :params (:params entry)})] (case (:type entry) :drop-shadow [:> drop-shadow-filter* props] :inner-shadow [:> inner-shadow-filter* props] diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index a17d9b3149..daaae968a0 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -124,6 +124,7 @@ [frontend-tests.ui.routes-test] [frontend-tests.ui.settings-password-schema-test] [frontend-tests.ui.settings-shortcuts-test] + [frontend-tests.ui.shape-filters-test] [frontend-tests.ui.shortcuts-labels-test] [frontend-tests.ui.sidebar-scroll-test] [frontend-tests.ui.stroke-menu-test] @@ -273,6 +274,7 @@ 'frontend-tests.text-editor-paste-guard-test 'frontend-tests.ui.settings-password-schema-test 'frontend-tests.ui.settings-shortcuts-test + 'frontend-tests.ui.shape-filters-test 'frontend-tests.ui.shortcuts-labels-test 'frontend-tests.ui.sidebar-scroll-test 'frontend-tests.ui.stroke-menu-test diff --git a/frontend/test/frontend_tests/ui/shape_filters_test.cljs b/frontend/test/frontend_tests/ui/shape_filters_test.cljs new file mode 100644 index 0000000000..0cfc20b149 --- /dev/null +++ b/frontend/test/frontend_tests/ui/shape_filters_test.cljs @@ -0,0 +1,47 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS SUBSIDIARY SL + +(ns frontend-tests.ui.shape-filters-test + (:require + ["react-dom/server" :as rds] + [app.common.types.shape :as cts] + [app.common.uuid :as uuid] + [app.main.ui.shapes.filters :as filters] + [cljs.test :as t :include-macros true] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(defn- render-filter + [shadow-style] + (let [shape (-> (cts/setup-shape {:type :rect :x 0 :y 0 :width 100 :height 100}) + (assoc :fills [{:fill-color "#4488ff" :fill-opacity 1}] + :shadow [{:id (uuid/next) + :style shadow-style + :offset-x 4 :offset-y 4 :blur 4 :spread 0 + :hidden false + :color {:color "#000000" :opacity 0.5}}]))] + (rds/renderToStaticMarkup + (mf/element filters/filters* #js {:filterId "filter" :shape shape})))) + +(defn- tags + [markup tag] + (re-seq (re-pattern (str "<" tag "[^>]*>")) markup)) + +;; Each filter primitive must name its result and read the previous one, or +;; the chain breaks: an inner shadow then covers the shape fill with the +;; shadow color (#11967). + +(t/deftest inner-shadow-filter-chain-is-wired + (let [markup (render-filter :inner-shadow) + blends (tags markup "feBlend")] + (t/is (str/includes? (first (tags markup "feFlood")) "result=")) + (t/is (= 2 (count blends))) + (t/is (every? #(str/includes? % "in2=") blends)) + (t/is (every? #(str/includes? % "result=") blends)))) + +(t/deftest drop-shadow-filter-chain-is-wired + (let [blends (tags (render-filter :drop-shadow) "feBlend")] + (t/is (every? #(and (str/includes? % "in2=") (str/includes? % "result=")) blends))))