Merge pull request #294 from NoRedInk/ink/spa-segmented-control

Ink/spa segmented control
This commit is contained in:
Aaron VonderHaar 2019-06-04 17:53:03 -07:00 committed by GitHub
commit db6946bc7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 308 additions and 72 deletions

View File

@ -50,6 +50,7 @@
"Nri.Ui.PremiumCheckbox.V3",
"Nri.Ui.PremiumCheckbox.V4",
"Nri.Ui.SegmentedControl.V6",
"Nri.Ui.SegmentedControl.V7",
"Nri.Ui.Select.V5",
"Nri.Ui.Select.V6",
"Nri.Ui.Svg.V1",

View File

@ -0,0 +1,232 @@
module Nri.Ui.SegmentedControl.V7 exposing (Config, Icon, Option, Width(..), view, viewSpa)
{-|
@docs Config, Icon, Option, Width, view, viewSpa
-}
import Accessibility.Styled exposing (..)
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Role as Role
import Css exposing (..)
import EventExtras.Styled as EventExtras
import Html.Styled as Html exposing (Html)
import Html.Styled.Attributes as Attr exposing (css, href)
import Html.Styled.Events as Events
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.Icon.V5 as Icon
import Nri.Ui.Util exposing (dashify)
{-|
- `onClick` : the message to produce when an option is selected (clicked) by the user
- `options`: the list of options available
- `selected`: the value of the currently-selected option
- `width`: how to size the segmented control
- `content`: the panel content for the selected option
-}
type alias Config a msg =
{ onClick : a -> msg
, options : List (Option a)
, selected : a
, width : Width
, content : Html msg
}
{-| -}
type alias Option a =
{ value : a
, icon : Maybe Icon
, label : String
}
{-| -}
type Width
= FitContent
| FillContainer
{-| -}
type alias Icon =
{ alt : String
, icon : Icon.IconType
}
{-| -}
view : Config a msg -> Html.Html msg
view config =
viewHelper Nothing config
{-| Creates a segmented control that supports SPA navigation.
You should always use this instead of `view` when building a SPA
and the segmented control options correspond to routes in the SPA.
The first parameter is a function that takes a `route` and returns the URL of that route.
-}
viewSpa : (route -> String) -> Config route msg -> Html msg
viewSpa toUrl config =
viewHelper (Just toUrl) config
viewHelper : Maybe (a -> String) -> Config a msg -> Html msg
viewHelper maybeToUrl config =
let
selected =
config.options
|> List.filter (\o -> o.value == config.selected)
|> List.head
in
div []
[ tabList
[ css
[ displayFlex
, cursor pointer
]
]
(List.map (viewTab maybeToUrl config) config.options)
, tabPanel
(List.filterMap identity
[ Maybe.map (Attr.id << panelIdFor) selected
, Maybe.map (Aria.labelledBy << tabIdFor) selected
, Just <| css [ paddingTop (px 10) ]
]
)
[ config.content
]
]
tabIdFor : Option a -> String
tabIdFor option =
"Nri-Ui-SegmentedControl-Tab-" ++ dashify option.label
panelIdFor : Option a -> String
panelIdFor option =
"Nri-Ui-SegmentedControl-Panel-" ++ dashify option.label
viewTab : Maybe (a -> String) -> Config a msg -> Option a -> Html.Html msg
viewTab maybeToUrl config option =
let
idValue =
tabIdFor option
element attrs children =
case maybeToUrl of
Nothing ->
-- This is for a non-SPA view
Html.button
(Events.onClick (config.onClick option.value)
:: attrs
)
children
Just toUrl ->
-- This is a for a SPA view
Html.a
(href (toUrl option.value)
:: EventExtras.onClickPreventDefaultForLinkWithHref
(config.onClick option.value)
:: attrs
)
children
in
element
(List.concat
[ [ Attr.id idValue
, Role.tab
, Aria.controls (panelIdFor option)
, css sharedTabStyles
]
, if option.value == config.selected then
[ css focusedTabStyles
, Aria.currentPage
]
else
[ css unFocusedTabStyles ]
, case config.width of
FitContent ->
[]
FillContainer ->
[ css expandingTabStyles ]
]
)
[ case option.icon of
Nothing ->
Html.text ""
Just icon ->
viewIcon icon
, Html.text option.label
]
viewIcon : Icon -> Html.Html msg
viewIcon icon =
Html.span
[ css [ marginRight (px 10) ] ]
[ Icon.icon icon ]
sharedTabStyles : List Style
sharedTabStyles =
[ padding2 (px 6) (px 20)
, height (px 45)
, Fonts.baseFont
, fontSize (px 15)
, fontWeight bold
, lineHeight (px 30)
, 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
]
focusedTabStyles : List Style
focusedTabStyles =
[ backgroundColor Colors.glacier
, boxShadow5 inset zero (px 3) zero (withAlpha 0.2 Colors.gray20)
, color Colors.navy
]
unFocusedTabStyles : List Style
unFocusedTabStyles =
[ backgroundColor Colors.white
, boxShadow5 inset zero (px -2) zero Colors.azure
, color Colors.azure
, hover [ backgroundColor Colors.glacier ]
]
expandingTabStyles : List Style
expandingTabStyles =
[ flexGrow (int 1)
, textAlign center
]

View File

@ -17,108 +17,111 @@ module Examples.SegmentedControl exposing
-}
import Accessibility.Styled
import Debug.Control as Control exposing (Control)
import Html.Styled as Html exposing (Html)
import Html.Styled.Attributes as Attr
import Html.Styled.Events as Events
import ModuleExample exposing (Category(..), ModuleExample)
import Nri.Ui.SegmentedControl.V6 exposing (Width(..))
import Nri.Ui.Icon.V5 as Icon
import Nri.Ui.SegmentedControl.V7 as SegmentedControl
{-| -}
type Msg
= Select Id
| SetFillContainer Bool
= Select ExampleOption
| ChangeOptions (Control Options)
type ExampleOption
= A
| B
| C
{-| -}
type alias State =
Nri.Ui.SegmentedControl.V6.Config Id Msg
{ selected : ExampleOption
, optionsControl : Control Options
}
type alias Options =
{ width : SegmentedControl.Width
, icon : Maybe SegmentedControl.Icon
, useSpa : Bool
}
{-| -}
example : (Msg -> msg) -> State -> ModuleExample msg
example parentMessage state =
{ name = "Nri.Ui.SegmentedControl.V6"
{ name = "Nri.Ui.SegmentedControl.V7"
, category = Widgets
, content =
List.map (Html.map parentMessage)
[ fillContainerCheckbox state.width
, Nri.Ui.SegmentedControl.V6.view state
]
[ Control.view ChangeOptions state.optionsControl
|> Html.fromUnstyled
, let
options =
Control.currentValue state.optionsControl
viewFn =
if options.useSpa then
SegmentedControl.viewSpa Debug.toString
else
SegmentedControl.view
in
viewFn
{ onClick = Select
, options =
[ A, B, C ]
|> List.map
(\i ->
{ icon = options.icon
, label = "Option " ++ Debug.toString i
, value = i
}
)
, selected = state.selected
, width = options.width
, content = Html.text ("[Content for " ++ Debug.toString state.selected ++ "]")
}
]
|> List.map (Html.map parentMessage)
}
{-| -}
init : State
init =
{ onClick = Select
, options =
[ { icon = Nothing
, id = "a"
, label = "Option A"
, value = "a"
}
, { icon = Nothing
, id = "b"
, label = "Option B"
, value = "b"
}
]
, selected = "a"
, width = FitContent
init : { r | help : String } -> State
init assets =
{ selected = A
, optionsControl =
Control.record Options
|> Control.field "width"
(Control.choice
( "FitContent", Control.value SegmentedControl.FitContent )
[ ( "FillContainer", Control.value SegmentedControl.FillContainer ) ]
)
|> Control.field "icon"
(Control.maybe False (Control.value { alt = "Help", icon = Icon.helpSvg assets }))
|> Control.field "which view function"
(Control.choice
( "view", Control.value False )
[ ( "viewSpa", Control.value True ) ]
)
}
fillContainerCheckbox : Width -> Html Msg
fillContainerCheckbox currentOption =
let
id =
"SegmentedControl-fill-container-checkbox"
isChecked =
case currentOption of
FitContent ->
Just False
FillContainer ->
Just True
in
Html.div []
[ Accessibility.Styled.checkbox "Fill container"
isChecked
[ Attr.id id
, Events.onCheck SetFillContainer
]
, Html.label
[ Attr.for id
]
[ Html.text "Fill Container" ]
]
{-| -}
update : Msg -> State -> ( State, Cmd Msg )
update msg state =
case msg of
Select id ->
( { state | selected = id }, Cmd.none )
SetFillContainer fillContainer ->
( { state
| width =
if fillContainer then
FillContainer
else
FitContent
}
( { state | selected = id }
, Cmd.none
)
-- INTERNAL
type alias Id =
String
ChangeOptions newOptions ->
( { state | optionsControl = newOptions }
, Cmd.none
)

View File

@ -55,7 +55,7 @@ init =
, clickableTextExampleState = Examples.ClickableText.init assets
, checkboxExampleState = Examples.Checkbox.init
, dropdownState = Examples.Dropdown.init
, segmentedControlState = Examples.SegmentedControl.init
, segmentedControlState = Examples.SegmentedControl.init assets
, selectState = Examples.Select.init
, tableExampleState = Examples.Table.init
, textAreaExampleState = TextAreaExample.init