Merge pull request #683 from NoRedInk/raven/tooltips-in-segmented-controls

allow tooltips in segmented controls
This commit is contained in:
Brian Hicks 2021-04-07 05:44:54 -05:00 committed by GitHub
commit 1c665d8346
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 368 additions and 18 deletions

View File

@ -9,8 +9,9 @@ Nri.Ui.Message.V2,upgrade to V3
Nri.Ui.Modal.V3,upgrade to V11
Nri.Ui.Modal.V10,upgrade to V11
Nri.Ui.RadioButton.V1,upgrade to V2
Nri.Ui.SegmentedControl.V11,upgrade to V13
Nri.Ui.SegmentedControl.V12,upgrade to V13
Nri.Ui.SegmentedControl.V11,upgrade to V14
Nri.Ui.SegmentedControl.V12,upgrade to V14
Nri.Ui.SegmentedControl.V13,upgrade to V14
Nri.Ui.Select.V5,upgrade to V7
Nri.Ui.Table.V4,upgrade to V5
Nri.Ui.Tabs.V6,upgrade to V7

1 Nri.Ui.Accordion.V1 upgrade to V2
9 Nri.Ui.Modal.V3 upgrade to V11
10 Nri.Ui.Modal.V10 upgrade to V11
11 Nri.Ui.RadioButton.V1 upgrade to V2
12 Nri.Ui.SegmentedControl.V11 upgrade to V13 upgrade to V14
13 Nri.Ui.SegmentedControl.V12 upgrade to V13 upgrade to V14
14 Nri.Ui.SegmentedControl.V13 upgrade to V14
15 Nri.Ui.Select.V5 upgrade to V7
16 Nri.Ui.Table.V4 upgrade to V5
17 Nri.Ui.Tabs.V6 upgrade to V7

View File

@ -54,6 +54,7 @@
"Nri.Ui.SegmentedControl.V11",
"Nri.Ui.SegmentedControl.V12",
"Nri.Ui.SegmentedControl.V13",
"Nri.Ui.SegmentedControl.V14",
"Nri.Ui.Select.V5",
"Nri.Ui.Select.V7",
"Nri.Ui.Slide.V1",

View File

@ -80,10 +80,13 @@ hint = 'upgrade to V11'
hint = 'upgrade to V2'
[forbidden."Nri.Ui.SegmentedControl.V11"]
hint = 'upgrade to V13'
hint = 'upgrade to V14'
[forbidden."Nri.Ui.SegmentedControl.V12"]
hint = 'upgrade to V13'
hint = 'upgrade to V14'
[forbidden."Nri.Ui.SegmentedControl.V13"]
hint = 'upgrade to V14'
[forbidden."Nri.Ui.Select.V5"]
hint = 'upgrade to V7'

View File

@ -0,0 +1,320 @@
module Nri.Ui.SegmentedControl.V14 exposing
( Option, view
, Radio, viewRadioGroup
, Positioning(..), Width(..)
)
{-| Changes from V13:
- Adds tooltip support to `viewRadioGroup`
@docs Option, view
@docs Radio, viewRadioGroup
@docs Positioning, Width
-}
import Accessibility.Styled exposing (..)
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Role as Role
import Accessibility.Styled.Style as Style
import Accessibility.Styled.Widget as Widget
import Css exposing (..)
import EventExtras
import Html.Styled
import Html.Styled.Attributes as Attributes exposing (css, href)
import Html.Styled.Events as Events
import Json.Encode as Encode
import Nri.Ui
import Nri.Ui.Colors.Extra exposing (withAlpha)
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as AttributesExtra
import Nri.Ui.Svg.V1 as Svg exposing (Svg)
import Nri.Ui.Tooltip.V2 as Tooltip
import Nri.Ui.Util exposing (dashify)
import TabsInternal.V2 as TabsInternal
{-| -}
type Positioning
= Left Width
| Center
{-| -}
type Width
= FitContent
| FillContainer
{-| -}
type alias Radio value msg =
{ value : value
, idString : String
, label : Html msg
, attributes : List (Attribute msg)
, tooltip : List (Tooltip.Attribute msg)
, icon : Maybe Svg
}
{-| Creates a set of radio buttons styled to look like a segmented control.
- `onSelect`: the message to produce when an option is selected (clicked) by the user
- `idString`: function to get the radio value as a string
- `options`: the list of options available
- `selected`: if present, the value of the currently-selected option
- `positioning`: how to position and size the segmented control
- `legend`:
- value read to screenreader users to explain the radio group's purpose <https://dequeuniversity.com/rules/axe/3.3/radiogroup?application=axeAPI>
- after lowercasing & dashifying, this value is used to group the radio buttons together
-}
viewRadioGroup :
{ onSelect : a -> msg
, options : List (Radio a msg)
, selected : Maybe a
, positioning : Positioning
, legend : String
}
-> Html msg
viewRadioGroup config =
let
numOptions =
List.length config.options
viewRadio index option =
let
isSelected =
Just option.value == config.selected
inner : List (Attribute msg) -> Html msg
inner extraAttrs =
Html.Styled.label
(css
-- ensure that the focus state is visible, even
-- though the radio button that technically has focus
-- is not
(Css.pseudoClass "focus-within"
[ Css.property "outline-style" "auto" ]
:: styles config.positioning numOptions index isSelected
)
:: extraAttrs
)
[ radio name option.idString isSelected <|
(Events.onCheck (\_ -> config.onSelect option.value)
:: css [ Css.opacity Css.zero ]
:: Attributes.attribute "data-nri-checked"
(if isSelected then
"true"
else
"false"
)
:: Style.invisible
)
, div [] [ viewIcon option.icon, option.label ]
]
in
case option.tooltip of
[] ->
inner []
_ ->
Tooltip.view
{ id = option.idString ++ "-tooltip"
, trigger = inner
}
option.tooltip
name =
dashify (String.toLower config.legend)
legendId =
"legend-" ++ name
in
div
[ Role.radioGroup
, Aria.labelledBy legendId
, css
[ displayFlex
, cursor pointer
, case config.positioning of
Left _ ->
justifyContent flexStart
Center ->
justifyContent center
]
]
(p (Attributes.id legendId :: Style.invisible) [ text config.legend ]
:: List.indexedMap viewRadio config.options
)
{-| Tooltip defaults: `[Tooltip.smallPadding, Tooltip.onBottom, Tooltip.fitToContent]`
-}
type alias Option value msg =
{ value : value
, idString : String
, label : Html msg
, attributes : List (Attribute msg)
, tabTooltip : List (Tooltip.Attribute msg)
, icon : Maybe Svg
, content : Html msg
}
{-|
- `focusAndSelect` : the message to produce when an option is selected by the user
- `options`: the list of options available
- `selected`: the value of the currently-selected option
- `positioning`: how to position and size the segmented control
- `toUrl`: a optional function that takes a `route` and returns the URL of that route. You should always use pass a `toUrl` function when the segmented control options correspond to routes in your SPA.
-}
view :
{ focusAndSelect : { select : a, focus : Maybe String } -> msg
, options : List (Option a msg)
, selected : a
, positioning : Positioning
, toUrl : Maybe (a -> String)
}
-> Html msg
view config =
let
toInternalTab : Option a msg -> TabsInternal.Tab a msg
toInternalTab option =
{ id = option.value
, idString = option.idString
, tabAttributes = option.attributes
, tabTooltip =
case config.positioning of
Left FillContainer ->
Tooltip.containerCss [ Css.width (Css.pct 100) ] :: option.tabTooltip
_ ->
option.tabTooltip
, tabView = [ viewIcon option.icon, option.label ]
, panelView = option.content
, spaHref = Maybe.map (\toUrl -> toUrl option.value) config.toUrl
}
{ tabList, tabPanels } =
TabsInternal.views
{ focusAndSelect = config.focusAndSelect
, selected = config.selected
, tabs = List.map toInternalTab config.options
, tabListStyles =
[ displayFlex
, cursor pointer
, marginBottom (px 10)
, case config.positioning of
Left _ ->
justifyContent flexStart
Center ->
justifyContent center
]
, tabStyles = styles config.positioning (List.length config.options)
}
in
div []
[ tabList
, tabPanels
]
viewIcon : Maybe Svg.Svg -> Html msg
viewIcon icon =
case icon of
Nothing ->
text ""
Just svg ->
svg
|> Svg.withWidth (px 18)
|> Svg.withHeight (px 18)
|> Svg.withCss
[ display inlineBlock
, verticalAlign textTop
, lineHeight (px 15)
, marginRight (px 8)
]
|> Svg.toHtml
styles : Positioning -> Int -> Int -> Bool -> List Style
styles positioning numEntries index isSelected =
[ sharedSegmentStyles numEntries index
, if isSelected then
focusedSegmentStyles
else
unFocusedSegmentStyles
, Css.batch <|
case positioning of
Left FillContainer ->
[ width (Css.pct 100)
, flexGrow (int 1)
, textAlign center
]
_ ->
[]
]
sharedSegmentStyles : Int -> Int -> Style
sharedSegmentStyles numEntries index =
[ padding2 (px 6) (px 15)
, height (px 45)
, Fonts.baseFont
, fontSize (px 15)
, fontWeight bold
, lineHeight (px 30)
, margin zero
, border3 (px 1) solid Colors.azure
, boxSizing borderBox
, cursor pointer
, property "transition" "background-color 0.2s, color 0.2s, box-shadow 0.2s, border 0.2s, border-width 0s"
, textDecoration none
, hover [ textDecoration none ]
, focus [ textDecoration none ]
]
++ (if index == 0 then
[ borderTopLeftRadius (px 8)
, borderBottomLeftRadius (px 8)
]
else if index == numEntries - 1 then
[ borderTopRightRadius (px 8)
, borderBottomRightRadius (px 8)
, borderLeft (px 0)
]
else
[ borderLeft (px 0) ]
)
|> Css.batch
focusedSegmentStyles : Style
focusedSegmentStyles =
[ backgroundColor Colors.glacier
, boxShadow5 inset zero (px 3) zero (withAlpha 0.2 Colors.gray20)
, color Colors.navy
]
|> Css.batch
unFocusedSegmentStyles : Style
unFocusedSegmentStyles =
[ backgroundColor Colors.white
, boxShadow5 inset zero (px -2) zero Colors.azure
, color Colors.azure
, hover [ backgroundColor Colors.frost ]
]
|> Css.batch

View File

@ -21,7 +21,7 @@ import Html.Styled.Attributes as Attributes exposing (css)
import Html.Styled.Events as Events
import KeyboardSupport exposing (Direction(..), Key(..))
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.SegmentedControl.V13 as SegmentedControl
import Nri.Ui.SegmentedControl.V14 as SegmentedControl
import Nri.Ui.Svg.V1 as Svg exposing (Svg)
import Nri.Ui.Tooltip.V2 as Tooltip
import Nri.Ui.UiIcon.V1 as UiIcon
@ -33,7 +33,7 @@ import Task
example : Example State Msg
example =
{ name = "SegmentedControl"
, version = 13
, version = 14
, state = init
, update = update
, subscriptions = \_ -> Sub.none
@ -63,7 +63,7 @@ example =
, SegmentedControl.viewRadioGroup
{ legend = "SegmentedControls 'viewSelectRadio' example"
, onSelect = SelectRadio
, options = List.take options.count (buildRadioOptions options.content)
, options = List.take options.count (buildRadioOptions state.radioTooltip options.content)
, selected = state.optionallySelected
, positioning = options.positioning
}
@ -146,10 +146,11 @@ buildOptions { content, longContent, tooltips } openTooltip =
]
buildRadioOptions : Content -> List (SegmentedControl.Radio Int msg)
buildRadioOptions content =
buildRadioOptions : Maybe Int -> Content -> List (SegmentedControl.Radio Int Msg)
buildRadioOptions currentlyHovered content =
let
buildOption value icon =
buildOption : Int -> ( String, Svg ) -> SegmentedControl.Radio Int Msg
buildOption value ( text, icon ) =
let
( icon_, label ) =
getIconAndLabel content
@ -160,18 +161,33 @@ buildRadioOptions content =
, label = label
, value = value
, idString = String.fromInt value
, tooltip =
[ Tooltip.plaintext text
, Tooltip.open (currentlyHovered == Just value)
, Tooltip.fitToContent
, Tooltip.onHover
(\hovered ->
HoverRadio
(if hovered then
Just value
else
Nothing
)
)
]
, attributes = []
}
in
List.indexedMap buildOption
[ UiIcon.leaderboard
, UiIcon.person
, UiIcon.performance
, UiIcon.gift
, UiIcon.document
, UiIcon.key
, UiIcon.badge
, UiIcon.hat
[ ( "Leaderboard", UiIcon.leaderboard )
, ( "Person", UiIcon.person )
, ( "Performance", UiIcon.performance )
, ( "Gift", UiIcon.gift )
, ( "Document", UiIcon.document )
, ( "Key", UiIcon.key )
, ( "Badge", UiIcon.badge )
, ( "Hat", UiIcon.hat )
]
@ -181,6 +197,7 @@ type alias State =
, pageTooltip : Maybe Page
, optionallySelected : Maybe Int
, optionsControl : Control Options
, radioTooltip : Maybe Int
}
@ -191,6 +208,7 @@ init =
, pageTooltip = Nothing
, optionallySelected = Nothing
, optionsControl = optionsControl
, radioTooltip = Nothing
}
@ -256,6 +274,7 @@ type Msg
| Focused (Result Dom.Error ())
| PageTooltip Page Bool
| SelectRadio Int
| HoverRadio (Maybe Int)
| ChangeOptions (Control Options)
@ -295,3 +314,8 @@ update msg state =
( { state | optionsControl = newOptions }
, Cmd.none
)
HoverRadio hovered ->
( { state | radioTooltip = hovered }
, Cmd.none
)

View File

@ -50,6 +50,7 @@
"Nri.Ui.SegmentedControl.V11",
"Nri.Ui.SegmentedControl.V12",
"Nri.Ui.SegmentedControl.V13",
"Nri.Ui.SegmentedControl.V14",
"Nri.Ui.Select.V5",
"Nri.Ui.Select.V7",
"Nri.Ui.Slide.V1",