mirror of
https://github.com/NoRedInk/noredink-ui.git
synced 2024-11-23 16:32:11 +03:00
copy v11 -> v12
This commit is contained in:
parent
f4a4c9aad5
commit
85bcfbe424
1
elm.json
1
elm.json
@ -44,6 +44,7 @@
|
||||
"Nri.Ui.PremiumCheckbox.V6",
|
||||
"Nri.Ui.RadioButton.V1",
|
||||
"Nri.Ui.SegmentedControl.V11",
|
||||
"Nri.Ui.SegmentedControl.V12",
|
||||
"Nri.Ui.Select.V5",
|
||||
"Nri.Ui.Select.V7",
|
||||
"Nri.Ui.Slide.V1",
|
||||
|
276
src/Nri/Ui/SegmentedControl/V12.elm
Normal file
276
src/Nri/Ui/SegmentedControl/V12.elm
Normal file
@ -0,0 +1,276 @@
|
||||
module Nri.Ui.SegmentedControl.V12 exposing
|
||||
( Option, view
|
||||
, Radio, viewRadioGroup
|
||||
, Width(..)
|
||||
)
|
||||
|
||||
{-| Changes from V10:
|
||||
|
||||
- change selection using left/right arrow keys
|
||||
- only currently-selected or first control is tabbable
|
||||
- tabpanel is tabbable
|
||||
- Uses TabsInternal under the hood
|
||||
- `viewSelect` renamed to `viewRadioGroup`, `SelectOption` renamed to `Radio`
|
||||
- `viewRadioGroup` uses native HTML radio input internally
|
||||
|
||||
@docs Option, view
|
||||
@docs Radio, viewRadioGroup
|
||||
@docs 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.Util exposing (dashify)
|
||||
import TabsInternal
|
||||
|
||||
|
||||
{-| -}
|
||||
type Width
|
||||
= FitContent
|
||||
| FillContainer
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Radio value msg =
|
||||
{ value : value
|
||||
, label : String
|
||||
, 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
|
||||
- `width`: how to 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
|
||||
, toString : a -> String
|
||||
, options : List (Radio a msg)
|
||||
, selected : Maybe a
|
||||
, width : Width
|
||||
, legend : String
|
||||
}
|
||||
-> Html msg
|
||||
viewRadioGroup config =
|
||||
let
|
||||
viewRadio option =
|
||||
let
|
||||
isSelected =
|
||||
Just option.value == config.selected
|
||||
in
|
||||
labelAfter
|
||||
[ 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.width isSelected
|
||||
)
|
||||
]
|
||||
(div [] [ viewIcon option.icon, text option.label ])
|
||||
(radio name (config.toString option.value) 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
|
||||
)
|
||||
)
|
||||
|
||||
name =
|
||||
dashify (String.toLower config.legend)
|
||||
|
||||
legendId =
|
||||
"legend-" ++ name
|
||||
in
|
||||
div
|
||||
[ Role.radioGroup
|
||||
, Aria.labelledBy legendId
|
||||
, css [ displayFlex, cursor pointer ]
|
||||
]
|
||||
(p (Attributes.id legendId :: Style.invisible) [ text config.legend ]
|
||||
:: List.map viewRadio config.options
|
||||
)
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Option value msg =
|
||||
{ value : value
|
||||
, label : String
|
||||
, attributes : List (Attribute msg)
|
||||
, icon : Maybe Svg
|
||||
, content : Html msg
|
||||
}
|
||||
|
||||
|
||||
{-|
|
||||
|
||||
- `onSelect` : the message to produce when an option is selected by the user
|
||||
- `onFocus` : the message to focus an element by id string
|
||||
- `toString` : function to get the option value as a string
|
||||
- `options`: the list of options available
|
||||
- `selected`: the value of the currently-selected option
|
||||
- `width`: how to 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 :
|
||||
{ onSelect : a -> msg
|
||||
, onFocus : String -> msg
|
||||
, toString : a -> String
|
||||
, options : List (Option a msg)
|
||||
, selected : a
|
||||
, width : Width
|
||||
, toUrl : Maybe (a -> String)
|
||||
}
|
||||
-> Html msg
|
||||
view config =
|
||||
let
|
||||
toInternalTab : Option a msg -> TabsInternal.Tab a msg
|
||||
toInternalTab option =
|
||||
{ id = option.value
|
||||
, idString = config.toString option.value
|
||||
, tabAttributes = option.attributes
|
||||
, tabView = [ viewIcon option.icon, text option.label ]
|
||||
, panelView = option.content
|
||||
, spaHref = Maybe.map (\toUrl -> toUrl option.value) config.toUrl
|
||||
}
|
||||
|
||||
{ tabList, tabPanels } =
|
||||
TabsInternal.views
|
||||
{ onSelect = config.onSelect
|
||||
, onFocus = config.onFocus
|
||||
, selected = config.selected
|
||||
, tabs = List.map toInternalTab config.options
|
||||
, tabListStyles = [ displayFlex, cursor pointer, marginBottom (px 10) ]
|
||||
, tabStyles = styles config.width
|
||||
}
|
||||
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 : Width -> Bool -> List Style
|
||||
styles width isSelected =
|
||||
[ sharedSegmentStyles
|
||||
, if isSelected then
|
||||
focusedSegmentStyles
|
||||
|
||||
else
|
||||
unFocusedSegmentStyles
|
||||
, case width of
|
||||
FitContent ->
|
||||
Css.batch []
|
||||
|
||||
FillContainer ->
|
||||
expandingTabStyles
|
||||
]
|
||||
|
||||
|
||||
sharedSegmentStyles : Style
|
||||
sharedSegmentStyles =
|
||||
[ padding2 (px 6) (px 20)
|
||||
, height (px 45)
|
||||
, Fonts.baseFont
|
||||
, fontSize (px 15)
|
||||
, fontWeight bold
|
||||
, lineHeight (px 30)
|
||||
, margin zero
|
||||
, firstOfType
|
||||
[ borderTopLeftRadius (px 8)
|
||||
, borderBottomLeftRadius (px 8)
|
||||
, borderLeft3 (px 1) solid Colors.azure
|
||||
]
|
||||
, lastOfType
|
||||
[ borderTopRightRadius (px 8)
|
||||
, borderBottomRightRadius (px 8)
|
||||
]
|
||||
, border3 (px 1) solid Colors.azure
|
||||
, borderLeft (px 0)
|
||||
, 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 ]
|
||||
]
|
||||
|> 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
|
||||
|
||||
|
||||
expandingTabStyles : Style
|
||||
expandingTabStyles =
|
||||
[ flexGrow (int 1)
|
||||
, textAlign center
|
||||
]
|
||||
|> 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.V11 as SegmentedControl
|
||||
import Nri.Ui.SegmentedControl.V12 as SegmentedControl
|
||||
import Nri.Ui.Svg.V1 as Svg exposing (Svg)
|
||||
import Nri.Ui.UiIcon.V1 as UiIcon
|
||||
import String exposing (toLower)
|
||||
@ -31,7 +31,7 @@ import Task
|
||||
{-| -}
|
||||
example : Example State Msg
|
||||
example =
|
||||
{ name = "Nri.Ui.SegmentedControl.V11"
|
||||
{ name = "Nri.Ui.SegmentedControl.V12"
|
||||
, state = init
|
||||
, update = update
|
||||
, subscriptions = \_ -> Sub.none
|
||||
|
@ -4,7 +4,7 @@ import Expect
|
||||
import Html.Attributes as Attributes
|
||||
import Html.Styled
|
||||
import Json.Encode as Encode
|
||||
import Nri.Ui.SegmentedControl.V11 as SegmentedControl
|
||||
import Nri.Ui.SegmentedControl.V12 as SegmentedControl
|
||||
import Test exposing (..)
|
||||
import Test.Html.Query as Query
|
||||
import Test.Html.Selector as Selector
|
||||
|
@ -40,6 +40,7 @@
|
||||
"Nri.Ui.PremiumCheckbox.V6",
|
||||
"Nri.Ui.RadioButton.V1",
|
||||
"Nri.Ui.SegmentedControl.V11",
|
||||
"Nri.Ui.SegmentedControl.V12",
|
||||
"Nri.Ui.Select.V5",
|
||||
"Nri.Ui.Select.V7",
|
||||
"Nri.Ui.Slide.V1",
|
||||
|
Loading…
Reference in New Issue
Block a user