NoRedInk / noredink-ui / Nri.Ui.Modal.V12

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


type Model

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.


type Msg

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.

Attributes


type Attribute

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.

Id accessors

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.

State check

isOpen : Model -> Basics.Bool