mirror of
https://github.com/NoRedInk/noredink-ui.git
synced 2024-12-24 14:13:20 +03:00
add new SegmentedControl.V14
This commit is contained in:
parent
74389ea939
commit
d8d09cb115
310
src/Nri/Ui/SegmentedControl/V14.elm
Normal file
310
src/Nri/Ui/SegmentedControl/V14.elm
Normal 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
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user