add new SegmentedControl.V14

This commit is contained in:
Brian Hicks 2021-04-06 14:50:09 -05:00
parent 74389ea939
commit d8d09cb115
2 changed files with 312 additions and 2 deletions

View File

@ -0,0 +1,310 @@
module Nri.Ui.SegmentedControl.V14 exposing
( Option, view
, Radio, viewRadioGroup
, Positioning(..), Width(..)
)
{-| Post-release patches:
- Fixes <https://github.com/NoRedInk/noredink-ui/issues/608>
Changes from V12:
- Adds tooltip support
- combine onFocus and onSelect into focusAndSelect msg handler (for tooltips)
@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)
, 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
- `toString`: 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
in
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
)
]
[ 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 ]
]
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