mirror of
https://github.com/NoRedInk/noredink-ui.git
synced 2024-12-25 14:42:37 +03:00
Merge pull request #294 from NoRedInk/ink/spa-segmented-control
Ink/spa segmented control
This commit is contained in:
commit
db6946bc7d
1
elm.json
1
elm.json
@ -50,6 +50,7 @@
|
|||||||
"Nri.Ui.PremiumCheckbox.V3",
|
"Nri.Ui.PremiumCheckbox.V3",
|
||||||
"Nri.Ui.PremiumCheckbox.V4",
|
"Nri.Ui.PremiumCheckbox.V4",
|
||||||
"Nri.Ui.SegmentedControl.V6",
|
"Nri.Ui.SegmentedControl.V6",
|
||||||
|
"Nri.Ui.SegmentedControl.V7",
|
||||||
"Nri.Ui.Select.V5",
|
"Nri.Ui.Select.V5",
|
||||||
"Nri.Ui.Select.V6",
|
"Nri.Ui.Select.V6",
|
||||||
"Nri.Ui.Svg.V1",
|
"Nri.Ui.Svg.V1",
|
||||||
|
232
src/Nri/Ui/SegmentedControl/V7.elm
Normal file
232
src/Nri/Ui/SegmentedControl/V7.elm
Normal 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
|
||||||
|
]
|
@ -17,108 +17,111 @@ module Examples.SegmentedControl exposing
|
|||||||
-}
|
-}
|
||||||
|
|
||||||
import Accessibility.Styled
|
import Accessibility.Styled
|
||||||
|
import Debug.Control as Control exposing (Control)
|
||||||
import Html.Styled as Html exposing (Html)
|
import Html.Styled as Html exposing (Html)
|
||||||
import Html.Styled.Attributes as Attr
|
import Html.Styled.Attributes as Attr
|
||||||
import Html.Styled.Events as Events
|
import Html.Styled.Events as Events
|
||||||
import ModuleExample exposing (Category(..), ModuleExample)
|
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
|
type Msg
|
||||||
= Select Id
|
= Select ExampleOption
|
||||||
| SetFillContainer Bool
|
| ChangeOptions (Control Options)
|
||||||
|
|
||||||
|
|
||||||
|
type ExampleOption
|
||||||
|
= A
|
||||||
|
| B
|
||||||
|
| C
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
{-| -}
|
||||||
type alias State =
|
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 : (Msg -> msg) -> State -> ModuleExample msg
|
||||||
example parentMessage state =
|
example parentMessage state =
|
||||||
{ name = "Nri.Ui.SegmentedControl.V6"
|
{ name = "Nri.Ui.SegmentedControl.V7"
|
||||||
, category = Widgets
|
, category = Widgets
|
||||||
, content =
|
, content =
|
||||||
List.map (Html.map parentMessage)
|
[ Control.view ChangeOptions state.optionsControl
|
||||||
[ fillContainerCheckbox state.width
|
|> Html.fromUnstyled
|
||||||
, Nri.Ui.SegmentedControl.V6.view state
|
, 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 : { r | help : String } -> State
|
||||||
init =
|
init assets =
|
||||||
{ onClick = Select
|
{ selected = A
|
||||||
, options =
|
, optionsControl =
|
||||||
[ { icon = Nothing
|
Control.record Options
|
||||||
, id = "a"
|
|> Control.field "width"
|
||||||
, label = "Option A"
|
(Control.choice
|
||||||
, value = "a"
|
( "FitContent", Control.value SegmentedControl.FitContent )
|
||||||
}
|
[ ( "FillContainer", Control.value SegmentedControl.FillContainer ) ]
|
||||||
, { icon = Nothing
|
)
|
||||||
, id = "b"
|
|> Control.field "icon"
|
||||||
, label = "Option B"
|
(Control.maybe False (Control.value { alt = "Help", icon = Icon.helpSvg assets }))
|
||||||
, value = "b"
|
|> Control.field "which view function"
|
||||||
}
|
(Control.choice
|
||||||
]
|
( "view", Control.value False )
|
||||||
, selected = "a"
|
[ ( "viewSpa", Control.value True ) ]
|
||||||
, width = FitContent
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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 -> ( State, Cmd Msg )
|
||||||
update msg state =
|
update msg state =
|
||||||
case msg of
|
case msg of
|
||||||
Select id ->
|
Select id ->
|
||||||
( { state | selected = id }, Cmd.none )
|
( { state | selected = id }
|
||||||
|
|
||||||
SetFillContainer fillContainer ->
|
|
||||||
( { state
|
|
||||||
| width =
|
|
||||||
if fillContainer then
|
|
||||||
FillContainer
|
|
||||||
|
|
||||||
else
|
|
||||||
FitContent
|
|
||||||
}
|
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ChangeOptions newOptions ->
|
||||||
|
( { state | optionsControl = newOptions }
|
||||||
-- INTERNAL
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
type alias Id =
|
|
||||||
String
|
|
||||||
|
@ -55,7 +55,7 @@ init =
|
|||||||
, clickableTextExampleState = Examples.ClickableText.init assets
|
, clickableTextExampleState = Examples.ClickableText.init assets
|
||||||
, checkboxExampleState = Examples.Checkbox.init
|
, checkboxExampleState = Examples.Checkbox.init
|
||||||
, dropdownState = Examples.Dropdown.init
|
, dropdownState = Examples.Dropdown.init
|
||||||
, segmentedControlState = Examples.SegmentedControl.init
|
, segmentedControlState = Examples.SegmentedControl.init assets
|
||||||
, selectState = Examples.Select.init
|
, selectState = Examples.Select.init
|
||||||
, tableExampleState = Examples.Table.init
|
, tableExampleState = Examples.Table.init
|
||||||
, textAreaExampleState = TextAreaExample.init
|
, textAreaExampleState = TextAreaExample.init
|
||||||
|
Loading…
Reference in New Issue
Block a user