fix validation visual feedback and disabled grey tick
This commit is contained in:
parent
fa2e2c2f28
commit
e947bb0ea1
|
@ -14,6 +14,9 @@
|
|||
(defn anim-sequence [animations]
|
||||
(.sequence animated (clj->js animations)))
|
||||
|
||||
(defn parallel [animations]
|
||||
(.parallel animated (clj->js animations)))
|
||||
|
||||
(defn anim-delay [duration]
|
||||
(.delay animated duration))
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
(ns status-im.components.text-field.styles)
|
||||
|
||||
|
||||
(def text-field-container
|
||||
{:position :relative
|
||||
:height 72
|
||||
:paddingTop 30
|
||||
:paddingBottom 7})
|
||||
|
||||
(def text-input
|
||||
{:fontSize 16
|
||||
:height 34
|
||||
:lineHeight 34
|
||||
:paddingBottom 5
|
||||
:textAlignVertical :top})
|
||||
|
||||
(defn label [top font-size color]
|
||||
{:position :absolute
|
||||
:top top
|
||||
:left 0
|
||||
:color color
|
||||
:fontSize font-size
|
||||
:backgroundColor :transparent})
|
||||
|
||||
(def label-float
|
||||
{})
|
||||
|
||||
(defn underline-container [backgroundColor]
|
||||
{:backgroundColor backgroundColor
|
||||
:height 1
|
||||
:alignItems :center})
|
||||
|
||||
(defn underline [backgroundColor width]
|
||||
{:backgroundColor backgroundColor
|
||||
:height 1
|
||||
:width width})
|
||||
|
||||
(defn error-text [color]
|
||||
{:color color
|
||||
:fontSize 12})
|
|
@ -0,0 +1,189 @@
|
|||
(ns status-im.components.text-field.view
|
||||
(:require [clojure.string :as s]
|
||||
[re-frame.core :refer [subscribe dispatch dispatch-sync]]
|
||||
[reagent.core :as r]
|
||||
[status-im.components.react :refer [react
|
||||
view
|
||||
text
|
||||
animated-text
|
||||
animated-view
|
||||
text-input
|
||||
touchable-opacity]]
|
||||
[status-im.components.text-field.styles :as st]
|
||||
[status-im.i18n :refer [label]]
|
||||
[status-im.components.animation :as anim]
|
||||
[status-im.utils.logging :as log]))
|
||||
|
||||
|
||||
(def config {:label-top 16
|
||||
:label-bottom 37
|
||||
:label-font-large 16
|
||||
:label-font-small 12
|
||||
:label-animation-duration 200})
|
||||
|
||||
(def default-props {:wrapperStyle {}
|
||||
:inputStyle {}
|
||||
:lineStyle {}
|
||||
:labelColor "#838c93"
|
||||
:lineColor "#0000001f"
|
||||
:focusLineColor "#0000001f"
|
||||
:errorColor "#d50000"
|
||||
:onFocus #()
|
||||
:onBlur #()
|
||||
:onChangeText #()
|
||||
:onChange #()})
|
||||
|
||||
(defn field-animation [{:keys [top to-top font-size to-font-size
|
||||
line-width to-line-width]}]
|
||||
(let [duration (:label-animation-duration config)
|
||||
animation (anim/parallel [(anim/timing top {:toValue to-top
|
||||
:duration duration})
|
||||
(anim/timing font-size {:toValue to-font-size
|
||||
:duration duration})
|
||||
(anim/timing line-width {:toValue to-line-width
|
||||
:duration duration})])]
|
||||
(anim/start animation (fn [arg]
|
||||
(when (.-finished arg)
|
||||
(log/debug "Field animation finished"))))))
|
||||
|
||||
; Invoked once before the component is mounted. The return value will be used
|
||||
; as the initial value of this.state.
|
||||
(defn get-initial-state [component]
|
||||
{:has-focus false
|
||||
:float-label? false
|
||||
:label-top 0
|
||||
:label-font-size 0
|
||||
:line-width (anim/create-value 0)
|
||||
:max-line-width 100})
|
||||
|
||||
; Invoked once, both on the client and server, immediately before the initial
|
||||
; rendering occurs. If you call setState within this method, render() will see
|
||||
; the updated state and will be executed only once despite the state change.
|
||||
(defn component-will-mount [component]
|
||||
(let [{:keys [value] :as props} (r/props component)
|
||||
data {:label-top (anim/create-value (if (s/blank? value)
|
||||
(:label-bottom config)
|
||||
(:label-top config)))
|
||||
:label-font-size (anim/create-value (if (s/blank? value)
|
||||
(:label-font-large config)
|
||||
(:label-font-small config)))
|
||||
:float-label? (if (s/blank? value) false true)}]
|
||||
(log/debug "component-will-mount")
|
||||
(r/set-state component data)))
|
||||
|
||||
; Invoked once, only on the client (not on the server), immediately after the
|
||||
; initial rendering occurs. At this point in the lifecycle, you can access any
|
||||
; refs to your children (e.g., to access the underlying DOM representation).
|
||||
; The componentDidMount() method of child components is invoked before that of
|
||||
; parent components.
|
||||
(defn component-did-mount [component]
|
||||
(let [props (r/props component)]
|
||||
(log/debug "component-did-mount:")))
|
||||
|
||||
; Invoked when a component is receiving new props. This method is not called for
|
||||
; the initial render. Use this as an opportunity to react to a prop transition
|
||||
; before render() is called by updating the state using this.setState().
|
||||
; The old props can be accessed via this.props. Calling this.setState() within
|
||||
; this function will not trigger an additional render.
|
||||
(defn component-will-receive-props [component new-props]
|
||||
(log/debug "component-will-receive-props: new-props=" new-props))
|
||||
|
||||
; Invoked before rendering when new props or state are being received. This method
|
||||
; is not called for the initial render or when forceUpdate is used. Use this as
|
||||
; an opportunity to return false when you're certain that the transition to the
|
||||
; new props and state will not require a component update.
|
||||
; If shouldComponentUpdate returns false, then render() will be completely skipped
|
||||
; until the next state change. In addition, componentWillUpdate and
|
||||
; componentDidUpdate will not be called.
|
||||
(defn should-component-update [component next-props next-state]
|
||||
(log/debug "should-component-update: " next-props next-state)
|
||||
true)
|
||||
|
||||
; Invoked immediately before rendering when new props or state are being received.
|
||||
; This method is not called for the initial render. Use this as an opportunity
|
||||
; to perform preparation before an update occurs.
|
||||
(defn component-will-update [component next-props next-state]
|
||||
(log/debug "component-will-update: " next-props next-state))
|
||||
|
||||
; Invoked immediately after the component's updates are flushed to the DOM.
|
||||
; This method is not called for the initial render. Use this as an opportunity
|
||||
; to operate on the DOM when the component has been updated.
|
||||
(defn component-did-update [component prev-props prev-state]
|
||||
(log/debug "component-did-update: " prev-props prev-state))
|
||||
|
||||
(defn on-focus [{:keys [component animation onFocus]}]
|
||||
(do
|
||||
(log/debug "input focused")
|
||||
(r/set-state component {:has-focus true
|
||||
:float-label? true})
|
||||
(field-animation animation)
|
||||
(when onFocus (onFocus))))
|
||||
|
||||
(defn on-blur [{:keys [component value animation onBlur]}]
|
||||
(do
|
||||
(log/debug "Input blurred")
|
||||
(r/set-state component {:has-focus false
|
||||
:float-label? (if (s/blank? value) false true)})
|
||||
(when (s/blank? value)
|
||||
(field-animation animation))
|
||||
(when onBlur (onBlur))))
|
||||
|
||||
(defn get-width [event]
|
||||
(.-width (.-layout (.-nativeEvent event))))
|
||||
|
||||
(defn reagent-render [data children]
|
||||
(let [component (r/current-component)
|
||||
{:keys [has-focus
|
||||
float-label?
|
||||
label-top
|
||||
label-font-size
|
||||
line-width
|
||||
max-line-width] :as state} (r/state component)
|
||||
{:keys [wrapperStyle inputStyle lineColor focusLineColor
|
||||
labelColor errorColor error label value onFocus onBlur
|
||||
onChangeText onChange] :as props} (merge default-props (r/props component))
|
||||
lineColor (if error errorColor lineColor)
|
||||
focusLineColor (if error errorColor focusLineColor)
|
||||
labelColor (if (and error (not float-label?)) errorColor labelColor)
|
||||
label (if error (str label " *") label)]
|
||||
(log/debug "reagent-render: " data state)
|
||||
[view (merge st/text-field-container wrapperStyle)
|
||||
[animated-text {:style (st/label label-top label-font-size labelColor)} label]
|
||||
[text-input {:style (merge st/text-input inputStyle)
|
||||
:placeholder ""
|
||||
:onFocus #(on-focus {:component component
|
||||
:animation {:top label-top
|
||||
:to-top (:label-top config)
|
||||
:font-size label-font-size
|
||||
:to-font-size (:label-font-small config)
|
||||
:line-width line-width
|
||||
:to-line-width max-line-width}
|
||||
:onFocus onFocus})
|
||||
:onBlur #(on-blur {:component component
|
||||
:value value
|
||||
:animation {:top label-top
|
||||
:to-top (:label-bottom config)
|
||||
:font-size label-font-size
|
||||
:to-font-size (:label-font-large config)
|
||||
:line-width line-width
|
||||
:to-line-width 0}
|
||||
:onBlur onBlur})
|
||||
:onChangeText #(onChangeText %)
|
||||
:onChange #(onChange %)} value]
|
||||
[view {:style (st/underline-container lineColor)
|
||||
:onLayout #(r/set-state component {:max-line-width (get-width %)})}
|
||||
[animated-view {:style (st/underline focusLineColor line-width)}]]
|
||||
[text {:style (st/error-text errorColor)} error]]))
|
||||
|
||||
(defn text-field [data children]
|
||||
(let [component-data {:get-initial-state get-initial-state
|
||||
:component-will-mount component-will-mount
|
||||
:component-did-mount component-did-mount
|
||||
:component-will-receive-props component-will-receive-props
|
||||
:should-component-update should-component-update
|
||||
:component-will-update component-will-update
|
||||
:component-did-update component-did-update
|
||||
:display-name "text-field"
|
||||
:reagent-render reagent-render}]
|
||||
(log/debug "Creating text-field component: " data)
|
||||
(r/create-class component-data)))
|
|
@ -173,6 +173,7 @@
|
|||
|
||||
(def address-explication-container
|
||||
{:flex 1
|
||||
:margin-top 30
|
||||
:paddingLeft 16
|
||||
:paddingRight 16})
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
image
|
||||
linear-gradient
|
||||
touchable-highlight]]
|
||||
[status-im.components.text-field.view :refer [text-field]]
|
||||
[status-im.utils.identicon :refer [identicon]]
|
||||
[status-im.components.toolbar :refer [toolbar]]
|
||||
[status-im.components.styles :refer [color-purple
|
||||
|
@ -28,7 +29,6 @@
|
|||
[status-im.contacts.styles :as st]))
|
||||
|
||||
|
||||
|
||||
(def toolbar-title
|
||||
[view toolbar-title-container
|
||||
[text {:style toolbar-title-text}
|
||||
|
@ -36,42 +36,43 @@
|
|||
|
||||
(defview contact-name-input [name]
|
||||
[]
|
||||
[text-input
|
||||
{:underlineColorAndroid "#0000001f"
|
||||
:placeholderTextColor "#838c93de"
|
||||
:style form-text-input
|
||||
:autoFocus true
|
||||
:placeholder (label :t/name)
|
||||
:onChangeText #(dispatch [:set-in [:new-contact :name] %])}
|
||||
name])
|
||||
[text-field
|
||||
{:error (if (str/blank? name) "" nil)
|
||||
:value name
|
||||
:label (label :t/name)
|
||||
:onChangeText #(dispatch [:set-in [:new-contact :name] %])}])
|
||||
|
||||
(defview contact-whisper-id-input [whisper-identity]
|
||||
[view button-input-container
|
||||
[text-input
|
||||
{:underlineColorAndroid "#0000001f"
|
||||
:placeholderTextColor "#838c93de"
|
||||
:style (merge form-text-input button-input)
|
||||
:autoFocus true
|
||||
:placeholder (label :t/address)
|
||||
:onChangeText #(dispatch [:set-in [:new-contact :whisper-identity] %])}
|
||||
whisper-identity]
|
||||
[scan-button #(dispatch [:scan-qr-code {:toolbar-title (label :t/new-contact)} :set-new-contact-from-qr])]])
|
||||
[]
|
||||
(let [error (if (str/blank? whisper-identity) "" nil)
|
||||
error (if (s/valid? ::v/whisper-identity whisper-identity)
|
||||
error
|
||||
"Please enter a valid address or scan a QR code")]
|
||||
[view button-input-container
|
||||
[text-field
|
||||
{:error error
|
||||
:value whisper-identity
|
||||
:wrapperStyle (merge button-input)
|
||||
:label (label :t/address)
|
||||
:onChangeText #(dispatch [:set-in [:new-contact :whisper-identity] %])}]
|
||||
[scan-button #(dispatch [:scan-qr-code {:toolbar-title (label :t/new-contact)} :set-new-contact-from-qr])]]))
|
||||
|
||||
(defview new-contact []
|
||||
[{:keys [name whisper-identity phone-number] :as new-contact} [:get :new-contact]]
|
||||
[view st/contact-form-container
|
||||
[toolbar {:background-color :white
|
||||
:nav-action {:image {:source {:uri :icon_back}
|
||||
:style icon-back}
|
||||
:handler #(dispatch [:navigate-back])}
|
||||
:custom-content toolbar-title
|
||||
:action {:image {:source {:uri (if (s/valid? ::v/contact new-contact)
|
||||
:icon_ok_blue
|
||||
:icon_ok_disabled)}
|
||||
:style icon-search}
|
||||
:handler #(dispatch [:add-new-contact (merge {:photo-path (identicon whisper-identity)} new-contact)])}}]
|
||||
[view st/form-container
|
||||
[contact-whisper-id-input whisper-identity]
|
||||
[contact-name-input name]]
|
||||
[view st/address-explication-container
|
||||
[text {:style st/address-explication} (label :t/address-explication)]]])
|
||||
(let [valid-contact? (s/valid? ::v/contact new-contact)]
|
||||
[view st/contact-form-container
|
||||
[toolbar {:background-color :white
|
||||
:nav-action {:image {:source {:uri :icon_back}
|
||||
:style icon-back}
|
||||
:handler #(dispatch [:navigate-back])}
|
||||
:custom-content toolbar-title
|
||||
:action {:image {:source {:uri (if valid-contact?
|
||||
:icon_ok_blue
|
||||
:icon_ok_disabled)}
|
||||
:style icon-search}
|
||||
:handler #(when valid-contact? dispatch [:add-new-contact (merge {:photo-path (identicon whisper-identity)} new-contact)])}}]
|
||||
[view st/form-container
|
||||
[contact-name-input name]
|
||||
[contact-whisper-id-input whisper-identity]]
|
||||
[view st/address-explication-container
|
||||
[text {:style st/address-explication} (label :t/address-explication)]]]))
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
|
||||
(def scan-button
|
||||
{:position :absolute
|
||||
:top 5
|
||||
:bottom 0
|
||||
:right 16
|
||||
:flex 1
|
||||
:height 50
|
||||
|
|
Loading…
Reference in New Issue