testId helperShadowstitleIdinitOpenopen, close to return (Model, Cmd Msg) rather than Msgcustom helper for adding arbitrary html attributes (primarily useful to make limiting the scope of selectors in tests easier by adding ids to modals)import Browser exposing (element)
import Browser.Dom as Dom
import Css exposing (padding, px)
import Html.Styled exposing (..)
import Html.Styled.Attributes exposing (id)
import Html.Styled.Events as Events
import Nri.Ui.FocusTrap.V1 as FocusTrap exposing (FocusTrap)
import Nri.Ui.Modal.V12 as Modal
import Task
main : Program () Modal.Model Msg
main =
Browser.element
{ init = \_ -> init
, view = toUnstyled << view
, update = update
, subscriptions = \model -> Sub.map ModalMsg (Modal.subscriptions model)
}
init : ( Modal.Model, Cmd Msg )
init =
let
( model, cmd ) =
-- When we load the page with a modal already open, we should return
-- the focus someplace sensible when the modal closes.
-- [This article](https://developer.paciellogroup.com/blog/2018/06/the-current-state-of-modal-dialog-accessibility/) recommends
-- focusing the main or body.
Modal.open
{ startFocusOn = Modal.closeButtonId
, returnFocusTo = "maincontent"
}
in
( model, Cmd.map ModalMsg cmd )
type Msg
= OpenModal String
| ModalMsg Modal.Msg
| CloseModal
| Focus String
| Focused (Result Dom.Error ())
update : Msg -> Modal.Model -> ( Modal.Model, Cmd Msg )
update msg model =
case msg of
OpenModal returnFocusTo ->
let
( newModel, cmd ) =
Modal.open
{ startFocusOn = Modal.closeButtonId
, returnFocusTo = returnFocusTo
}
in
( newModel, Cmd.map ModalMsg cmd )
ModalMsg modalMsg ->
let
( newModel, cmd ) =
Modal.update
{ dismissOnEscAndOverlayClick = True }
modalMsg
model
in
( newModel, Cmd.map ModalMsg cmd )
CloseModal ->
let
( newModel, cmd ) =
Modal.close model
in
( newModel, Cmd.map ModalMsg cmd )
Focus id ->
( model, Task.attempt Focused (Dom.focus id) )
Focused _ ->
( model, Cmd.none )
view : Modal.Model -> Html Msg
view model =
main_ [ id "maincontent" ]
[ button
[ id "open-modal"
, Events.onClick (OpenModal "open-modal")
]
[ text "Open Modal" ]
, Modal.view
{ title = "First kind of modal"
, wrapMsg = ModalMsg
, content = [ text "Modal Content" ]
, footer =
[ button
[ Events.onClick CloseModal
, id "last-element-id"
]
[ text "Close" ]
]
, focusTrap =
{ focus = Focus
, firstId = Modal.closeButtonId
, lastId = "last-element-id"
}
}
[ Modal.hideTitle
, Modal.css [ padding (px 10) ]
, Modal.custom [ id "first-modal" ]
, Modal.closeButton
]
model
]Include the close button.
Pass the id of the element that should receive focus when the modal closes.
...if a dialog was opened on page load, then focus could be placed on either the body or main element. If the trigger was removed from the DOM, then placing focus as close to the trigger’s DOM location would be ideal.
https://developer.paciellogroup.com/blog/2018/06/the-current-state-of-modal-dialog-accessibility/
Include the subscription if you want the modal to dismiss on Esc.
Do NOT use this function for attaching styles -- use the css helper instead.
import Html.Styled.Attribute exposing (id)
Modal.view
{ title = "Some Great Modal"
, wrapMsg = ModalMsg
, content = []
, footer = []
}
[ Modal.custom [ id "my-modal" ]]
modalStateId used for the h1 that labels the modal.
Useful if you're changing the modal contents and want to move the user's focus to the new modal's updated title to re-orient them.
FocusTrapcomes fromNri.Ui.FocusTrap.V1.