@@ -3,26 +3,33 @@ module Yoga.Block.Atom.Toggle.View (component, MandatoryProps, Props, PropsF) wh
33import Yoga.Prelude.View
44import Color as Color
55import Data.Interpolate (i)
6+import Data.Maybe (isNothing)
67import Effect.Class.Console as Console
8+import Effect.Unsafe (unsafePerformEffect)
79import Foreign.Object as Object
810import Framer.Motion as Motion
11+import Partial.Unsafe (unsafeCrashWith)
912import React.Basic.DOM (css)
1013import React.Basic.DOM as R
1114import React.Basic.Emotion as Emotion
15+import React.Basic.Hooks (reactComponent)
1216import React.Basic.Hooks as React
17+import Yoga.Block.Atom.Icon as Icon
1318import Yoga.Block.Atom.Toggle.Style as Style
19+import Yoga.Block.Atom.Toggle.Types (TogglePosition(..), flipToggle)
1420import Yoga.Block.Container.Style (colour)
21+import Yoga.Block.Icon.SVG as SVGIcon
1522
1623type PropsF f =
1724 ( className ∷ f String
18− , on ∷ f JSX
19− , off ∷ f JSX
25+ , left ∷ f JSX
26+ , right ∷ f JSX
2027 | Style.Props f (MandatoryProps InputProps)
2128 )
2229
2330type MandatoryProps r =
24− ( value ∷ Boolean
25− , onToggle ∷ Boolean -> Effect Unit
31+ ( togglePosition ∷ TogglePosition
32+ , setTogglePosition ∷ TogglePosition -> Effect Unit
2633 | r
2734 )
2835
@@ -32,7 +39,7 @@ data TappingState
3239
3340data DragState
3441 = NotDragging
35− | Dragging { startX ∷ Number }
42+ | Dragging { startX ∷ Number, currentX ∷ Number }
3643 | DragDone { startX ∷ Number, endX ∷ Number }
3744
3845derive instance eqDragState ∷ Eq DragState
@@ -40,8 +47,8 @@ derive instance eqDragState ∷ Eq DragState
4047instance showDragState ∷ Show DragState where
4148 show = case _ of
4249 NotDragging -> "NotDragging"
43− Dragging x -> "Dragging " <> (show x)
44− DragDone x -> "DragDone " <> (show x)
50+ Dragging x -> "Dragging " <> show x
51+ DragDone x -> "DragDone " <> show x
4552
4653type Props =
4754 PropsF Id
@@ -57,147 +64,213 @@ rawComponent =
5764 mkForwardRefComponent "Toggle" do
5865 \(props ∷ { | PropsOptional }) ref -> React.do
5966 let disabled = props.disabled
60− tapState <- useRef TapNotAllowed
61− dragState /\ setDragState <- React.useState' NotDragging
62− maxLeft /\ setMaxLeft <- useState' 0.0
6367 buttonRef <- useRef null
6468 toggleRef <- useRef null
65− leftState /\ setLeftState <- React.useState' 0.0
66− let
67− getWidth aRef = do
68− bbox <- getBoundingBoxFromRef aRef
69− pure $ bbox <#> _.width # fromMaybe 0.0
70− buttonWidth = getWidth buttonRef
71− toggleWidth = getWidth toggleRef
72− useLayoutEffectAlways do
73− when (maxLeft == 0.0) do
74− bw <- buttonWidth
75− tw <- toggleWidth
76− setMaxLeft (bw - tw)
77− mempty
78− let
79− toggleVariants =
80− { off: { x: 0.0 }
81− , on: { x: maxLeft }
82− }
83− toggleVariant = Motion.makeVariantLabels toggleVariants
84− buttonVariants =
85− { off: { backgroundColor: (Emotion.str <<< Color.cssStringRGBA <$> props.backgroundOn) ?|| Emotion.str colour.inputBackground }
86− , on: { backgroundColor: (Emotion.str <<< Color.cssStringRGBA <$> props.backgroundOff) ?|| Emotion.str colour.success }
87− }
88− buttonVariant = Motion.makeVariantLabels buttonVariants
89− hasFocus /\ setHasFocus <- useState' false
69+ tapState <- useRef TapNotAllowed
70+ dragState /\ setDragState <- React.useState' NotDragging
9071 useEffect dragState do
9172 case dragState of
9273 NotDragging -> mempty
93− Dragging { startX } -> writeRef tapState TapNotAllowed
74+ Dragging { startX, currentX } -> do
75+ writeRef tapState TapNotAllowed
9476 DragDone { startX, endX } -> do
9577 maybeBbox <- getBoundingBoxFromRef buttonRef
9678 for_ maybeBbox \bbox -> do
9779 if endX - startX <= (bbox.left - startX) + (bbox.width / 2.0) then do
98− props.onToggle false
80+ props.setTogglePosition ToggleIsLeft
9981 else do
100− props.onToggle true
82+ props.setTogglePosition ToggleIsRight
10183 mempty
102− pure
103− $ styled Motion.button
84+ let
85+ buttonVariants =
86+ { left: { backgroundColor: (Emotion.str <<< Color.cssStringRGBA <$> props.backgroundLeft) ?|| Emotion.str colour.inputBackground }
87+ , right: { backgroundColor: (Emotion.str <<< Color.cssStringRGBA <$> props.backgroundRight) ?|| Emotion.str colour.success }
88+ }
89+ buttonVariant = Motion.makeVariantLabels buttonVariants
90+ -- components
91+ let
92+ result =
93+ container
94+ [ animateTextPresence
95+ [ textContainer
96+ [ textOnContainer
97+ [ guard (props.togglePosition == ToggleIsRight)
98+ $ textOn
99+ [ props.left
100+ ?|| el_ Icon.component { icon: SVGIcon.on, stroke: Style.successTextColour }
101+ ]
102+ ]
103+ , textOffContainer
104+ [ guard (props.togglePosition == ToggleIsLeft)
105+ $ textOff
106+ [ props.right
107+ ?|| el_ Icon.component { icon: SVGIcon.off, stroke: Style.disabledTextColour }
108+ ]
109+ ]
110+ ]
111+ ]
112+ , toggle
113+ ]
114+ container =
115+ styled Motion.button
104116 { className: "ry-toggle"
105− , css: Style.button <> guard props.disabled Style.inputDisabled
106− , onFocus: handler_ $ setHasFocus true
107− , onBlur: handler_ $ setHasFocus false
117+ , css: Style.button
108118 , transition: Motion.transition { type: "tween", duration: 0.33, ease: "easeOut" }
109119 , variants: Motion.variants buttonVariants
110− , animate: Motion.animate if props.value then buttonVariant.on else buttonVariant.off
111− , value: show props.value
112− , onClick: handler preventDefault \_ -> props.onToggle (not props.value)
120+ , animate:
121+ Motion.animate case props.togglePosition of
122+ ToggleIsRight -> buttonVariant.right
123+ ToggleIsLeft -> buttonVariant.left
124+ , value: show props.togglePosition
125+ , onClick: handler preventDefault \_ -> props.setTogglePosition (flipToggle props.togglePosition)
113126 , style: props.style
114127 , _data: Object.singleton "testid" "toggle-testid"
115128 , role: "switch"
116129 , _aria: Object.singleton "checked" "switch"
117130 , ref: buttonRef
118131 }
119− [ styled R.div'
120− { className: "ry-toggle-text"
121− , css: Style.toggleTextContainer
122− }
123− [ styled R.div'
124− { className: "ry-toggle-text-on"
125− , css: Style.toggleText
126− }
127− [ el Motion.animatePresence {}
128− [ guard (props.value)
129− $ styled Motion.div
130− { className: "ry-toggle-text-on-container"
131− , css: Style.toggleOnText
132− , key: "ry-toggle-text-on-container"
133− , initial: Motion.initial $ css { scale: 0, opacity: 0 }
134− , animate: Motion.animate $ css { scale: 1, opacity: 1 }
135− , exit: Motion.exit $ css { scale: 0, opacity: 0 }
136− }
137− [ props.on ?|| R.text "I" ]
138− ]
139− ]
140− , styled R.div'
141− { className: "ry-toggle-text-off"
142− , css: Style.toggleText
143− }
144− [ el Motion.animatePresence {}
145− [ guard (not props.value)
146− $ styled Motion.div
147− { className: "ry-toggle-text-on-container"
148− , css: Style.toggleOnText
149− , key: "ry-toggle-text-on-container"
150− , initial: Motion.initial $ css { scale: 0, opacity: 0 }
151− , animate: Motion.animate $ css { scale: 1, opacity: 1 }
152− , exit: Motion.exit $ css { scale: 0, opacity: 0 }
153− }
154− [ props.off ?|| R.text "O" ]
155− ]
156− ]
157− ]
158− , styled Motion.div
132+ textContainer =
133+ styled R.div'
134+ { className: "ry-toggle-text"
135+ , css: Style.toggleTextContainer
136+ }
137+ textOnContainer =
138+ styled R.div'
139+ { className: "ry-toggle-text-on"
140+ , css: Style.toggleText
141+ }
142+ textOffContainer =
143+ styled R.div'
144+ { className: "ry-toggle-text-off"
145+ , css: Style.toggleText
146+ }
147+ textOn =
148+ el Motion.div
149+ { className: "ry-toggle-text-container"
150+ , key: "ry-toggle-text-on-container"
151+ , initial: Motion.initial $ css { scale: 0, opacity: 0 }
152+ , animate: Motion.animate $ css { scale: 1, opacity: 1 }
153+ , exit: Motion.exit $ css { scale: 0, opacity: 0 }
154+ }
155+ textOff =
156+ el Motion.div
157+ { className: "ry-toggle-text-container"
158+ , key: "ry-toggle-text-off-container"
159+ , initial: Motion.initial $ css { scale: 0, opacity: 0 }
160+ , animate: Motion.animate $ css { scale: 1, opacity: 1 }
161+ , exit: Motion.exit $ css { scale: 0, opacity: 0 }
162+ }
163+ animateTextPresence = el Motion.animatePresence {}
164+ toggle =
165+ el_ toggleCircle
166+ { buttonRef
167+ , toggleRef
168+ , togglePosition: props.togglePosition
169+ , setTogglePosition: props.setTogglePosition
170+ , dragState
171+ , setDragState
172+ , tapState
173+ }
174+ pure result
175+
176+toggleCircle ∷
177+ ReactComponent
178+ { setTogglePosition ∷ TogglePosition -> Effect Unit
179+ , toggleRef ∷ NodeRef
180+ , buttonRef ∷ NodeRef
181+ , togglePosition ∷ TogglePosition
182+ , dragState ∷ DragState
183+ , setDragState ∷ DragState -> Effect Unit
184+ , tapState ∷ Ref TappingState
185+ }
186+toggleCircle =
187+ unsafePerformEffect
188+ $ reactComponent "ToggleCircle" do
189+ \( { togglePosition
190+ , setTogglePosition
191+ , toggleRef
192+ , buttonRef
193+ , dragState
194+ , setDragState
195+ , tapState
196+ }
197+ ) -> React.do
198+ maxLeft /\ setMaxLeft <- useState' Nothing
199+ useEffectAlways do
200+ when (maxLeft == Nothing) do
201+ runMaybeT_ do
202+ b <- getBoundingBoxFromRef buttonRef # MaybeT
203+ t <- getBoundingBoxFromRef toggleRef # MaybeT
204+ let ml = b.width - t.width - (2.0 * (t.left - b.left))
205+ setMaxLeft (Just ml) # lift
206+ mempty
207+ let
208+ toggleVariants =
209+ { left: { x: 0.0 }
210+ , right: { x: maxLeft # fromMaybe 0.0 }
211+ }
212+ toggleVariant = Motion.makeVariantLabels toggleVariants
213+ pure
214+ $ styledLeaf Motion.div
159215 { className: "ry-toggle-toggle"
160− , css: Style.theToggle props
161216 , layout: Motion.layout true
162217 , onClick: handler stopPropagation mempty
218+ , onTouchStart: handler stopPropagation mempty
219+ , onTouchEnd: handler stopPropagation mempty
220+ , css: Style.theToggle
163221 , drag: Motion.drag "x"
164222 , dragMomentum: Motion.dragMomentum false
223+ , key: if isNothing maxLeft then "initialising" else "ready"
165224 , dragElastic: Motion.dragElastic false
166225 , dragConstraints:
167226 Motion.dragConstraints
168− { left: if props.value then negate maxLeft else zero
169− , right: if props.value then zero else maxLeft
227+ { left:
228+ case togglePosition of
229+ ToggleIsLeft -> zero
230+ ToggleIsRight -> negate (maxLeft # fromMaybe 0.0)
231+ , right:
232+ case togglePosition of
233+ ToggleIsRight -> zero
234+ ToggleIsLeft -> maxLeft # fromMaybe 0.0
170235 }
171236 , variants: Motion.variants toggleVariants
172237 , transition: Motion.transition { type: "tween", duration: 0.33, ease: "easeOut" }
173− , whileTap: Motion.prop $ css { scale: 0.85, transition: { type: "tween", duration: 0.10, ease: "easeInOut" } }
238+ , whileTap: Motion.prop $ css { scale: 1.1, transition: { type: "tween", duration: 0.10, ease: "easeInOut" } }
174239 , onTapStart: Motion.onTapStart \_ _ -> writeRef tapState TapAllowed
175240 , onTap:
176241 Motion.onTap \_ pi -> do
177242 ts <- readRef tapState
178243 case ts of
179− TapAllowed -> props.onToggle $ not props.value
244+ TapAllowed -> setTogglePosition (flipToggle togglePosition)
180245 _ -> mempty
181246 mempty
182247 , onTapCancel:
183248 Motion.onTapCancel \_ pi -> do
184249 writeRef tapState TapNotAllowed
185250 mempty
186− , animate: Motion.animate if props.value then toggleVariant.on else toggleVariant.off
251+ , initial: Motion.initial false
252+ , animate:
253+ Motion.animate case togglePosition of
254+ ToggleIsLeft -> toggleVariant.left
255+ ToggleIsRight -> toggleVariant.right
187256 , onDragStart:
188257 Motion.onDragStart \_ pi -> do
189258 maybeBBox <- getBoundingBoxFromRef toggleRef
190259 let x = maybeBBox <#> \bbox -> bbox.left + (bbox.width / 2.0)
191− setDragState $ Dragging { startX: x # fromMaybe pi.point.x }
260+ setDragState $ Dragging { startX: x # fromMaybe pi.point.x, currentX: pi.point.x }
261+ , onDrag:
262+ Motion.onDrag \_ pi -> do
263+ case dragState of
264+ Dragging { startX } -> do
265+ setDragState $ Dragging { startX, currentX: pi.point.x }
266+ other -> Console.warn $ i "Unexpected drag state " (show other) " in onDragEvent"
192267 , onDragEnd:
193268 Motion.onDragEnd \_ pi -> do
194269 case dragState of
195270 Dragging { startX } -> do
196271 maybeBBox <- getBoundingBoxFromRef toggleRef
197272 let x = maybeBBox <#> \bbox -> bbox.left + (bbox.width / 2.0)
198− setDragState (DragDone { startX, endX: x # fromMaybe pi.point.x })
273+ setDragState (DragDone { startX, endX: x # fromMaybe' \_ -> unsafeCrashWith "shit" })
199274 other -> Console.warn $ i "Unexpected drag state " (show other) " in onDragEvent"
200275 , ref: toggleRef
201276 }
202− []
203− ]