Add reagent support to transform prop keys inside vectors (#21937)

- Remove now unnecessary wrapper for reanimated/view
- Use kebab-case keywords for the hole-view component
- Use rn/StyleSheet.absoluteFill along with ClojureScript styles
- Fix keys inside view's `:transform` property
- Remove some uses of `merge` to pass styles to components
- Add tests for `convert-prop-value`
This commit is contained in:
Ulises Manuel 2025-02-07 16:50:22 -06:00 committed by GitHub
parent 857cb47ceb
commit 45b09603d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 181 additions and 142 deletions

View File

@ -10,11 +10,10 @@
([{:keys [style]} child]
(let [theme (quo.theme/use-theme)]
[rn/view
{:style (assoc style
:pointer-events :box-none
:background-color
(or (:background-color style)
(colors/theme-colors colors/white colors/neutral-80 theme)))}
{:style [style
{:pointer-events :box-none
:background-color (or (:background-color style)
(colors/theme-colors colors/white colors/neutral-80 theme))}]}
child])))
(def view (if platform/ios? blur/view view-android))

View File

@ -56,62 +56,63 @@
24 8
12))})
(defn style-container
(defn container-styles
[{:keys [size disabled? border-radius background-color border-color icon-only? icon-top
icon-left icon-right]}]
(merge {:height size
:align-items :center
:justify-content :center
:flex-direction (if icon-top :column :row)
:padding-horizontal (when-not (or icon-only? icon-left icon-right)
(case size
56 (if border-color 10 11)
40 16
32 12
24 7
16))
:padding-left (when-not (or icon-only? icon-left)
(case size
56 nil
40 16
32 12
24 8
16))
:padding-right (when-not (or icon-only? icon-right)
(case size
56 nil
40 16
32 12
24 8
16))
:padding-top (when-not (or icon-only? icon-left icon-right)
(case size
56 0
40 (if border-color 8 9)
32 (if border-color 4 5)
24 0
(if border-color 8 9)))
:padding-bottom (when-not (or icon-only? icon-left icon-right)
(case size
56 0
40 9
32 5
24 0
9))
:overflow :hidden
:background-color (if disabled? (colors/alpha background-color 0.3) background-color)
:border-radius (if border-radius
border-radius
(case size
56 12
40 12
32 10
24 8
12))
:border-color border-color
:border-width (when border-color 1)}
(when icon-only?
{:width size})
(when border-color
{:border-color border-color
:border-width 1})))
icon-left icon-right inner-style]}]
[{:height size
:align-items :center
:justify-content :center
:flex-direction (if icon-top :column :row)
:padding-horizontal (when-not (or icon-only? icon-left icon-right)
(case size
56 (if border-color 10 11)
40 16
32 12
24 7
16))
:padding-left (when-not (or icon-only? icon-left)
(case size
56 nil
40 16
32 12
24 8
16))
:padding-right (when-not (or icon-only? icon-right)
(case size
56 nil
40 16
32 12
24 8
16))
:padding-top (when-not (or icon-only? icon-left icon-right)
(case size
56 0
40 (if border-color 8 9)
32 (if border-color 4 5)
24 0
(if border-color 8 9)))
:padding-bottom (when-not (or icon-only? icon-left icon-right)
(case size
56 0
40 9
32 5
24 0
9))
:overflow :hidden
:background-color (if disabled? (colors/alpha background-color 0.3) background-color)
:border-radius (if border-radius
border-radius
(case size
56 12
40 12
32 10
24 8
12))
:border-color border-color
:border-width (when border-color 1)}
(when icon-only?
{:width size})
(when border-color
{:border-color border-color
:border-width 1})
inner-style])

View File

@ -62,12 +62,9 @@
:on-press on-press
:allow-multiple-presses? allow-multiple-presses?
:on-long-press on-long-press
:style (merge
(style/shape-style-container size border-radius)
container-style)}
:style [(style/shape-style-container size border-radius) container-style]}
[rn/view
{:style (merge
(style/style-container {:size size
{:style (style/container-styles {:size size
:disabled? disabled?
:border-radius border-radius
:background-color background-color
@ -75,8 +72,8 @@
:icon-only? icon-only?
:icon-top icon-top
:icon-left icon-left
:icon-right icon-right})
inner-style)}
:icon-right icon-right
:inner-style inner-style})}
(when overlay-customization-color
[customization-colors/overlay
{:customization-color overlay-customization-color

View File

@ -201,7 +201,7 @@
button-disabled? account-avatar-emoji account-avatar-type customization-color icon-avatar
context icon]}]
(let [theme (quo.theme/use-theme)]
[rn/view {:style (merge style/container container-style)}
[rn/view {:style [style/container container-style]}
(when (left-image-supported-types type)
[rn/view {:style style/left-container}
[left-image

View File

@ -18,5 +18,5 @@
:colors [color-top color-bottom]
:start {:x 0 :y 0}
:end {:x 0 :y 1}
:style (merge (style/root-container opacity height)
container-style)}])))
:style [(style/root-container opacity height)
container-style]}])))

View File

@ -131,7 +131,7 @@
:char-limit char-limit
:theme theme}])
[rn/view
{:style (merge (style/input-container colors-by-status small? disabled?) input-container-style)}
{:style [(style/input-container colors-by-status small? disabled?) input-container-style]}
(when-let [{:keys [icon-name]} left-icon]
[left-accessory
{:variant-colors variant-colors

View File

@ -36,7 +36,7 @@
(colors/theme-colors colors/neutral-100 colors/white theme))
:align-self :center
:height 280.48
:transform [{:rotate "-30deg"} {:translateY -30}]
:transform [{:rotate "-30deg"} {:translate-y -30}]
:opacity (when-not locked? 0.02)
:z-index 1})

View File

@ -48,7 +48,7 @@
:margin-left -4}
(if (= type :top)
{:top 0}
{:transform [{:rotateZ "180deg"}]
{:transform [{:rotate-z "180deg"}]
:bottom 0})
style)}]))

View File

@ -10,7 +10,7 @@
[{:keys [label data container-style blur?]}]
(let [theme (quo.theme/use-theme)
last-item (rn/use-memo #(last data) [data])]
[rn/view {:style (merge (style/container label) container-style)}
[rn/view {:style [(style/container label) container-style]}
(when label
[text/text
{:weight :medium
@ -29,4 +29,3 @@
[data-item/view data-item-props]
(when-not (= item last-item)
[rn/view {:style (style/settings-separator blur? theme)}])])]]))

View File

@ -24,7 +24,7 @@
separator (rn/use-memo (fn [] [rn/view
{:style (style/reorder-separator blur? theme)}])
[blur? theme])]
[rn/view {:style (merge (style/container label) container-style)}
[rn/view {:style [(style/container label) container-style]}
[text/text
{:weight :medium
:size :paragraph-2

View File

@ -11,7 +11,7 @@
(let [theme (quo.theme/use-theme)
settings-items (remove nil? data)
last-index (dec (count settings-items))]
[rn/view {:style (merge (style/container label) container-style)}
[rn/view {:style [(style/container label) container-style]}
(when label
[text/text
{:weight :medium

View File

@ -10,12 +10,7 @@
{:flex 1}))
(def avatar-overlay
{:position :absolute
:top 0
:right 0
:left 0
:bottom 0
:justify-content :center
{:justify-content :center
:align-items :center})
(defn qr-image

View File

@ -11,7 +11,7 @@
(defn- avatar-image
[{avatar-type :avatar
:as props}]
[rn/view {:style style/avatar-overlay}
[rn/view {:style [rn/stylesheet-absolute-fill style/avatar-overlay]}
[rn/view
{:style (case avatar-type
:wallet-account style/avatar-container-rounded

View File

@ -97,7 +97,7 @@
theme (quo.theme/use-theme)}
:as props}]
(let [[image-error? set-image-error] (rn/use-state false)]
[rn/view {:style (merge {:align-items :flex-start} container-style)}
[rn/view {:style [{:align-items :flex-start} container-style]}
[rn/view
{:style (style/container {:theme theme
:type type

View File

@ -14,9 +14,9 @@
(defn network-bridge-add
[{:keys [network state theme container-style on-press]}]
[rn/pressable
{:style (merge (style/container network state theme)
(style/add-container theme)
container-style)
{:style [(style/container network state theme)
(style/add-container theme)
container-style]
:on-press on-press}
[icon/icon :i/edit
{:size 12
@ -36,7 +36,7 @@
(if (= status :edit)
[network-bridge-add (assoc args :theme theme)]
[rn/pressable
{:style (merge (style/container network status theme) container-style)
{:style [(style/container network status theme) container-style]
:accessible true
:accessibility-label :container
:on-press on-press

View File

@ -211,3 +211,5 @@
(def linking (.-Linking react-native))
(defn open-url [link] (.openURL ^js linking link))
(def stylesheet-absolute-fill ^js (.. react-native -StyleSheet -absoluteFill))

View File

@ -21,9 +21,7 @@
["react-native-redash" :refer (withPause)]
[react-native.flat-list :as rn-flat-list]
[react-native.platform :as platform]
[react-native.utils :as rn.utils]
[reagent.core :as reagent]
[utils.transforms :as transforms]
[utils.worklets.core :as worklets.core]))
(def ^:const default-duration 300)
@ -36,13 +34,7 @@
;; Animated Components
(def create-animated-component (comp reagent/adapt-react-class (.-createAnimatedComponent reanimated)))
(def ^:private view* (reagent/adapt-react-class (.-View reanimated)))
(defn view
[& argv]
(let [[reagent-props children] (rn.utils/get-props-and-children argv)
updated-props (update reagent-props :style transforms/styles-with-vectors)]
(into [view* updated-props] children)))
(def view (reagent/adapt-react-class (.-View reanimated)))
(def scroll-view (reagent/adapt-react-class (.-ScrollView reanimated)))
(def image (reagent/adapt-react-class (.-Image reanimated)))
@ -186,4 +178,3 @@
(with-timing value
(clj->js {:duration duration
:easing (default-easing)})))))

View File

@ -0,0 +1,54 @@
(ns react-native.utils-test
(:require
[cljs.test :refer [deftest is testing]]
[utils.reagent :as sut]))
(sut/set-convert-props-in-vectors!)
(deftest convert-prop-value-test
;; `test-fn` transforms the result to be easily compared during testing
(let [test-fn (comp js->clj sut/convert-prop-value)]
(testing "camelCase keys are kept as is"
(let [props {:foo nil
:bar nil}]
(is (= (update-keys props name)
(test-fn props)))))
(testing "kebab-case keys are transformed"
(let [props {:foo nil
:bar nil}]
(is (= {"foo" nil
"bar" nil}
(test-fn props)))))
(testing "kebab-case keys are transformed when passed inside a vector"
(let [props-in-vector [{:foo nil
:bar nil}]]
(is (= [{"foo" nil
"bar" nil}]
(test-fn props-in-vector)))))
(testing "kebab-case keys are transformed recursively when the structure has vectors"
(let [props-with-vectors {:foo [{:foo-bar nil
:foo-baz nil}]
:bar [{:bar-baz nil}
{:bar-qux nil}]}]
(is (= {"foo" [{"fooBar" nil
"fooBaz" nil}]
"bar" [{"barBaz" nil} {"barQux" nil}]}
(test-fn props-with-vectors))))
(testing "Complex example"
(let [complex-props {:foo [{:foo-bar nil :foo-baz nil}]
:bar [{:bar-baz nil} {:bar-qux nil}]
:qux {:foo-qux :bar-qux}
:foo-bar-qux {:foo [{:bar :qux}
{:bar-qux nil}]
:foo-bar :qux
:foo-bar-qux [:foo :bar :qux {:foo-bar :foo-bar}]}}]
(is (= {"foo" [{"fooBar" nil "fooBaz" nil}]
"bar" [{"barBaz" nil} {"barQux" nil}]
"qux" {"fooQux" "bar-qux"}
"fooBarQux" {"foo" [{"bar" "qux"} {"barQux" nil}]
"fooBar" "qux"
"fooBarQux" ["foo" "bar" "qux" {"fooBar" "foo-bar"}]}}
(test-fn complex-props))))))))

View File

@ -15,16 +15,7 @@
(def flex-spacer {:flex 1})
(def absolute-fill
{:position :absolute
:top 0
:bottom 0
:left 0
:right 0})
(def hole
(merge absolute-fill
{:z-index 2 :opacity 0.95}))
(def hole {:z-index 2 :opacity 0.95})
(defn root-container
[padding-top]

View File

@ -174,10 +174,10 @@
:on-success-scan set-qr-code-succeeded
:on-failed-scan set-rescan-timeout}))}]]
[hole-view/hole-view
{:style style/hole
:holes [(assoc qr-view-finder :borderRadius 16)]}
{:style [rn/stylesheet-absolute-fill style/hole]
:holes [(assoc qr-view-finder :border-radius 16)]}
[quo/blur
{:style style/absolute-fill
{:style rn/stylesheet-absolute-fill
:blur-amount 10
:blur-type :transparent
:overlay-color colors/neutral-80-opa-80-blur

View File

@ -28,6 +28,7 @@
[status-im.setup.interceptors :as interceptors]
status-im.subs.root
[utils.i18n :as i18n]
[utils.reagent]
[status-im.setup.status-backend-client :as status-backend-client]))
;;;; re-frame RN setup
@ -35,6 +36,7 @@
(set! batching/fake-raf #(js/setTimeout % 0))
(def functional-compiler (reagent.core/create-compiler {:function-components true}))
(reagent.core/set-default-compiler! functional-compiler)
(utils.reagent/set-convert-props-in-vectors!)
(def adjust-resize 16)

27
src/utils/reagent.cljs Normal file
View File

@ -0,0 +1,27 @@
(ns utils.reagent
(:require [reagent.impl.template :as template]
[reagent.impl.util :as reagent.util]
[utils.transforms :as transforms]))
(defn convert-prop-value
"Based on `reagent.impl.template/kv-conv`.
Takes the prop map of a reagent component and returns a React-valid property JS Object
by transforming kebab-case keys -> camelCase, maps -> JS Objects and vectors -> arrays.
This version adds support to recursively transform properties inside vectors, to have a
more consistent developer experience in React Native."
[x]
(cond
(reagent.util/js-val? x) x
(reagent.util/named? x) (name x)
(map? x) (reduce-kv template/kv-conv #js {} x)
(vector? x) (transforms/map-array convert-prop-value x)
(coll? x) (clj->js x)
(ifn? x) (fn [& args]
(apply x args))
:else (clj->js x)))
(defn set-convert-props-in-vectors!
"We override the default reagent implementation with the one that supports vectors."
[]
(set! template/convert-prop-value convert-prop-value))

View File

@ -3,9 +3,7 @@
(:require
[camel-snake-kebab.core :as csk]
[cljs-bean.core :as clj-bean]
[oops.core :as oops]
[reagent.impl.template :as reagent.template]
[reagent.impl.util :as reagent.util]))
[oops.core :as oops]))
(defn js->clj [data] (cljs.core/js->clj data :keywordize-keys true))
@ -67,27 +65,10 @@
(when-not (= json "undefined")
(try (.parse js/JSON json) (catch js/Error _ (when (string? json) json)))))
(declare styles-with-vectors)
(defn ^:private convert-keys-and-values
"Takes a JS Object a key and a value.
Transforms the key from a Clojure style prop to a JS style prop, using the reagent cache.
Performs a mutual recursion transformation on the value using `styles-with-vectors`.
Based on `reagent.impl.template/kv-conv`."
[obj k v]
(doto obj
(oops/gobj-set (reagent.template/cached-prop-name k) (styles-with-vectors v))))
(defn styles-with-vectors
"Takes a Clojure style map or a Clojure vector of style maps and returns a JS Object
valid to use as React Native styles.
The transformation is done by performing mutual recursive calls with `convert-keys-and-values`.
Based on `reagent.impl.template/convert-prop-value`."
[x]
(cond (reagent.util/js-val? x) x
(reagent.util/named? x) (name x)
(map? x) (reduce-kv convert-keys-and-values #js {} x)
(vector? x) (to-array (mapv styles-with-vectors x))
:else (clj->js x)))
(defn map-array
"Performs an efficient `map` operation on `coll` but returns a JS array"
[f coll]
(let [js-array ^js (array)]
(doseq [e coll]
(.push js-array (f e)))
js-array))