Merge pull request #1277 from NoRedInk/bat/tooltips-with-menu

🔧 Menu tooltips
This commit is contained in:
Tessa 2023-02-15 10:00:08 -07:00 committed by GitHub
commit fef894cd2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 246 additions and 99 deletions

View File

@ -31,6 +31,7 @@ import Nri.Ui.Menu.V4 as Menu
import Nri.Ui.Spacing.V1 as Spacing
import Nri.Ui.Table.V6 as Table
import Nri.Ui.TextInput.V7 as TextInput
import Nri.Ui.Tooltip.V3 as Tooltip
import Nri.Ui.UiIcon.V1 as UiIcon
import Set exposing (Set)
import Svg.Styled as Svg
@ -123,6 +124,7 @@ view ellieLinkConfig state =
let
menuAttributes =
Control.currentValue state.settings
|> .attributes
|> List.map Tuple.second
isOpen name =
@ -139,28 +141,55 @@ view ellieLinkConfig state =
, version = version
, update = UpdateControls
, settings = state.settings
, mainType = Just "RootHtml.Html { focus : Maybe String, isOpen : Bool }"
, mainType = Just "RootHtml.Html Msg"
, extraCode =
[ "import Nri.Ui.Button.V10 as Button"
, "import Nri.Ui.ClickableSvg.V2 as ClickableSvg"
, "import Nri.Ui.ClickableText.V3 as ClickableText"
, "import Nri.Ui.Tooltip.V3 as Tooltip"
, "\ntype Msg = ToggleMenu { focus : Maybe String, isOpen : Bool } | ToggleTooltip Bool"
]
, renderExample = Code.unstyledView
, toExampleCode =
\settings ->
\{ attributes } ->
let
code : String
code =
moduleName
++ ".view "
++ "identity -- TODO: you will need a real msg type here"
++ ".view ToggleMenu"
++ Code.listMultiline
(("Menu.isOpen " ++ Code.bool (isOpen "interactiveExample"))
:: List.map Tuple.first settings
((if (Control.currentValue state.settings).withTooltip then
[ Code.fromModule "Menu" "withTooltip"
++ Code.listMultiline
[ Code.fromModule "Tooltip" "open " ++ (Code.bool <| Set.member "tooltip-0" state.openTooltips)
, Code.fromModule "Tooltip" "onToggle " ++ "ToggleTooltip"
, Code.fromModule "Tooltip" "plaintext " ++ Code.string "Tooltip content"
, Code.fromModule "Tooltip" "fitToContent"
]
2
]
else
[]
)
++ ("Menu.isOpen " ++ Code.bool (isOpen "interactiveExample"))
:: List.map Tuple.first attributes
)
1
++ Code.newlineWithIndent 1
++ "[]"
++ Code.listMultiline
[ (Code.fromModule moduleName "entry " ++ Code.string "unique-button-id" ++ " <|")
++ Code.newlineWithIndent 2
++ Code.anonymousFunction "attributes"
(Code.newlineWithIndent 2
++ (Code.fromModule "ClickableText" "button " ++ Code.string "Button")
++ Code.listMultiline
[ Code.fromModule "ClickableText" "small"
, Code.fromModule "ClickableText" "custom" ++ " attributes"
]
3
)
]
1
in
[ { sectionName = "Example"
, code = code
@ -173,8 +202,29 @@ view ellieLinkConfig state =
]
, div [ css [ Css.displayFlex, Css.justifyContent Css.center ] ]
[ Menu.view (FocusAndToggle "interactiveExample")
(Menu.isOpen (isOpen "interactiveExample") :: menuAttributes)
[]
((if (Control.currentValue state.settings).withTooltip then
[ Menu.withTooltip
[ Tooltip.open (Set.member "tooltip-0" state.openTooltips)
, Tooltip.onToggle (ToggleTooltip "tooltip-0")
, Tooltip.plaintext "Tooltip content"
, Tooltip.fitToContent
]
]
else
[]
)
++ Menu.isOpen (isOpen "interactiveExample")
:: menuAttributes
)
[ Menu.entry "customizable-example" <|
\attrs ->
ClickableText.button "Button"
[ ClickableText.small
, ClickableText.onClick (ConsoleLog "Interactive example")
, ClickableText.custom attrs
]
]
]
, Heading.h2
[ Heading.plaintext "Menu types"
@ -357,11 +407,20 @@ type alias State =
type alias Settings =
List ( String, Menu.Attribute Msg )
{ attributes : List ( String, Menu.Attribute Msg )
, withTooltip : Bool
}
initSettings : Control Settings
initSettings =
Control.record Settings
|> Control.field "attributes" initSettingAttributes
|> Control.field "withTooltip" (Control.bool False)
initSettingAttributes : Control (List ( String, Menu.Attribute Msg ))
initSettingAttributes =
ControlExtra.list
|> ControlExtra.optionalListItem "alignment" controlAlignment
|> ControlExtra.optionalBoolListItem "isDisabled" ( "Menu.isDisabled True", Menu.isDisabled True )
@ -503,6 +562,7 @@ controlMenuWidth =
type Msg
= ConsoleLog String
| UpdateControls (Control Settings)
| ToggleTooltip String Bool
| FocusAndToggle String { isOpen : Bool, focus : Maybe String }
| Focused (Result Dom.Error ())
@ -517,6 +577,12 @@ update msg state =
UpdateControls configuration ->
( { state | settings = configuration }, Cmd.none )
ToggleTooltip id True ->
( { state | openTooltips = Set.insert id state.openTooltips }, Cmd.none )
ToggleTooltip id False ->
( { state | openTooltips = Set.remove id state.openTooltips }, Cmd.none )
FocusAndToggle id { isOpen, focus } ->
( { state
| openMenu =

View File

@ -3,6 +3,7 @@ module Nri.Ui.Menu.V4 exposing
, isOpen, isDisabled
, opensOnHover
, defaultTrigger, button, clickableText, clickableSvg, clickableSvgWithoutIndicator
, withTooltip
, buttonId
, navMenuList, disclosure, dialog
, menuWidth, menuId, menuZIndex
@ -10,7 +11,11 @@ module Nri.Ui.Menu.V4 exposing
, Entry, group, entry
)
{-| Changes from V3:
{-| Patch changes:
- improve interoperability with Tooltip (Note that tooltip keyboard events are not fully supported!)
Changes from V3:
- improve composability with Button, ClickableText, and ClickableSvg
@ -33,6 +38,7 @@ A togglable menu view and related buttons.
## Triggering button options
@docs defaultTrigger, button, clickableText, clickableSvg, clickableSvgWithoutIndicator
@docs withTooltip
@docs buttonId
@ -66,6 +72,7 @@ import Nri.Ui.Fonts.V1
import Nri.Ui.Html.Attributes.V2 as AttributesExtra
import Nri.Ui.Shadows.V1 as Shadows
import Nri.Ui.Svg.V1 as Svg
import Nri.Ui.Tooltip.V3 as Tooltip
import Nri.Ui.WhenFocusLeaves.V1 as WhenFocusLeaves
@ -85,6 +92,7 @@ type alias MenuConfig msg =
, zIndex : Int
, opensOnHover : Bool
, purpose : Purpose
, tooltipAttributes : List (Tooltip.Attribute msg)
}
@ -100,6 +108,7 @@ defaultConfig =
, zIndex = 1
, opensOnHover = False
, purpose = NavMenu
, tooltipAttributes = []
}
@ -290,42 +299,22 @@ setButton button_ =
{-| -}
type Button msg
= Button (MenuConfig msg -> List (Html.Attribute msg) -> Html msg)
= DefaultTrigger String (List (Button.Attribute msg))
| Button String (List (Button.Attribute msg))
| ClickableText String (List (ClickableText.Attribute msg))
| ClickableSvg { includeIndicator : Bool } String Svg.Svg (List (ClickableSvg.Attribute msg))
defaultButton : String -> List (Button.Attribute msg) -> Button msg
defaultButton title attributes =
Button
(\menuConfig buttonAttributes ->
Button.button title
([ Button.tertiary
, Button.css
[ Css.justifyContent Css.spaceBetween
, Css.paddingLeft (Css.px 15)
, Css.paddingRight (Css.px 15)
, Css.color Colors.gray20 |> Css.important
, Css.fontWeight (Css.int 600)
, Css.hover [ Css.backgroundColor Colors.white ]
]
, Button.custom buttonAttributes
, if menuConfig.isDisabled then
Button.disabled
DefaultTrigger title attributes
else
Button.css []
, Button.rightIcon
(AnimatedIcon.arrowDownUp menuConfig.isOpen
|> (if menuConfig.isDisabled then
identity
else
Svg.withColor Colors.azure
)
)
]
++ attributes
)
)
{-| Warning: Tooltip keyboard events are not fully supported!
-}
withTooltip : List (Tooltip.Attribute msg) -> Attribute msg
withTooltip tooltipAttributes =
Attribute <| \config -> { config | tooltipAttributes = tooltipAttributes }
{-| Use Button with default styles as the triggering element for the Menu.
@ -339,73 +328,28 @@ defaultTrigger title attributes =
-}
button : String -> List (Button.Attribute msg) -> Attribute msg
button title attributes =
Button
(\menuConfig buttonAttributes ->
Button.button title
([ Button.custom buttonAttributes
, if menuConfig.isDisabled then
Button.disabled
else
Button.css []
, Button.rightIcon (AnimatedIcon.arrowDownUp menuConfig.isOpen)
]
++ attributes
)
)
|> setButton
setButton (Button title attributes)
{-| Use ClickableText as the triggering element for the Menu.
-}
clickableText : String -> List (ClickableText.Attribute msg) -> Attribute msg
clickableText title additionalAttributes =
Button
(\menuConfig buttonAttributes ->
ClickableText.button title
([ ClickableText.custom buttonAttributes
, ClickableText.disabled menuConfig.isDisabled
, ClickableText.rightIcon (AnimatedIcon.arrowDownUp menuConfig.isOpen)
]
++ additionalAttributes
)
)
|> setButton
setButton (ClickableText title additionalAttributes)
{-| Use ClickableSvg as the triggering element for the Menu.
-}
clickableSvg : String -> Svg.Svg -> List (ClickableSvg.Attribute msg) -> Attribute msg
clickableSvg title icon additionalAttributes =
Button
(\menuConfig buttonAttributes ->
ClickableSvg.button title
icon
([ ClickableSvg.custom buttonAttributes
, ClickableSvg.disabled menuConfig.isDisabled
, ClickableSvg.rightIcon (AnimatedIcon.arrowDownUp menuConfig.isOpen)
]
++ additionalAttributes
)
)
|> setButton
setButton (ClickableSvg { includeIndicator = True } title icon additionalAttributes)
{-| Use ClickableSvg as the triggering element for the Menu without a dropdown indicator
-}
clickableSvgWithoutIndicator : String -> Svg.Svg -> List (ClickableSvg.Attribute msg) -> Attribute msg
clickableSvgWithoutIndicator title icon additionalAttributes =
Button
(\menuConfig buttonAttributes ->
ClickableSvg.button title
icon
([ ClickableSvg.custom buttonAttributes
, ClickableSvg.disabled menuConfig.isDisabled
]
++ additionalAttributes
)
)
|> setButton
setButton (ClickableSvg { includeIndicator = False } title icon additionalAttributes)
viewCustom :
@ -551,7 +495,7 @@ viewCustom focusAndToggle config entries =
AttributesExtra.none
]
[ let
buttonAttributes =
triggerAttributes =
[ Aria.disabled config.isDisabled
, -- Whether the menu is open or closed, move to the
-- first menu item if the "down" arrow is pressed
@ -631,10 +575,36 @@ viewCustom focusAndToggle config entries =
Dialog _ ->
[ Aria.hasDialogPopUp ]
)
trigger tooltipAttributes =
case config.button of
DefaultTrigger title buttonAttributes ->
viewDefaultTrigger title config buttonAttributes (tooltipAttributes ++ triggerAttributes)
Button title buttonAttributes ->
viewButton title config buttonAttributes (tooltipAttributes ++ triggerAttributes)
ClickableText title clickableTextAttributes ->
viewClickableText title config clickableTextAttributes (tooltipAttributes ++ triggerAttributes)
ClickableSvg includeIndicator title icon clickableSvgAttributes ->
viewClickableSvg includeIndicator
title
icon
config
clickableSvgAttributes
(tooltipAttributes ++ triggerAttributes)
in
case config.button of
Button standardButton ->
standardButton config buttonAttributes
case config.tooltipAttributes of
[] ->
trigger []
_ ->
Tooltip.view
{ id = config.menuId ++ "-tooltip"
, trigger = trigger
}
config.tooltipAttributes
, div [ styleOuterContent contentVisible config ]
[ div
[ AttributesExtra.nriDescription "menu-hover-bridge"
@ -926,3 +896,82 @@ styleContainer =
, display inlineBlock
]
]
-- Trigger views
viewDefaultTrigger : String -> MenuConfig msg -> List (Button.Attribute msg) -> List (Html.Attribute msg) -> Html msg
viewDefaultTrigger title menuConfig buttonAttributes attributes =
Button.button title
([ Button.tertiary
, Button.css
[ Css.justifyContent Css.spaceBetween
, Css.paddingLeft (Css.px 15)
, Css.paddingRight (Css.px 15)
, Css.color Colors.gray20 |> Css.important
, Css.fontWeight (Css.int 600)
, Css.hover [ Css.backgroundColor Colors.white ]
]
, Button.custom attributes
, if menuConfig.isDisabled then
Button.disabled
else
Button.css []
, Button.rightIcon
(AnimatedIcon.arrowDownUp menuConfig.isOpen
|> (if menuConfig.isDisabled then
identity
else
Svg.withColor Colors.azure
)
)
]
++ buttonAttributes
)
viewButton : String -> MenuConfig msg -> List (Button.Attribute msg) -> List (Html.Attribute msg) -> Html msg
viewButton title menuConfig buttonAttributes attributes =
Button.button title
([ Button.custom attributes
, if menuConfig.isDisabled then
Button.disabled
else
Button.css []
, Button.rightIcon (AnimatedIcon.arrowDownUp menuConfig.isOpen)
]
++ buttonAttributes
)
viewClickableText : String -> MenuConfig msg -> List (ClickableText.Attribute msg) -> List (Html.Attribute msg) -> Html msg
viewClickableText title menuConfig clickableTextAttributes attributes =
ClickableText.button title
([ ClickableText.custom attributes
, ClickableText.disabled menuConfig.isDisabled
, ClickableText.rightIcon (AnimatedIcon.arrowDownUp menuConfig.isOpen)
]
++ clickableTextAttributes
)
viewClickableSvg : { includeIndicator : Bool } -> String -> Svg.Svg -> MenuConfig msg -> List (ClickableSvg.Attribute msg) -> List (Html.Attribute msg) -> Html msg
viewClickableSvg { includeIndicator } title icon menuConfig clickableSvgAttributes attributes =
ClickableSvg.button title
icon
([ ClickableSvg.custom attributes
, ClickableSvg.disabled menuConfig.isDisabled
]
++ (if includeIndicator then
[ ClickableSvg.rightIcon (AnimatedIcon.arrowDownUp menuConfig.isOpen) ]
else
[]
)
++ clickableSvgAttributes
)

View File

@ -91,6 +91,15 @@ pressSpaceKey { targetDetails } =
pressKey { targetDetails = targetDetails, keyCode = 32, shiftKey = False }
pressDownArrow :
{ targetDetails : List ( String, Encode.Value ) }
-> List Selector
-> ProgramTest model msg effect
-> ProgramTest model msg effect
pressDownArrow { targetDetails } =
pressKey { targetDetails = targetDetails, keyCode = 40, shiftKey = False }
pressRightArrow :
{ targetDetails : List ( String, Encode.Value ) }
-> List Selector

View File

@ -5,6 +5,7 @@ import Html.Styled as HtmlStyled
import Json.Encode as Encode
import Nri.Ui.ClickableText.V3 as ClickableText
import Nri.Ui.Menu.V4 as Menu
import Nri.Ui.Tooltip.V3 as Tooltip
import ProgramTest exposing (ProgramTest, ensureViewHas, ensureViewHasNot)
import Spec.KeyboardHelpers as KeyboardHelpers
import Test exposing (..)
@ -52,6 +53,20 @@ spec =
|> pressEscKey { targetId = "some-random-id" }
|> ensureViewHasNot (menuContentSelector menuContent)
|> ProgramTest.done
, test "Opens on down arrow" <|
\() ->
program []
|> KeyboardHelpers.pressDownArrow { targetDetails = targetDetails "hello-button" }
[ Selector.tag "button", Selector.id "hello-button" ]
|> ensureViewHas (menuContentSelector menuContent)
|> ProgramTest.done
, test "Opens on down arrow when there's a tooltip attached" <|
\() ->
program [ Menu.withTooltip [ Tooltip.onToggle ToggleTooltip ] ]
|> KeyboardHelpers.pressDownArrow { targetDetails = targetDetails "hello-button" }
[ Selector.tag "button", Selector.id "hello-button" ]
|> ensureViewHas (menuContentSelector menuContent)
|> ProgramTest.done
, describe "disclosure" <|
[ test "Close on esc key" <|
\() ->
@ -108,19 +123,27 @@ type alias Model =
{ isOpen : Bool }
type alias Msg =
Bool
type Msg
= ToggleMenu Bool
| ToggleTooltip Bool
program : List (Menu.Attribute Msg) -> ProgramTest Model Msg ()
program attributes =
ProgramTest.createSandbox
{ init = { isOpen = False }
, update = \msg _ -> { isOpen = msg }
, update =
\msg model ->
case msg of
ToggleMenu m ->
{ isOpen = m }
ToggleTooltip _ ->
model
, view =
\model ->
HtmlStyled.div []
[ Menu.view (\{ isOpen } -> isOpen)
[ Menu.view (\{ isOpen } -> ToggleMenu isOpen)
([ Menu.defaultTrigger menuButton []
, Menu.isOpen model.isOpen
]