diff --git a/elm.json b/elm.json index 613a9e68..b8161617 100644 --- a/elm.json +++ b/elm.json @@ -45,7 +45,9 @@ "Nri.Ui.SegmentedControl.V6", "Nri.Ui.Select.V5", "Nri.Ui.Svg.V1", + "Nri.Ui.Slide.V1", "Nri.Ui.SlideModal.V1", + "Nri.Ui.SlideModal.V2", "Nri.Ui.Table.V3", "Nri.Ui.Table.V4", "Nri.Ui.Tabs.V3", diff --git a/src/Nri/Ui/Slide/V1.elm b/src/Nri/Ui/Slide/V1.elm new file mode 100644 index 00000000..3d996263 --- /dev/null +++ b/src/Nri/Ui/Slide/V1.elm @@ -0,0 +1,107 @@ +module Nri.Ui.Slide.V1 exposing + ( AnimationDirection(..) + , withSlidingContents + , animateIn, animateOut + ) + +{-| Note: You'll almost certainly want to used keyed nodes if you're +using this module. + +@docs AnimationDirection +@docs withSlidingContents +@docs animateIn, animateOut + +-} + +import Css +import Css.Animations + + +{-| Slide from right to left or from left to right. +-} +type AnimationDirection + = FromRTL + | FromLTR + + +translateXBy : Float +translateXBy = + 700 + + +slideDuration : Css.Style +slideDuration = + Css.animationDuration (Css.ms 700) + + +slideTimingFunction : Css.Style +slideTimingFunction = + Css.property "animation-timing-function" "ease-in-out" + + +{-| Add this class to the container whose descendents are sliding. +-} +withSlidingContents : Css.Style +withSlidingContents = + Css.batch + [ Css.position Css.relative + , Css.overflowX Css.hidden + ] + + +{-| Add this style to the element you want to animate in. +-} +animateIn : AnimationDirection -> Css.Style +animateIn direction = + let + ( start, end ) = + case direction of + FromRTL -> + ( Css.px translateXBy, Css.zero ) + + FromLTR -> + ( Css.px -translateXBy, Css.zero ) + in + Css.batch + [ slideDuration + , slideTimingFunction + , Css.property "animation-delay" "-50ms" + , Css.animationName + (Css.Animations.keyframes + [ ( 0, [ Css.Animations.transform [ Css.translateX start ] ] ) + , ( 100, [ Css.Animations.transform [ Css.translateX end ] ] ) + ] + ) + ] + + +{-| Add this style to the element you want to animate out. +Note: this will absolutely position the element. +You must add `withSlidingContents` to one of its ancestors. +-} +animateOut : AnimationDirection -> Css.Style +animateOut direction = + let + ( start, end ) = + case direction of + FromRTL -> + ( Css.zero, Css.px -translateXBy ) + + FromLTR -> + ( Css.zero, Css.px translateXBy ) + in + Css.batch + [ Css.position Css.absolute + , Css.transform (Css.translate2 end Css.zero) + , Css.property "animation-delay" "-50ms" + , Css.batch + [ slideDuration + , slideTimingFunction + , Css.animationName + (Css.Animations.keyframes + [ ( 0, [ Css.Animations.transform [ Css.translateX start ] ] ) + , ( 100, [ Css.Animations.transform [ Css.translateX end ] ] ) + ] + ) + ] + ] diff --git a/src/Nri/Ui/SlideModal/V2.elm b/src/Nri/Ui/SlideModal/V2.elm new file mode 100644 index 00000000..71e3a5e9 --- /dev/null +++ b/src/Nri/Ui/SlideModal/V2.elm @@ -0,0 +1,411 @@ +module Nri.Ui.SlideModal.V2 exposing + ( Config, Panel + , State, closed, open + , view + ) + +{-| + +@docs Config, Panel +@docs State, closed, open +@docs view + +-} + +import Accessibility.Styled as Html exposing (..) +import Accessibility.Styled.Aria exposing (labelledBy) +import Accessibility.Styled.Role as Role +import Accessibility.Styled.Style +import Accessibility.Styled.Widget as Widget +import Color +import Css +import Css.Animations +import Css.Global +import Html.Styled +import Html.Styled.Attributes exposing (css) +import Html.Styled.Events exposing (onClick) +import Html.Styled.Keyed as Keyed +import Nri.Ui +import Nri.Ui.AssetPath exposing (Asset(..)) +import Nri.Ui.Button.V8 as Button +import Nri.Ui.Colors.Extra +import Nri.Ui.Colors.V1 as Colors +import Nri.Ui.Fonts.V1 as Fonts +import Nri.Ui.Icon.V3 as Icon +import Nri.Ui.Slide.V1 as Slide exposing (AnimationDirection(..)) +import Nri.Ui.Text.V2 as Text + + +{-| -} +type alias Config msg = + { panels : List Panel + , height : Css.Vh + , parentMsg : State -> msg + } + + +{-| -} +type State + = State + { currentPanelIndex : Maybe Int + , previousPanel : Maybe ( AnimationDirection, Panel ) + } + + +{-| Create the open state for the modal (the first panel will show). +-} +open : State +open = + State + { currentPanelIndex = Just 0 + , previousPanel = Nothing + } + + +{-| Close the modal. +-} +closed : State +closed = + State + { currentPanelIndex = Nothing + , previousPanel = Nothing + } + + +{-| View the modal (includes the modal backdrop). +-} +view : Config msg -> State -> Html msg +view config ((State { currentPanelIndex }) as state) = + case Maybe.andThen (summarize config.panels) currentPanelIndex of + Just summary -> + viewBackdrop + (viewModal config state summary) + + Nothing -> + Html.text "" + + +type alias Summary = + { current : Panel + , upcoming : List ( State, String ) + , previous : List ( State, String ) + } + + +summarize : List Panel -> Int -> Maybe Summary +summarize panels current = + let + indexedPanels = + List.indexedMap (\i { title } -> ( i, title )) panels + + toOtherPanel direction currentPanel ( i, title ) = + ( State + { currentPanelIndex = Just i + , previousPanel = + Just + ( direction + , { currentPanel | content = currentPanel.content } + ) + } + , title + ) + in + case List.drop current panels of + currentPanel :: rest -> + Just + { current = currentPanel + , upcoming = + indexedPanels + |> List.drop (current + 1) + |> List.map (toOtherPanel FromRTL currentPanel) + , previous = + indexedPanels + |> List.take current + |> List.map (toOtherPanel FromLTR currentPanel) + } + + [] -> + Nothing + + +viewModal : Config msg -> State -> Summary -> Html msg +viewModal config (State { previousPanel }) summary = + Keyed.node "div" + [ css + [ Css.boxSizing Css.borderBox + , Css.margin2 (Css.px 75) Css.auto + , Css.backgroundColor Colors.white + , Css.borderRadius (Css.px 20) + , Css.property "box-shadow" "0 1px 10px 0 rgba(0, 0, 0, 0.35)" + , Slide.withSlidingContents + ] + , Role.dialog + , Widget.modal True + , labelledBy (panelId summary.current) + ] + (case previousPanel of + Just ( direction, panelView ) -> + ( panelId panelView + , panelContainer config.height + [ Slide.animateOut direction ] + [ viewIcon panelView.icon + , Text.subHeading + [ span [ Html.Styled.Attributes.id (panelId panelView) ] [ Html.text panelView.title ] + ] + , viewContent panelView.content + ] + ) + :: viewModalContent config summary [ Slide.animateIn direction ] + + Nothing -> + viewModalContent config summary [] + ) + + +viewModalContent : Config msg -> Summary -> List Css.Style -> List ( String, Html msg ) +viewModalContent config summary styles = + [ ( panelId summary.current + , panelContainer config.height + styles + [ viewIcon summary.current.icon + , Text.subHeading + [ span [ Html.Styled.Attributes.id (panelId summary.current) ] + [ Html.text summary.current.title ] + ] + , viewContent summary.current.content + ] + ) + , ( panelId summary.current ++ "-footer" + , viewActiveFooter summary + |> Html.map config.parentMsg + ) + ] + + +viewBackdrop : Html msg -> Html msg +viewBackdrop modal = + Nri.Ui.styled div + "modal-backdrop-container" + (Css.backgroundColor (Nri.Ui.Colors.Extra.withAlpha 0.9 Colors.navy) + :: [ Css.height (Css.vh 100) + , Css.left Css.zero + , Css.overflow Css.hidden + , Css.position Css.fixed + , Css.top Css.zero + , Css.width (Css.pct 100) + , Css.zIndex (Css.int 200) + , Css.displayFlex + , Css.alignItems Css.center + , Css.justifyContent Css.center + ] + ) + [] + [ -- This global