module Gargantext.Components.Bootstrap.Cloak ( cloak ) where import Gargantext.Prelude import Data.Foldable (elem) import Data.Maybe (Maybe(..), fromMaybe) import Data.Tuple.Nested ((/\)) import Effect (Effect) import Effect.Timer (setTimeout) import Gargantext.Hooks.FirstEffect (useFirstEffect') import Gargantext.Utils.Reactix as R2 import Reactix as R import Toestand as T type Props = ( defaultSlot :: R.Element , cloakSlot :: R.Element , isDisplayed :: Boolean | Options ) type Options = ( idlingPhaseDuration :: Maybe Int -- Milliseconds , sustainingPhaseDuration :: Maybe Int -- Milliseconds ) options :: Record Options options = { idlingPhaseDuration : Nothing , sustainingPhaseDuration : Nothing } -- | Abstract component type easing the transition display between a content -- | component and transitional (or cloak) component -- | -- | -- | Inputs: -- | * `defaultSlot :: Element` transclude pattern providing elements -- | to be finally displayed -- | * `cloakSlot :: Element` transclude pattern proviging elements -- | displayed during first phases (see lifecycle explanation below) -- | * `isDisplayed :: Boolean` flag defining if the main content is -- | ready to be displayed -- | * `idlingPhaseDuration :: Maybe Int` if defined, perform idling -- | phase (see lifecyle explanation below) -- | * `sustainingPhaseDuration :: Maybe Int` if defined, perform sustaining -- | phase (see lifecyle explanation below) -- | -- | -- | The lifecycle is structured into 4 phases: -- | 1. OPTIONAL, a primary idle phase, where no component is being displayed -- | ↳ why? if the flag switched from "off" to "on" in a particularly fast -- | pace, it will jump the sustain and wait phases (ie. the display -- | of the transitional component, see below) which won't be -- [ needed anymore -- | 2. OPTIONAL a sustain phase, where the transitional component will be -- | displayed for a defined amount of time -- | ↳ why? same heuristic as above, if the switch is too fast, the cloak -- | will be displayed at such speed that it will create a flickering -- | effect, this phase will avoid it -- | 3. a waiting phase, where the cloak will be displayed until the switch -- | will be set to "on" -- | 4. a final display phase, where the main content component will be shown -- | -- | -- | Simple transition: -- | -- | ```purescript -- | cloak -- | { isDisplayed : onPendingFlag -- | , cloakSlot : blankPlaceholder {} -- | , defaultSlot : componentToBeDisplayed {} [] -- | , idlingPhaseDuration : Nothing -- | , sustainingPhaseDuration : Nothing -- | } -- | ``` -- | -- | -- | Smart transition -- | -- | ```purescript -- | cloak -- | { isDisplayed : onPendingFlag -- | , cloakSlot : blankPlaceholder {} -- | , defaultSlot : componentToBeDisplayed {} [] -- | -- Idling phase set up -- | -- -- | -- * if the computation for display makes less than 20ms, no -- | -- transition at all will be displayed, and the content will arrive -- | -- as soon as the 20ms delay is consumed -- | -- * if the computation takes more, following phases will be set up -- | -- (according to the configuration provided) -- | , idlingPhaseDuration : Just 20 -- | -- Sustaining phase set up -- | -- -- | -- * now let's say the computation talking above makes 35ms before -- | -- displaying its content, setting this parameter will avoid a: -- | -- 35ms (computation) - 20ms (idling) = 15ms (sustaining) -- | -- * this very short delay will be considered as a UI flickering -- | -- effect for the user, which is a UX smelly design -- | -- * by setting the sustaining phase to 400ms, we ensure a period -- | -- of time that eliminates this effect -- | , sustainingPhaseDuration : Just 400 -- | } -- | ``` cloak :: forall r. R2.OptLeaf Options Props r cloak = R2.optLeaf component options cname :: String cname = "b-cloak" component :: R.Component Props component = R.hooksComponent cname cpt where cpt props _ = do -- State phase /\ phaseBox <- R2.useBox' (Idle :: Phase) -- Computed let canCloakBeDisplayed = elem phase [ Sustain, Wait ] canContentBeDisplayed = elem phase [ Display ] -- Behaviors let execDisplayingPhaseOr :: (Unit -> Effect Unit) -> Effect Unit execDisplayingPhaseOr thunk = if props.isDisplayed then T.write_ Display phaseBox else thunk unit execWaitingPhase :: Unit -> Effect Unit execWaitingPhase _ = execDisplayingPhaseOr $ const $ T.write_ Wait phaseBox execSustainingPhase :: Unit -> Effect Unit execSustainingPhase _ = execDisplayingPhaseOr $ const $ T.write_ Sustain phaseBox <* setTimeout (fromMaybe 0 props.sustainingPhaseDuration) (execWaitingPhase unit) execIdlingPhase :: Unit -> Effect Unit execIdlingPhase _ = execDisplayingPhaseOr $ const $ T.write_ Idle phaseBox <* setTimeout (fromMaybe 0 props.idlingPhaseDuration) (execSustainingPhase unit) -- Effects useFirstEffect' $ execIdlingPhase unit R.useEffect2' props.isDisplayed phase $ if (props.isDisplayed && phase == Wait) then T.write_ Display phaseBox else pure unit -- Render pure $ R.fragment [ R2.when canCloakBeDisplayed props.cloakSlot , R2.when canContentBeDisplayed props.defaultSlot ] data Phase = Idle | Sustain | Wait | Display derive instance eqPhase :: Eq Phase