mirror of
https://github.com/NoRedInk/noredink-ui.git
synced 2024-12-02 23:52:22 +03:00
Merge pull request #683 from NoRedInk/raven/tooltips-in-segmented-controls
allow tooltips in segmented controls
This commit is contained in:
commit
1c665d8346
@ -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
elm.json
1
elm.json
@ -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",
|
||||
|
@ -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'
|
||||
|
320
src/Nri/Ui/SegmentedControl/V14.elm
Normal file
320
src/Nri/Ui/SegmentedControl/V14.elm
Normal 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
|
@ -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
|
||||
)
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user