Patch changes:
Changes from V11:
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.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 =
-- 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 { returnFocusTo = "maincontent", startFocusOn = Modal.closeButtonId }
|> Tuple.mapSecond (Task.attempt Focused << Dom.focus)
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 ->
Modal.open
{ returnFocusTo = returnFocusTo
, startFocusOn = Modal.closeButtonId
}
|> Tuple.mapSecond (Task.attempt Focused << Dom.focus)
ModalMsg modalMsg ->
let
( newModel, maybeFocus ) =
Modal.update
{ dismissOnEscAndOverlayClick = True }
modalMsg
model
in
( newModel
, Maybe.map (Task.attempt Focused << Dom.focus) maybeFocus
|> Maybe.withDefault Cmd.none
)
CloseModal ->
let
( newModel, maybeFocus ) =
Modal.close model
in
( newModel
, Maybe.map (Task.attempt Focused << Dom.focus) maybeFocus
|> Maybe.withDefault Cmd.none
)
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" ]
]
, focus = Focus
, firstId = Modal.closeButtonId
, lastId = "last-element-id"
}
[ Modal.hideTitle
, Modal.css [ padding (px 10) ]
, Modal.custom [ id "first-modal" ]
, Modal.closeButton
]
model
]
If you're an NRI employee working in the monorepo, you should use Nri.Effect.focus
instead of Dom.focus
. Test.Nri.Effect
exposes ensureFocused
, which helps you test whether the correct effect has been produced and whether the id you attempted to focus on actually exists in the DOM.
view : { title : String, wrapMsg : Msg -> msg, firstId : String, lastId : String, focus : String -> msg, content : List (Accessibility.Styled.Html msg), footer : List (Accessibility.Styled.Html msg) } -> List Attribute -> Model -> Accessibility.Styled.Html msg
init : Model
open : { returnFocusTo : String, startFocusOn : String } -> ( Model, String )
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/
The second part of the returned tuple is the id of the element to which focus should be returned.
You will need to explicitly move focus to this element!
close : Model -> ( Model, Maybe String )
The second part of the tuple is the id of the element to which focus should be returned.
You will need to explicitly move focus to this element!
If you're an NRI employee working in the monorepo, pass the second part of the tuple to Nri.Effect.maybeFocus
.
update : { dismissOnEscAndOverlayClick : Basics.Bool } -> Msg -> Model -> ( Model, Maybe String )
The second part of the tuple is the id of the element to which focus should be returned.
You will need to explicitly move focus to this element!
If you're an NRI employee working in the monorepo, pass the second part of the tuple to Nri.Effect.maybeFocus
.
subscriptions : Model -> Platform.Sub.Sub Msg
Include the subscription if you want the modal to dismiss on Esc
.
info : Attribute
This is the default theme.
warning : Attribute
closeButton : Attribute
Include the close button.
showTitle : Attribute
This is the default setting.
hideTitle : Attribute
testId : String -> Attribute
css : List Css.Style -> Attribute
custom : List (Accessibility.Styled.Attribute Basics.Never) -> Attribute
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 = []
, firstId : Modal.closeButtonId
, lastId : Modal.closeButtonId
, focus : Focus
}
[ Modal.custom [ id "my-modal" ], Modal.closeButton ]
modalState
atac : Accessibility.Styled.Html Basics.Never -> Attribute
Pass the Assistive Technology Announcement Center and Announcement Log.
HTML passed here will render after the footer.
closeButtonId : String
titleId : String
Id 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.
isOpen : Model -> Basics.Bool