@@ -5,13 +5,15 @@ import Data.Array as Array
55import Data.Interpolate (i)
66import Data.String.NonEmpty (NonEmptyString)
77import Data.String.NonEmpty as NonEmptyString
8−import Data.Symbol (SProxy(..))
8+import Effect.Unsafe (unsafePerformEffect)
99import Foreign.Object (Object)
1010import Foreign.Object as Object
1111import Framer.Motion as M
1212import Partial.Unsafe (unsafeCrashWith)
13−import React.Basic.DOM (CSS, css)
13+import React.Basic.DOM (CSS, Props_input, css)
1414import React.Basic.DOM as R
15+import React.Basic.Emotion (Style)
16+import React.Basic.Hooks (reactComponent)
1517import React.Basic.Hooks as React
1618import Web.HTML.HTMLInputElement as InputElement
1719import Yoga.Block.Atom.Icon as Icon
@@ -57,7 +59,7 @@ containerVariants =
5759 { focussed:
5860 css
5961 { clipPath
60− , transition: { duration: 0.7 }
62+ , transition: { duration: 0.6 }
6163 }
6264 , blurred:
6365 css
@@ -71,9 +73,6 @@ containerVariants =
7173type Point =
7274 { x ∷ Int, y ∷ Int }
7375
74−p ∷ Int -> Int -> Point
75−p x y = { x, y }
76−
7776drawPathUntil ∷ Int -> Array Point -> String
7877drawPathUntil idx thePath = do
7978 let fn { x, y } = i x "%" " " y "%"
@@ -83,7 +82,7 @@ drawPathUntil idx thePath = do
8382 i "polygon(" rendered ")"
8483
8584path ∷ Array Point
86−path = mkPath 3 7
85+path = mkPath 4 8
8786
8887mkPath ∷ Int -> Int -> Array Point
8988mkPath borderX borderY = do
@@ -96,13 +95,22 @@ mkPath borderX borderY = do
9695 , {- ⌜ -} p borderX borderY
9796 ]
9897 outside =
99− [ {- ⌜ -} p 0 0
100− , {- ⌞ -} p 0 100
101− , {- ⌟ -} p 100 100
102− , {- ⌝ -} p 100 0
103− , {- ⌜ -} p 0 0
98+ [ {-⌜ -} p 0 0
99+ , {-⌞ -} p 0 100
100+ , {- . -} p 25 100
101+ , {- . -} p 50 100
102+ , {- . -} p 75 100
103+ , {- ⌟-} p 100 100
104+ , {- ⌝-} p 100 0
105+ , {- ^ -} p 75 0
106+ , {- ^ -} p 50 0
107+ , {- ^ -} p 25 0
108+ , {-⌜ -} p 0 0
104109 ]
105110 inside <> outside <> (Array.reverse inside)
111+ where
112+ p ∷ Int -> Int -> Point
113+ p x y = { x, y }
106114
107115containerVariantLabels ∷
108116 { blurred ∷ M.VariantLabel
@@ -110,31 +118,67 @@ containerVariantLabels ∷
110118 }
111119containerVariantLabels = M.makeVariantLabels containerVariants
112120
113−rawComponent ∷ ∀ p. ReactComponent { | p }
114−rawComponent =
121+rawInput ∷ ∀ p. ReactComponent { | p }
122+rawInput =
123+ mkForwardRefComponent "Input" do
124+ \(props ∷ { | PropsOptional }) ref -> React.do
125+ let
126+ result = inputWrapper [ input ]
127+ inputWrapper = div </* { className: "ry-input-wrapper", css: Style.inputWrapper }
128+ input = emotionInput ref inputProps { className: "ry-input", css: Style.input }
129+ inputProps = props { type = HTMLInput.toString <$> props.type # unsafeUnOptional }
130+ pure result
131+
132+rawContainer ∷ ∀ p. ReactComponent { | p }
133+rawContainer =
115134 mkForwardRefComponent "Input" do
135+ \( props ∷
136+ { children ∷ Array JSX
137+ , label ∷ JSX
138+ , css ∷ OptionalProp Style
139+ , hasFocus ∷ Boolean
140+ , inputProps ∷ { | InputPropsF OptionalProp () }
141+ }
142+ ) propsRef -> React.do
143+ -- Left icon width to correctly place the label
144+ let
145+ aria ∷ Object String
146+ aria = props.inputProps._aria # opToMaybe # fold
147+ inputContainer =
148+ M.div
149+ </* M.motion
150+ { variants: M.variants containerVariants
151+ , animate: M.animate if props.hasFocus then containerVariantLabels.focussed else containerVariantLabels.blurred
152+ }
153+ { className: "ry-input-container"
154+ , css: Style.inputContainer props
155+ , _data:
156+ Object.fromHomogeneous
157+ { "invalid": aria # Object.lookup "invalid" # fromMaybe ""
158+ }
159+ }
160+ /> props.children
161+ pure
162+ $ div
163+ </* { className: "ry-label-and-input-wrapper"
164+ , css: Style.labelAndInputWrapper
165+ }
166+ /> [ inputContainer, props.label ]
167+
168+rawComponent ∷ ∀ p. ReactComponent (Record p)
169+rawComponent =
170+ mkForwardRefComponent "InputContainer" do
116171 \(props ∷ { | PropsOptional }) propsRef -> React.do
117− inputBbox /\ setInputBbox <- useState' (zero ∷ DOMRect)
118172 hasFocus /\ setHasFocus <- useState' false
119173 backupRef ∷ NodeRef <- useRef null -- [TODO] test this
120174 let ref = forwardedRefAsMaybe propsRef # fromMaybe backupRef
121− useEffectOnce do
122− maybeBBox <- getBoundingBoxFromRef ref
123− for_ maybeBBox setInputBbox
124− mempty
125175 let
126− focusInput = do
176+ focusInput =
127177 unless hasFocus do
128178 maybeHTMLElement <- getHTMLElementFromRef ref
129179 for_ maybeHTMLElement focus
130180 -- Left icon width to correctly place the label
131181 leftIconRef ∷ NodeRef <- useRef null
132− leftIconBbox /\ setLeftIconBbox <- useState' Nothing
133− useEffectAlways do
134− when (isJust (props.leading # opToMaybe) && leftIconBbox == Nothing) do
135− maybeBBox <- getBoundingBoxFromRef leftIconRef
136− for_ maybeBBox (setLeftIconBbox <<< Just)
137− mempty
138182 let (maybeValue ∷ Maybe String) = cast props.value # opToMaybe
139183 hasValue /\ setHasValue <- useState' ((maybeValue # isJust) && (maybeValue /= Just ""))
140184 let
@@ -148,39 +192,18 @@ rawComponent =
148192 maybeLabelText = props.label # opToMaybe
149193 mkLabel ∷ NonEmptyString -> JSX
150194 mkLabel labelText =
151− R.div'
152− </* { className: "ry-input-label-container", css: Style.labelContainer }
153− /> [ M.animateSharedLayout </ { type: M.switch }
154− /> [ guard (inputBbox /= zero)
155− $ M.div
156− </* { className: if renderLargeLabel then "ry-input-label-large" else "ry-input-label-small"
157− , layoutId: M.layoutId "ry-input-label"
158− , css:
159− if renderLargeLabel then
160− Style.labelLarge { leftIconWidth: leftIconBbox <#> _.width }
161− else
162− Style.labelSmall
163− , layout: M.layout true
164− , transition: M.transition { duration: 0.18, ease: "easeOut" }
165− , _data:
166− Object.fromHomogeneous
167− { "has-focus": show hasFocus
168− , "invalid": aria # Object.lookup "invalid" # fromMaybe ""
169− , "required": aria # Object.lookup "required" # fromMaybe ""
170− }
171− , initial: M.initial false
172− }
173− /> [ M.span
174− </ { onClick: handler preventDefault (const focusInput)
175− , layout: M.layout true
176− , layoutId: M.layoutId "ry-input-label-text"
177− , htmlFor: props.id
178− , id: labelId
179− }
180− /> [ R.text $ NonEmptyString.toString labelText ]
181− ]
182− ]
183− ]
195+ inputLabel
196+ </> { onClickLargeLabel: handler preventDefault (const focusInput)
197+ , isFocussed: hasFocus
198+ , isRequired: aria # Object.lookup "required" # (_ == Just "true")
199+ , isInvalid: aria # Object.lookup "invalid" # (_ == Just "true")
200+ , renderLargeLabel
201+ , leftIconRef
202+ , labelId
203+ , inputId: props.id
204+ , inputRef: ref
205+ , labelText
206+ }
184207 leading ∷ Maybe JSX
185208 leading =
186209 opToMaybe props.leading
@@ -205,51 +228,118 @@ rawComponent =
205228 setHasValue (v /= "")
206229 )
207230 onFocus = handler preventDefault (const $ unless hasFocus $ setHasFocus true)
208− inputWrapper =
231+ inputContainer =
209232 M.div
210233 </* M.motion
211234 { variants: M.variants containerVariants
212235 , animate: M.animate if hasFocus then containerVariantLabels.focussed else containerVariantLabels.blurred
213236 }
214− { className: "ry-input-wrapper"
215− , css: Style.inputWrapper
237+ { className: "ry-input-container"
238+ , css: Style.inputContainer props
216239 , _data:
217240 Object.fromHomogeneous
218241 { "invalid": aria # Object.lookup "invalid" # fromMaybe ""
219242 }
220243 }
221244 /> [ leading # foldMap \l -> div </ { ref: leftIconRef } /> [ l ]
222− , emotionInput
223− ref
224− ( props { type = HTMLInput.toString <$> props.type # unsafeUnOptional }
225− # setOrDelete (SProxy ∷ _ "placeholder") (maybePlaceholder # maybeToOp)
226− # setOrDelete (SProxy ∷ _ "value") (maybeValue # maybeToOp)
227− )
228− { className: "ry-input"
229− , css: Style.input
230− , onFocus: composeHandler props.onFocus onFocus
231− , onBlur: composeHandler props.onBlur onBlur
232− , _aria:
233− if props.label # opToMaybe # isJust then
234− aria # Object.insert "labelledby" labelId
235− else
236− aria
237− }
245+ , rawInput
246+ </> ( props
247+ { onFocus = composeHandler props.onFocus onFocus
248+ , onBlur = composeHandler props.onBlur onBlur
249+ , ref = ref
250+ , placeholder = maybePlaceholder # maybeToOp # unsafeUnOptional
251+ , _aria =
252+ if props.label # opToMaybe # isJust then
253+ aria # Object.insert "labelledby" labelId
254+ else
255+ aria
256+ }
257+ )
258+ -- ref
259+ -- ( props { type = HTMLInput.toString <$> props.type # unsafeUnOptional }
260+ -- # setOrDelete (SProxy ∷ _ "placeholder") (maybePlaceholder # maybeToOp)
261+ -- # setOrDelete (SProxy ∷ _ "value") (maybeValue # maybeToOp)
262+ -- )
263+ -- { className: "ry-input"
264+ -- , css: Style.input
265+ -- }
238266 , trailing # foldMap \t -> div </ {} /> [ t ]
239267 ]
240268 pure
241269 $ case props.type # opToMaybe of
242270 Just HTMLInput.Password -> password </> props
243271 _ -> case maybeLabelText of
244− Nothing -> inputWrapper
272+ Nothing -> inputContainer
245273 Just labelText ->
246274 div
247275 </* { className: "ry-label-and-input-wrapper"
248276 , css: Style.labelAndInputWrapper
249277 }
250− /> [ inputWrapper
251− , mkLabel $ labelText
278+ /> [ inputContainer, mkLabel labelText ]
279+
280+inputLabel ∷
281+ ReactComponent
282+ { onClickLargeLabel ∷ EventHandler
283+ , isRequired ∷ Boolean
284+ , isInvalid ∷ Boolean
285+ , isFocussed ∷ Boolean
286+ , renderLargeLabel ∷ Boolean
287+ , leftIconRef ∷ NodeRef
288+ , inputRef ∷ NodeRef
289+ , labelId ∷ String
290+ , inputId ∷ String
291+ , labelText ∷ NonEmptyString
292+ }
293+inputLabel =
294+ unsafePerformEffect
295+ $ reactComponent "Password" \props -> React.do
296+ inputBbox /\ setInputBbox <- useState' (zero ∷ DOMRect)
297+ useEffectOnce do
298+ maybeBBox <- getBoundingBoxFromRef props.inputRef
299+ for_ maybeBBox setInputBbox
300+ mempty
301+ -- Left Icon
302+ leftIconBbox /\ setLeftIconBbox <- useState' Nothing
303+ useEffectAlways do
304+ when (leftIconBbox == Nothing) do
305+ maybeBBox <- getBoundingBoxFromRef props.leftIconRef
306+ for_ maybeBBox (setLeftIconBbox <<< Just)
307+ mempty
308+ let container = div </* { className: "ry-input-label-container", css: Style.labelContainer }
309+ let
310+ labelSpan =
311+ M.span
312+ </ { onClick: props.onClickLargeLabel
313+ , layout: M.layout true
314+ , layoutId: M.layoutId "ry-input-label-text"
315+ , htmlFor: props.inputId
316+ , id: props.labelId
317+ }
318+ pure
319+ $ container
320+ [ M.animateSharedLayout </ { type: M.switch }
321+ /> [ guard (inputBbox /= zero)
322+ $ M.div
323+ </* { className: if props.renderLargeLabel then "ry-input-label-large" else "ry-input-label-small"
324+ , layoutId: M.layoutId "ry-input-label"
325+ , css:
326+ if props.renderLargeLabel then
327+ Style.labelLarge { leftIconWidth: leftIconBbox <#> _.width, inputWidth: inputBbox.width }
328+ else
329+ Style.labelSmall
330+ , layout: M.layout true
331+ , transition: M.transition { duration: 0.18, ease: "easeOut" }
332+ , _data:
333+ Object.fromHomogeneous
334+ { "has-focus": show props.isFocussed
335+ , "invalid": show props.isInvalid
336+ , "required": show props.isRequired
337+ }
338+ , initial: M.initial false
339+ }
340+ /> [ labelSpan [ R.text $ NonEmptyString.toString props.labelText ] ]
252341 ]
342+ ]
253343
254344password ∷ ∀ p. ReactComponent { | p }
255345password =
@@ -298,16 +388,16 @@ password =
298388 let leading = props.leading ?|| mempty
299389 pure
300390 $ div
301− </* { className: "ry-input-wrapper"
302− , css: Style.inputWrapper
303− }
391+ </* { className: "ry-input-wrapper", css: Style.inputContainer props }
304392 /> [ leading
305− , emotionInput
306− ref
307− props { type = HTMLInput.toString <$> props.type # unsafeUnOptional }
308− { className: "ry-input"
309− , css: Style.input
310− , type: if hidePassword then "password" else "text"
311− }
393+ , R.div' </ {}
394+ /> [ emotionInput
395+ ref
396+ props { type = HTMLInput.toString <$> props.type # unsafeUnOptional }
397+ { className: "ry-input"
398+ , css: Style.input
399+ , type: if hidePassword then "password" else "text"
400+ }
401+ ]
312402 , trailing
313403 ]