git.iko.soy/rowtype-yoga/ry-blocks
on main help sign in
Sheet
commit3d7df84f13eb74bd61450e06d2c0b56cdeb80a0c authorMark Eibes <mark.eibes@gmail.com> dateThu 8 Apr 2021, 22:32 (6 years ago) parente36e0d4
30 files changed+661−141
package.json+1−1 spago.dhall+2−1 src/Framer/Motion.js+5−2 src/Framer/Motion.purs+35−4 src/Framer/Motion/Hook.js+2−1 src/Framer/Motion/Hook.purs+11−1 src/Framer/Motion/Types.purs+33−4 src/Framer/MotionValue.js+3−1 src/Framer/MotionValue.purs+9−1 src/React/FocusTrap.purs+5−1 src/Yoga/Block/Atom/Input/Story.purs+21 src/Yoga/Block/Atom/Input/Style.purs+19−14 src/Yoga/Block/Atom/Input/View.purs+32−11 src/Yoga/Block/Atom/Input/View/HTMLInput.purs+1−1 src/Yoga/Block/Atom/Input/View/Label.purs+4−3 src/Yoga/Block/Atom/Popover/Story.purs+82−43 src/Yoga/Block/Atom/Segmented/View/ActiveIndicator.purs+1−1 src/Yoga/Block/Atom/Toggle/View.purs+1−1 src/Yoga/Block/Hook/Key.purs+3 src/Yoga/Block/Hook/UseKeyDown.purs+3−3 src/Yoga/Block/Hook/UseKeyUp.purs+9−8 src/Yoga/Block/Layout/Box/Style.purs+14−8 src/Yoga/Block/Molecule/Modal/Story.purs+8−2 src/Yoga/Block/Molecule/Modal/View.purs+23−12 src/Yoga/Block/Molecule/Sheet.purs+5 src/Yoga/Block/Molecule/Sheet/Story.purs+85 src/Yoga/Block/Molecule/Sheet/Style.purs+49 src/Yoga/Block/Molecule/Sheet/View.purs+180 src/Yoga/Block/Organism/Form/Story.purs+1−3 yarn.lock+14−14
package.json+1−1
@@ -57,4 +57,4 @@
5757 "react-popper": "^2.2.4",
5858 "react-syntax-highlighter": "^15.3.1"
5959 }
60−}
60+}
spago.dhall+2−1
@@ -4,7 +4,8 @@ You can edit this file as you like.
44-}
55{ name = "ry-blocks"
66, dependencies =
7− [ "console"
7+ [ "aff-promise"
8+ , "console"
89 , "debug"
910 , "effect"
1011 , "foreign-generic"
src/Framer/Motion.js+5−2
@@ -2,9 +2,9 @@ const framerMotion = require("framer-motion")
22const React = require("react")
33const Emotion = require("@emotion/core")
44
5−exports.animatePresence = () => framerMotion.AnimatePresence
5+exports.animatePresenceImpl = framerMotion.AnimatePresence
66
7−exports.animateSharedLayout = () => framerMotion.AnimateSharedLayout
7+exports.animateSharedLayoutImpl = framerMotion.AnimateSharedLayout
88
99const flattenDataProp = (props) => {
1010 let data = null;
@@ -64,3 +64,6 @@ exports.styledMotionElement_ = (component, props, areChildrenDynamic) => {
6464
6565exports.styledMotionElementKeyed_ = (component, props) =>
6666 exports.styledMotionElement_(component, props, true);
67+
68+exports.startImpl = (args) => (controls) => controls.start(args)
69+exports.stopImpl = (controls) => controls.stop
src/Framer/Motion.purs+35−4
@@ -9,14 +9,23 @@ module Framer.Motion
99 , elementKeyed
1010 , elementStyled
1111 , elementStyledKeyed
12+ , startAnimation
13+ , startAnimationDynamic
14+ , startAnimationVariant
15+ , stopAnimation
1216 ) where
1317
14−import Prelude (flip)
18+import Framer.Motion.Types
19+import Control.Promise (Promise)
20+import Control.Promise as Promise
1521import Data.Function.Uncurried (Fn2, runFn2)
22+import Effect (Effect)
23+import Effect.Aff (Aff)
1624import Framer.Motion.MotionComponent (_data, abbr, address, area, article, aside, audio, b, base, bdi, bdo, blockquote, body, br, button, canvas, caption, circle, cite, clipPath, code, col, colgroup, custom, datalist, dd, defs, del, desc, details, dfn, dialog, div, dl, dt, ellipse, em, embed, feBlend, feColorMatrix, feComponentTransfer, feComposite, feConvolveMatrix, feDiffuseLighting, feDisplacementMap, feDistantLight, feDropShadow, feFlood, feFuncA, feFuncB, feFuncG, feFuncR, feGaussianBlur, feImage, feMerge, feMergeNode, feMorphology, feOffset, fePointLight, feSpecularLighting, feSpotLight, feTile, feTurbulence, fieldset, figcaption, figure, filter, footer, foreignObject, form, g, h1, h2, h3, h4, h5, h6, head, header, hgroup, hr, html, i, iframe, image, img, input, ins, kbd, keygen, label, legend, li, line, linearGradient, link, main, map, mark, marker, mask, menu, menuitem, meta, metadata, meter, motionComponentImpl, nav, noscript, object, ol, optgroup, option, output, p, param, path, pattern, picture, polygon, polyline, pre, progress, q, radialGradient, rect, rp, rt, ruby, s, samp, script, section, select, small, source, span, stop, strong, style, sub, summary, sup, svg, svgAnimate, svgSwitch, symbol, table, tbody, td, text, textPath, textarea, tfoot, th, thead, time, title, tr, track, tspan, u, ul, use, var, video, view, wbr)
17−import Framer.Motion.Types
25+import Prelude (Unit, flip)
1826import Prim.Row (class Nub, class Union)
1927import React.Basic (JSX, ReactComponent)
28+import React.Basic.DOM (CSS)
2029import React.Basic.Emotion (Style)
2130import Record (disjointUnion)
2231import Untagged.Castable (class Castable, cast)
@@ -31,15 +40,21 @@ motion ∷
3140 Union baseProps (MotionProps ()) result => Nub result result => Castable motionSubset { | MotionProps () } => motionSubset -> Record baseProps -> Record result
3241motion = flip withMotion
3342
34−foreign import animatePresence ∷
43+foreign import animatePresenceImpl ∷ ∀ attrs. ReactComponent { | attrs }
44+
45+animatePresence ∷
3546 ∀ attrs attrs_.
3647 Union attrs attrs_ AnimatePresenceProps =>
3748 ReactComponent { | attrs }
49+animatePresence = animatePresenceImpl
3850
39−foreign import animateSharedLayout ∷
51+foreign import animateSharedLayoutImpl ∷ ∀ attrs. ReactComponent { | attrs }
52+
53+animateSharedLayout ∷
4054 ∀ attrs attrs_.
4155 Union attrs attrs_ AnimateSharedLayoutProps =>
4256 ReactComponent { | attrs }
57+animateSharedLayout = animateSharedLayoutImpl
4358
4459element ∷ ∀ p q. Union p q (MotionProps q) => ReactComponent { | p } -> { | p } -> JSX
4560element = runFn2 motionElement_
@@ -85,3 +100,19 @@ foreign import styledMotionElementKeyed_ ∷
85100 Fn2 (ReactComponent { className ∷ String | props })
86101 { className ∷ String, css ∷ Style, key ∷ String | props }
87102 JSX
103+
104+foreign import startImpl ∷ ∀ arg. arg -> AnimationControls -> Promise Unit
105+
106+foreign import stopImpl ∷ AnimationControls -> Effect Unit
107+
108+startAnimation ∷ CSS -> AnimationControls -> Aff Unit
109+startAnimation arg controls = Promise.toAff (startImpl arg controls)
110+
111+startAnimationDynamic ∷ ∀ arg. (arg -> CSS) -> AnimationControls -> Aff Unit
112+startAnimationDynamic arg controls = Promise.toAff (startImpl arg controls)
113+
114+startAnimationVariant ∷ VariantLabel -> AnimationControls -> Aff Unit
115+startAnimationVariant arg controls = Promise.toAff (startImpl arg controls)
116+
117+stopAnimation ∷ AnimationControls -> Effect Unit
118+stopAnimation = stopImpl
src/Framer/Motion/Hook.js+2−1
@@ -2,4 +2,5 @@ const framerMotion = require("framer-motion")
22
33exports.useViewportScrollImpl = framerMotion.useViewportScroll
44exports.useTransformImpl = framerMotion.useTransform
5−exports.useSpringImpl = framerMotion.useSpring
5+exports.useSpringImpl = framerMotion.useSpring
6+exports.useAnimationImpl = framerMotion.useAnimation
src/Framer/Motion/Hook.purs+11−1
@@ -1,5 +1,7 @@
11module Framer.Motion.Hook
22 ( useViewportScroll
3+ , useAnimation
4+ , UseAnimation
35 , UseViewportScroll
46 , useTransform
57 , useSpringWithMotionValue
@@ -20,6 +22,7 @@ import Data.TwoOrMore (TwoOrMore)
2022import Data.TwoOrMore as TwoOrMore
2123import Effect (Effect)
2224import Effect.Uncurried (EffectFn2, EffectFn4, runEffectFn2, runEffectFn4)
25+import Framer.Motion (AnimationControls)
2326import Literals.Undefined (Undefined, undefined)
2427import MotionValue (MotionValue)
2528import React.Basic.Hooks (Hook, unsafeHook)
@@ -121,10 +124,17 @@ useSpringWithMotionValue motionValue springProps =
121124 (cast springProps)
122125
123126useSpringWithNumber ∷
124− ∀ opts. Castable opts SpringProps => Number -> opts -> Hook (UseSpring Number) Number
127+ ∀ opts. Castable opts SpringProps => Number -> opts -> Hook (UseSpring (MotionValue Number)) (MotionValue Number)
125128useSpringWithNumber number springProps =
126129 unsafeHook do
127130 runEffectFn2
128131 useSpringImpl
129132 (cast number ∷ MotionValue Number |+| Number)
130133 (cast springProps)
134+
135+foreign import data UseAnimation ∷ Type -> Type
136+
137+foreign import useAnimationImpl ∷ Effect AnimationControls
138+
139+useAnimation ∷ Hook UseAnimation AnimationControls
140+useAnimation = unsafeHook useAnimationImpl
src/Framer/Motion/Types.purs+33−4
@@ -38,6 +38,9 @@ type MotionPropsF f r =
3838 , onDrag ∷ f OnDrag
3939 , onDragEnd ∷ f OnDragEnd
4040 , onDragStart ∷ f OnDragStart
41+ , onPan ∷ f OnPan
42+ , onPanEnd ∷ f OnPanEnd
43+ , onPanStart ∷ f OnPanStart
4144 , onHoverEnd ∷ f OnHoverEnd
4245 , onHoverStart ∷ f OnHoverStart
4346 , onLayoutAnimationComplete ∷ f OnLayoutAnimationComplete
@@ -76,6 +79,7 @@ type Exit =
7679
7780foreign import data AnimationControls ∷ Type
7881
82+
7983prop ∷ ∀ a b. Castable a b => a -> b
8084prop = cast
8185
@@ -110,12 +114,14 @@ dragMomentum = cast
110114type DragConstraints =
111115 Ref (Nullable Node) |+| BoundingBox2D |+| Undefined
112116
113−type DragElastic =
114− Boolean |+| Number |+| Undefined
117+type DragElastic = Boolean |+| Number |+| BoundingBox2D |+| Undefined
115118
116119dragElastic ∷ ∀ c. Castable c DragElastic => c -> DragElastic
117120dragElastic = cast
118121
122+dragElasticBoundingBox ∷ ∀ a. Castable a BoundingBox2D => a -> DragElastic
123+dragElasticBoundingBox x = cast ((cast x) ∷ BoundingBox2D)
124+
119125type BoundingBox2D =
120126 { left ∷ Int |+| Number |+| Undefined
121127 , right ∷ Int |+| Number |+| Undefined
@@ -141,6 +147,15 @@ type OnDragEnd =
141147type OnDrag =
142148 (EffectFn2 Event PanInfo Unit |+| Undefined)
143149
150+type OnPanStart =
151+ (EffectFn2 Event PanInfo Unit |+| Undefined)
152+
153+type OnPanEnd =
154+ (EffectFn2 Event PanInfo Unit |+| Undefined)
155+
156+type OnPan =
157+ (EffectFn2 Event PanInfo Unit |+| Undefined)
158+
144159type WhileTap =
145160 TargetAndTransition |+| String |+| Undefined
146161
@@ -271,6 +286,15 @@ onDragEnd = cast <<< toEffectFn
271286onDrag ∷ (Event -> PanInfo -> Effect Unit) -> OnDrag
272287onDrag fn2 = cast (mkEffectFn2 fn2)
273288
289+onPanStart ∷ (Event -> PanInfo -> Effect Unit) -> OnPanStart
290+onPanStart = cast <<< toEffectFn
291+
292+onPanEnd ∷ (Event -> PanInfo -> Effect Unit) -> OnPanEnd
293+onPanEnd = cast <<< toEffectFn
294+
295+onPan ∷ (Event -> PanInfo -> Effect Unit) -> OnPan
296+onPan fn2 = cast (mkEffectFn2 fn2)
297+
274298customProp ∷ ∀ a. a -> Foreign
275299customProp = unsafeToForeign
276300
@@ -292,8 +316,11 @@ transition = cast <<< css
292316drag ∷ ∀ a. Castable a Drag => a -> Drag
293317drag = cast
294318
295−dragConstraints ∷ ∀ a. Castable a BoundingBox2D => a -> DragConstraints
296−dragConstraints x = cast ((cast x) ∷ BoundingBox2D)
319+dragConstraints ∷ ∀ a. Castable a DragConstraints => a -> DragConstraints
320+dragConstraints = cast
321+
322+dragConstraintsBoundingBox ∷ ∀ a. Castable a BoundingBox2D => a -> DragConstraints
323+dragConstraintsBoundingBox x = cast ((cast x) ∷ BoundingBox2D)
297324
298325dragPropagation ∷ ∀ a. Castable a DragPropagation => a -> DragPropagation
299326dragPropagation = cast
@@ -345,3 +372,5 @@ switch = AnimateSharedLayoutType "switch"
345372
346373crossfade ∷ AnimateSharedLayoutType
347374crossfade = AnimateSharedLayoutType "crossfade"
375+
376+foreign import data AnimationPlaybackControls ∷ Type
src/Framer/MotionValue.js+3−1
@@ -7,4 +7,6 @@ exports.setImpl = v => render => mv => () => {
77}
88exports.isAnimating = mv => () => { return mv.isAnimating() }
99exports.stop = mv => () => { return mv.stop() }
10−exports.onChangeImpl = callback => mv => { return () => mv.onChange(callback); }
10+exports.onChangeImpl = callback => mv => { return () => mv.onChange(callback); }
11+exports.animateImpl = framerMotion.animate
12+exports.stopAnimation = (playbackControls) => playbackControls.stop
src/Framer/MotionValue.purs+9−1
@@ -2,7 +2,8 @@ module MotionValue where
22
33import Prelude
44import Effect (Effect)
5−import Effect.Uncurried (EffectFn1, mkEffectFn1, runEffectFn1)
5+import Effect.Uncurried (EffectFn1, EffectFn3, mkEffectFn1, runEffectFn1, runEffectFn3)
6+import Framer.Motion.Types (AnimationPlaybackControls)
67import React.Basic.Hooks (Hook, unsafeHook)
78
89foreign import data MotionValue ∷ Type -> Type
@@ -32,3 +33,10 @@ onChange = mkEffectFn1 >>> onChangeImpl
3233
3334useMotionValue ∷ ∀ a. a -> Hook (UseMotionValue a) (MotionValue a)
3435useMotionValue = unsafeHook <<< runEffectFn1 useMotionValueImpl
36+
37+foreign import animateImpl ∷ ∀ a options. EffectFn3 (MotionValue a) a { | options } AnimationPlaybackControls
38+
39+animate ∷ ∀ a options. a -> Record options -> MotionValue a -> Effect AnimationPlaybackControls
40+animate target opts value = runEffectFn3 animateImpl value target opts
41+
42+foreign import stopAnimation ∷ AnimationPlaybackControls -> Effect Unit
src/React/FocusTrap.purs+5−1
@@ -3,7 +3,8 @@ module React.FocusTrap (focusTrap) where
33import Prelude
44import Effect (Effect)
55import Prim.Row (class Union)
6−import React.Basic (ReactComponent)
6+import React.Basic (JSX, ReactComponent)
7+import Web.DOM (Node)
78import Web.HTML (HTMLElement)
89
910foreign import focusTrap_ ∷ ∀ p. ReactComponent { | p }
@@ -15,8 +16,11 @@ type Props =
1516 ( focusTrapOptions ∷
1617 { onActivate ∷ Effect Unit
1718 , onDeactivate ∷ Effect Unit
19+ , initialFocus ∷ Effect Node
20+ , fallbackFocus ∷ Effect Node
1821 }
1922 , active ∷ Boolean
2023 , paused ∷ Boolean
2124 , containerElements ∷ Array HTMLElement
25+ , children ∷ JSX
2226 )
src/Yoga/Block/Atom/Input/Story.purs+21
@@ -14,6 +14,7 @@ import Yoga ((/>), (</), (</>))
1414import Yoga.Block as Block
1515import Yoga.Block.Atom.Input as Input
1616import Yoga.Block.Atom.Input.Types as HTMLInput
17+import Yoga.Block.Container.Style (colour)
1718import Yoga.Block.Container.Style as Styles
1819
1920default ∷
@@ -45,6 +46,26 @@ input = do
4546 , element Input.component { label: nes (SProxy ∷ _ "Pig nose"), leading: R.text "🐽🤣" }
4647 , element Input.component { label: nes (SProxy ∷ _ "Pig nose"), trailing: R.text "🤫" }
4748 , element Input.component { label: nes (SProxy ∷ _ "Pig nose"), leading: R.text "🌭" }
49+ , Block.box
50+ </ { background: colour.backgroundLayer3
51+ }
52+ /> [ element Input.component
53+ { label: nes (SProxy ∷ _ "Different background")
54+ , leading: R.text "🌭"
55+ , forceSmallLabel: true
56+ }
57+ ]
58+ , Block.box
59+ </ { background: colour.backgroundLayer1
60+ }
61+ /> [ element Input.component
62+ { label: nes (SProxy ∷ _ "Another background")
63+ , background: colour.backgroundLayer3
64+ , textColour: "blue"
65+ , placeholderColour: colour.backgroundLayer4
66+ , leading: R.text "🌭"
67+ }
68+ ]
4869 , element Input.component { label: nes (SProxy ∷ _ "Pig nose"), leading: R.text "⭐", trailing: R.text "🔮" }
4970 ]
5071 , R.h2_ [ R.text "Generic Input" ]
src/Yoga/Block/Atom/Input/Style.purs+19−14
@@ -8,7 +8,10 @@ import Yoga.Block.Container.Style (colour)
88type Props :: forall k. (Type -> k) -> Row k -> Row k
99type Props f r =
1010 ( css ∷ f Style
11− , background ∷ f StyleProperty
11+ , background ∷ f String
12+ , textColour ∷ f String
13+ , placeholderColour ∷ f String
14+ , borderColour ∷ f String
1215 | r
1316 )
1417
@@ -55,8 +58,8 @@ labelContainer =
5558 , pointerEvents: none
5659 }
5760
58−labelSmall ∷ StyleProperty -> Style
59−labelSmall background =
61+labelSmall ∷ String -> String -> Style
62+labelSmall background textColour =
6063 css
6164 { fontSize: var "--s-1"
6265 , marginTop: str "calc(var(--s-3) * -1)"
@@ -70,8 +73,8 @@ labelSmall background =
7073 nest
7174 { fontWeight: str "500"
7275 , whiteSpace: str "nowrap"
73− , background
74− , color: str colour.text
76+ , background: str $ i "linear-gradient(" background "51%, transparent 51%, transparent)"
77+ , color: str textColour
7578 , borderRadius: var "--s-4"
7679 , paddingLeft: var "--s-4"
7780 , paddingRight: var "--s-4"
@@ -107,11 +110,10 @@ labelLarge { left, width } =
107110 , padding: _0
108111 , whiteSpace: nowrap -- force on one line
109112 , overflow: str "hidden"
110− , height: str "calc(var(--s0) * 0.95)"
111− , maxWidth: str $ i width "px"
113+ , height: str "calc(var(--s0))"
114+ , maxWidth: str $ i "calc(" width "px - 2ch)"
112115 , textOverflow: str "ellipsis"
113116 , marginTop: str "calc(var(--s-1) + var(--s-5))"
114− -- , marginBottom: str $ i "calc(" "var(--s-1)" "+" "var(--s-5)" " + " top "px" ")"
115117 , marginLeft: str $ i left "px"
116118 , marginRight: var "--input-side-padding"
117119 , color: str colour.placeholderText
@@ -171,7 +173,7 @@ inputContainer props = theCss <>? props.css
171173 , "--border-width": str "1px"
172174 , position: relative
173175 , boxSizing: borderBox
174− , background: (props.background ?|| str colour.interfaceBackground)
176+ , background: str (props.background ?|| colour.interfaceBackground)
175177 , display: flex
176178 , width: str "calc(var(--s4) * 2)"
177179 , """&[data-invalid="false"]""":
@@ -213,13 +215,13 @@ inputWrapper =
213215 , width: inherit
214216 }
215217
216−input ∷ Style
217−input =
218+input ∷ ∀ r. { | Props OptionalProp r } -> Style
219+input props =
218220 css
219221 { "&[type=text],&[type=search],&[type=password],&[type=number],&:not([type])":
220222 nest
221223 { background: str "transparent"
222− , color: str colour.text
224+ , color: str (props.textColour ?|| colour.text)
223225 , width: _100percent
224226 , minWidth: _0
225227 , margin: _0
@@ -230,6 +232,10 @@ input =
230232 , fontSize: str "calc(var(--s0) * 0.85)"
231233 , "--padding-top": var "--s-1"
232234 , "--padding-bottom": var "--s-1"
235+ , "&::placeholder":
236+ nest
237+ { color: str (props.placeholderColour ?|| colour.placeholderText)
238+ }
233239 , "&[aria-labelledby]":
234240 nest
235241 { paddingTop: str "calc(var(--padding-top) + (var(--s-5)/2))"
@@ -239,7 +245,7 @@ input =
239245 }
240246 , "&[type=search]":
241247 nest
242− { "&::-webkit-search-decoration, &::-webkit-search-cancel-button, &::-webkit-serch-results-button, &::-webkit-search-results-decoration":
248+ { "&::-webkit-search-decoration, &::-webkit-search-cancel-button, &::-webkit-search-results-button, &::-webkit-search-results-decoration":
243249 nest
244250 { "WebkitAppearance": none
245251 }
@@ -250,7 +256,6 @@ input =
250256 }
251257 }
252258
253−
254259plopAnimation ∷ StyleProperty
255260plopAnimation =
256261 keyframes
src/Yoga/Block/Atom/Input/View.purs+32−11
@@ -8,10 +8,12 @@ import Foreign.Object as Object
88import Framer.Motion as M
99import React.Basic.DOM (css)
1010import React.Basic.DOM as R
11−import React.Basic.Emotion as E
1211import React.Basic.Hooks as React
1312import Record.Builder as RB
1413import Type.Prelude (Proxy(..))
14+import Web.DOM.Node (toEventTarget)
15+import Web.Event.Event (EventType(..))
16+import Web.Event.EventTarget (addEventListener, eventListener, removeEventListener)
1517import Web.HTML.HTMLInputElement as InputElement
1618import Yoga.Block.Atom.Icon as Icon
1719import Yoga.Block.Atom.Input.Style as Style
@@ -78,8 +80,9 @@ rawComponent =
7880 aria = props._aria # opToMaybe # fold
7981 labelId ∷ String
8082 labelId = props.id # opToMaybe # fold # (_ <> "-label")
83+ renderSmallLabel = isTruthy props.forceSmallLabel || hasFocus || hasValue
8184 renderLargeLabel ∷ Boolean
82− renderLargeLabel = not (isTruthy props.forceSmallLabel) && not hasFocus && not hasValue
85+ renderLargeLabel = not renderSmallLabel
8386 maybeLabelText ∷ Maybe NonEmptyString
8487 maybeLabelText = props.label # opToMaybe
8588 mkLabel ∷ NonEmptyString -> JSX
@@ -94,7 +97,8 @@ rawComponent =
9497 , inputRef
9598 , parentRef:ref
9699 , labelText
97− , background: props.background ?|| E.str colour.interfaceBackground
100+ , background: props.background ?|| colour.interfaceBackground
101+ , textColour: props.textColour ?|| colour.text
98102 }
99103 leading ∷ Maybe JSX
100104 leading =
@@ -110,21 +114,35 @@ rawComponent =
110114 given <- props.placeholder # opToMaybe
111115 if isJust maybeLabelText && hasFocus then Just given else Nothing
112116 onBlur =
113− handler preventDefault
114− ( const do
117+ const do
115118 when hasFocus $ setHasFocus false
116− el <- getHTMLElementFromRef ref
119+ el <- getHTMLElementFromRef inputRef
117120 let inputEl = InputElement.fromHTMLElement =<< el
118121 for_ inputEl \ie -> do
119122 v <- InputElement.value ie
120123 setHasValue (v /= "")
121− )
122− onFocus = handler preventDefault (const $ unless hasFocus $ setHasFocus true)
124+
125+ onFocus = const $ unless hasFocus $ setHasFocus true
126+ useEffectAlways do
127+ maybeInputRef <- readRefMaybe inputRef
128+ case maybeInputRef of
129+ Nothing -> mempty
130+ Just theRef -> do
131+ let target = toEventTarget theRef
132+ blurListener <- eventListener onBlur
133+ focusListener <- eventListener onFocus
134+ addEventListener (EventType "blur") blurListener true target
135+ addEventListener (EventType "focus") focusListener true target
136+ pure do
137+ removeEventListener (EventType "blur") blurListener true target
138+ removeEventListener (EventType "focus") focusListener true target
139+
140+ let
123141 inputProps ∷ { | PropsOptional }
124142 inputProps =
125143 props
126− { onFocus = cast (composeHandler onFocus props.onFocus)
127− , onBlur = cast (composeHandler onBlur props.onBlur)
144+ { onFocus = props.onFocus
145+ , onBlur = props.onBlur
128146 , placeholder = maybePlaceholder # maybeToOp
129147 , _aria =
130148 if props.label # opToMaybe # isJust then
@@ -151,6 +169,9 @@ rawComponent =
151169 </ { hasFocus: hasFocus
152170 , isInvalid: aria # Object.lookup "invalid" <#> (_ == "true") # maybeToOp
153171 , css: props.css
172+ , background: props.background
173+ , borderColour: props.borderColour
174+ , textColor: props.textColour
154175 , ref
155176 }
156177 /> [ leading # foldMap \l -> div </ {} /> [ l ]
@@ -224,7 +245,7 @@ password =
224245 ref
225246 (props { type = InputTypes.toString <$> props.type })
226247 { className: "ry-input"
227− , css: Style.input
248+ , css: Style.input props
228249 , type: if hidePassword then "password" else "text"
229250 }
230251 ]
src/Yoga/Block/Atom/Input/View/HTMLInput.purs+1−1
@@ -36,6 +36,6 @@ rawComponent =
3636 \(props ∷ { | PropsOptional }) ref -> React.do
3737 let
3838 inputWrapper = div </* { className: "ry-input-wrapper", css: Style.inputWrapper }
39− input = emotionInput ref inputProps { className: "ry-input", css: Style.input }
39+ input = emotionInput ref inputProps { className: "ry-input" , css: Style.input props }
4040 inputProps = props { type = HTMLInput.toString <$> props.type }
4141 pure do inputWrapper [ input ]
src/Yoga/Block/Atom/Input/View/Label.purs+4−3
@@ -25,7 +25,8 @@ type Props =
2525 , inputRef ∷ NodeRef
2626 , parentRef ∷ NodeRef
2727 , labelText ∷ NonEmptyString
28− , background ∷ StyleProperty
28+ , background ∷ String
29+ , textColour ∷ String
2930 }
3031
3132component ∷ ReactComponent Props
@@ -82,7 +83,7 @@ component =
8283 </* { className: "ry-input-label-small"
8384 , layout: M.layout true
8485 , layoutId: M.layoutId "ry-input-label"
85− , css: Style.labelSmall props.background
86+ , css: Style.labelSmall props.background props.textColour
8687 , transition: M.transition { duration: 0.18, ease: "easeOut" }
8788 , _data:
8889 Object.fromHomogeneous
@@ -105,7 +106,7 @@ component =
105106 if props.renderLargeLabel then
106107 Style.labelLarge { left: largeLeft, width: largeWidth }
107108 else
108− Style.labelSmall props.background
109+ Style.labelSmall props.background props.textColour
109110 , transition: M.transition { duration: 0.18, ease: "easeOut" }
110111 , _data:
111112 Object.fromHomogeneous
src/Yoga/Block/Atom/Popover/Story.purs+82−43
@@ -3,7 +3,7 @@ module Yoga.Block.Atom.Popover.Story where
33import Prelude
44import Data.Array as Array
55import Data.Foldable (for_, traverse_)
6−import Data.Maybe (Maybe(..))
6+import Data.Maybe (Maybe(..), maybe)
77import Data.Monoid (guard)
88import Data.Nullable (null)
99import Data.String as String
@@ -13,13 +13,14 @@ import Data.Tuple.Nested ((/\))
1313import Effect (Effect)
1414import Effect.Uncurried (mkEffectFn1)
1515import Effect.Unsafe (unsafePerformEffect)
16+import Foreign.Object as Object
1617import Framer.Motion (animatePresence)
1718import Framer.Motion as Motion
18−import React.Basic (JSX, ReactComponent, element, elementKeyed, fragment)
19+import React.Basic (JSX, ReactComponent, element, fragment)
1920import React.Basic.DOM (CSS, css)
2021import React.Basic.DOM as R
2122import React.Basic.DOM.Events (preventDefault, targetValue)
22−import React.Basic.Emotion (str)
23+import React.Basic.Emotion (elementKeyed, str)
2324import React.Basic.Emotion as E
2425import React.Basic.Events (handler, handler_)
2526import React.Basic.Hooks (reactComponent, readRefMaybe)
@@ -30,7 +31,7 @@ import React.Basic.Popper.Placement.Types as Placement
3031import Unsafe.Coerce (unsafeCoerce)
3132import Web.HTML.HTMLElement (focus)
3233import Web.HTML.HTMLElement as HTMLElement
33−import Yoga ((/>), (</), (</*), (</>))
34+import Yoga ((/>), (</), (</>))
3435import Yoga.Block as Block
3536import Yoga.Block.Atom.Icon as Icon
3637import Yoga.Block.Atom.Input as Input
@@ -38,8 +39,9 @@ import Yoga.Block.Atom.Input.Types as HTMLInput
3839import Yoga.Block.Atom.Popover as Popover
3940import Yoga.Block.Container.Style (colour)
4041import Yoga.Block.Container.Style as Styles
42+import Yoga.Block.Hook.Key as KeyCode
43+import Yoga.Block.Hook.UseKeyDown (useKeyDown)
4144import Yoga.Block.Icon.SVG as Icons
42−import Yoga.Block.Internal.CSS (nest)
4345import Yoga.Block.Layout.Box as Box
4446import Yoga.Block.Layout.Stack as Stack
4547
@@ -78,19 +80,46 @@ popover = do
7880 referenceElement /\ setReferenceElement <- React.useState' nullRef
7981 inputRef <- React.useRef null
8082 text /\ setText <- React.useState' ""
83+ active /\ modifyActive <- React.useState Nothing
8184 clicking /\ setClicking <- React.useState' false
8285 visible /\ setVisible <- React.useState' false
83− selectedAuthors /\ modSelectedAuthors' <- React.useState []
86+ selectedAuthors /\ modSelectedAuthors <- React.useState []
8487 let
85− modSelectedAuthors f = modSelectedAuthors' (f >>> Array.sort)
88+ addToSelected a = modSelectedAuthors (_ `Array.union` [ a ])
8689 matchingAuthors =
8790 authors
8891 # (_ Array.\\ selectedAuthors)
8992 # Array.filter
90− ( \a ->
91− String.contains (String.Pattern (String.toLower text))
92− (String.toLower a)
93+ ( String.toLower
94+ >>> String.contains (String.Pattern (String.toLower text))
9395 )
96+ useKeyDown
97+ $ case _ of
98+ KeyCode.Down ->
99+ when visible do
100+ let first = Array.head matchingAuthors
101+ modifyActive case _ of
102+ Just elem ->
103+ maybe first Just do
104+ idx <- Array.findIndex (eq elem) matchingAuthors
105+ Array.index matchingAuthors (idx + 1)
106+ Nothing -> first
107+ KeyCode.Up ->
108+ when visible do
109+ let last = Array.last matchingAuthors
110+ modifyActive case _ of
111+ Just elem ->
112+ maybe last Just do
113+ idx <- Array.findIndex (eq elem) matchingAuthors
114+ Array.index matchingAuthors (idx - 1)
115+ Nothing -> last
116+ KeyCode.Return ->
117+ when visible do
118+ for_ active addToSelected
119+ KeyCode.Backspace ->
120+ when (text == "") do
121+ modSelectedAuthors (Array.dropEnd 1)
122+ _ -> mempty
94123 let
95124 inputWrapper = R.div' </ { ref: unsafeCoerce (mkEffectFn1 setReferenceElement) }
96125 pill t =
@@ -102,7 +131,8 @@ popover = do
102131 { fontSize: "var(--s-1)"
103132 , borderRadius: "var(--s-2)"
104133 , padding: "var(--s-2)"
105− , background: colour.highlightAlpha33
134+ , background: colour.highlightAlpha50
135+ , userSelect: "none"
106136 }
107137 , space: "4px"
108138 , justify: "flex-end"
@@ -133,6 +163,12 @@ popover = do
133163 }
134164 {}
135165 /> pills
166+ trailing =
167+ R.div'
168+ </ { style: css { fontSize: "var(--s1)", paddingBottom: "4px", userSelect: "none" }
169+ , onClick: handler_ (setVisible true)
170+ }
171+ /> [ R.text "▾" ]
136172 input =
137173 Input.component
138174 </> { id: "author"
@@ -150,19 +186,21 @@ popover = do
150186 else
151187 text
152188 , onChange: handler targetValue $ traverse_ setText
153− , onBlur: handler preventDefault (const (setVisible false))
189+ , onBlur: handler preventDefault (const $ unless clicking (setVisible false))
154190 , onFocus: handler_ (setVisible true)
155191 , leading
192+ , trailing
193+ , _data: Object.singleton "has-focus" (show visible)
156194 , autoComplete: "off"
157195 , css: E.css { width: E.percent 100.0 }
158196 }
159197 pop children =
160− elementKeyed motionPopover
198+ Motion.elementKeyed motionPopover
161199 $ Motion.motion
162200 { initial:
163− css { maxHeight: 0, opacity: 1.0 }
201+ css { maxHeight: 0, opacity: 0.0 }
164202 , exit:
165− css { maxHeight: 0, opacity: 1.0 }
203+ css { maxHeight: 0, opacity: 0.0 }
166204 , animate:
167205 css
168206 { maxHeight: 400
@@ -192,7 +230,7 @@ popover = do
192230 , padding: "0"
193231 }
194232 , key: "popover"
195− , children
233+ , children: [ R.ul' </ {} /> children ]
196234 }
197235 box =
198236 Box.component
@@ -209,33 +247,34 @@ popover = do
209247 , exit: Motion.exit containerVariant.exit
210248 }
211249 entry a =
212− Motion.div
213− </* { key: a
214− , variants: Motion.variants itemVariants
215− , css:
216− E.css
217− { "&:hover":
218− nest
219− { borderLeft: E.str $ "calc(var(--s-1) / 2) solid " <> colour.highlight
220− , paddingLeft: E.str "calc(var(--s-1) / 2)"
221− , background: E.str colour.backgroundLayer1
222− }
223− , padding: E.str "var(--s-1)"
224− , borderBottom: E.str $ "solid 1px " <> colour.backgroundLayer1
225− , background: E.str colour.backgroundLayer3
226− , margin: E.str "0"
227− , fontSize: E.str "calc(var(--s0) * 0.75)"
228− }
229− , className: "item"
230− , onClick:
231− handler_ do
232− maybeRefElem <- readRefMaybe inputRef
233− for_ (maybeRefElem >>= HTMLElement.fromNode) focus
234− modSelectedAuthors (_ `Array.union` [ a ])
235− , onMouseDown: handler_ (setClicking true)
236− , onMouseUp: handler_ (setClicking false)
237− }
238− /> [ R.text a ]
250+ elementKeyed Motion.li
251+ { key: a
252+ , variants: Motion.variants itemVariants
253+ , css:
254+ E.css
255+ { padding: E.str "var(--s-1)"
256+ , borderBottom: E.str $ "solid 1px " <> colour.backgroundLayer1
257+ , background: E.str colour.backgroundLayer3
258+ , margin: E.str "0"
259+ , fontSize: E.str "calc(var(--s0) * 0.75)"
260+ }
261+ <> ( guard (Just a == active)
262+ $ E.css
263+ { borderLeft: E.str $ "calc(var(--s-1) / 2) solid " <> colour.highlight
264+ , paddingLeft: E.str "calc(var(--s-1) / 2)"
265+ , background: E.str colour.backgroundLayer1
266+ }
267+ )
268+ , className: "item"
269+ , onClick:
270+ handler_ do
271+ maybeRefElem <- readRefMaybe inputRef
272+ addToSelected a
273+ for_ (maybeRefElem >>= HTMLElement.fromNode) focus
274+ , onMouseDown: handler_ (setClicking true)
275+ , onMouseUp: handler_ (setClicking false)
276+ , children: [ R.text a ]
277+ }
239278 pure
240279 $ fragment
241280 [ inputWrapper [ input ]
src/Yoga/Block/Atom/Segmented/View/ActiveIndicator.purs+1−1
@@ -124,8 +124,8 @@ component =
124124 activeWidth # MotionValue.set v.width
125125 props.updateActiveIndex newIdx
126126 setDragX Nothing
127− , dragConstraints: Motion.dragConstraints { left: 0, right: 0, top: 0, bottom: 0 }
128127 , dragElastic: Motion.dragElastic false
128+ , dragConstraints: Motion.dragConstraintsBoundingBox { left: 0, right: 0, top: 0, bottom: 0 }
129129 , transition:
130130 Motion.transition
131131 { type: "tween"
src/Yoga/Block/Atom/Toggle/View.purs+1−1
@@ -237,7 +237,7 @@ toggleCircle =
237237 , key: if isNothing maxLeft then "initialising" else "ready"
238238 , dragElastic: Motion.dragElastic false
239239 , dragConstraints:
240− Motion.dragConstraints
240+ Motion.dragConstraintsBoundingBox
241241 { left:
242242 case togglePosition of
243243 ToggleIsLeft -> zero
src/Yoga/Block/Hook/Key.purs+3
@@ -18,9 +18,11 @@ data KeyCode
1818 | Right
1919 | Down
2020 | Delete
21+ | Backspace
2122
2223keyCodeToInt ∷ KeyCode -> Int
2324keyCodeToInt = case _ of
25+ Backspace -> 8
2426 Return -> 13
2527 Escape -> 27
2628 End -> 35
@@ -33,6 +35,7 @@ keyCodeToInt = case _ of
3335
3436intToKeyCode ∷ Int -> Maybe KeyCode
3537intToKeyCode = case _ of
38+ 8 -> Just Backspace
3639 13 -> Just Return
3740 27 -> Just Escape
3841 35 -> Just End
src/Yoga/Block/Hook/UseKeyDown.purs+3−3
@@ -11,9 +11,9 @@ import Web.Event.EventTarget (addEventListener, eventListener, removeEventListen
1111import Web.HTML (window)
1212import Web.HTML.Window as Win
1313
14−newtype UseKeyDown hooks
15− = UseKeyDown
14+newtype UseKeyDown hooks = UseKeyDown
1615 (UseEffect Unit hooks)
16+
1717derive instance ntUseKeyDown ∷ Newtype (UseKeyDown hooks) _
1818
1919useKeyDown ∷ (KeyCode -> Effect Unit) -> Hook UseKeyDown Unit
@@ -30,4 +30,4 @@ useKeyDown doWhat = do
3030 pure $ removeEventListener eventTypeKeyDown listener false (Win.toEventTarget win)
3131
3232eventTypeKeyDown ∷ EventType
33−eventTypeKeyDown = EventType "keydown"
33+eventTypeKeyDown = EventType "keypress"
src/Yoga/Block/Hook/UseKeyUp.purs+9−8
@@ -4,26 +4,27 @@ import Prelude
44import Data.Maybe (Maybe(..))
55import Data.Newtype (class Newtype)
66import Effect (Effect)
7−import Yoga.Block.Hook.Key (KeyCode, getKeyCode, intToKeyCode)
8−import React.Basic.Hooks (Hook, UseEffect, coerceHook, useEffect)
7+import React.Basic.Hooks (Hook, UseEffect, coerceHook, useEffectOnce)
98import Web.Event.Event (EventType(..))
109import Web.Event.EventTarget (addEventListener, eventListener, removeEventListener)
1110import Web.HTML (window)
1211import Web.HTML.Window as Win
12+import Yoga.Block.Hook.Key (KeyCode, getKeyCode, intToKeyCode)
1313
14−newtype UseKeyUp hooks
15− = UseKeyUp
14+newtype UseKeyUp hooks = UseKeyUp
1615 (UseEffect Unit hooks)
16+
1717derive instance ntUseKeyUp ∷ Newtype (UseKeyUp hooks) _
1818
1919useKeyUp ∷ (KeyCode -> Effect Unit) -> Hook UseKeyUp Unit
2020useKeyUp doWhat = do
2121 coerceHook React.do
22− useEffect unit do
22+ useEffectOnce do
2323 listener <-
24− eventListener \event -> case getKeyCode event >>= intToKeyCode of
25− Just keyCode -> doWhat keyCode
26− Nothing -> pure unit
24+ eventListener \event -> do
25+ case getKeyCode event >>= intToKeyCode of
26+ Just keyCode -> doWhat keyCode
27+ Nothing -> pure unit
2728 win <- window
2829 addEventListener eventTypeKeyUp listener false (Win.toEventTarget win)
2930 pure (removeEventListener eventTypeKeyUp listener false (Win.toEventTarget win))
src/Yoga/Block/Layout/Box/Style.purs+14−8
@@ -3,20 +3,26 @@ module Yoga.Block.Layout.Box.Style where
33import Yoga.Prelude.Style
44import Data.Interpolate (i)
55
6+type Props :: forall k. (Type -> k) -> Row k -> Row k
67type Props f r =
78 ( css ∷ f Style
89 , padding ∷ f StyleProperty
9− , borderWidth ∷ f String
10− , invert ∷ f Boolean
10+ , border ∷ f StyleProperty
11+ , borderRadius ∷ f StyleProperty
12+ , foreground ∷ f StyleProperty
13+ , background ∷ f String
1114 | r
1215 )
1316
1417box ∷ ∀ p. { | Props OptionalProp p } -> Style
1518box props = styles <>? props.css
1619 where
17− styles =
18− css
19− { padding: props.padding ?|| (1.0 # em)
20− , border: str $ i ((props.borderWidth) ?|| "0") " solid"
21− , margin: _0
22− }
20+ styles =
21+ css
22+ { padding: props.padding ?|| (1.0 # em)
23+ , border: (props.border ?|| str "0 solid")
24+ , borderRadius: props.borderRadius ?|| mempty
25+ , background: (str <$> props.background) ?|| color transparent
26+ , color: props.foreground ?|| str "inherit"
27+ , margin: _0
28+ }
src/Yoga/Block/Molecule/Modal/Story.purs+8−2
@@ -20,6 +20,7 @@ import Web.HTML.HTMLDocument (toNonElementParentNode)
2020import Web.HTML.Window (document)
2121import Yoga as Y
2222import Yoga.Block as Block
23+import Yoga.Block.Container.Style (colour)
2324import Yoga.Block.Container.Style as Styles
2425import Yoga.Block.Molecule.Modal as Modal
2526
@@ -65,9 +66,14 @@ modal = do
6566 # foldMap \(modalElement ∷ Element) ->
6667 element Modal.component
6768 { content:
68− Y.el Block.box {} [ Y.leaf Block.input { label: NonEmptyString "A label that should fit" } ]
69+ Y.el Block.box
70+ { borderRadius: E.str "var(--s3)"
71+ , background: colour.backgroundLayer1
72+ , padding: E.str "var(--s3) var(--s2)"
73+ }
74+ [ Y.leaf Block.input { label: NonEmptyString "A label that should fit" } ]
6975 , isOpen
70− , setIsOpen
76+ , onDismiss: setIsOpen false
7177 , target: modalElement
7278 }
7379 ]
src/Yoga/Block/Molecule/Modal/View.purs+23−12
@@ -6,17 +6,21 @@ import Effect.Unsafe (unsafePerformEffect)
66import Framer.Motion (withMotion)
77import Framer.Motion as Motion
88import React.Basic.DOM (createPortal, css)
9+import React.Basic.DOM as R
910import React.Basic.Emotion as Emotion
1011import React.Basic.Hooks (reactComponent)
1112import React.Basic.Hooks as React
13+import React.FocusTrap (focusTrap)
1214import Web.DOM (Element)
15+import Yoga.Block.Hook.Key as KeyCode
16+import Yoga.Block.Hook.UseKeyDown (useKeyDown)
1317import Yoga.Block.Layout.Imposter as Imposter
1418import Yoga.Block.Molecule.Modal.Style as Style
1519
1620type Props =
1721 { content ∷ JSX
1822 , isOpen ∷ Boolean
19− , setIsOpen ∷ Boolean -> Effect Unit
23+ , onDismiss ∷ Effect Unit
2024 , target ∷ Element
2125 | ()
2226 }
@@ -24,24 +28,31 @@ type Props =
2428component ∷ ReactComponent Props
2529component =
2630 unsafePerformEffect
27− $ reactComponent "Modal Wrapper" \{ content, isOpen, setIsOpen, target } -> React.do
31+ $ reactComponent "Modal Wrapper" \{ content, isOpen, onDismiss, target } -> React.do
32+ useKeyDown case _ of
33+ KeyCode.Escape -> onDismiss
34+ _ -> mempty
2835 clickAwayRef <- React.useRef Nullable.null
2936 let
3037 toRender ∷ JSX
3138 toRender =
32− fragment
33− [ Motion.animatePresence </ {} /> [ guard isOpen $ element clickaway { theRef: clickAwayRef, hide: setIsOpen false } ]
34− , Motion.animatePresence </ {} /> [ guard isOpen $ element window { clickAwayRef, hide: setIsOpen false, content } ]
35− ]
39+ React.element focusTrap
40+ { active: isOpen
41+ , children:
42+ R.div' </ {}
43+ /> [ Motion.animatePresence </ {} /> [ guard isOpen $ element clickaway { theRef: clickAwayRef, onDismiss } ]
44+ , Motion.animatePresence </ {} /> [ guard isOpen $ element window { clickAwayRef, onDismiss, content } ]
45+ ]
46+ }
3647 pure (createPortal toRender target)
3748
38−clickaway ∷ ReactComponent { theRef ∷ Ref (Nullable Node), hide ∷ Effect Unit }
49+clickaway ∷ ReactComponent { theRef ∷ Ref (Nullable Node), onDismiss ∷ Effect Unit }
3950clickaway =
4051 unsafePerformEffect
41− $ reactComponent "Modal Clickaway" \{ theRef, hide } -> React.do
52+ $ reactComponent "Modal Clickaway" \{ theRef, onDismiss } -> React.do
4253 pure $ Emotion.elementKeyed Motion.div
4354 $ { key: "ry-modal-clickaway"
44− , onClick: handler_ hide
55+ , onClick: handler_ onDismiss
4556 , className: "ry-modal-clickaway"
4657 , css: Style.clickaway
4758 , initial: Motion.prop $ css { opacity: 0.0 }
@@ -50,17 +61,17 @@ clickaway =
5061 , ref: theRef
5162 }
5263
53−window ∷ ReactComponent { clickAwayRef ∷ NodeRef, content ∷ JSX, hide ∷ Effect Unit }
64+window ∷ ReactComponent { clickAwayRef ∷ NodeRef, content ∷ JSX, onDismiss ∷ Effect Unit }
5465window =
5566 unsafePerformEffect
56− $ reactComponent "Modal Window" \{ clickAwayRef, content, hide } -> React.do
67+ $ reactComponent "Modal Window" \{ clickAwayRef, content, onDismiss } -> React.do
5768 imposterRef <- useRef null
5869 pure
5970 $ Emotion.element motionImposter
6071 ( { className: "ry-modal-window"
6172 , css: Style.modal
6273 , ref: imposterRef
63− , onClick: handler_ hide
74+ , onClick: handler_ onDismiss
6475 , children:
6576 [ Motion.div
6677 </* { className: "ry-modal"
src/Yoga/Block/Molecule/Sheet.pursnew file+5
@@ -0,0 +1,5 @@
1+module Yoga.Block.Molecule.Sheet
2+ ( module Yoga.Block.Molecule.Sheet.View
3+ ) where
4+
5+import Yoga.Block.Molecule.Sheet.View (component, Props)
src/Yoga/Block/Molecule/Sheet/Story.pursnew file+85
@@ -0,0 +1,85 @@
1+module Yoga.Block.Molecule.Sheet.Story where
2+
3+import Prelude
4+import Data.Foldable (foldMap)
5+import Data.Maybe (Maybe(..), isNothing)
6+import Data.String.NonEmpty.Internal (NonEmptyString(..))
7+import Data.Tuple.Nested ((/\))
8+import Effect (Effect)
9+import Effect.Unsafe (unsafePerformEffect)
10+import React.Basic (JSX, element, fragment)
11+import React.Basic.DOM as R
12+import React.Basic.Emotion as E
13+import React.Basic.Events (handler_)
14+import React.Basic.Hooks (reactComponent, useEffectAlways)
15+import React.Basic.Hooks as React
16+import Web.DOM (Element)
17+import Web.DOM.NonElementParentNode (getElementById)
18+import Web.HTML (window)
19+import Web.HTML.HTMLDocument (toNonElementParentNode)
20+import Web.HTML.Window (document)
21+import Yoga ((/>), (</), (</>))
22+import Yoga as Y
23+import Yoga.Block as Block
24+import Yoga.Block.Atom.Button.Types as ButtonType
25+import Yoga.Block.Container.Style (colour)
26+import Yoga.Block.Container.Style as Styles
27+import Yoga.Block.Molecule.Sheet as Sheet
28+
29+default ∷
30+ { decorators ∷ Array (Effect JSX -> JSX)
31+ , title ∷ String
32+ }
33+default =
34+ { title: "Molecule/Sheet"
35+ , decorators:
36+ [ \storyFn ->
37+ R.div_
38+ [ element E.global { styles: Styles.global }
39+ , unsafePerformEffect storyFn
40+ ]
41+ ]
42+ }
43+
44+sheet ∷ Effect JSX
45+sheet = do
46+ pure $ React.element compo {}
47+ where
48+ compo =
49+ unsafePerformEffect
50+ $ reactComponent "Sheet Story" \{} -> React.do
51+ isOpen /\ setIsOpen <- React.useState' true
52+ maybeModalElement /\ setModalElement <- React.useState' Nothing
53+ useEffectAlways do
54+ when (isNothing maybeModalElement) do
55+ container <- getElementById "modal-container" =<< (map toNonElementParentNode $ document =<< window)
56+ setModalElement container
57+ mempty
58+ pure
59+ $ fragment
60+ [ R.h2_ [ R.text "No Options" ]
61+ , Y.el R.button'
62+ { onClick: handler_ (setIsOpen true)
63+ }
64+ [ R.text "Show Sheet"
65+ ]
66+ , R.div { id: "modal-container" }
67+ , maybeModalElement
68+ # foldMap \(modalElement ∷ Element) ->
69+ element Sheet.component
70+ { content:
71+ Block.stack </ {}
72+ /> [ R.text "Who are you?"
73+ , Block.cluster </ { justify: "flex-end" }
74+ /> [ Block.button </ {} /> [ R.text "hi" ]
75+ , Block.button
76+ </ { buttonType: ButtonType.Dangerous, onClick: handler_ (setIsOpen false)
77+ }
78+ /> [ R.text "Close" ]
79+ ]
80+ ]
81+ , isOpen
82+ , onDismiss: setIsOpen false
83+ , target: modalElement
84+ }
85+ ]
src/Yoga/Block/Molecule/Sheet/Style.pursnew file+49
@@ -0,0 +1,49 @@
1+module Yoga.Block.Molecule.Sheet.Style where
2+
3+import Yoga.Prelude.Style
4+
5+import Data.Interpolate (i)
6+import Yoga.Block.Container.Style (colour)
7+
8+type Props :: forall k. (Type -> k) -> Row k -> Row k
9+type Props f r =
10+ ( css ∷ f Style
11+ | r
12+ )
13+
14+sheet ∷ Style
15+sheet = styles
16+ where
17+ styles =
18+ css
19+ { zIndex: str "3"
20+ , position: fixed
21+ , left: str "0"
22+ , width: str "100vw"
23+ , background: str colour.backgroundLayer5
24+ , borderRadius: str $ "var(--s2) var(--s2) 0 0"
25+ , boxShadow: str $ "0 0 var(--s3) rgba(0,0,0,0.1)"
26+ , height: str "100vh"
27+ }
28+
29+sheetContent ∷ Style
30+sheetContent = styles
31+ where
32+ styles =
33+ css
34+ { padding: str "var(--s1) var(--s0)"
35+ , maxHeight: str "calc(100vh - var(--s3))"
36+ , overflow: scroll
37+ }
38+
39+clickaway ∷ Style
40+clickaway =
41+ css
42+ { width: 100.0 # vw
43+ , height: 100.0 # vh
44+ , position: fixed
45+ , left: _0
46+ , top: _0
47+ , backdropFilter: str "blur(4px) brightness(80%)"
48+ , zIndex: str "3"
49+ }
src/Yoga/Block/Molecule/Sheet/View.pursnew file+180
@@ -0,0 +1,180 @@
1+module Yoga.Block.Molecule.Sheet.View (component, Props) where
2+
3+import Yoga.Prelude.View (Effect, JSX, Maybe(..), Node, NodeRef, Nullable, ReactComponent, Ref, Unit, bind, discard, element, for_, fromMaybe, getOffsetHeightFromRef, guard, handler_, maybe, mempty, min, negate, null, pure, useEffectAlways, useRef, when, ($), (+), (-), (/), (/>), (<), (<#>), (<$>), (<*>), (</), (</*), (=<<), (==), (>), (>>=))
4+import Data.Int as Int
5+import Data.Nullable as Nullable
6+import Data.Traversable (sequence)
7+import Debug (spy)
8+import Effect.Class.Console (log)
9+import Effect.Unsafe (unsafePerformEffect)
10+import Framer.Motion as Motion
11+import Math as Math
12+import MotionValue (useMotionValue)
13+import MotionValue as MotionValue
14+import React.Basic.DOM (createPortal, css)
15+import React.Basic.DOM as R
16+import React.Basic.Emotion as Emotion
17+import React.Basic.Hooks (reactComponent)
18+import React.Basic.Hooks as React
19+import React.FocusTrap (focusTrap)
20+import Web.DOM (Element)
21+import Web.DOM.Node (isEqualNode)
22+import Web.DOM.Node as Node
23+import Web.Event.Event as Event
24+import Web.HTML as HTML
25+import Web.HTML.Window (innerHeight)
26+import Yoga.Block.Hook.Key as KeyCode
27+import Yoga.Block.Hook.UseKeyDown (useKeyDown)
28+import Yoga.Block.Molecule.Sheet.Style as Style
29+
30+type Props =
31+ { content ∷ JSX
32+ , isOpen ∷ Boolean
33+ , onDismiss ∷ Effect Unit
34+ , target ∷ Element
35+ | ()
36+ }
37+
38+component ∷ ReactComponent Props
39+component =
40+ unsafePerformEffect
41+ $ reactComponent "Sheet Wrapper" \{ content, isOpen, onDismiss, target } -> React.do
42+ useKeyDown case _ of
43+ KeyCode.Escape -> onDismiss
44+ _ -> mempty
45+ clickAwayRef <- React.useRef Nullable.null
46+ let
47+ toRender ∷ JSX
48+ toRender =
49+ React.element focusTrap
50+ { active: isOpen
51+ , children:
52+ R.div' </ {}
53+ /> [ Motion.animatePresence </ {} /> [ guard isOpen $ element clickaway { theRef: clickAwayRef, onDismiss } ]
54+ , element window { clickAwayRef, onDismiss, content, isOpen }
55+ ]
56+ }
57+ pure (createPortal toRender target)
58+
59+clickaway ∷ ReactComponent { theRef ∷ Ref (Nullable Node), onDismiss ∷ Effect Unit }
60+clickaway =
61+ unsafePerformEffect
62+ $ reactComponent "Sheet Clickaway" \{ theRef, onDismiss } -> React.do
63+ pure $ Emotion.elementKeyed Motion.div
64+ $ { key: "ry-modal-clickaway"
65+ , onClick: handler_ onDismiss
66+ , className: "ry-modal-clickaway"
67+ , css: Style.clickaway
68+ , initial: Motion.prop $ css { opacity: 0.0 }
69+ , animate: Motion.prop $ css { opacity: 1.0 }
70+ , exit: Motion.prop $ css { opacity: 0.0 }
71+ , ref: theRef
72+ }
73+
74+type WindowProps =
75+ { clickAwayRef ∷ NodeRef, content ∷ JSX, isOpen ∷ Boolean, onDismiss ∷ Effect Unit }
76+
77+window ∷ ReactComponent WindowProps
78+window =
79+ unsafePerformEffect
80+ $ reactComponent "Sheet Window" \({ clickAwayRef, content, onDismiss, isOpen } ∷ WindowProps) -> React.do
81+ ref ∷ NodeRef <- useRef null
82+ contentRef <- useRef null
83+ velocityRef <- useRef 0.0
84+ animationRef <- useRef Nothing
85+ top <- useMotionValue 0.0
86+ let
87+ getWindowHeight = do
88+ HTML.window >>= innerHeight <#> Int.toNumber
89+ getMaxHeight ∷ Effect Number
90+ getMaxHeight = do
91+ wh <- getWindowHeight
92+ maybeOffsetHeight <- getOffsetHeightFromRef contentRef
93+ pure $ maybe (0.0) (\x -> min wh (wh - x)) maybeOffsetHeight
94+ targetIsContentNode ev = do
95+ maybeContentNode <- React.readRefMaybe contentRef
96+ fromMaybe false <$> (sequence $ isEqualNode <$> maybeContentNode <*> (Node.fromEventTarget =<< Event.target ev))
97+ useEffectAlways do
98+ maxHeight <- getMaxHeight
99+ MotionValue.set maxHeight top
100+ mempty
101+ pure
102+ $ Motion.animatePresence
103+ </ {}
104+ /> [ guard isOpen $ Motion.div
105+ </* { className: "ry-sheet-background"
106+ , css: Style.sheet
107+ , layout: Motion.layout true
108+ , style: css { top }
109+ , ref
110+ , initial: Motion.initial $ css { y: "100%" }
111+ , animate:
112+ Motion.animate
113+ $ css
114+ { y: "0%"
115+ , transition: { type: "spring", stiffness: 1200.0, damping: 90.0 }
116+ }
117+ , exit: Motion.exit $ css { top: "100%" }
118+ , onPanStart:
119+ Motion.onPanStart \ev pi -> do
120+ maybeRunningAnimation <- React.readRef animationRef
121+ for_ maybeRunningAnimation MotionValue.stopAnimation
122+ React.writeRef animationRef Nothing
123+ , onPan:
124+ Motion.onPan \ev pi -> do
125+ oldY <- MotionValue.get top
126+ maxHeight <- getMaxHeight
127+ React.writeRef velocityRef pi.velocity.y
128+ let
129+ newNaiveValue = pi.delta.y + oldY
130+ newY =
131+ if newNaiveValue < maxHeight then
132+ negate (Math.pow (negate pi.delta.y) 0.33) + oldY
133+ else
134+ newNaiveValue
135+ when (Math.abs pi.velocity.y > 100.0) do
136+ MotionValue.set newY top
137+ , onPanEnd:
138+ Motion.onPanEnd \ev pi -> do
139+ velocity <- React.readRef velocityRef
140+ let _ = spy "shit" velocity
141+ yValue <- MotionValue.get top
142+ maxHeight <- getMaxHeight
143+ windowHeight <- getWindowHeight
144+ let percentPosition = spy "per " $ ((yValue - maxHeight) / (windowHeight - maxHeight))
145+ let
146+ target =
147+ if percentPosition > 0.7 then
148+ windowHeight
149+ else
150+ if percentPosition < 0.2 then
151+ maxHeight
152+ else
153+ if velocity > 70.0 then
154+ windowHeight
155+ else
156+ maxHeight
157+ animation <-
158+ MotionValue.animate target
159+ { type: "spring"
160+ , velocity: pi.velocity.y
161+ , stiffness: 1200.0
162+ , damping: 90.0
163+ , onComplete:
164+ do
165+ log "complete"
166+ React.writeRef animationRef Nothing
167+ when (target == windowHeight) do
168+ onDismiss
169+ }
170+ top
171+ React.writeRef animationRef (Just animation)
172+ }
173+ /> [ R.div'
174+ </* { ref: contentRef
175+ , css: Style.sheetContent
176+ , className: "ry-sheet-content"
177+ }
178+ /> [ R.div_ [ content ] ]
179+ ]
180+ ]
src/Yoga/Block/Organism/Form/Story.purs+1−3
@@ -1,20 +1,18 @@
11module Yoga.Block.Organism.Form.Story where
22
33import Prelude
4−import Data.Either (Either(..))
54import Data.Lens.Record (prop)
65import Data.Maybe (Maybe(..))
76import Data.String.NonEmpty (NonEmptyString, nes)
87import Data.Tuple.Nested ((/\))
98import Effect (Effect)
109import Effect.Unsafe (unsafePerformEffect)
11−import Foreign.Object as Object
1210import React.Basic (JSX, element, fragment)
1311import React.Basic.DOM as R
1412import React.Basic.DOM.Events (preventDefault)
1513import React.Basic.Emotion as E
1614import React.Basic.Events (handler)
17−import React.Basic.Hooks (reactComponent, useState, useState')
15+import React.Basic.Hooks (reactComponent, useState')
1816import React.Basic.Hooks as React
1917import Type.Prelude (Proxy(..))
2018import Yoga ((</>))
yarn.lock+14−14
@@ -4827,22 +4827,22 @@ fragment-cache@^0.2.1:
48274827 map-cache "^0.2.2"
48284828
48294829framer-motion@^4.0.3:
4830− version "4.0.3"
4831− resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-4.0.3.tgz#e20a32c6825dc08c98e130640cb6441ec2920b7d"
4832− integrity sha512-5p9VPpgPctMJuNW8CqHSk9k/v3xAUpReWzlgKl1a1fsvqzcFgNGENZPfBwHuukanvMmh3trzO5jZNX0Kc8/pAA==
4830+ version "4.1.2"
4831+ resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-4.1.2.tgz#2fffb7700ca1c3405e697294d155b1aae51c1031"
4832+ integrity sha512-nVieffLWWmK5gog46PsoecgJCsFV+n6A5GOqoGXkWtvB9jWcscRVIdGzAaPKo2uiJHkGSyXUHU8+lt6YOQVRqw==
48334833 dependencies:
4834− framesync "5.2.3"
4834+ framesync "5.3.0"
48354835 hey-listen "^1.0.8"
4836− popmotion "9.3.4"
4836+ popmotion "9.3.5"
48374837 style-value-types "4.1.4"
48384838 tslib "^2.1.0"
48394839 optionalDependencies:
48404840 "@emotion/is-prop-valid" "^0.8.2"
48414841
4842−framesync@5.2.3:
4843− version "5.2.3"
4844− resolved "https://registry.yarnpkg.com/framesync/-/framesync-5.2.3.tgz#73e26c83e108fb95f05515ee5c6fdf20c9fd47ab"
4845− integrity sha512-5PxjYm5RxvsT68a9trOOL/61POxL7DcHrbx+j/50CR33mvBdp0aUTI+EfrMeVXprx3XOZ37NWg6r5ZQQOA2arA==
4842+framesync@5.3.0:
4843+ version "5.3.0"
4844+ resolved "https://registry.yarnpkg.com/framesync/-/framesync-5.3.0.tgz#0ecfc955e8f5a6ddc8fdb0cc024070947e1a0d9b"
4845+ integrity sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==
48464846 dependencies:
48474847 tslib "^2.1.0"
48484848
@@ -7455,12 +7455,12 @@ polished@^3.4.4:
74557455 dependencies:
74567456 "@babel/runtime" "^7.12.5"
74577457
7458−popmotion@9.3.4:
7459− version "9.3.4"
7460− resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.3.4.tgz#9dea29e752491396b0beebc360d9837598f24c7c"
7461− integrity sha512-CwUJwVEkhXZg7ZCtWLrO2lK40g/J+cEwAV0bPxOq83g2UNrvN2HTcUDnlf/rq6QeMKJPwDvXd6R8SsJO5BjDXg==
7458+popmotion@9.3.5:
7459+ version "9.3.5"
7460+ resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.3.5.tgz#e821aff3424a021b0f2c93922db31c55cfe64149"
7461+ integrity sha512-Lr2rq8OP0j8D7CO2/6eO17ALeFCxjx1hfTGbMg+TLqFj+KZSGOoj6gRBVTzDINGqo6LQrORQSSSDaCL5OrB3bw==
74627462 dependencies:
7463− framesync "5.2.3"
7463+ framesync "5.3.0"
74647464 hey-listen "^1.0.8"
74657465 style-value-types "4.1.4"
74667466 tslib "^2.1.0"