diff --git a/elm.json b/elm.json index 4bd64ab6..91ee75ee 100644 --- a/elm.json +++ b/elm.json @@ -45,6 +45,7 @@ "Nri.Ui.Modal.V3", "Nri.Ui.Modal.V4", "Nri.Ui.Modal.V5", + "Nri.Ui.Modal.V6", "Nri.Ui.Outline.V2", "Nri.Ui.Page.V2", "Nri.Ui.Page.V3", @@ -96,4 +97,4 @@ "test-dependencies": { "elm-explorations/test": "1.2.0 <= v < 2.0.0" } -} \ No newline at end of file +} diff --git a/src/Nri/Ui/Modal/V6.elm b/src/Nri/Ui/Modal/V6.elm new file mode 100644 index 00000000..17d4fd1f --- /dev/null +++ b/src/Nri/Ui/Modal/V6.elm @@ -0,0 +1,295 @@ +module Nri.Ui.Modal.V6 exposing + ( Model, init + , Msg, update, subscriptions + , open, close + , info, warning, FocusableElementAttrs + , viewContent, viewFooter + , closeButton + ) + +{-| Changes from V5: + + - Removes button helpers, now that we can use Nri.Ui.Button.V9 directly + +These changes have required major API changes. Be sure to wire up subscriptions! + + import Html.Styled exposing (..) + import Nri.Ui.Button.V9 as Button + import Nri.Ui.Modal.V6 as Modal + + view : Modal.State -> Html Msg + view state = + Modal.info + { title = { title = "Modal Header", visibleTitle = True } + , wrapMsg = ModalMsg + , content = + \{ onlyFocusableElement } -> + div [] + [ Modal.viewContent [ text "Content goes here!" ] + , Modal.viewFooter + [ Button.button "Continue" + [ Button.primary + , Button.onClick DoSomthing + , Button.custom onlyFocusableElement + ] + , text "`onlyFocusableElement` will trap the focus on the 'Continue' button." + ] + ] + } + state + + subscriptions : Modal.State -> Sub Msg + subscriptions state = + Modal.subscriptions state + + +## State and updates + +@docs Model, init +@docs Msg, update, subscriptions + +@docs open, close + + +## Views + + +### Modals + +@docs info, warning, FocusableElementAttrs + + +### View containers + +@docs viewContent, viewFooter + + +## X icon + +@docs closeButton + +-} + +import Accessibility.Modal as Modal +import Accessibility.Style +import Accessibility.Styled as Html exposing (..) +import Accessibility.Styled.Style +import Accessibility.Styled.Widget as Widget +import Color +import Css +import Css.Global +import Html as Root +import Html.Attributes exposing (style) +import Html.Styled.Attributes exposing (css) +import Html.Styled.Events exposing (onClick) +import Nri.Ui +import Nri.Ui.Colors.Extra +import Nri.Ui.Colors.V1 as Colors +import Nri.Ui.Fonts.V1 as Fonts +import Nri.Ui.SpriteSheet +import Nri.Ui.Svg.V1 + + +{-| -} +type alias Model = + Modal.Model + + +{-| -} +init : Model +init = + Modal.init + + +{-| -} +type alias Msg = + Modal.Msg + + +{-| Include the subscription if you want the modal to dismiss on `Esc`. +-} +subscriptions : Model -> Sub Msg +subscriptions = + Modal.subscriptions + + +{-| -} +update : { dismissOnEscAndOverlayClick : Bool } -> Msg -> Model -> ( Model, Cmd Msg ) +update config msg model = + Modal.update config msg model + + +{-| -} +close : Msg +close = + Modal.close + + +{-| Pass the id of the element that focus should return to when the modal closes. +-} +open : String -> Msg +open = + Modal.open + + +{-| -} +type alias FocusableElementAttrs msg = + { onlyFocusableElement : List (Attribute msg) + , firstFocusableElement : List (Attribute msg) + , lastFocusableElement : List (Attribute msg) + } + + +{-| -} +info : + { visibleTitle : Bool + , title : String + , content : FocusableElementAttrs msg -> Html msg + , wrapMsg : Msg -> msg + } + -> Model + -> Html msg +info config model = + view { overlayColor = Colors.navy, titleColor = Colors.navy } config model + + +{-| -} +warning : + { visibleTitle : Bool + , title : String + , content : FocusableElementAttrs msg -> Html msg + , wrapMsg : Msg -> msg + } + -> Model + -> Html msg +warning config model = + view { overlayColor = Colors.gray20, titleColor = Colors.red } config model + + +view : + { overlayColor : Css.Color, titleColor : Css.Color } + -> + { visibleTitle : Bool + , title : String + , content : FocusableElementAttrs msg -> Html msg + , wrapMsg : Msg -> msg + } + -> Model + -> Html msg +view { overlayColor, titleColor } config model = + Modal.view + { overlayColor = toOverlayColor overlayColor + , wrapMsg = config.wrapMsg + , modalAttributes = modalStyles + , title = viewTitle titleColor { title = config.title, visibleTitle = config.visibleTitle } + , content = + \{ onlyFocusableElement, firstFocusableElement, lastFocusableElement } -> + { onlyFocusableElement = List.map Html.Styled.Attributes.fromUnstyled onlyFocusableElement + , firstFocusableElement = List.map Html.Styled.Attributes.fromUnstyled firstFocusableElement + , lastFocusableElement = List.map Html.Styled.Attributes.fromUnstyled lastFocusableElement + } + |> config.content + |> toUnstyled + } + model + |> fromUnstyled + + +toOverlayColor : Css.Color -> String +toOverlayColor color = + toCssString (Nri.Ui.Colors.Extra.withAlpha 0.9 color) + + +modalStyles : List (Root.Attribute Never) +modalStyles = + [ style "width" "600px" + , style "max-height" "calc(100vh - 100px)" + , style "padding" "40px 0 40px 0" + , style "margin" "75px auto" + , style "background-color" (toCssString Colors.white) + , style "border-radius" "20px" + , style "box-shadow" "0 1px 10px 0 rgba(0, 0, 0, 0.35)" + , style "position" "relative" -- required for closeButtonContainer + ] + + +{-| -} +viewTitle : Css.Color -> { visibleTitle : Bool, title : String } -> ( String, List (Root.Attribute Never) ) +viewTitle color { visibleTitle, title } = + ( title + , if visibleTitle then + [ style "font-weight" "700" + , style "line-height" "27px" + , style "margin" "0 49px" + , style "font-size" "20px" + , style "text-align" "center" + , style "color" (toCssString color) + ] + + else + Accessibility.Style.invisible + ) + + +toCssString : Css.Color -> String +toCssString = + Color.toCssString << Nri.Ui.Colors.Extra.toCoreColor + + +{-| -} +viewContent : List (Html msg) -> Html msg +viewContent = + Nri.Ui.styled div + "modal-content" + [ Css.overflowY Css.auto + , Css.padding2 (Css.px 30) (Css.px 40) + , Css.width (Css.pct 100) + , Css.minHeight (Css.px 150) + , Css.boxSizing Css.borderBox + ] + [] + + +{-| -} +viewFooter : List (Html msg) -> Html msg +viewFooter = + Nri.Ui.styled div + "modal-footer" + [ Css.alignItems Css.center + , Css.displayFlex + , Css.flexDirection Css.column + , Css.flexGrow (Css.int 2) + , Css.flexWrap Css.noWrap + , Css.margin4 (Css.px 20) Css.zero Css.zero Css.zero + , Css.width (Css.pct 100) + ] + [] + + + +--BUTTONS + + +{-| -} +closeButton : (Msg -> msg) -> List (Attribute msg) -> Html msg +closeButton wrapMsg focusableElementAttrs = + Nri.Ui.styled button + "close-button-container" + [ Css.position Css.absolute + , Css.top Css.zero + , Css.right Css.zero + , Css.padding (Css.px 25) + , Css.borderWidth Css.zero + , Css.width (Css.px 75) + , Css.backgroundColor Css.transparent + , Css.cursor Css.pointer + , Css.color Colors.azure + , Css.hover [ Css.color Colors.azureDark ] + , Css.property "transition" "color 0.1s" + ] + (Widget.label "Close modal" + :: Html.Styled.Attributes.map wrapMsg (onClick Modal.close) + :: focusableElementAttrs + ) + [ Nri.Ui.Svg.V1.toHtml Nri.Ui.SpriteSheet.xSvg + ] diff --git a/styleguide-app/Examples/Modal.elm b/styleguide-app/Examples/Modal.elm index df1b56bb..e69534fc 100644 --- a/styleguide-app/Examples/Modal.elm +++ b/styleguide-app/Examples/Modal.elm @@ -12,9 +12,10 @@ import Css.Global import Html as Root import Html.Styled.Attributes exposing (css) import ModuleExample exposing (Category(..), ModuleExample) +import Nri.Ui.Button.V9 as Button import Nri.Ui.Checkbox.V5 as Checkbox import Nri.Ui.Colors.V1 as Colors -import Nri.Ui.Modal.V5 as Modal +import Nri.Ui.Modal.V6 as Modal {-| -} @@ -45,29 +46,44 @@ init = {-| -} example : (Msg -> msg) -> State -> ModuleExample msg example parentMessage state = - { name = "Nri.Ui.Modal.V5" + { name = "Nri.Ui.Modal.V6" , category = Modals , content = - [ Modal.launchButton InfoModalMsg [] "Launch Info Modal" - , Modal.launchButton WarningModalMsg [] "Launch Warning Modal" + [ Button.button "Launch Info Modal" + [ Button.onClick (InfoModalMsg (Modal.open "launch-info-modal")) + , Button.custom + [ Html.Styled.Attributes.id "launch-info-modal" + , css [ Css.marginRight (Css.px 16) ] + ] + , Button.secondary + , Button.medium + ] + , Button.button "Launch Warning Modal" + [ Button.onClick (WarningModalMsg (Modal.open "launch-warning-modal")) + , Button.custom [ Html.Styled.Attributes.id "launch-warning-modal" ] + , Button.secondary + , Button.medium + ] , Modal.info - { title = { title = "Modal.info", visibleTitle = state.visibleTitle } + { title = "Modal.info" + , visibleTitle = state.visibleTitle , wrapMsg = InfoModalMsg , content = viewContent state InfoModalMsg - (Modal.primaryButton ForceClose "Continue") - (Modal.secondaryButton ForceClose "Close") + Button.primary + Button.secondary } state.infoModal , Modal.warning - { title = { title = "Modal.warning", visibleTitle = state.visibleTitle } + { title = "Modal.warning" + , visibleTitle = state.visibleTitle , wrapMsg = WarningModalMsg , content = viewContent state WarningModalMsg - (Modal.dangerButton ForceClose "Continue") - (Modal.secondaryButton ForceClose "Close") + Button.danger + Button.secondary } state.warningModal ] @@ -78,37 +94,106 @@ example parentMessage state = viewContent : State -> (Modal.Msg -> Msg) - -> (List (Root.Attribute Msg) -> Html Msg) - -> (List (Root.Attribute Msg) -> Html Msg) + -> Button.Attribute Msg + -> Button.Attribute Msg -> Modal.FocusableElementAttrs Msg -> Html Msg -viewContent state wrapMsg primaryButton secondaryButton focusableElementAttrs = - div [] - [ if state.showX then - Modal.closeButton wrapMsg focusableElementAttrs.firstFocusableElement - - else - text "" - , Modal.viewContent [ viewSettings state ] - , if state.showContinue && state.showSecondary then - Modal.viewFooter - [ primaryButton [] - , secondaryButton focusableElementAttrs.lastFocusableElement +viewContent state wrapMsg firstButtonStyle secondButtonStyle focusableElementAttrs = + case ( state.showX, state.showContinue, state.showSecondary ) of + ( True, True, True ) -> + div [] + [ Modal.closeButton wrapMsg focusableElementAttrs.firstFocusableElement + , Modal.viewContent [ viewSettings state ] + , Modal.viewFooter + [ Button.button "Continue" + [ firstButtonStyle + , Button.onClick ForceClose + ] + , Button.button "Close" + [ secondButtonStyle + , Button.onClick ForceClose + , Button.custom focusableElementAttrs.lastFocusableElement + ] + ] ] - else if state.showContinue then - Modal.viewFooter - [ primaryButton focusableElementAttrs.lastFocusableElement + ( True, False, True ) -> + div [] + [ Modal.closeButton wrapMsg focusableElementAttrs.firstFocusableElement + , Modal.viewContent [ viewSettings state ] + , Modal.viewFooter + [ Button.button "Close" + [ secondButtonStyle + , Button.onClick ForceClose + , Button.custom focusableElementAttrs.lastFocusableElement + ] + ] ] - else if state.showSecondary then - Modal.viewFooter - [ secondaryButton focusableElementAttrs.lastFocusableElement + ( True, False, False ) -> + div [] + [ Modal.closeButton wrapMsg focusableElementAttrs.firstFocusableElement + , Modal.viewContent [ viewSettings state ] ] - else - text "" - ] + ( True, True, False ) -> + div [] + [ Modal.closeButton wrapMsg focusableElementAttrs.firstFocusableElement + , Modal.viewContent [ viewSettings state ] + , Modal.viewFooter + [ Button.button "Continue" + [ firstButtonStyle + , Button.onClick ForceClose + , Button.custom focusableElementAttrs.lastFocusableElement + ] + ] + ] + + ( False, True, True ) -> + div [] + [ Modal.viewContent [ viewSettings state ] + , Modal.viewFooter + [ Button.button "Continue" + [ firstButtonStyle + , Button.onClick ForceClose + , Button.custom focusableElementAttrs.firstFocusableElement + ] + , Button.button "Close" + [ secondButtonStyle + , Button.onClick ForceClose + , Button.custom focusableElementAttrs.lastFocusableElement + ] + ] + ] + + ( False, False, True ) -> + div [] + [ Modal.viewContent [ viewSettings state ] + , Modal.viewFooter + [ Button.button "Close" + [ secondButtonStyle + , Button.onClick ForceClose + , Button.custom focusableElementAttrs.lastFocusableElement + ] + ] + ] + + ( False, True, False ) -> + div [] + [ Modal.viewContent [ viewSettings state ] + , Modal.viewFooter + [ Button.button "Continue" + [ firstButtonStyle + , Button.onClick ForceClose + , Button.custom focusableElementAttrs.lastFocusableElement + ] + ] + ] + + ( False, False, False ) -> + div [] + [ Modal.viewContent [ viewSettings state ] + ] viewSettings : State -> Html Msg