Add popper

This commit is contained in:
Mark Eibes 2020-12-17 22:53:10 +01:00
parent 93cb3b5fd5
commit 774db186ef
28 changed files with 1107 additions and 268 deletions

View File

@ -15,7 +15,6 @@
"@testing-library/react": "^11.0.4",
"@testing-library/user-event": "^12.1.7",
"babel-loader": "^8.1.0",
"source-map": "GerHobbelt/source-map#patch-8",
"clean-webpack-plugin": "^3.0.0",
"compression-webpack-plugin": "^6.0.3",
"framer-motion": "^2.7.0",
@ -25,6 +24,7 @@
"purescript": "^0.13.8",
"react-refresh": "^0.8.3",
"rimraf": "^3.0.2",
"source-map": "GerHobbelt/source-map#patch-8",
"source-map-loader": "^1.1.2",
"spago": "^0.16.0",
"svg2psreact": "^2.1.0",
@ -50,8 +50,10 @@
},
"dependencies": {
"@emotion/core": "^10.0.28",
"@popperjs/core": "^2.6.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-popper": "^2.2.4",
"react-syntax-highlighter": "^15.3.1"
}
}

View File

@ -41,7 +41,7 @@ zip (TwoOrMore as) (TwoOrMore bs) =
}
findIndex ∷ ∀ a. (a -> Boolean) -> TwoOrMore a -> Maybe Int
findIndex f (TwoOrMore { first, second, rest }) =
findIndex f (TwoOrMore { first, second, rest }) = do
if f first then
Just 0
else

View File

@ -1,6 +1,7 @@
const framerMotion = require("framer-motion")
exports.divImpl = framerMotion.motion.div
exports.buttonImpl = framerMotion.motion.button
exports.h1Impl = framerMotion.motion.h1
exports.svgImpl = framerMotion.motion.svg
exports.pathImpl = framerMotion.motion.path

View File

@ -1,6 +1,21 @@
module Framer.Motion
( animate
, makeVariantLabels
, WhileTap
, whileTap
, callback
, class EffectFnMaker
, toEffectFn
, OnTap
, TapInfo
, OnTapStart
, OnTapEnd
, onTapStart
, onTapEnd
, onTapCancel
, onTap
, OnTapCancel
, TargetAndTransition
, MakeVariantLabel
, div
, h1
@ -14,6 +29,7 @@ module Framer.Motion
, prop
, Drag
, DragMomentum
, button
, dragMomentum
, DragPropagation
, DragElastic
@ -63,13 +79,13 @@ import Prelude
import Data.Nullable (Nullable)
import Data.Symbol (class IsSymbol, SProxy, reflectSymbol)
import Effect (Effect)
import Effect.Uncurried (EffectFn2, mkEffectFn2)
import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, mkEffectFn2)
import Foreign.Object (Object)
import Heterogeneous.Mapping (class HMapWithIndex, class MappingWithIndex, hmapWithIndex)
import Literals.Undefined (Undefined)
import Prim.Row (class Nub, class Union)
import React.Basic (JSX, ReactComponent)
import React.Basic.DOM (CSS, Props_div, Props_h1, css)
import React.Basic.DOM (CSS, Props_div, Props_h1, Props_button, css)
import React.Basic.DOM.Internal (SharedSVGProps)
import React.Basic.DOM.SVG (Props_svg, Props_rect, Props_path)
import React.Basic.Events (EventHandler)
@ -85,6 +101,8 @@ import Yoga.Block.Internal (Id)
foreign import divImpl ∷ ∀ a. ReactComponent { | a }
foreign import buttonImpl ∷ ∀ a. ReactComponent { | a }
foreign import h1Impl ∷ ∀ a. ReactComponent { | a }
foreign import svgImpl ∷ ∀ a. ReactComponent { | a }
@ -171,14 +189,63 @@ type OnDragEnd =
type OnDrag =
(EffectFn2 Event PanInfo Unit |+| Undefined)
type WhileTap =
TargetAndTransition |+| String |+| Undefined
type OnTap =
(EffectFn2 Event TapInfo Unit |+| Undefined)
type OnTapStart =
(EffectFn2 Event TapInfo Unit |+| Undefined)
type OnTapEnd =
(EffectFn2 Event TapInfo Unit |+| Undefined)
type OnTapCancel =
(EffectFn2 Event TapInfo Unit |+| Undefined)
onTapStart ∷ (Event -> TapInfo -> Effect Unit) -> OnTapStart
onTapStart = cast <<< toEffectFn
onTapEnd ∷ (Event -> TapInfo -> Effect Unit) -> OnTapEnd
onTapEnd = cast <<< toEffectFn
onTap ∷ (Event -> TapInfo -> Effect Unit) -> OnTap
onTap fn2 = cast (mkEffectFn2 fn2)
onTapCancel ∷ (Event -> TapInfo -> Effect Unit) -> OnTap
onTapCancel fn2 = cast (mkEffectFn2 fn2)
type TapInfo =
{ x ∷ Number, y ∷ Number }
-- Can contain "transition" and "transitionEnd"
type TargetAndTransition =
CSS
type DragPropagation =
Boolean |+| Undefined
whileTap ∷ ∀ c. Castable c WhileTap => c -> WhileTap
whileTap = cast
class EffectFnMaker fn effectFn | fn -> effectFn where
toEffectFn ∷ fn -> effectFn
instance callbackableEffectFn2 ∷ EffectFnMaker (a -> b -> Effect c) (EffectFn2 a b c) where
toEffectFn = mkEffectFn2
instance callbackableEffectFn1 ∷ EffectFnMaker (a -> Effect b) (EffectFn1 a b) where
toEffectFn = mkEffectFn1
callback ∷ ∀ a c f. Castable c a => EffectFnMaker f c => f -> a
callback = cast <<< toEffectFn
onDragStart ∷ (Event -> PanInfo -> Effect Unit) -> OnDragStart
onDragStart fn2 = cast (mkEffectFn2 fn2)
onDragStart = cast <<< toEffectFn
onDragEnd ∷ (Event -> PanInfo -> Effect Unit) -> OnDragEnd
onDragEnd fn2 = cast (mkEffectFn2 fn2)
onDragEnd = cast <<< toEffectFn
onDrag ∷ (Event -> PanInfo -> Effect Unit) -> OnDrag
onDrag fn2 = cast (mkEffectFn2 fn2)
@ -198,6 +265,11 @@ type MotionPropsF f r =
, transition ∷ f Transition
, layout ∷ f Layout
, layoutId ∷ f String |+| Undefined
, whileTap ∷ f WhileTap
, onTap ∷ f OnTap
, onTapStart ∷ f OnTapStart
, onTapEnd ∷ f OnTapEnd
, onTapCancel ∷ f OnTapCancel
, exit ∷ f Exit
| r
)
@ -249,6 +321,9 @@ makeVariantLabels = hmapWithIndex MakeVariantLabel
div ∷ ∀ attrs attrs_. Union attrs attrs_ (MotionProps + Props_div) => ReactComponent { | attrs }
div = divImpl
button ∷ ∀ attrs attrs_. Union attrs attrs_ (MotionProps + Props_button) => ReactComponent { | attrs }
button = buttonImpl
svg ∷ ∀ attrs attrs_. Union attrs attrs_ (MotionProps + (SharedSVGProps Props_svg)) => ReactComponent { | attrs }
svg = svgImpl

View File

@ -6,4 +6,5 @@ exports.setImpl = v => render => mv => () => {
mv.set(v, render)
}
exports.isAnimating = mv => () => { return mv.isAnimating() }
exports.stop = mv => () => { return mv.stop() }
exports.stop = mv => () => { return mv.stop() }
exports.onChangeImpl = callback => mv => { return () => mv.onChange(callback); }

View File

@ -2,7 +2,7 @@ module MotionValue where
import Prelude
import Effect (Effect)
import Effect.Uncurried (EffectFn1, runEffectFn1)
import Effect.Uncurried (EffectFn1, mkEffectFn1, runEffectFn1)
import React.Basic.Hooks (Hook, unsafeHook)
foreign import data MotionValue ∷ Type -> Type
@ -21,9 +21,14 @@ setButDoNotRender v = setImpl v false
set ∷ ∀ a. a -> MotionValue a -> Effect Unit
set v = setImpl v true
foreign import isAnimating ∷ ∀ a. (MotionValue a) -> Effect Boolean
foreign import isAnimating ∷ ∀ a. MotionValue a -> Effect Boolean
foreign import stop ∷ ∀ a. (MotionValue a) -> Effect Unit
foreign import stop ∷ ∀ a. MotionValue a -> Effect Unit
foreign import onChangeImpl ∷ ∀ a. EffectFn1 a Unit -> MotionValue a -> Effect (Effect Unit)
onChange ∷ ∀ a. (a -> Effect Unit) -> MotionValue a -> Effect (Effect Unit)
onChange = mkEffectFn1 >>> onChangeImpl
useMotionValue ∷ ∀ a. a -> Hook (UseMotionValue a) (MotionValue a)
useMotionValue initialValue =

View File

@ -0,0 +1,3 @@
const reactPopper = require('react-popper')
exports.usePopperImpl = reactPopper.usePopper

View File

@ -0,0 +1,64 @@
module React.Basic.Popper.Hook where
import Prelude
import Data.String as String
import Data.Tuple (Tuple(..))
import Effect.Uncurried (EffectFn3, runEffectFn3)
import Foreign.Object (Object)
import Foreign.Object as Object
import Prim.Row (class Union)
import React.Basic.DOM (CSS)
import Unsafe.Coerce (unsafeCoerce)
import Yoga.Prelude.View (Hook, NodeRef, unsafeHook)
foreign import data UsePopper ∷ Type -> Type -> Type
type ReferenceElement =
NodeRef
type PopperElement =
NodeRef
type ArrowElement =
NodeRef
type DataAttributes =
{ popper ∷ Object String, arrow ∷ Object String }
type Styles =
{ popper ∷ CSS, arrow ∷ CSS }
type PopperData =
{ styles ∷ Styles, attributes ∷ DataAttributes }
foreign import data Modifier ∷ Type
modifierArrow ∷ ArrowElement -> Modifier
modifierArrow element = unsafeCoerce { name: "arrow", options: { element } }
modifierOffset ∷ { x ∷ Number, y ∷ Number } -> Modifier
modifierOffset { x, y } = unsafeCoerce { name: "offset", options: { offset: [ x, y ] } }
type Options =
( modifiers ∷ Array Modifier
, strategy ∷ String
)
foreign import usePopperImpl ∷ ∀ opts. EffectFn3 ReferenceElement PopperElement { | opts } PopperData
toDataAttributes ∷ Object String -> Object String
toDataAttributes o = Object.toArrayWithKey (Tuple <<< dropPrefix) o # Object.fromFoldable
where
dropPrefix = String.drop (String.length "data-")
usePopper ∷ ∀ opts opts_. Union opts opts_ Options => ReferenceElement -> PopperElement -> { | opts } -> Hook (UsePopper { | opts }) PopperData
usePopper elemRef popperRef opts =
unsafeHook do
result <- runEffectFn3 usePopperImpl elemRef popperRef opts
pure
$ result
{ attributes
{ popper = toDataAttributes result.attributes.popper
, arrow = toDataAttributes result.attributes.arrow
}
}

View File

@ -0,0 +1,205 @@
module React.Basic.Popper.Story where
import Prelude
import Color as Color
import Data.Maybe (Maybe(..))
import Data.Nullable (null)
import Data.Tuple.Nested ((/\))
import Effect (Effect)
import Effect.Uncurried (mkEffectFn1)
import Effect.Unsafe (unsafePerformEffect)
import Framer.Motion (onTap)
import Framer.Motion as Motion
import React.Basic (JSX, element, fragment)
import React.Basic.DOM as R
import React.Basic.Emotion as E
import React.Basic.Hooks as React
import React.Basic.Popper.Hook (modifierArrow, modifierOffset, usePopper)
import Unsafe.Coerce (unsafeCoerce)
import Yoga.Block as Block
import Yoga.Block.Atom.Toggle as Toggle
import Yoga.Block.Container.Style (DarkOrLightMode(..))
import Yoga.Block.Container.Style as Styles
import Yoga.Block.Internal.CSS (nest)
import Yoga.Prelude.View (NodeRef, el, styled, styledLeaf)
default ∷
{ decorators ∷ Array (Effect JSX -> JSX)
, title ∷ String
}
default =
{ title: "Popper"
, decorators:
[ \storyFn ->
R.div_
[ element E.global { styles: Styles.global }
, unsafePerformEffect storyFn
]
]
}
popper ∷ Effect JSX
popper = do
example <- mkBasicExample
pure
$ fragment
[ R.div_
[ R.h2_ [ R.text "Some Popper" ]
, element example {}
]
]
where
nullRef ∷ NodeRef
nullRef = unsafeCoerce null
mkBasicExample =
React.reactComponent "Popper example" \p -> React.do
referenceElement /\ setReferenceElement <- React.useState' nullRef
popperElement /\ setPopperElement <- React.useState' nullRef
arrowElement /\ setArrowElement <- React.useState' nullRef
{ styles, attributes } <-
usePopper referenceElement popperElement
{ modifiers:
[ modifierArrow arrowElement
, modifierOffset { x: 0.0, y: 8.0 }
]
}
pure
$ fragment
$ [ element R.button'
{ type: "button"
, ref: unsafeCoerce (mkEffectFn1 setReferenceElement)
, children: [ R.text "Reference element" ]
}
, styled R.div'
{ className: "popper-element"
, css:
E.css
{ background: E.str "darkslateblue"
, borderRadius: E.str "8px"
, display: E.str "block"
, padding: E.str "4px 8px"
, "&[data-popper-placement^='top'] > .popper-arrow":
nest { bottom: E.str "-4px" }
, "&[data-popper-placement^='bottom'] > .popper-arrow":
nest { top: E.str "-4px" }
, "&[data-popper-placement^='left'] > .popper-arrow":
nest { right: E.str "-4px" }
, "&[data-popper-placement^='right'] > .popper-arrow":
nest { left: E.str "-4px" }
}
, ref: unsafeCoerce (mkEffectFn1 setPopperElement)
, style: styles.popper
, _data: attributes.popper
}
[ R.text "Popper Element"
, styledLeaf R.div'
{ className: "popper-arrow"
, id: "arrow"
, css:
E.css
{ position: E.str "absolute"
, width: E.str "8px"
, height: E.str "8px"
, zIndex: E.str "-1"
, "&::before":
nest
{ position: E.str "absolute"
, width: E.str "8px"
, height: E.str "8px"
, zIndex: E.str "-1"
, content: E.str "''"
, transform: E.str "rotate(45deg)"
, background: E.str "darkslateblue"
}
}
, ref: unsafeCoerce (mkEffectFn1 setArrowElement)
, style: styles.arrow
, _data: attributes.arrow
}
]
]
animatedPopper ∷ Effect JSX
animatedPopper = do
example <- mkBasicExample
pure
$ fragment
[ R.div_
[ R.h2_ [ R.text "Some Popper" ]
, element example {}
]
]
where
nullRef ∷ NodeRef
nullRef = unsafeCoerce null
mkBasicExample =
React.reactComponent "Popper example" \p -> React.do
referenceElement /\ setReferenceElement <- React.useState' nullRef
popperElement /\ setPopperElement <- React.useState' nullRef
arrowElement /\ setArrowElement <- React.useState' nullRef
{ styles, attributes } <-
usePopper referenceElement popperElement
{ modifiers:
[ modifierArrow arrowElement
, modifierOffset { x: 0.0, y: 8.0 }
]
}
pure
$ fragment
$ [ element R.div'
{ ref: unsafeCoerce (mkEffectFn1 setReferenceElement)
, children: [ R.text "Reference element" ]
}
, el Motion.animatePresence {}
[ styled Motion.div
{ className: "popper-element"
, key: "heinz"
, css:
E.css
{ background: E.str "darkslateblue"
, borderRadius: E.str "8px"
, display: E.str "block"
, padding: E.str "4px 8px"
, "&[data-popper-placement^='top'] > .popper-arrow":
nest { bottom: E.str "-4px" }
, "&[data-popper-placement^='bottom'] > .popper-arrow":
nest { top: E.str "-4px" }
, "&[data-popper-placement^='left'] > .popper-arrow":
nest { right: E.str "-4px" }
, "&[data-popper-placement^='right'] > .popper-arrow":
nest { left: E.str "-4px" }
}
, ref: unsafeCoerce (mkEffectFn1 setPopperElement)
, style: styles.popper
, _data: attributes.popper
}
[ R.text "Popper Element"
, styledLeaf R.div'
{ className: "popper-arrow"
, id: "arrow"
, css:
E.css
{ position: E.str "absolute"
, width: E.str "8px"
, height: E.str "8px"
, zIndex: E.str "-1"
, "&::before":
nest
{ position: E.str "absolute"
, width: E.str "8px"
, height: E.str "8px"
, zIndex: E.str "-1"
, content: E.str "''"
, transform: E.str "rotate(45deg)"
, background: E.str "darkslateblue"
}
}
, ref: unsafeCoerce (mkEffectFn1 setArrowElement)
, style: styles.arrow
, _data: attributes.arrow
}
]
]
]

View File

@ -28,6 +28,7 @@ el_ x props = Hooks.element x props
styled ∷
∀ props.
Lacks "children" props =>
Lacks "keyp" props =>
ReactComponent { className ∷ String, children ∷ Array JSX | props } ->
{ className ∷ String, css ∷ Emotion.Style | props } -> Array JSX -> JSX
styled x props children = Emotion.element x (Record.insert (SProxy ∷ SProxy "children") children props)

View File

@ -22,7 +22,7 @@ centre = Centre.component
cluster ∷ ∀ p q. Union p q Cluster.Props => ReactComponent { | p }
cluster = Cluster.component
container ∷ ReactComponent Container.Props
container ∷ ∀ p q. Union p q Container.Props => ReactComponent { | p }
container = Container.component
imposter ∷ ∀ p q. Union p q Imposter.Props => ReactComponent { | p }
@ -34,7 +34,7 @@ modal = Modal.component
range ∷ ∀ p q. Union p q Range.Props => ReactComponent { | p }
range = Range.component
segmented ∷ ReactComponent Segmented.ComponentProps
segmented ∷ ReactComponent Segmented.Props
segmented = Segmented.component
sidebar ∷ ∀ p q. Union p q Sidebar.Props => ReactComponent { | p }

View File

@ -2,7 +2,6 @@ module Yoga.Block.Atom.Range.Style where
import Yoga.Prelude.Style
import Data.Interpolate (i)
import React.Basic.Emotion (inlineBlock)
import Yoga.Block.Container.Style (colour)
type Props f r =

View File

@ -2,4 +2,4 @@ module Yoga.Block.Atom.Segmented
( module Yoga.Block.Atom.Segmented.View
) where
import Yoga.Block.Atom.Segmented.View (component, Props, ComponentProps)
import Yoga.Block.Atom.Segmented.View (component, Props, Item)

View File

@ -15,6 +15,7 @@ cluster =
{ overflow: auto
, flex: str "1"
, display: str "inline-flex"
, userSelect: none
}
segmented ∷ Style
@ -30,27 +31,25 @@ segmented = styles
, minHeight: str "min-content"
, background: str colour.background10
, boxShadow: str "inset 0 1 0 rgba(0,0,0,0.1)"
, borderRadius: str "10px"
, borderRadius: var "--s-1"
, border: str $ i "1px solid " colour.background15
, borderBottom: str $ i "1px solid " colour.background20
, padding: _0
, overflow: scroll
, userSelect: none
}
activeElement ∷ Style
activeElement =
css
{ position: absolute
, borderRadius: str "8px"
, borderRadius: str "calc(var(--s-1) * 0.75)"
, background: str colour.interfaceBackground
, border: str $ i "1px solid " colour.interfaceBackgroundShadow
, borderTop: str $ i "1px solid " colour.interfaceBackgroundHighlight
, borderBottom: str $ i "1px solid " colour.interfaceBackgroundShadow
, boxShadow: str "0 0 1px rgba(0,0,0,0.55)"
, margin: _0
, boxShadow: str "0 1px 2px rgba(20,20,20,0.67)"
, padding: _0
, zIndex: str "3"
, cursor: str "grab"
}
button ∷ { isFirst ∷ Boolean, isLast ∷ Boolean } -> Style
@ -60,17 +59,21 @@ button { isFirst, isLast } =
, appearance: none
, color: str colour.text
, border: none
, margin: _0
, padding: _0
, fontSize: str "var(--s0)"
, marginLeft: str if isFirst then "1px" else "0"
, marginRight: str if isLast then "1px" else "0"
, boxSizing: borderBox
, zIndex: str "3"
, minWidth: var "--s3"
, "&:active": nest { outline: str "0" }
, "&:focus": nest { outline: none }
, "&:focus > .ry-segmented-button__content":
nest
{ borderColor: str colour.highlight
, transition: str "all 0.083s ease 0.083s"
}
, userSelect: none
}
buttonContent ∷ { isFirst ∷ Boolean, isLast ∷ Boolean } -> Style
@ -78,16 +81,18 @@ buttonContent { isFirst, isLast } =
css
{ "&:active": nest { outline: str "0" }
, "&:focus": nest { outline: none }
, paddingTop: str "1px"
, paddingBottom: str "1px"
, paddingLeft: str if isFirst then edgePadding else inBetweenPadding
, paddingRight: str if isLast then edgePadding else inBetweenPadding
, borderRadius: str "8px"
, borderRadius: str "calc(var(--s-1) * 0.75)"
, border: str $ i borderSize " solid transparent"
, display: flex
, alignItems: center
, outlineRight: str "1px solid red"
, justifyContent: center
, margin: _0
, minHeight: _100percent
, overflow: visible
, userSelect: none
}
where
borderSize = "var(--s-4)"

View File

@ -1,14 +1,7 @@
module Yoga.Block.Atom.Segmented.View (component, Props, Item, MandatoryProps, PropsF, ComponentProps) where
module Yoga.Block.Atom.Segmented.View (component, Item, Props) where
import Yoga.Prelude.View
import Control.Alt ((<|>))
import Control.Monad.Maybe.Trans (MaybeT(..), runMaybeT)
import Control.Monad.Trans.Class (lift)
import Control.MonadZero as MZ
import Data.Array as A
import Data.FoldableWithIndex (foldMapWithIndex)
import Data.FunctorWithIndex (mapWithIndex)
import Data.Maybe (fromMaybe', isJust, maybe)
import Data.Newtype (wrap)
import Data.Time.Duration (Milliseconds(..))
import Data.Traversable (traverse)
@ -17,15 +10,9 @@ import Data.TwoOrMore as TwoOrMore
import Effect.Aff (delay)
import Effect.Unsafe (unsafePerformEffect)
import Foreign.Object as Object
import Framer.Motion (VariantLabel)
import Framer.Motion as Motion
import Hooks.Key as Key
import Hooks.Scroll (useScrollPosition)
import Hooks.UseResize (useResize)
import Literals.Undefined (undefined)
import MotionValue (useMotionValue)
import MotionValue as MotionValue
import Partial.Unsafe (unsafeCrashWith)
import Math as Math
import React.Basic.DOM (css)
import React.Basic.DOM as R
import React.Basic.Emotion as E
@ -33,214 +20,23 @@ import React.Basic.Extra.Hooks.UseKeyDown (useKeyDown)
import React.Basic.Hooks (reactComponent)
import React.Basic.Hooks as React
import React.Basic.Hooks.Aff (useAff)
import Unsafe.Coerce (unsafeCoerce)
import Untagged.Castable (cast)
import Web.HTML.HTMLElement (HTMLElement, blur, focus, getBoundingClientRect)
import Web.HTML.HTMLElement as HTMLElement
import Yoga.Block.Atom.Segmented.Style as Style
type PropsF f =
(
| MandatoryProps + Style.Props f ()
)
type MandatoryProps r =
( activeItemRefs ∷ TwoOrMore (Ref (Nullable Node))
, activeItemIndex ∷ Int
, updateActiveIndex ∷ Int -> Effect Unit
| r
)
type Props =
PropsF Id
activeComponent ∷ ∀ p p_. Union (MandatoryProps p) p_ Props => ReactComponent { | MandatoryProps p }
activeComponent = rawActiveComponent
indexToVariant ∷ Int -> VariantLabel
indexToVariant = show >>> unsafeCoerce
type BBox =
{ top ∷ Number, left ∷ Number, width ∷ Number, height ∷ Number }
findOverlapping ∷ Int -> TwoOrMore BBox -> Number -> Int
findOverlapping activeIndex styles x =
fromMaybe activeIndex do
curr <- styles TwoOrMore.!! activeIndex
let fst = TwoOrMore.head styles
let lst = TwoOrMore.last styles
let inside e = (e.left < x) && (e.left + e.width) >= x
let tooFarLeft = MZ.guard (x <= fst.left + fst.width) $> 0
let tooFarRight = MZ.guard (x >= lst.left) $> TwoOrMore.length styles - 1
TwoOrMore.findIndex inside styles <|> tooFarLeft <|> tooFarRight
handleDrag ∷
{ activeItemIndex ∷ Int
, animationVariants ∷ TwoOrMore BBox
, x ∷ Number
} ->
{ left ∷ Number, width ∷ Number }
handleDrag { x, activeItemIndex, animationVariants } = do
let idx = findOverlapping activeItemIndex animationVariants x
let av = animationVariants
let firstVariant = av # TwoOrMore.head
let lastVariant = av # TwoOrMore.last
let baseVariant = av TwoOrMore.!! idx # fromMaybe' \_ -> unsafeCrashWith "shit"
let
closestVariant =
if x >= (baseVariant.left + (baseVariant.width / 2.0)) then
av TwoOrMore.!! (idx + 1) # fromMaybe lastVariant
else
av TwoOrMore.!! (idx - 1) # fromMaybe firstVariant
let
greater /\ smaller =
if baseVariant.left > closestVariant.left then
baseVariant /\ closestVariant
else
closestVariant /\ baseVariant
-- Total
let rangeStart = smaller.left + (smaller.width / 2.0)
let rangeEnd = greater.left + (greater.width / 2.0)
let range = rangeEnd - rangeStart
let ratio = ((x - rangeStart) / range)
let interpolatedWidth = (greater.width * ratio) + smaller.width * (1.0 - ratio)
-- Right
let rangeStartRight = smaller.left + smaller.width
let rangeEndRight = greater.left + greater.width
let rangeRight = rangeEndRight - rangeStartRight
let ratioRight = ((x + (interpolatedWidth / 2.0) - rangeStartRight) / rangeRight)
-- Left
let rangeStartLeft = smaller.left
let rangeEndLeft = greater.left
let rangeLeft = rangeEndLeft - rangeStartLeft
let ratioLeft = (((x - (interpolatedWidth / 2.0)) - rangeStartLeft) / rangeLeft)
-- Individual
let left = rangeStartLeft + (ratioLeft * rangeLeft)
let right = rangeStartRight + (ratioRight * rangeRight)
let width = right - left
if x < firstVariant.left then
{ left: firstVariant.left, width: firstVariant.width }
else
if x >= lastVariant.left + lastVariant.width then do
{ left: lastVariant.left, width: lastVariant.width }
else do
{ left, width }
rawActiveComponent ∷ ∀ p. ReactComponent { | p }
rawActiveComponent =
mkForwardRefComponent "SegmentedActive" do
\(props ∷ { | Props }) ref -> React.do
maybeAnimationVariants /\ setVariants <- useState' Nothing
maybeDragX /\ setDragX <- useState' Nothing
{ scrollX } <- useScrollPosition
activeLeft <- useMotionValue 0.0
activeWidth <- useMotionValue 0.0
useEffectAlways do
_ <-
runMaybeT do
rawStyles <- traverse getStyle props.activeItemRefs
let styles = rawStyles <#> \s -> s { left = s.left + scrollX }
unless (maybeAnimationVariants == Just styles) do
setVariants (Just styles) # lift
mempty
useLayoutEffect maybeDragX do
case maybeDragX, maybeAnimationVariants of
Just x, Just animationVariants -> do
let { left, width } = handleDrag { activeItemIndex: props.activeItemIndex, animationVariants, x }
activeLeft # MotionValue.set left
activeWidth # MotionValue.set width
_, _ -> mempty
mempty
let
variants ∷ Motion.Variants
variants = case maybeAnimationVariants of
Just animationVariants ->
animationVariants
# foldMapWithIndex (\i s -> Object.singleton (show i) (css s))
# Motion.variantsFromObject
Nothing -> (cast undefined) ∷ Motion.Variants
pure $ maybeAnimationVariants
# foldMap \animationVariants ->
styledLeaf Motion.div
{ css: Style.activeElement
, variants
, className: "ry-active-segmented-element"
, initial: Motion.initial (indexToVariant props.activeItemIndex)
, drag: Motion.drag "x"
, dragMomentum: Motion.dragMomentum false
, animate: Motion.animate $ indexToVariant props.activeItemIndex
, layout: Motion.layout true
, style:
css
{ left: activeLeft
, width: activeWidth
}
, onDragStart:
Motion.onDragStart \_ pi -> do
setDragX (Just pi.point.x)
, onDrag:
Motion.onDrag \_ pi -> do
when (isJust maybeDragX) $ setDragX (Just pi.point.x)
, onDragEnd:
Motion.onDragEnd \_ pi -> do
let x = maybeDragX # fromMaybe' \_ -> unsafeCrashWith "no x should not happen"
let newIdx = findOverlapping props.activeItemIndex animationVariants x
let v = animationVariants TwoOrMore.!! newIdx # fromMaybe' \_ -> unsafeCrashWith "omg"
setDragX Nothing
activeLeft # MotionValue.set v.left
activeWidth # MotionValue.set v.width
props.updateActiveIndex newIdx
, dragConstraints: Motion.dragConstraints { left: 0, right: 0 }
, dragElastic: Motion.dragElastic false
, transition:
Motion.transition
{ type: "tween", duration: if isJust maybeDragX then 0.0 else 0.167, ease: "easeOut" }
, _aria: Object.fromHomogeneous { hidden: "true" }
, ref
}
getStyle ∷
Ref (Nullable Node) ->
MaybeT Effect BBox
getStyle itemRef = do
node <- MaybeT $ readRefMaybe itemRef
htmlElement <- MaybeT $ pure $ HTMLElement.fromNode node
br <- lift $ getBoundingClientRect htmlElement
pure
{ width: br.width
, height: br.height
, left: br.left
, top: br.top
}
import Yoga.Block.Atom.Segmented.View.ActiveIndicator as ActiveIndicator
type Item =
{ id ∷ String, value ∷ String }
type ComponentProps =
type Props =
{ buttonContents ∷ TwoOrMore Item
, activeIndex ∷ Int
, updateActiveIndex ∷ Int -> Effect Unit
}
getHTMLElementAtIndex ∷ Int -> TwoOrMore (NodeRef) -> Effect (Maybe HTMLElement)
getHTMLElementAtIndex idx refs =
runMaybeT do
ref <- refs TwoOrMore.!! idx # pure >>> wrap
node <- React.readRefMaybe ref # wrap
HTMLElement.fromNode node # pure >>> wrap
blurAtIndex ∷ Int -> TwoOrMore (Ref (Nullable Node)) -> Effect Unit
blurAtIndex idx refs = do
getHTMLElementAtIndex idx refs >>= traverse_ blur
focusAtIndex ∷ Int -> TwoOrMore (Ref (Nullable Node)) -> Effect Unit
focusAtIndex idx refs = do
getHTMLElementAtIndex idx refs >>= traverse_ focus
component ∷ ReactComponent ComponentProps
component ∷ ReactComponent Props
component =
unsafePerformEffect
$ reactComponent "Segmented" \({ buttonContents, activeIndex, updateActiveIndex } ∷ ComponentProps) -> React.do
$ reactComponent "Segmented" \({ buttonContents, activeIndex, updateActiveIndex } ∷ Props) -> React.do
-------------------------------------------
-- Store button refs for animation purposes
itemRefs /\ setItemRefs ∷ Maybe (TwoOrMore _) /\ _ <- useState' Nothing
@ -248,6 +44,7 @@ component =
refs <- traverse (const createRef) buttonContents
setItemRefs (Just refs)
mempty
windowWidth /\ setWindowWidth <- useState' 0.0
-------------------------------------------
-- Support keyboard input
let
@ -272,10 +69,15 @@ component =
-- Ensure redraw on window resize
windowSize <- useResize
useAff { windowSize } do
delay (200.0 # Milliseconds)
delay
if Math.abs (windowWidth - windowSize.width) < 10.0 then
100.0 # Milliseconds
else
10.0 # Milliseconds
liftEffect do -- force rerender
refs <- traverse (const createRef) buttonContents
setItemRefs (Just refs)
setWindowWidth windowSize.width
let
children ∷ Array JSX
children = refsAndContents <#> mapWithIndex contentToChild # maybe mempty TwoOrMore.toArray
@ -320,12 +122,28 @@ component =
, children:
itemRefs
# foldMap \activeItemRefs ->
React.element activeComponent
React.element ActiveIndicator.component
{ activeItemRefs
, activeItemIndex: activeIndex
, updateActiveIndex
, windowWidth
}
A.: children
}
]
}
getHTMLElementAtIndex ∷ Int -> TwoOrMore (NodeRef) -> Effect (Maybe HTMLElement)
getHTMLElementAtIndex idx refs =
runMaybeT do
ref <- refs TwoOrMore.!! idx # pure >>> wrap
node <- React.readRefMaybe ref # wrap
HTMLElement.fromNode node # pure >>> wrap
blurAtIndex ∷ Int -> TwoOrMore (Ref (Nullable Node)) -> Effect Unit
blurAtIndex idx refs = do
getHTMLElementAtIndex idx refs >>= traverse_ blur
focusAtIndex ∷ Int -> TwoOrMore (Ref (Nullable Node)) -> Effect Unit
focusAtIndex idx refs = do
getHTMLElementAtIndex idx refs >>= traverse_ focus

View File

@ -0,0 +1,185 @@
module Yoga.Block.Atom.Segmented.View.ActiveIndicator (Props, component) where
import Yoga.Prelude.View
import Control.MonadZero as MZ
import Data.Traversable (traverse)
import Data.TwoOrMore (TwoOrMore)
import Data.TwoOrMore as TwoOrMore
import Effect.Unsafe (unsafePerformEffect)
import Foreign.Object as Object
import Framer.Motion (VariantLabel)
import Framer.Motion as Motion
import Hooks.Scroll (useScrollPosition)
import Literals.Undefined (undefined)
import MotionValue (useMotionValue)
import MotionValue as MotionValue
import Partial.Unsafe (unsafeCrashWith)
import React.Basic.DOM (css)
import React.Basic.Emotion as Emotion
import React.Basic.Hooks (reactComponent)
import React.Basic.Hooks as React
import Unsafe.Coerce (unsafeCoerce)
import Yoga.Block.Atom.Segmented.Style as Style
type Props =
{ activeItemRefs ∷ TwoOrMore (Ref (Nullable Node))
, activeItemIndex ∷ Int
, updateActiveIndex ∷ Int -> Effect Unit
, windowWidth ∷ Number
}
component ∷ ReactComponent Props
component =
unsafePerformEffect
$ reactComponent "SegmentedActive" do
\(props ∷ Props) -> React.do
maybeAnimationVariants /\ setVariants <- useState' Nothing
maybeDragX /\ setDragX <- useState' Nothing
{ scrollX } <- useScrollPosition
activeLeft <- useMotionValue 0.0
activeWidth <- useMotionValue 0.0
useEffectAlways do
_ <-
runMaybeT do
rawStyles <- traverse getStyle props.activeItemRefs
let styles = rawStyles <#> \s -> s { left = s.left + scrollX }
unless (maybeAnimationVariants == Just styles) do
setVariants (Just styles) # lift
mempty
useLayoutEffect maybeDragX do
case maybeDragX, maybeAnimationVariants of
Just x, Just animationVariants -> do
let { left, width } = handleDrag { activeItemIndex: props.activeItemIndex, animationVariants, x }
activeLeft # MotionValue.set left
activeWidth # MotionValue.set width
_, _ -> mempty
mempty
let
variants ∷ Motion.Variants
variants = case maybeAnimationVariants of
Just animationVariants ->
animationVariants
# foldMapWithIndex (\i s -> Object.singleton (show i) (css s))
# Motion.variantsFromObject
Nothing -> (cast undefined) ∷ Motion.Variants
pure $ maybeAnimationVariants
# foldMap \animationVariants ->
Emotion.elementKeyed Motion.div
{ css: Style.activeElement
, key: show props.windowWidth -- to force rerender
, variants
, className: "ry-active-segmented-element"
, initial: Motion.initial (indexToVariant props.activeItemIndex)
, drag: Motion.drag "x"
, dragMomentum: Motion.dragMomentum false
, animate: Motion.animate $ indexToVariant props.activeItemIndex
, layout: Motion.layout true
, whileTap: Motion.whileTap $ css { scale: 0.9 }
, style:
css
{ left: activeLeft
, width: activeWidth
}
, onDragStart:
Motion.onDragStart \_ pi -> do
setDragX (Just pi.point.x)
, onDrag:
Motion.onDrag \_ pi -> do
when (isJust maybeDragX) $ setDragX (Just pi.point.x)
, onDragEnd:
Motion.onDragEnd \_ pi -> do
let x = maybeDragX # fromMaybe' \_ -> unsafeCrashWith "no x should not happen"
let newIdx = findOverlapping props.activeItemIndex animationVariants x
let v = animationVariants TwoOrMore.!! newIdx # fromMaybe' \_ -> unsafeCrashWith "omg"
setDragX Nothing
activeLeft # MotionValue.set v.left
activeWidth # MotionValue.set v.width
props.updateActiveIndex newIdx
, dragConstraints: Motion.dragConstraints { left: 0, right: 0 }
, dragElastic: Motion.dragElastic false
, transition:
Motion.transition
{ type: "tween", duration: if isJust maybeDragX then 0.0 else 0.167, ease: "easeOut" }
, _aria: Object.fromHomogeneous { hidden: "true" }
}
getStyle ∷
Ref (Nullable Node) ->
MaybeT Effect BBox
getStyle itemRef = do
br <- MaybeT $ getBoundingBoxFromRef itemRef
pure
{ width: br.width
, height: br.height
, left: br.left
, top: br.top
}
indexToVariant ∷ Int -> VariantLabel
indexToVariant = show >>> unsafeCoerce
type BBox =
{ top ∷ Number, left ∷ Number, width ∷ Number, height ∷ Number }
findOverlapping ∷ Int -> TwoOrMore BBox -> Number -> Int
findOverlapping activeIndex styles x =
fromMaybe activeIndex do
curr <- styles TwoOrMore.!! activeIndex
let fst = TwoOrMore.head styles
let lst = TwoOrMore.last styles
let inside e = (e.left < x) && (e.left + e.width) >= x
let tooFarLeft = MZ.guard (x <= fst.left + fst.width) $> 0
let tooFarRight = MZ.guard (x >= lst.left) $> TwoOrMore.length styles - 1
TwoOrMore.findIndex inside styles <|> tooFarLeft <|> tooFarRight
handleDrag ∷
{ activeItemIndex ∷ Int
, animationVariants ∷ TwoOrMore BBox
, x ∷ Number
} ->
{ left ∷ Number, width ∷ Number }
handleDrag { x, activeItemIndex, animationVariants } = do
let idx = findOverlapping activeItemIndex animationVariants x
let av = animationVariants
let firstVariant = av # TwoOrMore.head
let lastVariant = av # TwoOrMore.last
let baseVariant = av TwoOrMore.!! idx # fromMaybe' \_ -> unsafeCrashWith "shit"
let
closestVariant =
if x >= (baseVariant.left + (baseVariant.width / 2.0)) then
av TwoOrMore.!! (idx + 1) # fromMaybe lastVariant
else
av TwoOrMore.!! (idx - 1) # fromMaybe firstVariant
let
greater /\ smaller =
if baseVariant.left > closestVariant.left then
baseVariant /\ closestVariant
else
closestVariant /\ baseVariant
-- Total
let rangeStart = smaller.left + (smaller.width / 2.0)
let rangeEnd = greater.left + (greater.width / 2.0)
let range = rangeEnd - rangeStart
let ratio = ((x - rangeStart) / range)
let interpolatedWidth = (greater.width * ratio) + smaller.width * (1.0 - ratio)
-- Right
let rangeStartRight = smaller.left + smaller.width
let rangeEndRight = greater.left + greater.width
let rangeRight = rangeEndRight - rangeStartRight
let ratioRight = ((x + (interpolatedWidth / 2.0) - rangeStartRight) / rangeRight)
-- Left
let rangeStartLeft = smaller.left
let rangeEndLeft = greater.left
let rangeLeft = rangeEndLeft - rangeStartLeft
let ratioLeft = (((x - (interpolatedWidth / 2.0)) - rangeStartLeft) / rangeLeft)
-- Individual
let left = rangeStartLeft + (ratioLeft * rangeLeft)
let right = rangeStartRight + (ratioRight * rangeRight)
let width = right - left
if x < firstVariant.left then
{ left: firstVariant.left, width: firstVariant.width }
else
if x >= lastVariant.left + lastVariant.width then do
{ left: lastVariant.left, width: lastVariant.width }
else do
{ left, width }

View File

@ -0,0 +1,5 @@
module Yoga.Block.Atom.Toggle
( module Yoga.Block.Atom.Toggle.View
) where
import Yoga.Block.Atom.Toggle.View (component, Props)

View File

@ -0,0 +1,15 @@
module Yoga.Block.Atom.Toggle.Spec where
import Yoga.Prelude.Spec
import Yoga.Block.Atom.Toggle as Toggle
spec ∷ Spec Unit
spec =
after_ cleanup do
describe "The toggle" do
it "renders without errors" do
void
$ renderComponent Toggle.component
{ value: true
, onToggle: mempty
}

View File

@ -0,0 +1,71 @@
module Yoga.Block.Atom.Toggle.Story where
import Prelude
import Color as Color
import Data.Maybe (Maybe(..))
import Data.Tuple.Nested ((/\))
import Effect (Effect)
import React.Basic (JSX, element, fragment)
import React.Basic.DOM as R
import React.Basic.Hooks as React
import Yoga.Block as Block
import Yoga.Block.Atom.Toggle as Toggle
import Yoga.Block.Container.Style (DarkOrLightMode(..))
default ∷
{ title ∷ String
}
default =
{ title: "Atom/Toggle"
}
toggle ∷ Effect JSX
toggle = do
example <- mkBasicExample
darkLight <- mkDarkLightToggle
pure
$ fragment
[ R.div_
[ R.h2_ [ R.text "Basics" ]
, element example {}
, R.h2_ [ R.text "Dark Light toggle" ]
, element darkLight {}
]
]
where
mkBasicExample =
React.reactComponent "Toggle example" \p -> React.do
isOn /\ turnOnOrOff <- React.useState' false
pure
$ element Toggle.component
{ value: isOn
, onToggle: turnOnOrOff
}
mkDarkLightToggle =
React.reactComponent "Toggle dark night example" \p -> React.do
isOn /\ turnOnOrOff <- React.useState' false
theme /\ setTheme <- React.useState' Nothing
let
content =
element Toggle.component
{ value: isOn
, onToggle:
\b -> do
turnOnOrOff b
setTheme (Just if b then DarkMode else LightMode)
, on: R.text "🌒"
, off: R.text "🌞"
, backgroundOn:
Color.hsl 205.0 1.0 0.93
, backgroundOff:
Color.hsl 260.0 0.7 0.45
}
pure
$ case theme of
Nothing -> element Block.container { content }
Just themeVariant ->
element Block.container
{ content
, themeVariant: themeVariant
}

View File

@ -0,0 +1,85 @@
module Yoga.Block.Atom.Toggle.Style where
import Yoga.Prelude.Style
import Yoga.Block.Container.Style (colour)
type Props f r =
( css ∷ f Style
, backgroundOn ∷ f Color
, backgroundOff ∷ f Color
| r
)
button ∷ Style
button =
css
{ position: relative
, background: str colour.inputBackground
, border: str $ "1px solid " <> colour.inputBorder
, borderRadius: str "calc(var(--s2) / 2)"
, height: var "--s2"
, width: str "calc(1.15 * var(--s3))"
, margin: _0
, padding: _0
}
theToggle ∷ ∀ p. { | Props OptionalProp p } -> Style
theToggle props =
css
{ width: var "--s2"
, height: var "--s2"
, background: str $ colour.interfaceBackground
, border: none
, borderRadius: str $ "calc(var(--s2) / 2)"
, position: absolute
, top: str "-1px"
, left: _0
, margin: _0
, boxShadow: str "0 0.5px 3px rgba(0,0,0,0.50)"
}
toggleTextContainer ∷ Style
toggleTextContainer =
css
{ width: _100percent
, border: none
, fontWeight: str "bold"
, position: absolute
, top: _0
, lineHeight: var "--s2"
, height: var "--s2"
, display: flex
}
toggleText ∷ Style
toggleText =
css
{ textAlign: str "left"
, margin: _0
, width: str "50%"
, height: str "100%"
, display: flex
, justifyContent: center
, alignItems: center
, color: str colour.interfaceTextDisabled
, "&:first-child":
nest
{ color: str colour.successText
}
}
inputDisabled ∷ Style
inputDisabled =
css
{ "input[type=toggle]::-webkit-slider-thumb": nested thumbStyleDisabled
, "input[type=toggle]::-moz-toggle-thumb": nested thumbStyleDisabled
}
where
thumbStyleDisabled =
css
{ background: str "#fcfcfc"
, boxShadow: str "0 calc(var(--s-4)/2) var(--s-3) rgba(88,88,88,0.2)"
}
disabled ∷ Style
disabled = css { backgroundColor: str colour.background30 }

View File

@ -0,0 +1,203 @@
module Yoga.Block.Atom.Toggle.View (component, MandatoryProps, Props, PropsF) where
import Yoga.Prelude.View
import Color as Color
import Data.Interpolate (i)
import Effect.Class.Console as Console
import Foreign.Object as Object
import Framer.Motion as Motion
import React.Basic.DOM (css)
import React.Basic.DOM as R
import React.Basic.Emotion as Emotion
import React.Basic.Hooks as React
import Yoga.Block.Atom.Toggle.Style as Style
import Yoga.Block.Container.Style (colour)
type PropsF f =
( className ∷ f String
, on ∷ f JSX
, off ∷ f JSX
| Style.Props f (MandatoryProps InputProps)
)
type MandatoryProps r =
( value ∷ Boolean
, onToggle ∷ Boolean -> Effect Unit
| r
)
data TappingState
= TapAllowed
| TapNotAllowed
data DragState
= NotDragging
| Dragging { startX ∷ Number }
| DragDone { startX ∷ Number, endX ∷ Number }
derive instance eqDragState ∷ Eq DragState
instance showDragState ∷ Show DragState where
show = case _ of
NotDragging -> "NotDragging"
Dragging x -> "Dragging " <> (show x)
DragDone x -> "DragDone " <> (show x)
type Props =
PropsF Id
type PropsOptional =
PropsF OptionalProp
component ∷ ∀ p p_. Union p p_ Props => ReactComponent { | MandatoryProps p }
component = rawComponent
rawComponent ∷ ∀ p. ReactComponent (Record p)
rawComponent =
mkForwardRefComponent "Toggle" do
\(props ∷ { | PropsOptional }) ref -> React.do
let disabled = props.disabled
tapState <- useRef TapNotAllowed
dragState /\ setDragState <- React.useState' NotDragging
maxLeft /\ setMaxLeft <- useState' 0.0
buttonRef <- useRef null
toggleRef <- useRef null
leftState /\ setLeftState <- React.useState' 0.0
let
getWidth aRef = do
bbox <- getBoundingBoxFromRef aRef
pure $ bbox <#> _.width # fromMaybe 0.0
buttonWidth = getWidth buttonRef
toggleWidth = getWidth toggleRef
useLayoutEffectAlways do
when (maxLeft == 0.0) do
bw <- buttonWidth
tw <- toggleWidth
setMaxLeft (bw - tw)
mempty
let
toggleVariants =
{ off: { x: 0.0 }
, on: { x: maxLeft }
}
toggleVariant = Motion.makeVariantLabels toggleVariants
buttonVariants =
{ off: { backgroundColor: (Emotion.str <<< Color.cssStringRGBA <$> props.backgroundOn) ?|| Emotion.str colour.inputBackground }
, on: { backgroundColor: (Emotion.str <<< Color.cssStringRGBA <$> props.backgroundOff) ?|| Emotion.str colour.success }
}
buttonVariant = Motion.makeVariantLabels buttonVariants
hasFocus /\ setHasFocus <- useState' false
useEffect dragState do
case dragState of
NotDragging -> mempty
Dragging { startX } -> writeRef tapState TapNotAllowed
DragDone { startX, endX } -> do
maybeBbox <- getBoundingBoxFromRef buttonRef
for_ maybeBbox \bbox -> do
if endX - startX <= (bbox.left - startX) + (bbox.width / 2.0) then do
props.onToggle false
else do
props.onToggle true
mempty
pure
$ styled Motion.button
{ className: "ry-toggle"
, css: Style.button <> guard props.disabled Style.inputDisabled
, onFocus: handler_ $ setHasFocus true
, onBlur: handler_ $ setHasFocus false
, transition: Motion.transition { type: "tween", duration: 0.33, ease: "easeOut" }
, variants: Motion.variants buttonVariants
, animate: Motion.animate if props.value then buttonVariant.on else buttonVariant.off
, value: show props.value
, onClick: handler preventDefault \_ -> props.onToggle (not props.value)
, style: props.style
, _data: Object.singleton "testid" "toggle-testid"
, role: "switch"
, _aria: Object.singleton "checked" "switch"
, ref: buttonRef
}
[ styled R.div'
{ className: "ry-toggle-text"
, css: Style.toggleTextContainer
}
[ styled R.div'
{ className: "ry-toggle-text-on"
, css: Style.toggleText
}
[ el Motion.animatePresence {}
[ guard (props.value)
$ styled Motion.div
{ className: "ry-toggle-text-on-container"
, css: Style.toggleOnText
, key: "ry-toggle-text-on-container"
, initial: Motion.initial $ css { scale: 0, opacity: 0 }
, animate: Motion.animate $ css { scale: 1, opacity: 1 }
, exit: Motion.exit $ css { scale: 0, opacity: 0 }
}
[ props.on ?|| R.text "I" ]
]
]
, styled R.div'
{ className: "ry-toggle-text-off"
, css: Style.toggleText
}
[ el Motion.animatePresence {}
[ guard (not props.value)
$ styled Motion.div
{ className: "ry-toggle-text-on-container"
, css: Style.toggleOnText
, key: "ry-toggle-text-on-container"
, initial: Motion.initial $ css { scale: 0, opacity: 0 }
, animate: Motion.animate $ css { scale: 1, opacity: 1 }
, exit: Motion.exit $ css { scale: 0, opacity: 0 }
}
[ props.off ?|| R.text "O" ]
]
]
]
, styled Motion.div
{ className: "ry-toggle-toggle"
, css: Style.theToggle props
, layout: Motion.layout true
, onClick: handler stopPropagation mempty
, drag: Motion.drag "x"
, dragMomentum: Motion.dragMomentum false
, dragElastic: Motion.dragElastic false
, dragConstraints:
Motion.dragConstraints
{ left: if props.value then negate maxLeft else zero
, right: if props.value then zero else maxLeft
}
, variants: Motion.variants toggleVariants
, transition: Motion.transition { type: "tween", duration: 0.33, ease: "easeOut" }
, whileTap: Motion.prop $ css { scale: 0.85, transition: { type: "tween", duration: 0.10, ease: "easeInOut" } }
, onTapStart: Motion.onTapStart \_ _ -> writeRef tapState TapAllowed
, onTap:
Motion.onTap \_ pi -> do
ts <- readRef tapState
case ts of
TapAllowed -> props.onToggle $ not props.value
_ -> mempty
mempty
, onTapCancel:
Motion.onTapCancel \_ pi -> do
writeRef tapState TapNotAllowed
mempty
, animate: Motion.animate if props.value then toggleVariant.on else toggleVariant.off
, onDragStart:
Motion.onDragStart \_ pi -> do
maybeBBox <- getBoundingBoxFromRef toggleRef
let x = maybeBBox <#> \bbox -> bbox.left + (bbox.width / 2.0)
setDragState $ Dragging { startX: x # fromMaybe pi.point.x }
, onDragEnd:
Motion.onDragEnd \_ pi -> do
case dragState of
Dragging { startX } -> do
maybeBBox <- getBoundingBoxFromRef toggleRef
let x = maybeBBox <#> \bbox -> bbox.left + (bbox.width / 2.0)
setDragState (DragDone { startX, endX: x # fromMaybe pi.point.x })
other -> Console.warn $ i "Unexpected drag state " (show other) " in onDragEvent"
, ref: toggleRef
}
[]
]

View File

@ -1,18 +1,18 @@
module Yoga.Block.Container.Spec where
import Yoga.Prelude.Spec
import Yoga.Block.Container as Container
import React.Basic.DOM as R
import React.Basic.Hooks (reactChildrenFromArray)
import React.Basic.Hooks (JSX)
import Yoga.Block.Container as Container
spec ∷ Spec Unit
spec =
after_ cleanup do
describe "The container" do
it "renders without errors" do
void $ renderComponent Container.component { children: reactChildrenFromArray [] }
void $ renderComponent Container.component { content: mempty ∷ JSX }
it "displays its children" do
let children = reactChildrenFromArray [ R.text "Test Text" ]
{ findByText } <- renderComponent Container.component { children }
let content = R.text "Test Text"
{ findByText } <- renderComponent Container.component { content }
elem <- findByText "Test Text"
elem `textContentShouldEqual` "Test Text"

View File

@ -1,15 +1,14 @@
module Yoga.Block.Container.Story where
import Prelude
import Yoga.Block.Layout.Cluster as Cluster
import Effect (Effect)
import React.Basic (JSX, element, fragment)
import React.Basic.DOM as R
import Yoga (el, styledLeaf)
import Yoga.Block.Container as Container
import Yoga.Block.Container.Style (inputFocus)
import Yoga.Block.Layout.Cluster as Cluster
import Yoga.Block.Layout.Stack as Stack
import Effect (Effect)
import React.Basic (JSX, element)
import React.Basic.DOM as R
import React.Basic.Hooks (reactChildrenFromArray)
import Yoga (el, styledLeaf)
default ∷ { title ∷ String }
default = { title: "Pages/Container" }
@ -18,8 +17,8 @@ container ∷ Effect JSX
container =
pure
( element Container.component
{ children:
reactChildrenFromArray
{ content:
fragment
[ R.text "Content"
, el Stack.component {}
[ el Cluster.component {}

View File

@ -8,8 +8,27 @@ import Foreign.Object as Object
import Heterogeneous.Mapping (class HMapWithIndex, class MappingWithIndex, hmap, hmapWithIndex)
import Unsafe.Coerce (unsafeCoerce)
data DarkOrLightMode
= DarkMode
| LightMode
lightModeStyle ∷ Style
lightModeStyle = unsafeCoerce lightModeVariables
darkModeStyle ∷ Style
darkModeStyle = unsafeCoerce darkModeVariables
darkMode ∷ Style
darkMode = mkGlobal (Just DarkMode)
lightMode ∷ Style
lightMode = mkGlobal (Just LightMode)
global ∷ Style
global =
global = mkGlobal Nothing
mkGlobal ∷ Maybe DarkOrLightMode -> Style
mkGlobal maybeMode =
css
{ "body, html":
nested
@ -17,7 +36,8 @@ global =
{ minHeight: 100.0 # vh
, minWidth: 100.0 # vw
, lineHeight: str "1.15"
, "-webkit-text-size-adjust": _100percent
, "WebkitTextSizeAdjust": _100percent
, transition: str "background,color 0.33s ease-in"
}
, ":root":
nested $ variables
@ -35,7 +55,10 @@ global =
, color: str colour.text
, margin: str "0"
}
<> colourTheme defaultColours
<> case maybeMode of
Nothing -> autoSwitchColourTheme
Just DarkMode -> darkModeStyle
Just LightMode -> lightModeStyle
, "pre,code":
nest
{ fontFamily: str "var(--monoFont)"
@ -81,7 +104,10 @@ defaultColours =
, background80: darken 0.8 lightBg
, background90: darken 0.9 lightBg
, background100: darken 1.0 lightBg
, success
, successText
, interfaceBackground: lightBg
, interfaceTextDisabled: darken 0.33 lightBg
, interfaceBackgroundHighlight: darken 0.07 lightBg
, interfaceBackgroundShadow: darken 0.1 lightBg
, inputBackground: darken 0.03 lightBg
@ -108,10 +134,13 @@ defaultColours =
, background90: lighten 0.9 darkBg
, background100: lighten 1.0 darkBg
, interfaceBackground: lighten 0.4 darkBg
, interfaceTextDisabled: lighten 0.8 darkBg
, interfaceBackgroundHighlight: lighten 0.5 darkBg
, interfaceBackgroundShadow: lighten 0.4 darkBg
, inputBackground: lighten 0.10 darkBg
, inputBorder: lighten 0.17 darkBg
, success
, successText
, highlight
, text: lightBg
}
@ -119,6 +148,10 @@ defaultColours =
where
highlight = Color.rgb 0x00 0x99 0xFF
success = Color.rgb 20 200 60
successText = Color.rgb 250 250 250
-- highlight = Color.rgb 0x10 0x45 0x4A
darkBg = Color.rgb 0 0 0
@ -143,11 +176,14 @@ type FlatTheme a =
, background90 ∷ a
, background100 ∷ a
, interfaceBackground ∷ a
, interfaceTextDisabled ∷ a
, interfaceBackgroundHighlight ∷ a
, interfaceBackgroundShadow ∷ a
, inputBackground ∷ a
, inputBorder ∷ a
, highlight ∷ a
, success ∷ a
, successText ∷ a
, text ∷ a
}
@ -172,8 +208,8 @@ colour =
hmap (\x -> "var(" <> x <> ")")
$ makeCSSVarLabels defaultColours.light
colourTheme ∷ Colours -> Style
colourTheme { dark, light } = lightT
autoSwitchColourTheme ∷ Style
autoSwitchColourTheme = lightT
where
darkT ∷ Style
darkT = unsafeCoerce darkObj
@ -182,18 +218,30 @@ colourTheme { dark, light } = lightT
darkObj =
Object.fromHomogeneous defaultColours.dark
# Object.foldMap \k v ->
Object.singleton ("--" <> k) (str (Color.toHexString v))
Object.singleton ("--" <> k) (str (Color.cssStringRGBA v))
lightObj ∷ Object StyleProperty
lightObj =
Object.fromHomogeneous defaultColours.light
# Object.foldMap \k v ->
Object.singleton ("--" <> k) (str (Color.toHexString v))
Object.singleton ("--" <> k) (str (Color.cssStringRGBA v))
# Object.insert "@media (prefers-color-scheme: dark)" (nested darkT)
lightT ∷ Style
lightT = unsafeCoerce lightObj
lightModeVariables ∷ Object StyleProperty
lightModeVariables =
Object.fromHomogeneous defaultColours.light
# Object.foldMap \k v ->
Object.singleton ("--" <> k) (str (Color.cssStringRGBA v))
darkModeVariables ∷ Object StyleProperty
darkModeVariables =
Object.fromHomogeneous defaultColours.dark
# Object.foldMap \k v ->
Object.singleton ("--" <> k) (str (Color.cssStringRGBA v))
variables ∷ Style
variables =
css

View File

@ -1,22 +1,43 @@
module Yoga.Block.Container.View (component, Props) where
module Yoga.Block.Container.View
( component, Props, PropsF
) where
import Yoga.Prelude.View
import Data.Array as Array
import Effect.Unsafe (unsafePerformEffect)
import React.Basic.DOM as R
import React.Basic.Emotion as E
import React.Basic.Hooks (reactComponentWithChildren)
import React.Basic.Hooks (reactComponent)
import Unsafe.Coerce (unsafeCoerce)
import Yoga.Block.Container.Style (DarkOrLightMode)
import Yoga.Block.Container.Style as Styles
type Props =
{ children ∷ ReactChildren JSX }
type PropsF f =
( content ∷ JSX
, themeVariant ∷ f DarkOrLightMode
)
component ∷ ReactComponent Props
component =
unsafePerformEffect
$ reactComponentWithChildren "Container" \({ children } ∷ Props) -> React.do
type Props =
( | PropsF Id )
component ∷ ∀ p q. Union p q Props => ReactComponent { | p }
component = rawComponent
rawComponent ∷ ∀ p. ReactComponent { | p }
rawComponent =
unsafeCoerce
$ unsafePerformEffect
$ reactComponent "Container" \({ content, themeVariant } ∷ { | PropsF OptionalProp }) -> React.do
pure
$ R.div_
$ Array.cons
(element E.global { styles: Styles.global })
(reactChildrenToArray children)
( element E.global
{ styles:
case opToMaybe themeVariant of
Nothing -> Styles.global
Just Styles.DarkMode -> Styles.darkMode
Just Styles.LightMode -> Styles.lightMode
}
)
[ content
]

View File

@ -1,17 +1,27 @@
module Yoga.Prelude.Default
( module Prelude
, module Control.Alt
, module Control.Monad.Maybe.Trans
, module Control.Monad.Trans.Class
, module Data.Maybe
, module Data.Either
, module Effect
, module Effect.Class
, module Data.Monoid
, module Data.Foldable
, module Data.FoldableWithIndex
, module Data.FunctorWithIndex
) where
import Prelude
import Control.Alt ((<|>))
import Control.Monad.Maybe.Trans (MaybeT(..), runMaybeT)
import Control.Monad.Trans.Class (lift)
import Data.Either (Either(..), note, hush)
import Data.Foldable (foldMap, for_, intercalate, traverse_)
import Data.Maybe (Maybe(..), fromMaybe)
import Data.FoldableWithIndex (foldMapWithIndex)
import Data.FunctorWithIndex (mapWithIndex)
import Data.Maybe (Maybe(..), fromMaybe, fromMaybe', isJust, maybe)
import Data.Monoid (guard)
import Effect (Effect)
import Effect.Class (liftEffect)

View File

@ -9,21 +9,34 @@ module Yoga.Prelude.View
, module React.Basic.DOM.Events
, module Type.Row
, module Web.DOM
, module Web.HTML.HTMLElement
, module Data.Nullable
, module Untagged.Castable
, NodeRef
, getBoundingBoxFromRef
) where
import Yoga.Prelude.Default
import Data.Nullable (Nullable, notNull, null)
import Prim.Row (class Union)
import React.Basic.DOM (Props_div)
import React.Basic.DOM.Events (preventDefault, targetValue)
import React.Basic.DOM.Events (stopPropagation, preventDefault, targetValue)
import React.Basic.Events (class Merge, EventFn, EventHandler, SyntheticEvent, handler, handler_, merge, mergeImpl, syntheticEvent, unsafeEventFn)
import React.Basic.Hooks (type (/\), Component, Hook, JSX, Pure, ReactChildren, ReactComponent, ReactContext, Ref, Render, UnsafeReference(..), UseContext, UseDebugValue, UseEffect, UseLayoutEffect, UseMemo, UseReducer, UseRef, UseState, coerceHook, consumer, contextConsumer, contextProvider, createContext, displayName, element, elementKeyed, empty, fragment, keyed, memo, provider, reactChildrenFromArray, reactChildrenToArray, reactComponentFromHook, readRef, readRefMaybe, unsafeHook, unsafeRenderEffect, useContext, useDebugValue, useEffect, useEffectAlways, useEffectOnce, useLayoutEffect, useLayoutEffectAlways, useLayoutEffectOnce, useMemo, useReducer, useRef, useState, useState', writeRef, (/\))
import Type.Row (type (+))
import Untagged.Castable (cast)
import Web.DOM (Node)
import Web.HTML.HTMLElement (HTMLElement, DOMRect, blur, focus, getBoundingClientRect)
import Web.HTML.HTMLElement as HTMLElement
import Yoga (el, el_, styled, styledLeaf, yogaElement)
import Yoga.Block.Internal (DivProps, DivPropsF, Id, InputProps, InputPropsF, OptionalProp(..), _0, appendIfDefined, createRef, dangerous, emotionDiv, emotionInput, getOr, getOrFlipped, ifTrue, isTruthy, maybeToOp, mkForwardRefComponent, mkForwardRefComponentEffect, opToMaybe, unsafeDiv, unsafeEmotion, unsafeUnOptional, (<>?), (?||))
type NodeRef =
Ref (Nullable Node)
getBoundingBoxFromRef ∷ Ref (Nullable Node) -> Effect (Maybe DOMRect)
getBoundingBoxFromRef itemRef =
runMaybeT do
node <- MaybeT $ readRefMaybe itemRef
htmlElement <- MaybeT $ pure $ HTMLElement.fromNode node
lift $ getBoundingClientRect htmlElement

View File

@ -1160,6 +1160,11 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.5.4.tgz#de25b5da9f727985a3757fd59b5d028aba75841a"
integrity sha512-ZpKr+WTb8zsajqgDkvCEWgp6d5eJT6Q63Ng2neTbzBO76Lbe91vX/iVIW9dikq+Fs3yEo+ls4cxeXABD2LtcbQ==
"@popperjs/core@^2.6.0":
version "2.6.0"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.6.0.tgz#f022195afdfc942e088ee2101285a1d31c7d727f"
integrity sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw==
"@reach/router@^1.3.3":
version "1.3.4"
resolved "https://registry.yarnpkg.com/@reach/router/-/router-1.3.4.tgz#d2574b19370a70c80480ed91f3da840136d10f8c"