mirror of
https://github.com/NoRedInk/noredink-ui.git
synced 2024-11-14 03:48:13 +03:00
Bring disclosure back and keep alog with dialog
This commit is contained in:
parent
0a9695073f
commit
dc59910584
@ -1,7 +1,7 @@
|
|||||||
module Nri.Ui.Menu.V3 exposing
|
module Nri.Ui.Menu.V3 exposing
|
||||||
( view, button, custom, Config
|
( view, button, custom, Config
|
||||||
, Attribute, Button, ButtonAttribute
|
, Attribute, Button, ButtonAttribute
|
||||||
, alignment, isDisabled, menuWidth, buttonId, menuId, menuZIndex, opensOnHover, dialog
|
, alignment, isDisabled, menuWidth, buttonId, menuId, menuZIndex, opensOnHover, disclosure, dialog
|
||||||
, Alignment(..)
|
, Alignment(..)
|
||||||
, icon, wrapping, hasBorder, buttonWidth
|
, icon, wrapping, hasBorder, buttonWidth
|
||||||
, TitleWrapping(..)
|
, TitleWrapping(..)
|
||||||
@ -30,7 +30,7 @@ A togglable menu view and related buttons.
|
|||||||
|
|
||||||
## Menu attributes
|
## Menu attributes
|
||||||
|
|
||||||
@docs alignment, isDisabled, menuWidth, buttonId, menuId, menuZIndex, opensOnHover, dialog
|
@docs alignment, isDisabled, menuWidth, buttonId, menuId, menuZIndex, opensOnHover, disclosure, dialog
|
||||||
@docs Alignment
|
@docs Alignment
|
||||||
|
|
||||||
|
|
||||||
@ -121,6 +121,7 @@ type alias ButtonConfig =
|
|||||||
|
|
||||||
type Purpose
|
type Purpose
|
||||||
= NavMenu
|
= NavMenu
|
||||||
|
| Disclosure { lastId : String }
|
||||||
| Dialog ExitFocusManager
|
| Dialog ExitFocusManager
|
||||||
|
|
||||||
|
|
||||||
@ -215,6 +216,21 @@ opensOnHover value =
|
|||||||
Attribute <| \config -> { config | 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.
|
{-| 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/).
|
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 } ->
|
Dialog { firstId, lastId } ->
|
||||||
Key.onKeyDownPreventDefault
|
Key.onKeyDownPreventDefault
|
||||||
[ Key.escape
|
[ Key.escape
|
||||||
@ -552,7 +592,7 @@ viewCustom config =
|
|||||||
[ Aria.disabled config.isDisabled
|
[ Aria.disabled config.isDisabled
|
||||||
, -- Whether the menu is open or closed, move to the
|
, -- Whether the menu is open or closed, move to the
|
||||||
-- first menu item if the "down" arrow is pressed
|
-- 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
|
case ( config.purpose, maybeFirstFocusableElementId, maybeLastFocusableElementId ) of
|
||||||
( NavMenu, Just firstFocusableElementId, Just lastFocusableElementId ) ->
|
( NavMenu, Just firstFocusableElementId, Just lastFocusableElementId ) ->
|
||||||
Key.onKeyDownPreventDefault
|
Key.onKeyDownPreventDefault
|
||||||
@ -606,6 +646,11 @@ viewCustom config =
|
|||||||
, Aria.controls [ config.menuId ]
|
, Aria.controls [ config.menuId ]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Disclosure _ ->
|
||||||
|
[ Aria.expanded config.isOpen
|
||||||
|
, Aria.controls [ config.menuId ]
|
||||||
|
]
|
||||||
|
|
||||||
Dialog _ ->
|
Dialog _ ->
|
||||||
[ AttributesExtra.none ]
|
[ AttributesExtra.none ]
|
||||||
)
|
)
|
||||||
@ -631,6 +676,9 @@ viewCustom config =
|
|||||||
NavMenu ->
|
NavMenu ->
|
||||||
Role.menu
|
Role.menu
|
||||||
|
|
||||||
|
Disclosure _ ->
|
||||||
|
AttributesExtra.none
|
||||||
|
|
||||||
Dialog _ ->
|
Dialog _ ->
|
||||||
Role.dialog
|
Role.dialog
|
||||||
, Aria.labelledBy config.buttonId
|
, Aria.labelledBy config.buttonId
|
||||||
|
@ -262,6 +262,35 @@ view ellieLinkConfig state =
|
|||||||
button buttonAttributes [ text "Custom Menu trigger button" ]
|
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.button (with Menu.dialog)"
|
||||||
, Menu.view
|
, Menu.view
|
||||||
(menuAttributes
|
(menuAttributes
|
||||||
@ -280,7 +309,7 @@ view ellieLinkConfig state =
|
|||||||
[ TextInput.id "username-input"
|
[ TextInput.id "username-input"
|
||||||
]
|
]
|
||||||
, TextInput.view "Password" []
|
, TextInput.view "Password" []
|
||||||
, Button.button "Log in"
|
, Button.button "Log in dialog"
|
||||||
[ Button.primary
|
[ Button.primary
|
||||||
, Button.id "login__button"
|
, Button.id "login__button"
|
||||||
, Button.fillContainerWidth
|
, Button.fillContainerWidth
|
||||||
|
@ -64,6 +64,15 @@ pressTabKey { targetDetails } =
|
|||||||
pressKey { targetDetails = targetDetails, keyCode = 9, shiftKey = False }
|
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 :
|
pressEscKey :
|
||||||
{ targetDetails : List ( String, Encode.Value ) }
|
{ targetDetails : List ( String, Encode.Value ) }
|
||||||
-> List Selector
|
-> List Selector
|
||||||
|
@ -52,6 +52,26 @@ spec =
|
|||||||
|> pressEscKey { targetId = Nothing }
|
|> pressEscKey { targetId = Nothing }
|
||||||
|> ensureViewHasNot (menuContentSelector menuContent)
|
|> ensureViewHasNot (menuContentSelector menuContent)
|
||||||
|> ProgramTest.done
|
|> 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" <|
|
, describe "dialog" <|
|
||||||
[ test "Close on esc key" <|
|
[ test "Close on esc key" <|
|
||||||
\() ->
|
\() ->
|
||||||
@ -62,14 +82,23 @@ spec =
|
|||||||
|> pressEscKey { targetId = Nothing }
|
|> pressEscKey { targetId = Nothing }
|
||||||
|> ensureViewHasNot (menuContentSelector menuContent)
|
|> ensureViewHasNot (menuContentSelector menuContent)
|
||||||
|> ProgramTest.done
|
|> ProgramTest.done
|
||||||
, test "Closes after tab on lastId" <|
|
, test "Selects firstId after tab on lastId" <|
|
||||||
\() ->
|
\() ->
|
||||||
program [ Menu.dialog { firstId = "hello-button", lastId = "last-button" } ]
|
program [ Menu.dialog { firstId = "hello-button", lastId = "last-button" } ]
|
||||||
|> clickMenuButton
|
|> clickMenuButton
|
||||||
|> ensureViewHas (menuContentSelector menuContent)
|
|> ensureViewHas (menuContentSelector menuContent)
|
||||||
-- NOTE: unable to simulate pressTabKey with other targetId since those decoders will fail
|
-- NOTE: unable to simulate pressTabKey with other targetId since those decoders will fail
|
||||||
|> pressTabKey { targetId = Just "last-button" }
|
|> 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
|
|> ProgramTest.done
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -182,6 +211,13 @@ pressTabKey { targetId } =
|
|||||||
[ Selector.class "Container" ]
|
[ 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 : Maybe String } -> ProgramTest model msg effect -> ProgramTest model msg effect
|
||||||
pressEscKey { targetId } =
|
pressEscKey { targetId } =
|
||||||
KeyboardHelpers.pressEscKey
|
KeyboardHelpers.pressEscKey
|
||||||
|
Loading…
Reference in New Issue
Block a user