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]
|
(defn anim-sequence [animations]
|
||||||
(.sequence animated (clj->js animations)))
|
(.sequence animated (clj->js animations)))
|
||||||
|
|
||||||
|
(defn parallel [animations]
|
||||||
|
(.parallel animated (clj->js animations)))
|
||||||
|
|
||||||
(defn anim-delay [duration]
|
(defn anim-delay [duration]
|
||||||
(.delay animated 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
|
(def address-explication-container
|
||||||
{:flex 1
|
{:flex 1
|
||||||
|
:margin-top 30
|
||||||
:paddingLeft 16
|
:paddingLeft 16
|
||||||
:paddingRight 16})
|
:paddingRight 16})
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
image
|
image
|
||||||
linear-gradient
|
linear-gradient
|
||||||
touchable-highlight]]
|
touchable-highlight]]
|
||||||
|
[status-im.components.text-field.view :refer [text-field]]
|
||||||
[status-im.utils.identicon :refer [identicon]]
|
[status-im.utils.identicon :refer [identicon]]
|
||||||
[status-im.components.toolbar :refer [toolbar]]
|
[status-im.components.toolbar :refer [toolbar]]
|
||||||
[status-im.components.styles :refer [color-purple
|
[status-im.components.styles :refer [color-purple
|
||||||
|
@ -28,7 +29,6 @@
|
||||||
[status-im.contacts.styles :as st]))
|
[status-im.contacts.styles :as st]))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(def toolbar-title
|
(def toolbar-title
|
||||||
[view toolbar-title-container
|
[view toolbar-title-container
|
||||||
[text {:style toolbar-title-text}
|
[text {:style toolbar-title-text}
|
||||||
|
@ -36,42 +36,43 @@
|
||||||
|
|
||||||
(defview contact-name-input [name]
|
(defview contact-name-input [name]
|
||||||
[]
|
[]
|
||||||
[text-input
|
[text-field
|
||||||
{:underlineColorAndroid "#0000001f"
|
{:error (if (str/blank? name) "" nil)
|
||||||
:placeholderTextColor "#838c93de"
|
:value name
|
||||||
:style form-text-input
|
:label (label :t/name)
|
||||||
:autoFocus true
|
:onChangeText #(dispatch [:set-in [:new-contact :name] %])}])
|
||||||
:placeholder (label :t/name)
|
|
||||||
:onChangeText #(dispatch [:set-in [:new-contact :name] %])}
|
|
||||||
name])
|
|
||||||
|
|
||||||
(defview contact-whisper-id-input [whisper-identity]
|
(defview contact-whisper-id-input [whisper-identity]
|
||||||
|
[]
|
||||||
|
(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
|
[view button-input-container
|
||||||
[text-input
|
[text-field
|
||||||
{:underlineColorAndroid "#0000001f"
|
{:error error
|
||||||
:placeholderTextColor "#838c93de"
|
:value whisper-identity
|
||||||
:style (merge form-text-input button-input)
|
:wrapperStyle (merge button-input)
|
||||||
:autoFocus true
|
:label (label :t/address)
|
||||||
:placeholder (label :t/address)
|
:onChangeText #(dispatch [:set-in [:new-contact :whisper-identity] %])}]
|
||||||
: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])]]))
|
||||||
whisper-identity]
|
|
||||||
[scan-button #(dispatch [:scan-qr-code {:toolbar-title (label :t/new-contact)} :set-new-contact-from-qr])]])
|
|
||||||
|
|
||||||
(defview new-contact []
|
(defview new-contact []
|
||||||
[{:keys [name whisper-identity phone-number] :as new-contact} [:get :new-contact]]
|
[{:keys [name whisper-identity phone-number] :as new-contact} [:get :new-contact]]
|
||||||
|
(let [valid-contact? (s/valid? ::v/contact new-contact)]
|
||||||
[view st/contact-form-container
|
[view st/contact-form-container
|
||||||
[toolbar {:background-color :white
|
[toolbar {:background-color :white
|
||||||
:nav-action {:image {:source {:uri :icon_back}
|
:nav-action {:image {:source {:uri :icon_back}
|
||||||
:style icon-back}
|
:style icon-back}
|
||||||
:handler #(dispatch [:navigate-back])}
|
:handler #(dispatch [:navigate-back])}
|
||||||
:custom-content toolbar-title
|
:custom-content toolbar-title
|
||||||
:action {:image {:source {:uri (if (s/valid? ::v/contact new-contact)
|
:action {:image {:source {:uri (if valid-contact?
|
||||||
:icon_ok_blue
|
:icon_ok_blue
|
||||||
:icon_ok_disabled)}
|
:icon_ok_disabled)}
|
||||||
:style icon-search}
|
:style icon-search}
|
||||||
:handler #(dispatch [:add-new-contact (merge {:photo-path (identicon whisper-identity)} new-contact)])}}]
|
:handler #(when valid-contact? dispatch [:add-new-contact (merge {:photo-path (identicon whisper-identity)} new-contact)])}}]
|
||||||
[view st/form-container
|
[view st/form-container
|
||||||
[contact-whisper-id-input whisper-identity]
|
[contact-name-input name]
|
||||||
[contact-name-input name]]
|
[contact-whisper-id-input whisper-identity]]
|
||||||
[view st/address-explication-container
|
[view st/address-explication-container
|
||||||
[text {:style st/address-explication} (label :t/address-explication)]]])
|
[text {:style st/address-explication} (label :t/address-explication)]]]))
|
||||||
|
|
|
@ -78,7 +78,7 @@
|
||||||
|
|
||||||
(def scan-button
|
(def scan-button
|
||||||
{:position :absolute
|
{:position :absolute
|
||||||
:top 5
|
:bottom 0
|
||||||
:right 16
|
:right 16
|
||||||
:flex 1
|
:flex 1
|
||||||
:height 50
|
:height 50
|
||||||
|
|
Loading…
Reference in New Issue