Merge pull request #380 from NoRedInk/lab/modal-without-bottom-buttons

Lab/modal without bottom buttons
This commit is contained in:
Tessa 2019-10-23 13:04:33 -07:00 committed by GitHub
commit a76904e9d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 585 additions and 129 deletions

View File

@ -53,6 +53,7 @@
"Nri.Ui.Modal.V5",
"Nri.Ui.Modal.V6",
"Nri.Ui.Modal.V7",
"Nri.Ui.Modal.V8",
"Nri.Ui.Outline.V2",
"Nri.Ui.Page.V2",
"Nri.Ui.Page.V3",

453
src/Nri/Ui/Modal/V8.elm Normal file
View File

@ -0,0 +1,453 @@
module Nri.Ui.Modal.V8 exposing
( Model, init
, Msg, update, subscriptions
, open, close
, info, warning
, ViewFuncs
, Focusable
, multipleFocusableElementView, onlyFocusableElementView
)
{-| Changes from V7:
- More customizable attributes
- Rather than accepting any number of attributes, Modal provides one callback that returns a focusable
- viewFooter has been merged into viewContent
- viewContent and closeButton are now callbacks that are pre-configured with settings
(previously you passed config through)
```
import Html.Styled exposing (..)
import Nri.Ui.Button.V9 as Button
import Nri.Ui.Modal.V8 as Modal
type Msg
= ModalMsg Modal.Msg
| DoSomething
view : Modal.Model -> Html Msg
view state =
Modal.info
{ title = "Modal Header"
, wrapMsg = ModalMsg
, visibleTitle = True
}
(\{viewContent, closeButton} ->
Modal.onlyFocusableElementView
(\{ onlyFocusableElement } ->
div []
[ viewContent {
, content = [ text "Content goes here!" ]
, footer =
[ Button.button "Continue"
[ Button.primary
, Button.onClick DoSomething
, Button.custom onlyFocusableElement
]
, text "`onlyFocusableElement` will trap the focus on the 'Continue' button."
]
}
visibleTitle
]
)
)
state
subscriptions : Modal.Model -> Sub Msg
subscriptions state =
Modal.subscriptions state
view init
--> text "" -- a closed modal
```
## State and updates
@docs Model, init
@docs Msg, update, subscriptions
@docs open, close
## Views
### Modals
@docs info, warning
@docs ViewFuncs
### Focusable
@docs Focusable
@docs multipleFocusableElementView, onlyFocusableElementView
-}
import Accessibility.Modal.Copy as Modal
import Accessibility.Styled as Html exposing (..)
import Accessibility.Styled.Widget as Widget
import Color.Transparent as Transparent
import Css
import Css.Transitions
import Html.Styled.Attributes as Attributes exposing (css)
import Html.Styled.Events exposing (onClick)
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
type alias Config msg =
{ visibleTitle : Bool
, title : String
, wrapMsg : Msg -> msg
}
{-| -}
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
{-| -}
info :
Config msg
-> (ViewFuncs msg -> Focusable msg)
-> Model
-> Html msg
info config getFocusable model =
view Info config getFocusable model
{-| -}
warning :
Config msg
-> (ViewFuncs msg -> Focusable msg)
-> Model
-> Html msg
warning config getFocusable model =
view Warning config getFocusable model
type Theme
= Info
| Warning
themeToOverlayColor : Theme -> Css.Color
themeToOverlayColor theme =
case theme of
Info ->
Colors.navy
Warning ->
Colors.gray20
themeToTitleColor : Theme -> Css.Color
themeToTitleColor theme =
case theme of
Info ->
Colors.navy
Warning ->
Colors.red
{-| -}
type Focusable msg
= Focusable (Modal.Attribute msg) (List (Modal.Attribute msg))
{-| -}
multipleFocusableElementView :
({ firstFocusableElement : List (Html.Attribute msg)
, lastFocusableElement : List (Html.Attribute msg)
, autofocusElement : Html.Attribute msg
}
-> Html msg
)
-> Focusable msg
multipleFocusableElementView f =
Focusable (Modal.multipleFocusableElementView (\attributes -> f attributes)) []
{-| -}
onlyFocusableElementView : (List (Html.Attribute msg) -> Html msg) -> Focusable msg
onlyFocusableElementView f =
Focusable (Modal.onlyFocusableElementView (\attributes -> f attributes)) [ Modal.autofocusOnLastElement ]
{-| -}
type alias ViewFuncs msg =
{ viewContent : { content : List (Html msg), footer : List (Html msg) } -> Html msg
, closeButton : List (Html.Attribute msg) -> Html msg
}
view :
Theme
-> Config msg
-> (ViewFuncs msg -> Focusable msg)
-> Model
-> Html msg
view theme config getFocusable model =
let
viewFuncs : ViewFuncs msg
viewFuncs =
{ viewContent = viewContent config.visibleTitle
, closeButton = closeButton config.wrapMsg
}
focusables =
case getFocusable viewFuncs of
Focusable fst rst ->
fst :: rst
in
Modal.view
config.wrapMsg
config.title
([ Modal.overlayColor (Nri.Ui.Colors.Extra.withAlpha 0.9 (themeToOverlayColor theme))
, Modal.custom
[ Css.width (Css.px 600)
, Css.margin2 (Css.px 50) Css.auto
, Css.borderRadius (Css.px 20)
, Css.boxShadow5 Css.zero (Css.px 1) (Css.px 10) Css.zero (Css.rgba 0 0 0 0.35)
, Css.backgroundColor Colors.white
]
, if config.visibleTitle then
Modal.titleStyles
[ Fonts.baseFont
, Css.fontWeight (Css.int 700)
, Css.paddingTop (Css.px 40)
, Css.paddingBottom (Css.px 20)
, Css.margin Css.zero
, Css.fontSize (Css.px 20)
, Css.textAlign Css.center
, Css.color (themeToTitleColor theme)
]
else
Modal.titleStyles
[ -- https://snook.ca/archives/html_and_css/hiding-content-for-accessibility
Css.property "clip" "rect(1px, 1px, 1px, 1px)"
, Css.position Css.absolute
, Css.height (Css.px 1)
, Css.width (Css.px 1)
, Css.overflow Css.hidden
, Css.margin (Css.px -1)
, Css.padding Css.zero
, Css.border Css.zero
]
]
++ focusables
)
model
|> List.singleton
|> div [ css [ Css.position Css.relative, Css.zIndex (Css.int 1) ] ]
{-| -}
viewContent : Bool -> { content : List (Html msg), footer : List (Html msg) } -> Html msg
viewContent visibleTitle { content, footer } =
div []
[ viewInnerContent content visibleTitle (not (List.isEmpty footer))
, viewFooter footer
]
{-| -}
viewInnerContent : List (Html msg) -> Bool -> Bool -> Html msg
viewInnerContent children visibleTitle visibleFooter =
let
titleHeight =
if visibleTitle then
45
else
0
footerHeight =
if visibleFooter then
180
else
0
modalTitleStyles =
if visibleTitle then
[]
else
[ Css.borderTopLeftRadius (Css.px 20)
, Css.borderTopRightRadius (Css.px 20)
, Css.overflowY Css.hidden
]
modalFooterStyles =
if visibleFooter then
[]
else
[ Css.borderBottomLeftRadius (Css.px 20)
, Css.borderBottomRightRadius (Css.px 20)
, Css.overflowY Css.hidden
]
in
div
[ css (modalTitleStyles ++ modalFooterStyles)
]
[ div
[ css
[ Css.overflowY Css.auto
, Css.minHeight (Css.px 150)
, Css.maxHeight
(Css.calc (Css.vh 100)
Css.minus
(Css.px (footerHeight + titleHeight + 145))
)
, Css.width (Css.pct 100)
, Css.boxSizing Css.borderBox
, Css.paddingLeft (Css.px 40)
, Css.paddingRight (Css.px 40)
, if visibleTitle then
Css.paddingTop Css.zero
else
Css.paddingTop (Css.px 40)
, if visibleFooter then
Css.paddingBottom Css.zero
else
Css.paddingBottom (Css.px 40)
, if visibleFooter then
shadow (Transparent.customOpacity 0.15) (Css.px 16)
else
shadow (Transparent.customOpacity 0.4) (Css.px 30)
]
]
children
]
shadow : Transparent.Opacity -> Css.Px -> Css.Style
shadow opacity bottomShadowHeight =
let
to =
Transparent.fromRGBA { red = 0, green = 0, blue = 0, alpha = opacity }
|> Transparent.toRGBAString
in
Css.batch
[ -- Shadows for indicating that the content is scrollable
[ "/* TOP shadow */"
, "top linear-gradient(to top, rgb(255, 255, 255), rgb(255, 255, 255)) local,"
, "top linear-gradient(to top, rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.15)) scroll,"
, ""
, "/* BOTTOM shadow */"
, "bottom linear-gradient(to bottom, rgb(255, 255, 255), rgb(255, 255, 255)) local,"
, "bottom linear-gradient(to bottom, rgba(255, 255, 255, 0), " ++ to ++ ") scroll"
]
|> String.join "\n"
|> Css.property "background"
, Css.backgroundSize2 (Css.pct 100) bottomShadowHeight
, Css.backgroundRepeat Css.noRepeat
]
{-| -}
viewFooter : List (Html msg) -> Html msg
viewFooter children =
if List.isEmpty children then
Html.text ""
else
div
[ css
[ 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.paddingBottom (Css.px 40)
, Css.width (Css.pct 100)
]
]
children
--BUTTONS
{-| -}
closeButton : (Msg -> msg) -> List (Html.Attribute msg) -> Html msg
closeButton wrapMsg focusableElementAttrs =
button
(Widget.label "Close modal"
:: Attributes.map wrapMsg (onClick Modal.close)
:: css
[ -- in the upper-right corner of the modal
Css.position Css.absolute
, Css.top Css.zero
, Css.right Css.zero
-- make the hitspace extend all the way to the corner
, Css.width (Css.px 40)
, Css.height (Css.px 40)
, Css.padding4 (Css.px 20) (Css.px 20) (Css.px 2) Css.zero
-- apply button styles
, Css.borderWidth Css.zero
, Css.backgroundColor Css.transparent
, Css.cursor Css.pointer
, Css.color Colors.azure
, Css.hover [ Css.color Colors.azureDark ]
, Css.Transitions.transition [ Css.Transitions.color 0.1 ]
]
:: focusableElementAttrs
)
[ Nri.Ui.Svg.V1.toHtml Nri.Ui.SpriteSheet.xSvg
]

View File

@ -16,7 +16,7 @@ import Nri.Ui.Button.V9 as Button
import Nri.Ui.Checkbox.V5 as Checkbox
import Nri.Ui.ClickableText.V3 as ClickableText
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Modal.V7 as Modal
import Nri.Ui.Modal.V8 as Modal
import Nri.Ui.Text.V4 as Text
@ -50,7 +50,7 @@ init =
{-| -}
example : (Msg -> msg) -> State -> ModuleExample msg
example parentMessage state =
{ name = "Nri.Ui.Modal.V7"
{ name = "Nri.Ui.Modal.V8"
, category = Modals
, content =
[ viewSettings state
@ -69,181 +69,185 @@ example parentMessage state =
, Button.secondary
, Button.medium
]
, Modal.info
{ title = "Modal.info"
, visibleTitle = state.visibleTitle
, wrapMsg = InfoModalMsg
}
(viewContent state InfoModalMsg Button.primary)
, let
params =
( state, InfoModalMsg, Button.primary )
in
Modal.info { title = "Modal.info", wrapMsg = InfoModalMsg, visibleTitle = state.visibleTitle }
(getFocusable params)
state.infoModal
, Modal.warning
{ title = "Modal.warning"
, visibleTitle = state.visibleTitle
, wrapMsg = WarningModalMsg
}
(viewContent state WarningModalMsg Button.danger)
, let
params =
( state, WarningModalMsg, Button.danger )
in
Modal.warning { title = "Modal.warning", wrapMsg = WarningModalMsg, visibleTitle = state.visibleTitle }
(getFocusable params)
state.warningModal
]
|> List.map (Html.map parentMessage)
}
viewContent :
State
-> (Modal.Msg -> Msg)
-> Button.Attribute Msg
-> List (Modal.Attribute Msg)
viewContent state wrapMsg firstButtonStyle =
getFocusable :
( State, Modal.Msg -> Msg, Button.Attribute Msg )
-> Modal.ViewFuncs Msg
-> Modal.Focusable Msg
getFocusable ( state, wrapMsg, firstButtonStyle ) { viewContent, closeButton } =
case ( state.showX, state.showContinue, state.showSecondary ) of
( True, True, True ) ->
[ Modal.multipleFocusableElementView
(\focusableElementAttrs ->
Modal.multipleFocusableElementView
(\{ firstFocusableElement, autofocusElement, lastFocusableElement } ->
div []
[ Modal.closeButton wrapMsg focusableElementAttrs.firstFocusableElement
, Modal.viewContent [ viewModalContent state.longContent ]
, Modal.viewFooter
[ Button.button "Continue"
[ firstButtonStyle
, Button.onClick ForceClose
, Button.large
, Button.custom [ focusableElementAttrs.autofocusElement ]
[ closeButton firstFocusableElement
, viewContent
{ content = [ viewModalContent state.longContent ]
, footer =
[ Button.button "Continue"
[ firstButtonStyle
, Button.onClick ForceClose
, Button.large
, Button.custom [ autofocusElement ]
]
, ClickableText.button "Close"
[ ClickableText.onClick ForceClose
, ClickableText.large
, ClickableText.custom
(css [ Css.marginTop (Css.px 12) ]
:: lastFocusableElement
)
]
]
, ClickableText.button "Close"
[ ClickableText.onClick ForceClose
, ClickableText.large
, ClickableText.custom
(css [ Css.marginTop (Css.px 20) ]
:: focusableElementAttrs.lastFocusableElement
)
]
]
}
]
)
]
( True, False, True ) ->
[ Modal.multipleFocusableElementView
(\focusableElementAttrs ->
Modal.multipleFocusableElementView
(\{ firstFocusableElement, autofocusElement, lastFocusableElement } ->
div []
[ Modal.closeButton wrapMsg focusableElementAttrs.firstFocusableElement
, Modal.viewContent [ viewModalContent state.longContent ]
, Modal.viewFooter
[ ClickableText.button "Close"
[ ClickableText.onClick ForceClose
, ClickableText.large
, ClickableText.custom
(css [ Css.marginTop (Css.px 20) ]
:: focusableElementAttrs.lastFocusableElement
)
[ closeButton firstFocusableElement
, viewContent
{ content = [ viewModalContent state.longContent ]
, footer =
[ ClickableText.button "Close"
[ ClickableText.onClick ForceClose
, ClickableText.large
, ClickableText.custom
(css [ Css.marginTop (Css.px 12) ]
:: autofocusElement
:: lastFocusableElement
)
]
]
]
}
]
)
]
( True, False, False ) ->
[ Modal.multipleFocusableElementView
(\focusableElementAttrs ->
Modal.onlyFocusableElementView
(\onlyFocusableElement ->
div []
[ Modal.closeButton wrapMsg focusableElementAttrs.firstFocusableElement
, Modal.viewContent [ viewModalContent state.longContent ]
[ closeButton onlyFocusableElement
, viewContent
{ content = [ viewModalContent state.longContent ]
, footer = []
}
]
)
]
( True, True, False ) ->
[ Modal.autofocusOnLastElement
, Modal.multipleFocusableElementView
(\focusableElementAttrs ->
Modal.multipleFocusableElementView
(\{ firstFocusableElement, autofocusElement, lastFocusableElement } ->
div []
[ Modal.closeButton wrapMsg focusableElementAttrs.firstFocusableElement
, Modal.viewContent [ viewModalContent state.longContent ]
, Modal.viewFooter
[ Button.button "Continue"
[ firstButtonStyle
, Button.onClick ForceClose
, Button.custom focusableElementAttrs.lastFocusableElement
, Button.large
[ closeButton firstFocusableElement
, viewContent
{ content = [ viewModalContent state.longContent ]
, footer =
[ Button.button "Continue"
[ firstButtonStyle
, Button.onClick ForceClose
, Button.custom (autofocusElement :: lastFocusableElement)
, Button.large
]
]
]
}
]
)
]
( False, True, True ) ->
[ Modal.multipleFocusableElementView
(\focusableElementAttrs ->
Modal.multipleFocusableElementView
(\{ firstFocusableElement, autofocusElement, lastFocusableElement } ->
div []
[ Modal.viewContent [ viewModalContent state.longContent ]
, Modal.viewFooter
[ Button.button "Continue"
[ firstButtonStyle
, Button.onClick ForceClose
, Button.custom focusableElementAttrs.firstFocusableElement
, Button.large
[ viewContent
{ content = [ viewModalContent state.longContent ]
, footer =
[ Button.button "Continue"
[ firstButtonStyle
, Button.onClick ForceClose
, Button.custom (autofocusElement :: firstFocusableElement)
, Button.large
]
, ClickableText.button "Close"
[ ClickableText.onClick ForceClose
, ClickableText.large
, ClickableText.custom
(css [ Css.marginTop (Css.px 12) ]
:: lastFocusableElement
)
]
]
, ClickableText.button "Close"
[ ClickableText.onClick ForceClose
, ClickableText.large
, ClickableText.custom
(css [ Css.marginTop (Css.px 20) ]
:: focusableElementAttrs.lastFocusableElement
)
]
]
}
]
)
]
( False, False, True ) ->
[ Modal.autofocusOnLastElement
, Modal.multipleFocusableElementView
(\focusableElementAttrs ->
Modal.onlyFocusableElementView
(\onlyFocusableElement ->
div []
[ Modal.viewContent [ viewModalContent state.longContent ]
, Modal.viewFooter
[ ClickableText.button "Close"
[ ClickableText.onClick ForceClose
, ClickableText.large
, ClickableText.custom
(css
[ Css.marginTop
(Css.px 20)
]
:: focusableElementAttrs.lastFocusableElement
)
[ viewContent
{ content = [ viewModalContent state.longContent ]
, footer =
[ ClickableText.button "Close"
[ ClickableText.onClick ForceClose
, ClickableText.large
, ClickableText.custom
(css [ Css.marginTop (Css.px 12) ]
:: onlyFocusableElement
)
]
]
]
}
]
)
]
( False, True, False ) ->
[ Modal.autofocusOnLastElement
, Modal.multipleFocusableElementView
(\focusableElementAttrs ->
Modal.onlyFocusableElementView
(\onlyFocusableElement ->
div []
[ Modal.viewContent [ viewModalContent state.longContent ]
, Modal.viewFooter
[ Button.button "Continue"
[ firstButtonStyle
, Button.onClick ForceClose
, Button.custom [ focusableElementAttrs.autofocusElement ]
, Button.large
[ viewContent
{ content = [ viewModalContent state.longContent ]
, footer =
[ Button.button "Continue"
[ firstButtonStyle
, Button.onClick ForceClose
, Button.custom onlyFocusableElement
, Button.large
]
]
]
}
]
)
]
( False, False, False ) ->
[ Modal.multipleFocusableElementView
(\focusableElementAttrs ->
Modal.onlyFocusableElementView
(\_ ->
div []
[ Modal.viewContent [ viewModalContent state.longContent ]
[ viewContent
{ content = [ viewModalContent state.longContent ]
, footer = []
}
]
)
]
viewModalContent : Bool -> Html msg
@ -251,13 +255,11 @@ viewModalContent longContent =
Text.mediumBody
[ span [ css [ whiteSpace preLine ] ]
[ if longContent then
"""
Soufflé pastry chocolate cake danish muffin. Candy wafer pastry ice cream cheesecake toffee cookie cake carrot cake. Macaroon pie jujubes gummies cookie pie. Gummi bears brownie pastry carrot cake cotton candy. Jelly-o sweet roll biscuit cake soufflé lemon drops tiramisu marshmallow macaroon. Chocolate jelly halvah marzipan macaroon cupcake sweet cheesecake carrot cake.
"""Soufflé pastry chocolate cake danish muffin. Candy wafer pastry ice cream cheesecake toffee cookie cake carrot cake. Macaroon pie jujubes gummies cookie pie. Gummi bears brownie pastry carrot cake cotton candy. Jelly-o sweet roll biscuit cake soufflé lemon drops tiramisu marshmallow macaroon. Chocolate jelly halvah marzipan macaroon cupcake sweet cheesecake carrot cake.
Sesame snaps pastry muffin cookie. Powder powder sweet roll toffee cake icing. Chocolate cake sweet roll gingerbread icing chupa chups sweet roll sesame snaps. Chocolate croissant chupa chups jelly beans toffee. Jujubes sweet wafer marshmallow halvah jelly. Liquorice sesame snaps sweet.
Sesame snaps pastry muffin cookie. Powder powder sweet roll toffee cake icing. Chocolate cake sweet roll gingerbread icing chupa chups sweet roll sesame snaps. Chocolate croissant chupa chups jelly beans toffee. Jujubes sweet wafer marshmallow halvah jelly. Liquorice sesame snaps sweet.
Tootsie roll icing jelly danish ice cream tiramisu sweet roll. Fruitcake ice cream dragée. Bear claw sugar plum sweet jelly beans bonbon dragée tart. Gingerbread chocolate sweet. Apple pie danish toffee sugar plum jelly beans donut. Chocolate cake croissant caramels chocolate bar. Jelly beans caramels toffee chocolate cake liquorice. Toffee pie sugar plum cookie toffee muffin. Marzipan marshmallow marzipan liquorice tiramisu.
"""
Tootsie roll icing jelly danish ice cream tiramisu sweet roll. Fruitcake ice cream dragée. Bear claw sugar plum sweet jelly beans bonbon dragée tart. Gingerbread chocolate sweet. Apple pie danish toffee sugar plum jelly beans donut. Chocolate cake croissant caramels chocolate bar. Jelly beans caramels toffee chocolate cake liquorice. Toffee pie sugar plum cookie toffee muffin. Marzipan marshmallow marzipan liquorice tiramisu."""
|> text
else