NoRedInk/noredink-ui/25.2.0

NOTE: the latest version is 35.9.3

Nri.Ui.Modal.V12

Changes fro V11:

  • remove use of FocusTrap type alias
  • return ids to focus on instead of cmds (in order to make modals more testable via the effect pattern with program-test)
  • use tesk9/accessible-html-with-css for SR-only content
  • simplify internal attributes pattern
  • adds Modal.atac to make a hole for the ATAC to render inside the modal
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.

{ title : String
, wrapMsg : Msg -> msg
, firstId : String
, lastId : String
, focus : String -> msg
, content : List (Html msg)
, footer : List (Html msg)
}
-> List Attribute
-> Model
-> Html msg
type Model
{ 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
{ dismissOnEscAndOverlayClick : 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.

Include the subscription if you want the modal to dismiss on Esc.

Attributes

This is the default theme.

Include the close button.

This is the default setting.

testId : String -> Attribute
css : List Style -> Attribute
custom : List (Attribute 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 : Html 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 -> Bool

Modules