mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 09:37:02 +00:00
ui: Implement modal focus trap
This commit is contained in:
@@ -37,6 +37,7 @@ import Html.Attributes
|
|||||||
, value
|
, value
|
||||||
)
|
)
|
||||||
import Html.Events as Events
|
import Html.Events as Events
|
||||||
|
import Modal
|
||||||
import Route exposing (Route)
|
import Route exposing (Route)
|
||||||
|
|
||||||
|
|
||||||
@@ -79,6 +80,8 @@ reset model =
|
|||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= ClearFlash
|
= ClearFlash
|
||||||
|
| ModalFocused Modal.Msg
|
||||||
|
| ModalUnfocused
|
||||||
| OnMailboxNameInput String
|
| OnMailboxNameInput String
|
||||||
| OpenMailbox
|
| OpenMailbox
|
||||||
| ShowRecent Bool
|
| ShowRecent Bool
|
||||||
@@ -94,6 +97,15 @@ update msg model session =
|
|||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ModalFocused message ->
|
||||||
|
( model
|
||||||
|
, Modal.updateSession message session
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
ModalUnfocused ->
|
||||||
|
( model, session, Modal.resetFocusCmd (ModalFocused >> model.mapMsg) )
|
||||||
|
|
||||||
OnMailboxNameInput name ->
|
OnMailboxNameInput name ->
|
||||||
( { model | mailboxName = name }
|
( { model | mailboxName = name }
|
||||||
, session
|
, session
|
||||||
@@ -165,7 +177,7 @@ frame { model, session, activePage, activeMailbox, modal, content } =
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
, div [ class "navbar-bg" ] [ text "" ]
|
, div [ class "navbar-bg" ] [ text "" ]
|
||||||
, frameModal modal
|
, Modal.view (ModalUnfocused |> model.mapMsg) modal
|
||||||
, div [ class "page" ] (errorFlash model session.flash :: content)
|
, div [ class "page" ] (errorFlash model session.flash :: content)
|
||||||
, footer []
|
, footer []
|
||||||
[ div [ class "footer" ]
|
[ div [ class "footer" ]
|
||||||
@@ -178,18 +190,6 @@ frame { model, session, activePage, activeMailbox, modal, content } =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
frameModal : Maybe (Html msg) -> Html msg
|
|
||||||
frameModal maybeModal =
|
|
||||||
case maybeModal of
|
|
||||||
Just modal ->
|
|
||||||
div [ class "modal-mask" ]
|
|
||||||
[ div [ class "modal well" ] [ modal ]
|
|
||||||
]
|
|
||||||
|
|
||||||
Nothing ->
|
|
||||||
text ""
|
|
||||||
|
|
||||||
|
|
||||||
errorFlash : Model msg -> Maybe Session.Flash -> Html msg
|
errorFlash : Model msg -> Maybe Session.Flash -> Html msg
|
||||||
errorFlash model maybeFlash =
|
errorFlash model maybeFlash =
|
||||||
let
|
let
|
||||||
|
|||||||
58
ui/src/Modal.elm
Normal file
58
ui/src/Modal.elm
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
module Modal exposing (Msg, resetFocusCmd, updateSession, view)
|
||||||
|
|
||||||
|
import Browser.Dom as Dom
|
||||||
|
import Data.Session as Session exposing (Session)
|
||||||
|
import Html exposing (Html, div, span, text)
|
||||||
|
import Html.Attributes exposing (class, id, tabindex)
|
||||||
|
import Html.Events exposing (onFocus)
|
||||||
|
import Task
|
||||||
|
|
||||||
|
|
||||||
|
type alias Msg =
|
||||||
|
Result Dom.Error ()
|
||||||
|
|
||||||
|
|
||||||
|
{-| Creates a command to focus the modal dialog.
|
||||||
|
-}
|
||||||
|
resetFocusCmd : (Msg -> msg) -> Cmd msg
|
||||||
|
resetFocusCmd resultMsg =
|
||||||
|
Task.attempt resultMsg (Dom.focus domId)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Updates a Session with an error Flash if the resetFocusCmd failed.
|
||||||
|
-}
|
||||||
|
updateSession : Msg -> Session -> Session
|
||||||
|
updateSession result session =
|
||||||
|
case result of
|
||||||
|
Ok () ->
|
||||||
|
session
|
||||||
|
|
||||||
|
Err (Dom.NotFound missingDomId) ->
|
||||||
|
let
|
||||||
|
flash =
|
||||||
|
{ title = "DOM element not found"
|
||||||
|
, table = [ ( "Element ID", missingDomId ) ]
|
||||||
|
}
|
||||||
|
in
|
||||||
|
Session.showFlash flash session
|
||||||
|
|
||||||
|
|
||||||
|
view : msg -> Maybe (Html msg) -> Html msg
|
||||||
|
view unfocusedMsg maybeModal =
|
||||||
|
case maybeModal of
|
||||||
|
Just modal ->
|
||||||
|
div [ class "modal-mask" ]
|
||||||
|
[ span [ onFocus unfocusedMsg, tabindex 0 ] []
|
||||||
|
, div [ id domId, class "modal well", tabindex -1 ] [ modal ]
|
||||||
|
, span [ onFocus unfocusedMsg, tabindex 0 ] []
|
||||||
|
]
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
text ""
|
||||||
|
|
||||||
|
|
||||||
|
{-| DOM ID of the modal dialog.
|
||||||
|
-}
|
||||||
|
domId : String
|
||||||
|
domId =
|
||||||
|
"modal-dialog"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
module Page.Mailbox exposing (Model, Msg, init, load, subscriptions, update, view)
|
module Page.Mailbox exposing (Model, Msg, init, load, subscriptions, update, view)
|
||||||
|
|
||||||
import Api
|
import Api
|
||||||
|
import Browser.Dom as Dom
|
||||||
import Data.Message as Message exposing (Message)
|
import Data.Message as Message exposing (Message)
|
||||||
import Data.MessageHeader exposing (MessageHeader)
|
import Data.MessageHeader exposing (MessageHeader)
|
||||||
import Data.Session as Session exposing (Session)
|
import Data.Session as Session exposing (Session)
|
||||||
@@ -51,6 +52,7 @@ import Html.Events as Events
|
|||||||
import HttpUtil
|
import HttpUtil
|
||||||
import Json.Decode as D
|
import Json.Decode as D
|
||||||
import Json.Encode as E
|
import Json.Encode as E
|
||||||
|
import Modal
|
||||||
import Route
|
import Route
|
||||||
import Task
|
import Task
|
||||||
import Time exposing (Posix)
|
import Time exposing (Posix)
|
||||||
@@ -174,6 +176,7 @@ type Msg
|
|||||||
| PurgedMailbox (Result HttpUtil.Error ())
|
| PurgedMailbox (Result HttpUtil.Error ())
|
||||||
| OnSearchInput String
|
| OnSearchInput String
|
||||||
| Tick Posix
|
| Tick Posix
|
||||||
|
| ModalFocused Modal.Msg
|
||||||
|
|
||||||
|
|
||||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
@@ -262,6 +265,11 @@ update msg model =
|
|||||||
MessageBody bodyMode ->
|
MessageBody bodyMode ->
|
||||||
( { model | bodyMode = bodyMode }, Cmd.none )
|
( { model | bodyMode = bodyMode }, Cmd.none )
|
||||||
|
|
||||||
|
ModalFocused message ->
|
||||||
|
( { model | session = Modal.updateSession message model.session }
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
OnSearchInput searchInput ->
|
OnSearchInput searchInput ->
|
||||||
updateSearchInput model searchInput
|
updateSearchInput model searchInput
|
||||||
|
|
||||||
@@ -293,13 +301,13 @@ update msg model =
|
|||||||
( model, Cmd.none )
|
( model, Cmd.none )
|
||||||
|
|
||||||
PurgeMailboxPrompt ->
|
PurgeMailboxPrompt ->
|
||||||
( { model | promptPurge = True }, Cmd.none )
|
( { model | promptPurge = True }, Modal.resetFocusCmd ModalFocused )
|
||||||
|
|
||||||
PurgeMailboxCanceled ->
|
PurgeMailboxCanceled ->
|
||||||
( { model | promptPurge = False }, Cmd.none )
|
( { model | promptPurge = False }, Cmd.none )
|
||||||
|
|
||||||
PurgeMailboxConfirmed ->
|
PurgeMailboxConfirmed ->
|
||||||
updatePurge model
|
updateTriggerPurge model
|
||||||
|
|
||||||
PurgedMailbox (Ok _) ->
|
PurgedMailbox (Ok _) ->
|
||||||
( model, Cmd.none )
|
( model, Cmd.none )
|
||||||
@@ -358,8 +366,10 @@ updateMessageResult model message =
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
updatePurge : Model -> ( Model, Cmd Msg )
|
{-| Updates model and triggers commands to purge this mailbox.
|
||||||
updatePurge model =
|
-}
|
||||||
|
updateTriggerPurge : Model -> ( Model, Cmd Msg )
|
||||||
|
updateTriggerPurge model =
|
||||||
let
|
let
|
||||||
cmd =
|
cmd =
|
||||||
Cmd.batch
|
Cmd.batch
|
||||||
|
|||||||
@@ -223,6 +223,10 @@ h3 {
|
|||||||
padding: 10px !important;
|
padding: 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
/** BUTTONS */
|
/** BUTTONS */
|
||||||
|
|
||||||
.button-bar {
|
.button-bar {
|
||||||
|
|||||||
Reference in New Issue
Block a user