From ff3ba6c0f064d6d24fbd802c0abe220b2d0bced3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulises=20Manuel=20C=C3=A1rdenas?= <90291778+ulisesmac@users.noreply.github.com> Date: Fri, 3 Mar 2023 15:03:38 -0600 Subject: [PATCH] Add new text input component --- src/quo2/components/input/style.cljs | 170 +++++++++++++++++ src/quo2/components/input/view.cljs | 173 ++++++++++++++++++ src/quo2/core.cljs | 4 + src/quo2/foundations/colors.cljs | 5 + .../contexts/quo_preview/inputs/input.cljs | 115 ++++++++++++ src/status_im2/contexts/quo_preview/main.cljs | 4 + 6 files changed, 471 insertions(+) create mode 100644 src/quo2/components/input/style.cljs create mode 100644 src/quo2/components/input/view.cljs create mode 100644 src/status_im2/contexts/quo_preview/inputs/input.cljs diff --git a/src/quo2/components/input/style.cljs b/src/quo2/components/input/style.cljs new file mode 100644 index 0000000000..0df55f5a35 --- /dev/null +++ b/src/quo2/components/input/style.cljs @@ -0,0 +1,170 @@ +(ns quo2.components.input.style + (:require [quo2.components.markdown.text :as text] + [quo2.foundations.colors :as colors])) + +(def variants-colors + "Colors that keep the same across input's status change" + {:light {:label colors/neutral-50 + :icon colors/neutral-50 + :cursor (colors/custom-color :blue 50) + :button-border colors/neutral-30 + :clear-icon colors/neutral-40 + :password-icon colors/neutral-50} + :light-blur {:label colors/neutral-80-opa-40 + :icon colors/neutral-80-opa-70 + :cursor (colors/custom-color :blue 50) + :button-border colors/neutral-80-opa-30 + :password-icon colors/neutral-100 + :clear-icon colors/neutral-80-opa-30} + :dark {:label colors/neutral-40 + :icon colors/neutral-40 + :cursor (colors/custom-color :blue 60) + :button-border colors/neutral-70 + :password-icon colors/white + :clear-icon colors/neutral-60} + :dark-blur {:label colors/white-opa-40 + :icon colors/white-opa-70 + :cursor colors/white + :button-border colors/white-opa-10 + :password-icon colors/white + :clear-icon colors/white-opa-10}}) + +(def status-colors + {:light {:default {:border-color colors/neutral-20 + :placeholder colors/neutral-40 + :text colors/neutral-100} + :focus {:border-color colors/neutral-40 + :placeholder colors/neutral-30 + :text colors/neutral-100} + :error {:border-color colors/danger-opa-40 + :placeholder colors/neutral-40 + :text colors/neutral-100} + :disabled {:border-color colors/neutral-20 + :placeholder colors/neutral-40 + :text colors/neutral-40}} + :light-blur {:default {:border-color colors/neutral-80-opa-10 + :placeholder colors/neutral-80-opa-40 + :text colors/neutral-100} + :focus {:border-color colors/neutral-80-opa-20 + :placeholder colors/neutral-80-opa-20 + :text colors/neutral-100} + :error {:border-color colors/danger-opa-40 + :placeholder colors/neutral-80-opa-40 + :text colors/neutral-100} + :disabled {:border-color colors/neutral-80-opa-10 + :placeholder colors/neutral-80-opa-30 + :text colors/neutral-80-opa-30}} + :dark {:default {:border-color colors/neutral-80 + :placeholder colors/neutral-50 + :text colors/white} + :focus {:border-color colors/neutral-60 + :placeholder colors/neutral-60 + :text colors/white} + :error {:border-color colors/danger-opa-40 + :placeholder colors/white-opa-40 + :text colors/white} + :disabled {:border-color colors/neutral-80 + :placeholder colors/neutral-40 + :text colors/neutral-40}} + :dark-blur {:default {:border-color colors/white-opa-10 + :placeholder colors/white-opa-40 + :text colors/white} + :focus {:border-color colors/white-opa-40 + :placeholder colors/white-opa-20 + :text colors/white} + :error {:border-color colors/danger-opa-40 + :placeholder colors/white-opa-40 + :text colors/white} + :disabled {:border-color colors/white-opa-10 + :placeholder colors/white-opa-20 + :text colors/white-opa-20}}}) + +(defn input-container + [colors-by-status small? disabled?] + {:flex-direction :row + :padding-horizontal 8 + :border-width 1 + :border-color (:border-color colors-by-status) + :border-radius (if small? 10 14) + :opacity (if disabled? 0.3 1)}) + +(defn left-icon-container + [small?] + {:margin-left (if small? 0 4) + :margin-right (if small? 4 8) + :margin-top (if small? 5 9) + :height 20 + :width 20}) + +(defn icon + [colors-by-variant] + {:color (:icon colors-by-variant) + :size 20}) + +(defn input + [colors-by-status small? multiple-lines?] + (merge (text/text-style {:size :paragraph-1 :weight :regular}) + {:flex 1 + :text-align-vertical :top + :padding-horizontal 0 + :padding-vertical (if small? 4 8) + :color (:text colors-by-status)} + (when-not multiple-lines? + {:height (if small? 30 38)}))) + +(defn right-icon-touchable-area + [small?] + {:margin-left (if small? 4 8) + :padding-right (if small? 0 4) + :padding-top (if small? 5 9)}) + +(defn password-icon + [variant-colors] + {:size 20 + :color (:password-icon variant-colors)}) + +(defn clear-icon + [variant-colors] + {:size 20 + :color (:clear-icon variant-colors)}) + +(def texts-container + {:flex 1 + :flex-direction :row + :height 18 + :margin-bottom 8}) + +(def label-container {:flex 1}) + +(defn label-color + [variant-colors] + {:color (:label variant-colors)}) + +(def counter-container + {:flex 1 + :align-items :flex-end}) + +(defn counter-color + [current-chars char-limit variant-colors] + {:color (if (> current-chars char-limit) + colors/danger-60 + (:label variant-colors))}) + +(defn button + [colors-by-variant small?] + {:justify-content :center + :align-items :center + :height 24 + :border-width 1 + :border-color (:button-border colors-by-variant) + :border-radius 8 + :margin-vertical (if small? 3 7) + :margin-left 4 + :margin-right (if small? -4 0) + :padding-horizontal 7 + :padding-top 1.5 + :padding-bottom 2.5}) + +(defn button-text + [colors-by-status] + {:color (:text colors-by-status)}) diff --git a/src/quo2/components/input/view.cljs b/src/quo2/components/input/view.cljs new file mode 100644 index 0000000000..eb86dffefb --- /dev/null +++ b/src/quo2/components/input/view.cljs @@ -0,0 +1,173 @@ +(ns quo2.components.input.view + (:require [oops.core :as oops] + [quo2.components.icon :as icon] + [quo2.components.input.style :as style] + [quo2.components.markdown.text :as text] + [react-native.core :as rn] + [reagent.core :as reagent])) + +(defn- label-&-counter + [{:keys [label current-chars char-limit variant-colors]}] + (let [count-text (when char-limit (str current-chars "/" char-limit))] + [rn/view {:style style/texts-container} + [rn/view {:style style/label-container} + [text/text + {:style (style/label-color variant-colors) + :weight :medium + :size :paragraph-2} + label]] + [rn/view {:style style/counter-container} + [text/text + {:style (style/counter-color current-chars char-limit variant-colors) + :weight :regular + :size :paragraph-2} + count-text]]])) + +(defn- left-accessory + [{:keys [variant-colors small icon-name]}] + [rn/view {:style (style/left-icon-container small)} + [icon/icon icon-name (style/icon variant-colors)]]) + +(defn- right-accessory + [{:keys [variant-colors small disabled on-press icon-style-fn icon-name]}] + [rn/touchable-opacity + {:style (style/right-icon-touchable-area small) + :disabled disabled + :on-press on-press} + [icon/icon icon-name (icon-style-fn variant-colors)]]) + +(defn- right-button + [{:keys [variant-colors colors-by-status small disabled on-press text]}] + [rn/touchable-opacity + {:style (style/button variant-colors small) + :disabled disabled + :on-press on-press} + [rn/text {:style (style/button-text colors-by-status)} + text]]) + +(def ^:private custom-props + "Custom properties that must be removed from properties map passed to InputText." + [:type :variant :error :right-icon :left-icon :disabled :small :button :label + :char-limit :on-char-limit-reach :icon-name]) + +(defn- base-input + [{:keys [on-change-text on-char-limit-reach]}] + (let [status (reagent/atom :default) + on-focus #(reset! status :focus) + on-blur #(reset! status :default) + multiple-lines? (reagent/atom false) + set-multiple-lines! #(let [height (oops/oget % "nativeEvent.contentSize.height")] + (if (> height 57) + (reset! multiple-lines? true) + (reset! multiple-lines? false))) + char-count (reagent/atom 0) + update-char-limit! (fn [new-text char-limit] + (when on-change-text (on-change-text new-text)) + (let [amount-chars (count new-text)] + (reset! char-count amount-chars) + (when (>= amount-chars char-limit) + (on-char-limit-reach amount-chars))))] + (fn [{:keys [variant error right-icon left-icon disabled small button label char-limit + multiline clearable] + :or {variant :light} + :as props}] + (let [status-path (cond + disabled :disabled + error :error + :else @status) + colors-by-status (get-in style/status-colors [variant status-path]) + variant-colors (style/variants-colors variant) + clean-props (apply dissoc props custom-props)] + [rn/view + (when (or label char-limit) + [label-&-counter + {:variant-colors variant-colors + :label label + :current-chars @char-count + :char-limit char-limit}]) + [rn/view {:style (style/input-container colors-by-status small disabled)} + (when-let [{:keys [icon-name]} left-icon] + [left-accessory + {:variant-colors variant-colors + :small small + :icon-name icon-name}]) + [rn/text-input + (cond-> {:style (style/input colors-by-status small @multiple-lines?) + :placeholder-text-color (:placeholder colors-by-status) + :cursor-color (:cursor variant-colors) + :editable (not disabled) + :on-focus on-focus + :on-blur on-blur} + :always (merge clean-props) + multiline (assoc :on-content-size-change set-multiple-lines!) + char-limit (assoc :on-change-text #(update-char-limit! char-limit %)))] + (when-let [{:keys [on-press icon-name style-fn]} right-icon] + [right-accessory + {:variant-colors variant-colors + :small small + :disabled disabled + :icon-style-fn style-fn + :icon-name icon-name + :on-press (fn [] + (when clearable (reset! char-count 0)) + (on-press))}]) + (when-let [{:keys [on-press text]} button] + [right-button + {:colors-by-status colors-by-status + :variant-colors variant-colors + :small small + :disabled disabled + :on-press on-press + :text text}])]])))) + +(defn- password-input + [_] + (let [password-shown? (reagent/atom false)] + (fn [props] + [base-input + (assoc props + :auto-capitalize :none + :auto-complete :new-password + :secure-text-entry (not @password-shown?) + :right-icon {:style-fn style/password-icon + :icon-name (if @password-shown? :i/hide :i/reveal) + :on-press #(swap! password-shown? not)})]))) + +(defn input + "This input supports the following properties: + - :type - Can be `:text`(default) or `:password`. + - :variant - :light(default), :light-blur, :dark or :dark-blur. + - :small - Boolean to specify if this input is rendered in its small version. + - :multiline - Boolean to specify if this input support multiple lines. + - :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. + - :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. + - :on-clear - Function executed when the clear button is pressed. + - :button - Map containing `:on-press` & `:text` keys, if provided renders a button + - :label - A label for this input. + - :char-limit - A number to set a maximum char limit for this input. + - :on-char-limit-reach - Function executed each time char limit is reached or exceeded. + and supports the usual React Native's TextInput properties to control its behaviour: + - :value + - :default-value + - :on-change + - :on-change-text + ... + " + [{:keys [type clearable on-clear on-change-text icon-name] + :or {type :text} + :as props}] + (let [base-props (cond-> props + icon-name (assoc-in [:left-icon :icon-name] icon-name) + clearable (assoc :right-icon + {:style-fn style/clear-icon + :icon-name :i/clear + :on-press #(when on-clear (on-clear))}) + on-change-text (assoc :on-change-text + (fn [new-text] + (on-change-text new-text) + (reagent/flush))))] + (if (= type :password) + [password-input base-props] + [base-input base-props]))) diff --git a/src/quo2/core.cljs b/src/quo2/core.cljs index 74c008c687..e35029ee58 100644 --- a/src/quo2/core.cljs +++ b/src/quo2/core.cljs @@ -30,6 +30,7 @@ quo2.components.icon quo2.components.info.info-message quo2.components.info.information-box + quo2.components.input.view quo2.components.list-items.channel quo2.components.list-items.menu-item quo2.components.list-items.preview-list @@ -139,6 +140,9 @@ (def drawer-buttons quo2.components.drawers.drawer-buttons.view/view) (def permission-context quo2.components.drawers.permission-context.view/view) +;;;; INPUTS +(def input quo2.components.input.view/input) + ;;;; LIST ITEMS (def channel-list-item quo2.components.list-items.channel/list-item) (def menu-item quo2.components.list-items.menu-item/menu-item) diff --git a/src/quo2/foundations/colors.cljs b/src/quo2/foundations/colors.cljs index ffc7c9f165..1f9ec57032 100644 --- a/src/quo2/foundations/colors.cljs +++ b/src/quo2/foundations/colors.cljs @@ -60,6 +60,7 @@ ;;Blur (def neutral-5-opa-70 (alpha neutral-5 0.7)) +(def neutral-80-blur-opa-80 "rgba(25,36,56,0.8)") (def neutral-90-opa-70 (alpha neutral-90 0.7)) ;;80 with transparency @@ -152,6 +153,10 @@ (def success-60-opa-40 (alpha success-60 0.4)) ;;;;Danger +(def danger "#E95460") + +;; Danger with transparency +(def danger-opa-40 (alpha danger 0.4)) ;;Solid (def danger-50 "#E65F5C") diff --git a/src/status_im2/contexts/quo_preview/inputs/input.cljs b/src/status_im2/contexts/quo_preview/inputs/input.cljs new file mode 100644 index 0000000000..aab416815a --- /dev/null +++ b/src/status_im2/contexts/quo_preview/inputs/input.cljs @@ -0,0 +1,115 @@ +(ns status-im2.contexts.quo-preview.inputs.input + (:require + [clojure.string :as string] + [quo2.components.input.view :as quo2] + [quo2.foundations.colors :as colors] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im2.contexts.quo-preview.preview :as preview])) + +(def descriptor + [{:label "Type:" + :key :type + :type :select + :options [{:key :text + :value "Text"} + {:key :password + :value "Password"}]} + {:label "Variant:" + :key :variant + :type :select + :options [{:key :light + :value "Light"} + {:key :dark + :value "Dark"} + {:key :light-blur + :value "Light blur"} + {:key :dark-blur + :value "Dark blur"}]} + {:label "Error:" + :key :error + :type :boolean} + {:label "Icon:" + :key :icon-name + :type :boolean} + {:label "Disabled:" + :key :disabled + :type :boolean} + {:label "Clearable:" + :key :clearable + :type :boolean} + {:label "Small:" + :key :small + :type :boolean} + {:label "Multiline:" + :key :multiline + :type :boolean} + {:label "Button:" + :key :button + :type :boolean} + {:label "Label:" + :key :label + :type :text} + {:label "Char limit:" + :key :char-limit + :type :select + :options [{:key 10 + :value "10"} + {:key 50 + :value "50"} + {:key 100 + :value "100"}]} + {:label "Value:" + :key :value + :type :text}]) + +(defn cool-preview + [] + (let [state (reagent/atom {:type :text + :variant :light-blur + :placeholder "Type something" + :error false + :icon-name false + :value "" + :clearable false + :on-char-limit-reach #(js/alert + (str "Char limit reached: " %))})] + (fn [] + (let [background-color (case (:variant @state) + :dark-blur "rgb(39, 61, 81)" + :dark colors/neutral-95 + :light-blur "rgb(233,247,247)" + :white) + blank-label? (string/blank? (:label @state)) + icon? (boolean (:icon-name @state)) + button-props {:on-press #(js/alert "Button pressed!") + :text "My button"}] + [rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!} + [rn/view {:style {:padding-bottom 150}} + [rn/view {:style {:flex 1}} + [preview/customizer state descriptor]] + [rn/view + {:style {:flex 1 + :align-items :center + :padding-vertical 60 + :background-color background-color}} + [rn/view {:style {:width 300}} + [quo2/input + (cond-> @state + :always (assoc + :on-clear #(swap! state assoc :value "") + :on-change-text #(swap! state assoc :value %)) + (:button @state) (assoc :button button-props) + blank-label? (dissoc :label) + icon? (assoc :icon-name :i/placeholder))]]]]])))) + +(defn preview-input + [] + [rn/view + {:style {:background-color (colors/theme-colors colors/white colors/neutral-90) + :flex 1}} + [rn/flat-list + {:style {:flex 1} + :keyboardShouldPersistTaps :always + :header [cool-preview] + :key-fn str}]]) diff --git a/src/status_im2/contexts/quo_preview/main.cljs b/src/status_im2/contexts/quo_preview/main.cljs index fa93774993..9b4493deae 100644 --- a/src/status_im2/contexts/quo_preview/main.cljs +++ b/src/status_im2/contexts/quo_preview/main.cljs @@ -71,6 +71,7 @@ [status-im2.contexts.quo-preview.tags.status-tags :as status-tags] [status-im2.contexts.quo-preview.tags.tags :as tags] [status-im2.contexts.quo-preview.tags.token-tag :as token-tag] + [status-im2.contexts.quo-preview.inputs.input :as input] [status-im2.contexts.quo-preview.wallet.lowest-price :as lowest-price] [status-im2.contexts.quo-preview.wallet.network-amount :as network-amount] [status-im2.contexts.quo-preview.wallet.network-breakdown :as network-breakdown] @@ -165,6 +166,9 @@ {:name :information-box :insets {:top false} :component information-box/preview-information-box}] + :inputs [{:name :input + :insets {:top false} + :component input/preview-input}] :list-items [{:name :channel :insets {:top false} :component channel/preview-channel}