Bring disclosure back and keep alog with dialog

This commit is contained in:
Mariano Abel Coca 2022-11-07 12:24:08 -03:00
parent 0a9695073f
commit dc59910584
4 changed files with 128 additions and 6 deletions

View File

@ -1,7 +1,7 @@
module Nri.Ui.Menu.V3 exposing
( view, button, custom, Config
, Attribute, Button, ButtonAttribute
, alignment, isDisabled, menuWidth, buttonId, menuId, menuZIndex, opensOnHover, dialog
, alignment, isDisabled, menuWidth, buttonId, menuId, menuZIndex, opensOnHover, disclosure, dialog
, Alignment(..)
, icon, wrapping, hasBorder, buttonWidth
, TitleWrapping(..)
@ -30,7 +30,7 @@ A togglable menu view and related buttons.
## Menu attributes
@docs alignment, isDisabled, menuWidth, buttonId, menuId, menuZIndex, opensOnHover, dialog
@docs alignment, isDisabled, menuWidth, buttonId, menuId, menuZIndex, opensOnHover, disclosure, dialog
@docs Alignment
@ -121,6 +121,7 @@ type alias ButtonConfig =
type Purpose
= NavMenu
| Disclosure { lastId : String }
| Dialog ExitFocusManager
@ -215,6 +216,21 @@ opensOnHover value =
Attribute <| \config -> { config | opensOnHover = value }
{-| Makes the menu behave as a disclosure.
For more information, please read [Disclosure (Show/Hide) pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/).
You will need to pass in the last focusable element in the disclosed content in order for:
- any focusable elements in the disclosed content to be keyboard accessible
- the disclosure to close appropriately when the user tabs past all of the disclosed content
-}
disclosure : { lastId : String } -> Attribute msg
disclosure exitFocusManager =
Attribute (\config -> { config | purpose = Disclosure exitFocusManager })
{-| Makes the menu behave as a dialog.
For more information, please read [Dialog pattern](https://w3c.github.io/aria-practices/examples/dialog-modal/dialog.html/).
@ -485,6 +501,30 @@ viewCustom config =
)
]
Disclosure { lastId } ->
Key.onKeyDown
[ Key.escape
(config.focusAndToggle
{ isOpen = False
, focus = Just config.buttonId
}
)
, WhenFocusLeaves.toDecoder
{ firstId = config.buttonId
, lastId = lastId
, tabBackAction =
config.focusAndToggle
{ isOpen = False
, focus = Nothing
}
, tabForwardAction =
config.focusAndToggle
{ isOpen = False
, focus = Nothing
}
}
]
Dialog { firstId, lastId } ->
Key.onKeyDownPreventDefault
[ Key.escape
@ -552,7 +592,7 @@ viewCustom config =
[ Aria.disabled config.isDisabled
, -- Whether the menu is open or closed, move to the
-- first menu item if the "down" arrow is pressed
-- as long as it's not a Dialog
-- as long as it's not a Disclosed or Dialog
case ( config.purpose, maybeFirstFocusableElementId, maybeLastFocusableElementId ) of
( NavMenu, Just firstFocusableElementId, Just lastFocusableElementId ) ->
Key.onKeyDownPreventDefault
@ -606,6 +646,11 @@ viewCustom config =
, Aria.controls [ config.menuId ]
]
Disclosure _ ->
[ Aria.expanded config.isOpen
, Aria.controls [ config.menuId ]
]
Dialog _ ->
[ AttributesExtra.none ]
)
@ -631,6 +676,9 @@ viewCustom config =
NavMenu ->
Role.menu
Disclosure _ ->
AttributesExtra.none
Dialog _ ->
Role.dialog
, Aria.labelledBy config.buttonId

View File

@ -262,6 +262,35 @@ view ellieLinkConfig state =
button buttonAttributes [ text "Custom Menu trigger button" ]
}
)
, ( "Menu.button (with Menu.disclosure)"
, Menu.view
(menuAttributes
++ [ Menu.buttonId "with_controls__button"
, Menu.menuId "with_controls__menu"
, Menu.disclosure { lastId = "login__button" }
]
)
{ isOpen = isOpen "with_controls"
, focusAndToggle = FocusAndToggle "with_controls"
, entries =
[ Menu.entry "username-input" <|
\attrs ->
div []
[ TextInput.view "Username"
[ TextInput.id "username-input"
]
, TextInput.view "Password" []
, Button.button "Log in disclosure"
[ Button.primary
, Button.id "login__button"
, Button.fillContainerWidth
, Button.css [ Css.marginTop (Css.px 15) ]
]
]
]
, button = Menu.button defaultButtonAttributes "Log In"
}
)
, ( "Menu.button (with Menu.dialog)"
, Menu.view
(menuAttributes
@ -280,7 +309,7 @@ view ellieLinkConfig state =
[ TextInput.id "username-input"
]
, TextInput.view "Password" []
, Button.button "Log in"
, Button.button "Log in dialog"
[ Button.primary
, Button.id "login__button"
, Button.fillContainerWidth

View File

@ -64,6 +64,15 @@ pressTabKey { targetDetails } =
pressKey { targetDetails = targetDetails, keyCode = 9, shiftKey = False }
pressTabBackKey :
{ targetDetails : List ( String, Encode.Value ) }
-> List Selector
-> ProgramTest model msg effect
-> ProgramTest model msg effect
pressTabBackKey { targetDetails } =
pressKey { targetDetails = targetDetails, keyCode = 9, shiftKey = True }
pressEscKey :
{ targetDetails : List ( String, Encode.Value ) }
-> List Selector

View File

@ -52,6 +52,26 @@ spec =
|> pressEscKey { targetId = Nothing }
|> ensureViewHasNot (menuContentSelector menuContent)
|> ProgramTest.done
, describe "disclosure" <|
[ test "Close on esc key" <|
\() ->
program [ Menu.disclosure { lastId = "last-button" } ]
-- Menu opens on mouse click and closes on esc key
|> clickMenuButton
|> ensureViewHas (menuContentSelector menuContent)
|> pressEscKey { targetId = Nothing }
|> ensureViewHasNot (menuContentSelector menuContent)
|> ProgramTest.done
, test "Closes after tab on lastId" <|
\() ->
program [ Menu.disclosure { lastId = "last-button" } ]
|> clickMenuButton
|> ensureViewHas (menuContentSelector menuContent)
-- NOTE: unable to simulate pressTabKey with other targetId since those decoders will fail
|> pressTabKey { targetId = Just "last-button" }
|> ensureViewHasNot (menuContentSelector menuContent)
|> ProgramTest.done
]
, describe "dialog" <|
[ test "Close on esc key" <|
\() ->
@ -62,14 +82,23 @@ spec =
|> pressEscKey { targetId = Nothing }
|> ensureViewHasNot (menuContentSelector menuContent)
|> ProgramTest.done
, test "Closes after tab on lastId" <|
, test "Selects firstId after tab on lastId" <|
\() ->
program [ Menu.dialog { firstId = "hello-button", lastId = "last-button" } ]
|> clickMenuButton
|> ensureViewHas (menuContentSelector menuContent)
-- NOTE: unable to simulate pressTabKey with other targetId since those decoders will fail
|> pressTabKey { targetId = Just "last-button" }
|> ensureViewHasNot (menuContentSelector menuContent)
|> ensureViewHas (menuContentSelector menuContent)
|> ProgramTest.done
, test "Selects lastId after back tab on firstId" <|
\() ->
program [ Menu.dialog { firstId = "hello-button", lastId = "last-button" } ]
|> clickMenuButton
|> ensureViewHas (menuContentSelector menuContent)
-- NOTE: unable to simulate pressTabKey with other targetId since those decoders will fail
|> pressTabBackKey { targetId = Just "hellow-button" }
|> ensureViewHas (menuContentSelector menuContent)
|> ProgramTest.done
]
]
@ -182,6 +211,13 @@ pressTabKey { targetId } =
[ Selector.class "Container" ]
pressTabBackKey : { targetId : Maybe String } -> ProgramTest model msg effect -> ProgramTest model msg effect
pressTabBackKey { targetId } =
KeyboardHelpers.pressTabBackKey
{ targetDetails = targetDetails targetId }
[ Selector.class "Container" ]
pressEscKey : { targetId : Maybe String } -> ProgramTest model msg effect -> ProgramTest model msg effect
pressEscKey { targetId } =
KeyboardHelpers.pressEscKey