[#16858] Input text multiline height (#17536)

* Fix preview screen receiving wrong parameter
* Fix input multiline style on iOS and counter label always showed
* Fix icon metadata and wrong values passed to svg icons
* Use quo/input in add contact sheet
This commit is contained in:
Ulises Manuel 2023-10-17 16:45:03 -06:00 committed by GitHub
parent 495aee584e
commit b6e2e8cba4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 212 additions and 239 deletions

View File

@ -13,37 +13,37 @@
(and (string? color) (and (string? color)
(not (string/blank? color))))) (not (string/blank? color)))))
(defn- image-icon-style
[{:keys [color no-color size container-style theme]}]
(cond-> {:width size
:height size}
(not no-color)
(assoc :tint-color
(if (and (string? color) (not (string/blank? color)))
color
(colors/theme-colors colors/neutral-100 colors/white theme)))
:always
(merge container-style)))
(defn memo-icon-fn (defn memo-icon-fn
[{:keys [color color-2 no-color [{:keys [color color-2 container-style size accessibility-label]
container-style size accessibility-label theme] :or {accessibility-label :icon}
:or {accessibility-label :icon}} :as props}
icon-name] icon-name]
(let [size (or size 20)] (let [size (or size 20)]
^{:key icon-name} (with-meta
(if-let [svg-icon (icons.svg/get-icon icon-name size)] (if-let [svg-icon (icons.svg/get-icon icon-name size)]
[svg-icon [svg-icon
(cond-> {:size size (cond-> {:size size
:accessibility-label accessibility-label :accessibility-label accessibility-label
:style container-style} :style container-style}
(and color (valid-color? color)) (assoc :color color)
(and color (valid-color? color)) (and color-2 (valid-color? color-2)) (assoc :color-2 color-2))]
(assoc :color color) [rn/image
{:style (image-icon-style (assoc props :size size))
(and color-2 (valid-color? color-2)) :accessibility-label accessibility-label
(assoc :color-2 color-2))] :source (icons/icon-source (str (name icon-name) size))}])
[rn/image {:key icon-name})))
{:style
(merge {:width size
:height size}
(when (not no-color)
{:tint-color (if (and (string? color) (not (string/blank? color)))
color
(colors/theme-colors colors/neutral-100 colors/white theme))})
container-style)
:accessibility-label accessibility-label
:source (icons/icon-source (str (name icon-name) size))}])))
(def ^:private themed-icon (memoize (quo.theme/with-theme memo-icon-fn))) (def ^:private themed-icon (memoize (quo.theme/with-theme memo-icon-fn)))

View File

@ -70,6 +70,7 @@
:border-radius (if small? 10 12) :border-radius (if small? 10 12)
:opacity (if disabled? 0.3 1)}) :opacity (if disabled? 0.3 1)})
(defn left-icon-container (defn left-icon-container
[small?] [small?]
{:margin-left (if small? 0 4) {:margin-left (if small? 0 4)
@ -84,15 +85,21 @@
(defn input (defn input
[colors-by-status small? multiple-lines? weight] [colors-by-status small? multiple-lines? weight]
(let [base-props (assoc (text/text-style {:size :paragraph-1 :weight (or weight :regular)}) (let [padding (if small? 4 8)
:flex 1 base-props (assoc (text/text-style {:size :paragraph-1 :weight (or weight :regular)})
:padding-right 0 :flex 1
:padding-left (if small? 4 8) :padding-right 0
:padding-vertical (if small? 4 8) :padding-left padding
:color (:text colors-by-status))] :padding-top padding
:padding-bottom padding
:color (:text colors-by-status))]
(if multiple-lines? (if multiple-lines?
(assoc base-props :text-align-vertical :top) (assoc base-props
(assoc base-props :height (if small? 30 38) :line-height nil)))) :text-align-vertical :top
:line-height 22)
(assoc base-props
:height (if small? 30 38)
:line-height nil))))
(defn right-icon-touchable-area (defn right-icon-touchable-area
[small?] [small?]
@ -107,8 +114,9 @@
(defn clear-icon (defn clear-icon
[variant-colors] [variant-colors]
{:size 20 {:size 20
:color (:clear-icon variant-colors)}) :color (:clear-icon variant-colors)
:color-2 colors/white})
(def texts-container (def texts-container
{:flex-direction :row {:flex-direction :row

View File

@ -1,31 +1,32 @@
(ns quo.components.inputs.input.view (ns quo.components.inputs.input.view
(:require (:require [oops.core :as oops]
[oops.core :as oops] [quo.components.icon :as icon]
[quo.components.icon :as icon] [quo.components.inputs.input.style :as style]
[quo.components.inputs.input.style :as style] [quo.components.markdown.text :as text]
[quo.components.markdown.text :as text] [quo.theme :as quo.theme]
[quo.theme :as quo.theme] [react-native.core :as rn]
[react-native.core :as rn] [react-native.platform :as platform]
[reagent.core :as reagent])) [reagent.core :as reagent]))
(defn- label-&-counter (defn- label-&-counter
[{:keys [label current-chars char-limit variant-colors]}] [{:keys [label current-chars char-limit variant-colors]}]
(let [count-text (when char-limit (str current-chars "/" char-limit))] [rn/view
[rn/view {:accessibility-label :input-labels
{:accessibility-label :input-labels :style style/texts-container}
:style style/texts-container} [rn/view {:style style/label-container}
[rn/view {:style style/label-container} [text/text
[text/text {:style (style/label-color variant-colors)
{:style (style/label-color variant-colors) :weight :medium
:weight :medium :size :paragraph-2}
:size :paragraph-2} label]]
label]] (when-let [count-text (some->> char-limit
(str current-chars "/"))]
[rn/view {:style style/counter-container} [rn/view {:style style/counter-container}
[text/text [text/text
{:style (style/counter-color current-chars char-limit variant-colors) {:style (style/counter-color current-chars char-limit variant-colors)
:weight :regular :weight :regular
:size :paragraph-2} :size :paragraph-2}
count-text]]])) count-text]])])
(defn- left-accessory (defn- left-accessory
[{:keys [variant-colors small? icon-name]}] [{:keys [variant-colors small? icon-name]}]
@ -56,16 +57,19 @@
(def ^:private custom-props (def ^:private custom-props
"Custom properties that must be removed from properties map passed to InputText." "Custom properties that must be removed from properties map passed to InputText."
[:type :blur? :theme :error? :right-icon :left-icon :disabled? :small? :button [:type :blur? :theme :error? :right-icon :left-icon :disabled? :small? :button
:label :char-limit :on-char-limit-reach :icon-name :multiline? :on-focus :on-blur]) :label :char-limit :on-char-limit-reach :icon-name :multiline? :on-focus :on-blur
:container-style])
(defn- base-input (defn- base-input
[{:keys [on-change-text on-char-limit-reach container-style weight]}] [{:keys [on-change-text on-char-limit-reach weight]}]
(let [status (reagent/atom :default) (let [status (reagent/atom :default)
internal-on-focus #(reset! status :focus) internal-on-focus #(reset! status :focus)
internal-on-blur #(reset! status :default) internal-on-blur #(reset! status :default)
multiple-lines? (reagent/atom false) multiple-lines? (reagent/atom false)
set-multiple-lines! #(let [height (oops/oget % "nativeEvent.contentSize.height")] set-multiple-lines! #(let [height (oops/oget % "nativeEvent.contentSize.height")
(if (> height 57) ;; In Android height comes with padding
min-height (if platform/android? 40 22)]
(if (> height min-height)
(reset! multiple-lines? true) (reset! multiple-lines? true)
(reset! multiple-lines? false))) (reset! multiple-lines? false)))
char-count (reagent/atom 0) char-count (reagent/atom 0)
@ -76,7 +80,7 @@
(when (>= amount-chars char-limit) (when (>= amount-chars char-limit)
(on-char-limit-reach amount-chars))))] (on-char-limit-reach amount-chars))))]
(fn [{:keys [blur? theme error? right-icon left-icon disabled? small? button (fn [{:keys [blur? theme error? right-icon left-icon disabled? small? button
label char-limit multiline? clearable? on-focus on-blur] label char-limit multiline? clearable? on-focus on-blur container-style]
:as props}] :as props}]
(let [status-kw (cond (let [status-kw (cond
disabled? :disabled disabled? :disabled
@ -92,8 +96,7 @@
:label label :label label
:current-chars @char-count :current-chars @char-count
:char-limit char-limit}]) :char-limit char-limit}])
[rn/view [rn/view {:style (style/input-container colors-by-status small? disabled?)}
{:style (style/input-container colors-by-status small? disabled?)}
(when-let [{:keys [icon-name]} left-icon] (when-let [{:keys [icon-name]} left-icon]
[left-accessory [left-accessory
{:variant-colors variant-colors {:variant-colors variant-colors
@ -160,7 +163,7 @@
- :icon-name - The name of an icon to display at the left of the input. - :icon-name - The name of an icon to display at the left of the input.
- :error? - Boolean to specify it this input marks an error. - :error? - Boolean to specify it this input marks an error.
- :disabled? - Boolean to specify if this input is disabled or not. - :disabled? - Boolean to specify if this input is disabled or not.
- :clearable? - Booolean to specify if this input has a clear button at the end. - :clearable? - Boolean to specify if this input has a clear button at the end.
- :on-clear - Function executed when the clear button is pressed. - :on-clear - Function executed when the clear button is pressed.
- :button - Map containing `:on-press` & `:text` keys, if provided renders a button - :button - Map containing `:on-press` & `:text` keys, if provided renders a button
- :label - A string to set as label for this input. - :label - A string to set as label for this input.

View File

@ -1,28 +1,16 @@
(ns status-im2.contexts.add-new-contact.style (ns status-im2.contexts.add-new-contact.style
(:require (:require [quo.foundations.colors :as colors]
[quo.foundations.colors :as colors] [react-native.safe-area :as safe-area]))
[quo.foundations.typography :as typography]
[react-native.platform :as platform]
[react-native.safe-area :as safe-area]))
(defn container-outer (defn container-outer
[] []
{:style {:flex 1 {:flex 1
:background-color (colors/theme-colors colors/white colors/neutral-95) :background-color (colors/theme-colors colors/white colors/neutral-95)
:justify-content :space-between :justify-content :space-between
:align-items :center :align-items :center
:padding-vertical 16 :padding-horizontal 20})
:padding-horizontal 20
:border-radius 20}}) (def container-inner {:align-items :flex-start})
(def container-inner
{:style {:flex-direction :column
:align-items :flex-start}})
(def container-text-input
{:style {:flex-direction :row
:justify-content :space-between}})
(def container-invalid (def container-invalid
{:style {:flex-direction :row {:style {:flex-direction :row
@ -35,27 +23,21 @@
:weight :semi-bold :weight :semi-bold
:style {:margin-top 32 :style {:margin-top 32
:margin-bottom 6 :margin-bottom 6
:color (colors/theme-colors :color (colors/theme-colors colors/neutral-100 colors/white)}})
colors/neutral-100
colors/white)}})
(defn text-subtitle (defn text-subtitle
[] []
{:size :paragraph-1 {:size :paragraph-1
:weight :regular :weight :regular
:style {:margin-bottom 20 :style {:margin-bottom 20
:color (colors/theme-colors :color (colors/theme-colors colors/neutral-100 colors/white)}})
colors/neutral-100
colors/white)}})
(defn text-description (defn text-description
[] []
{:size :paragraph-2 {:size :paragraph-2
:weight :medium :weight :medium
:style {:margin-bottom 6 :style {:margin-bottom 6
:color (colors/theme-colors :color (colors/theme-colors colors/neutral-50 colors/neutral-40)}})
colors/neutral-50
colors/neutral-40)}})
(def icon-invalid (def icon-invalid
{:size 16 {:size 16
@ -67,46 +49,14 @@
:style {:margin-left 4 :style {:margin-left 4
:color colors/danger-50}}) :color colors/danger-50}})
(defn text-input-container (def input-and-scan-container
[invalid?] {:flex-direction :row
{:style {:padding-top 1 :align-items :flex-start})
:padding-left 12
:padding-right 7
:padding-bottom 7
:margin-right 10
:flex 1
:flex-direction :row
:background-color (colors/theme-colors
colors/white
colors/neutral-95)
:border-width 1
:border-radius 12
:border-color (if invalid?
colors/danger-50-opa-40
(colors/theme-colors
colors/neutral-20
colors/neutral-80))}})
(defn text-input (def scan-button-container
[] {:margin-left 12
{:accessibility-label :enter-contact-code-input :height 66
:auto-capitalize :none :justify-content :flex-end})
:placeholder-text-color (colors/theme-colors
colors/neutral-40
colors/neutral-50)
:multiline true
:style
(merge typography/monospace
typography/paragraph-1
{:flex 1
:margin-right 5
:margin-top (if platform/android?
4
0)
:padding 0
:color (colors/theme-colors
colors/black
colors/white)})})
(def found-user (def found-user
{:padding-top 16 {:padding-top 16

View File

@ -1,16 +1,15 @@
(ns status-im2.contexts.add-new-contact.views (ns status-im2.contexts.add-new-contact.views
(:require (:require [clojure.string :as string]
[clojure.string :as string] [quo.core :as quo]
[quo.core :as quo] [react-native.clipboard :as clipboard]
[react-native.clipboard :as clipboard] [react-native.core :as rn]
[react-native.core :as rn] [reagent.core :as reagent]
[reagent.core :as reagent] [status-im.qr-scanner.core :as qr-scanner]
[status-im.qr-scanner.core :as qr-scanner] [status-im2.contexts.add-new-contact.style :as style]
[status-im2.contexts.add-new-contact.style :as style] [utils.address :as address]
[utils.address :as address] [utils.debounce :as debounce]
[utils.debounce :as debounce] [utils.i18n :as i18n]
[utils.i18n :as i18n] [utils.re-frame :as rf]))
[utils.re-frame :as rf]))
(defn found-contact (defn found-contact
[public-key] [public-key]
@ -38,94 +37,104 @@
:style (style/found-user-key)} :style (style/found-user-key)}
(address/get-shortened-compressed-key compressed-key)]]]]))) (address/get-shortened-compressed-key compressed-key)]]]])))
(defn- header
[]
[:<>
[quo/button
{:type :grey
:icon-only? true
:accessibility-label :new-contact-close-button
:size 32
:on-press #(rf/dispatch [:navigate-back])}
:i/close]
[quo/text (style/text-title) (i18n/label :t/add-a-contact)]
[quo/text (style/text-subtitle) (i18n/label :t/find-your-friends)]])
(defn- search-input
[]
(reagent/with-let [input-value (reagent/atom nil)
input-ref (atom nil)
clear-input (fn []
(reset! input-value nil)
(rf/dispatch [:contacts/clear-new-identity]))
paste-on-input #(clipboard/get-string
(fn [clipboard-text]
(reset! input-value clipboard-text)
(rf/dispatch [:contacts/set-new-identity clipboard-text nil])))]
(let [{:keys [scanned]} (rf/sub [:contacts/new-identity])
empty-input? (and (string/blank? @input-value)
(string/blank? scanned))]
[rn/view {:style style/input-and-scan-container}
[quo/input
{:accessibility-label :enter-contact-code-input
:ref #(reset! input-ref %)
:container-style {:flex 1}
:auto-capitalize :none
:multiline? true
:blur-on-submit true
:return-key-type :done
:label (i18n/label :t/ens-or-chat-key)
:placeholder (i18n/label :t/type-some-chat-key)
:clearable? (not empty-input?)
:on-clear clear-input
:button (when empty-input?
{:on-press paste-on-input
:text (i18n/label :t/paste)})
;; NOTE: `scanned` has priority over `@input-value`, we clean it when the input
;; is updated so that it's `nil` and `@input-value` is shown.
;; To fastly clean it, we use `dispatch-sync`.
;; This call could be avoided if `::qr-scanner/scan-code` were able to receive a
;; callback function, not only a re-frame event as callback.
:value (or scanned @input-value)
:on-change-text (fn [new-text]
(reset! input-value new-text)
(as-> [:contacts/set-new-identity new-text nil] $
(if (string/blank? scanned)
(debounce/debounce-and-dispatch $ 600)
(rf/dispatch-sync $))))}]
[rn/view {:style style/scan-button-container}
[quo/button
{:type :outline
:icon-only? true
:size 40
:on-press #(rf/dispatch [::qr-scanner/scan-code
{:handler :contacts/qr-code-scanned}])}
:i/scan]]])
(finally
(rf/dispatch [:contacts/clear-new-identity]))))
(defn- invalid-text
[message]
[rn/view style/container-invalid
[quo/icon :i/alert style/icon-invalid]
[quo/text style/text-invalid
(i18n/label (or message :t/invalid-ens-or-key))]])
(defn new-contact (defn new-contact
[] []
(let [clipboard (reagent/atom nil) (let [{:keys [public-key ens state msg]} (rf/sub [:contacts/new-identity])
default-value (reagent/atom nil)] customization-color (rf/sub [:profile/customization-color])]
(fn [] [rn/keyboard-avoiding-view {:style {:flex 1}}
(clipboard/get-string #(reset! clipboard %)) [rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!}
(let [{:keys [input scanned public-key ens state msg]} [rn/view {:style (style/container-outer)}
(rf/sub [:contacts/new-identity]) [rn/view {:style style/container-inner}
customization-color (rf/sub [:profile/customization-color]) [header]
invalid? (= state :invalid) [search-input]
show-paste-button? (and (not (string/blank? @clipboard)) (case state
(string/blank? @default-value) :invalid [invalid-text msg]
(string/blank? input))] :valid [found-contact public-key]
[rn/keyboard-avoiding-view nil)]
{:style {:flex 1}} [quo/button
[rn/touchable-without-feedback {:type :primary
{:on-press rn/dismiss-keyboard!} :customization-color customization-color
[rn/view (style/container-outer) :size 40
[rn/view style/container-inner :container-style style/button-view-profile
[quo/button :accessibility-label :new-contact-button
{:type :grey :icon-left :i/profile
:icon-only? true :disabled? (not= state :valid)
:accessibility-label :new-contact-close-button :on-press (fn []
:size 32 (rf/dispatch [:navigate-back])
:on-press (rf/dispatch [:chat.ui/show-profile public-key ens])
(fn [] (js/setTimeout #(rf/dispatch [:contacts/clear-new-identity])
(reset! clipboard nil) 600))}
(reset! default-value nil) (i18n/label :t/view-profile)]]]]))
(rf/dispatch [:contacts/clear-new-identity])
(rf/dispatch [:navigate-back]))} :i/close]
[quo/text (style/text-title)
(i18n/label :t/add-a-contact)]
[quo/text (style/text-subtitle)
(i18n/label :t/find-your-friends)]
[quo/text (style/text-description)
(i18n/label :t/ens-or-chat-key)]
[rn/view style/container-text-input
[rn/view (style/text-input-container invalid?)
[rn/text-input
(merge (style/text-input)
{:default-value (or scanned @default-value input)
:placeholder (i18n/label :t/type-some-chat-key)
:on-change-text (fn [v]
(reset! default-value v)
(debounce/debounce-and-dispatch
[:contacts/set-new-identity v nil]
600))
:blur-on-submit true
:return-key-type :done})]
(when show-paste-button?
[quo/button
{:type :outline
:size 24
:container-style {:margin-top 6}
:on-press
(fn []
(reset! default-value @clipboard)
(rf/dispatch
[:contacts/set-new-identity @clipboard nil]))}
(i18n/label :t/paste)])]
[quo/button
{:type :outline
:icon-only? true
:size 40
:on-press #(rf/dispatch
[::qr-scanner/scan-code
{:handler :contacts/qr-code-scanned}])}
:i/scan]]
(when invalid?
[rn/view style/container-invalid
[quo/icon :i/alert style/icon-invalid]
[quo/text style/text-invalid
(i18n/label (or msg :t/invalid-ens-or-key))]])
(when (= state :valid)
[found-contact public-key])]
[quo/button
{:type :primary
:customization-color customization-color
:size 40
:container-style style/button-view-profile
:accessibility-label :new-contact-button
:icon-left :i/profile
:disabled? (not= state :valid)
:on-press
(fn []
(reset! clipboard nil)
(reset! default-value nil)
(rf/dispatch [:contacts/clear-new-identity])
(rf/dispatch [:navigate-back])
(rf/dispatch [:chat.ui/show-profile public-key ens]))}
(i18n/label :t/view-profile)]]]]))))

View File

@ -22,7 +22,7 @@
:type :boolean} :type :boolean}
{:key :small? {:key :small?
:type :boolean} :type :boolean}
{:key :multiline {:key :multiline?
:type :boolean} :type :boolean}
{:key :button {:key :button
:type :boolean} :type :boolean}
@ -53,10 +53,11 @@
(fn [] (fn []
(let [blank-label? (string/blank? (:label @state))] (let [blank-label? (string/blank? (:label @state))]
[preview/preview-container [preview/preview-container
{:state state {:component-container-style {:margin-bottom 200}
:descriptor descriptor :state state
:blur? (:blur? @state) :descriptor descriptor
:show-blur-background? true} :blur? (:blur? @state)
:show-blur-background? true}
[quo/input [quo/input
(cond-> (assoc @state (cond-> (assoc @state
:on-clear? #(swap! state assoc :value "") :on-clear? #(swap! state assoc :value "")

View File

@ -81,3 +81,5 @@
(def dispatch re-frame/dispatch) (def dispatch re-frame/dispatch)
(def reg-fx re-frame/reg-fx) (def reg-fx re-frame/reg-fx)
(def dispatch-sync re-frame/dispatch-sync)