Merge pull request #913 from NoRedInk/bat/switch

Switch.V2
This commit is contained in:
Juliano Solanho 2022-04-20 18:55:17 -03:00 committed by GitHub
commit e72a635126
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 530 additions and 76 deletions

View File

@ -7,6 +7,7 @@ Nri.Ui.RadioButton.V2,upgrade to V4
Nri.Ui.RadioButton.V3,upgrade to V4
Nri.Ui.SideNav.V1,upgrade to V3
Nri.Ui.SideNav.V2,upgrade to V3
Nri.Ui.Switch.V1,upgrade to V2
Nri.Ui.Table.V4,upgrade to V5
Nri.Ui.Tabs.V6,upgrade to V7
Nri.Ui.Tooltip.V1,upgrade to V2

1 Nri.Ui.Accordion.V1 upgrade to V3
7 Nri.Ui.RadioButton.V3 upgrade to V4
8 Nri.Ui.SideNav.V1 upgrade to V3
9 Nri.Ui.SideNav.V2 upgrade to V3
10 Nri.Ui.Switch.V1 upgrade to V2
11 Nri.Ui.Table.V4 upgrade to V5
12 Nri.Ui.Tabs.V6 upgrade to V7
13 Nri.Ui.Tooltip.V1 upgrade to V2

View File

@ -61,6 +61,7 @@
"Nri.Ui.Sprite.V1",
"Nri.Ui.Svg.V1",
"Nri.Ui.Switch.V1",
"Nri.Ui.Switch.V2",
"Nri.Ui.Table.V4",
"Nri.Ui.Table.V5",
"Nri.Ui.Tabs.V6",

View File

@ -114,6 +114,9 @@ hint = 'upgrade to V3'
[forbidden."Nri.Ui.SideNav.V2"]
hint = 'upgrade to V3'
[forbidden."Nri.Ui.Switch.V1"]
hint = 'upgrade to V2'
[forbidden."Nri.Ui.Table.V4"]
hint = 'upgrade to V5'

View File

@ -73,7 +73,6 @@ describe('UI tests', function () {
// Loading's color contrast check seems to change behavior depending on whether Percy snapshots are taken or not
'Loading': ['color-contrast'],
'RadioButton': ['duplicate-id'],
'Switch': ['aria-allowed-attr'],
}
const specialProcessing = {

View File

@ -1,6 +1,7 @@
module Nri.Ui.Colors.Extra exposing
( toCssColor, fromCssColor
, withAlpha
, toCssString
)
{-| Helpers for working with colors.
@ -10,6 +11,7 @@ module Nri.Ui.Colors.Extra exposing
@docs toCssColor, fromCssColor
@docs withAlpha
@docs toCssString
-}
@ -44,3 +46,9 @@ withAlpha 0.5 grassland -- "{ value = "rgba(86, 191, 116, 0.5)", color = Compati
withAlpha : Float -> Css.Color -> Css.Color
withAlpha alpha { red, green, blue } =
Css.rgba red green blue alpha
{-| -}
toCssString : Css.Color -> String
toCssString =
SolidColor.toRGBString << fromCssColor

View File

@ -21,11 +21,10 @@ import Css exposing (..)
import Html.Styled as Html exposing (Html)
import Html.Styled.Attributes exposing (css)
import Html.Styled.Events
import Nri.Ui.Colors.Extra
import Nri.Ui.Colors.Extra exposing (toCssString)
import Nri.Ui.Colors.V1
import Nri.Ui.CssVendorPrefix.V1 as CssVendorPrefix
import Nri.Ui.Table.V5
import SolidColor
import Svg.Styled as Svg
import Svg.Styled.Attributes as SvgAttributes
@ -363,8 +362,3 @@ sortArrow direction active =
[ Svg.polygon [ SvgAttributes.points "0 6 4 0 8 6 0 6" ] []
]
]
toCssString : Css.Color -> String
toCssString =
SolidColor.toRGBString << Nri.Ui.Colors.Extra.fromCssColor

381
src/Nri/Ui/Switch/V2.elm Normal file
View File

@ -0,0 +1,381 @@
module Nri.Ui.Switch.V2 exposing
( view
, Attribute
, selected
, containerCss, labelCss, custom, nriDescription, testId
, onSwitch, disabled
)
{-|
# Changes from V1:
- Fixes invalid ARIA use, [conformance requirements](https://www.w3.org/TR/html-aria/#docconformance)
- labels should only support strings (this is the only way they're actually used in practice)
- extends API to be more consistent with other form/control components
- Use Colors values instead of hardcoded hex strings
- Move the status (selected or not selected) to the list api
- REQUIRE label and id always
- Move custom attributes to the container
- change disabled to take a bool (which I think is the slighty more common pattern)
@docs view
### Attributes
@docs Attribute
@docs selected
@docs containerCss, labelCss, custom, nriDescription, testId
@docs onSwitch, disabled
-}
import Accessibility.Styled as Html exposing (Html)
import Accessibility.Styled.Widget as Widget
import Css exposing (Color, Style)
import Css.Global as Global
import Css.Media
import Html.Styled.Attributes as Attributes
import Html.Styled.Events as Events
import Nri.Ui.Colors.Extra exposing (toCssString)
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as Extra
import Nri.Ui.Svg.V1 exposing (Svg)
import Svg.Styled as Svg
import Svg.Styled.Attributes as SvgAttributes
{-| -}
type Attribute msg
= Attribute (Config msg -> Config msg)
{-| What is the status of the Switch, selected or not?
-}
selected : Bool -> Attribute msg
selected isSelected =
Attribute <| \config -> { config | isSelected = isSelected }
{-| Specify what happens when the switch is toggled.
-}
onSwitch : (Bool -> msg) -> Attribute msg
onSwitch onSwitch_ =
Attribute <| \config -> { config | onSwitch = Just onSwitch_ }
{-| Explicitly specify that you want this switch to be disabled. If you don't
specify `onSwitch`, this is the default, but it's provided so you don't have
to resort to `filterMap` or similar to build a clean list of attributes.
-}
disabled : Bool -> Attribute msg
disabled isDisabled =
Attribute <| \config -> { config | isDisabled = isDisabled }
{-| Pass custom attributes through to be attached to the underlying input.
Do NOT use this helper to add css styles, as they may not be applied the way
you want/expect if underlying styles change.
Instead, please use `containerCss` or `labelCss`.
-}
custom : List (Html.Attribute Never) -> Attribute msg
custom custom_ =
Attribute <| \config -> { config | custom = config.custom ++ custom_ }
{-| -}
nriDescription : String -> Attribute msg
nriDescription description =
custom [ Extra.nriDescription description ]
{-| -}
testId : String -> Attribute msg
testId id_ =
custom [ Extra.testId id_ ]
{-| Adds CSS to the Switch container.
-}
containerCss : List Css.Style -> Attribute msg
containerCss styles =
Attribute <| \config -> { config | containerCss = config.containerCss ++ styles }
{-| Adds CSS to the element containing the label text.
Note that these styles don't apply to the literal HTML label element, since it contains the icon SVG as well.
-}
labelCss : List Css.Style -> Attribute msg
labelCss styles =
Attribute <| \config -> { config | labelCss = config.labelCss ++ styles }
type alias Config msg =
{ onSwitch : Maybe (Bool -> msg)
, containerCss : List Style
, labelCss : List Style
, isDisabled : Bool
, isSelected : Bool
, custom : List (Html.Attribute Never)
}
defaultConfig : Config msg
defaultConfig =
{ onSwitch = Nothing
, containerCss = []
, labelCss = []
, isDisabled = False
, isSelected = False
, custom = []
}
{-| Render a switch. The boolean here indicates whether the switch is on
or not.
-}
view : { label : String, id : String } -> List (Attribute msg) -> Html msg
view { label, id } attrs =
let
config =
List.foldl (\(Attribute update) -> update) defaultConfig attrs
notOperable =
config.onSwitch == Nothing || config.isDisabled
in
Html.label
([ Attributes.id (id ++ "-container")
, Attributes.css
[ Css.display Css.inlineFlex
, Css.alignItems Css.center
, Css.position Css.relative
, Css.pseudoClass "focus-within"
[ Global.descendants
[ Global.class "switch-slider"
[ stroke Colors.azure
, Css.property "stroke-width" "3px"
]
]
]
, Css.cursor
(if notOperable then
Css.notAllowed
else
Css.pointer
)
, Css.batch config.containerCss
]
, Attributes.for id
]
++ List.map (Attributes.map never) config.custom
)
[ viewCheckbox
{ id = id
, onCheck = config.onSwitch
, isDisabled = config.isDisabled
, selected = config.isSelected
}
, Nri.Ui.Svg.V1.toHtml
(viewSwitch
{ id = id
, isSelected = config.isSelected
, isDisabled = notOperable
}
)
, Html.span
[ Attributes.css
[ Css.fontWeight (Css.int 600)
, Css.color Colors.navy
, Css.paddingLeft (Css.px 5)
, Fonts.baseFont
, Css.batch config.labelCss
]
]
[ Html.text label ]
]
viewCheckbox :
{ id : String
, onCheck : Maybe (Bool -> msg)
, selected : Bool
, isDisabled : Bool
}
-> Html msg
viewCheckbox config =
Html.checkbox config.id
(Just config.selected)
[ Attributes.id config.id
, Attributes.css
[ Css.position Css.absolute
, Css.top (Css.px 10)
, Css.left (Css.px 10)
, Css.zIndex (Css.int 0)
, Css.opacity (Css.num 0)
]
, case ( config.onCheck, config.isDisabled ) of
( Just onCheck, False ) ->
Events.onCheck onCheck
_ ->
Widget.disabled True
]
viewSwitch :
{ id : String
, isSelected : Bool
, isDisabled : Bool
}
-> Svg
viewSwitch config =
let
shadowFilterId =
config.id ++ "-shadow-filter"
shadowBoxId =
config.id ++ "-shadow-box"
in
Svg.svg
[ SvgAttributes.width "43"
, SvgAttributes.height "32"
, SvgAttributes.viewBox "0 0 43 32"
, SvgAttributes.css
[ Css.zIndex (Css.int 1)
, if config.isDisabled then
Css.opacity (Css.num 0.4)
else
Css.opacity (Css.num 1)
]
]
[ Svg.defs []
[ Svg.filter
[ SvgAttributes.id shadowFilterId
, SvgAttributes.width "105%"
, SvgAttributes.height "106.7%"
, SvgAttributes.x "-2.5%"
, SvgAttributes.y "-3.3%"
, SvgAttributes.filterUnits "objectBoundingBox"
]
[ Svg.feOffset
[ SvgAttributes.dy "2"
, SvgAttributes.in_ "SourceAlpha"
, SvgAttributes.result "shadowOffsetInner1"
]
[]
, Svg.feComposite
[ SvgAttributes.in_ "shadowOffsetInner1"
, SvgAttributes.in2 "SourceAlpha"
, SvgAttributes.k2 "-1"
, SvgAttributes.k3 "1"
, SvgAttributes.operator "arithmetic"
, SvgAttributes.result "shadowInnerInner1"
]
[]
, Svg.feColorMatrix
[ SvgAttributes.in_ "shadowInnerInner1"
, SvgAttributes.values "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
]
[]
]
, Svg.rect
[ SvgAttributes.id shadowBoxId
, SvgAttributes.width "40"
, SvgAttributes.height "30"
, SvgAttributes.x "0"
, SvgAttributes.y "0"
, SvgAttributes.rx "15"
]
[]
]
, Svg.g
[ SvgAttributes.fill "none"
, SvgAttributes.fillRule "even-odd"
, SvgAttributes.transform "translate(1, 1)"
]
[ Svg.g []
[ Svg.use
[ SvgAttributes.xlinkHref ("#" ++ shadowBoxId)
, SvgAttributes.css
[ if config.isSelected then
Css.fill Colors.glacier
else
Css.fill Colors.gray92
, transition "fill 0.2s"
]
]
[]
, Svg.use
[ SvgAttributes.xlinkHref ("#" ++ shadowBoxId)
, SvgAttributes.fill "#000"
, SvgAttributes.filter ("url(#" ++ shadowFilterId ++ ")")
]
[]
]
, Svg.g
[ SvgAttributes.css
[ if config.isSelected then
Css.transform (Css.translateX (Css.px 11))
else
Css.transform (Css.translateX (Css.px 0))
, transition "transform 0.2s ease-in-out"
]
]
[ Svg.circle
[ SvgAttributes.cx "15"
, SvgAttributes.cy "15"
, SvgAttributes.r "14.5"
, SvgAttributes.fill "#FFF"
, SvgAttributes.css
[ if config.isSelected then
stroke Colors.azure
else
stroke Colors.gray75
, transition "stroke 0.1s"
]
, SvgAttributes.class "switch-slider"
]
[]
, Svg.path
[ SvgAttributes.strokeLinecap "round"
, SvgAttributes.strokeLinejoin "round"
, SvgAttributes.strokeWidth "3"
, SvgAttributes.d "M8 15.865L12.323 20 21.554 10"
, SvgAttributes.css
[ if config.isSelected then
stroke Colors.azure
else
stroke Colors.white
, transition "stroke 0.2s"
]
]
[]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml
stroke : Color -> Style
stroke color =
Css.property "stroke" (toCssString color)
transition : String -> Css.Style
transition transitionRules =
Css.Media.withMediaQuery
[ "(prefers-reduced-motion: no-preference)" ]
[ Css.property "transition" transitionRules ]

View File

@ -1,4 +1,4 @@
module Debug.Control.View exposing (codeFromList, codeFromListWithIndentLevel, view)
module Debug.Control.View exposing (codeFromList, codeFromListWithHardcoded, codeFromListWithIndentLevel, view)
import Css exposing (..)
import Css.Media exposing (withMedia)
@ -97,6 +97,13 @@ viewSection name children =
)
codeFromListWithHardcoded : List String -> List ( String, a ) -> String
codeFromListWithHardcoded hardcodes elements =
List.map (\v -> ( v, () )) hardcodes
++ List.map (Tuple.mapSecond (always ())) elements
|> codeFromList
codeFromList : List ( String, a ) -> String
codeFromList =
codeFromListWithIndentLevel 1

View File

@ -8,71 +8,137 @@ module Examples.Switch exposing (Msg, State, example)
import Accessibility.Styled.Key as Key
import Category
import CommonControls
import Debug.Control as Control exposing (Control)
import Debug.Control.Extra as ControlExtra
import Debug.Control.View as ControlView
import Example exposing (Example)
import Html.Styled as Html
import Nri.Ui.Heading.V2 as Heading
import Nri.Ui.Switch.V1 as Switch
import KeyboardSupport exposing (Key(..))
import Nri.Ui.Switch.V2 as Switch
moduleName : String
moduleName =
"Switch"
version : Int
version =
2
example : Example State Msg
example =
{ name = moduleName
, version = version
, state = init
, update = update
, subscriptions = \_ -> Sub.none
, preview =
[ Switch.view { label = "Toggle Off", id = "preview-switch-a" }
[ Switch.selected False
, Switch.custom [ Key.tabbable False ]
]
, Switch.view { label = "Toggle On", id = "preview-switch-b" }
[ Switch.selected True
, Switch.custom [ Key.tabbable False ]
]
]
, view =
\ellieLinkConfig state ->
let
currentValue =
Control.currentValue state.settings
in
[ ControlView.view
{ ellieLinkConfig = ellieLinkConfig
, name = moduleName
, version = version
, update = UpdateSettings
, settings = state.settings
, mainType = "RootHtml.Html msg"
, extraImports = []
, toExampleCode =
\{ label, attributes } ->
[ { sectionName = "Example"
, code =
moduleName
++ ".view"
++ " \""
++ label
++ "\"\t"
++ ControlView.codeFromListWithHardcoded
[ "Switch.selected "
++ Debug.toString state.selected
++ "\n-- , Switch.onSwitch Switch -- <- you'll need to wire in a Msg for the Switch to work"
]
attributes
}
]
}
, Switch.view { label = currentValue.label, id = "view-switch-example" }
(Switch.selected state.selected
:: Switch.onSwitch Switch
:: List.map Tuple.second currentValue.attributes
)
]
, categories = [ Category.Inputs ]
, keyboardSupport =
[ { keys = [ Space ]
, result = "Toggle the Switch state"
}
]
}
{-| -}
type alias State =
Bool
{ selected : Bool
, settings : Control Settings
}
init : State
init =
{ selected = True
, settings = controlSettings
}
type alias Settings =
{ label : String
, attributes : List ( String, Switch.Attribute Msg )
}
controlSettings : Control Settings
controlSettings =
Control.record Settings
|> Control.field "label" (Control.string "Show pandas in results")
|> Control.field "attributes" initAttributes
initAttributes : Control (List ( String, Switch.Attribute msg ))
initAttributes =
ControlExtra.list
|> CommonControls.disabledListItem moduleName Switch.disabled
{-| -}
type Msg
= Switch Bool
| UpdateSettings (Control Settings)
example : Example State Msg
example =
{ name = "Switch"
, version = 1
, state = True
, update = \(Switch new) _ -> ( new, Cmd.none )
, subscriptions = \_ -> Sub.none
, preview =
[ Switch.view
[ Switch.label (Html.text "Toggle On")
, Switch.custom [ Key.tabbable False ]
]
False
, Switch.view
[ Switch.label (Html.text "Toggle Off")
, Switch.custom [ Key.tabbable False ]
]
True
]
, view =
\ellieLinkConfig interactiveIsOn ->
[ Heading.h2 [ Heading.style Heading.Subhead ] [ Html.text "Interactive" ]
, Switch.view
[ Switch.onSwitch Switch
, Switch.id "switch-interactive"
, Switch.label
(if interactiveIsOn then
Html.text "On"
update : Msg -> State -> ( State, Cmd Msg )
update msg state =
case msg of
Switch bool ->
( { state | selected = bool }
, Cmd.none
)
else
Html.text "Off"
)
]
interactiveIsOn
, Heading.h2 [ Heading.style Heading.Subhead ] [ Html.text "Disabled (On)" ]
, Switch.view
[ Switch.disabled
, Switch.id "switch-disabled-on"
, Switch.label (Html.text "Permanently on")
]
True
, Heading.h2 [ Heading.style Heading.Subhead ] [ Html.text "Disabled (Off)" ]
, Switch.view
[ Switch.disabled
, Switch.id "switch-disabled-off"
, Switch.label (Html.text "Permanently off")
]
False
]
, categories = [ Category.Inputs ]
, keyboardSupport = [{- TODO -}]
}
UpdateSettings settings ->
( { state | settings = settings }
, Cmd.none
)

View File

@ -1,6 +1,5 @@
module SwitchExampleSpec exposing (suite)
import Accessibility.Aria as Aria
import ProgramTest exposing (..)
import Routes exposing (Route)
import Test exposing (..)
@ -20,17 +19,11 @@ suite =
\() ->
app route
|> ensureViewHas [ text "Nri.Ui.Switch" ]
-- switch starts with aria-checked=true and text "On"
|> ensureViewHas
[ attribute (Aria.checked (Just True))
, text "On"
]
-- switch starts with checked=true
|> ensureViewHas [ checked True ]
-- user can click the first switch
|> check "switch-interactive" "On" False
-- the switch now has aria-checked=false and text "Off"
|> ensureViewHas
[ attribute (Aria.checked (Just False))
, text "Off"
]
|> check "view-switch-example" "Show pandas in results" False
-- the switch now has checked=false
|> ensureViewHas [ checked False ]
|> done
]

View File

@ -57,6 +57,7 @@
"Nri.Ui.Sprite.V1",
"Nri.Ui.Svg.V1",
"Nri.Ui.Switch.V1",
"Nri.Ui.Switch.V2",
"Nri.Ui.Table.V4",
"Nri.Ui.Table.V5",
"Nri.Ui.Tabs.V6",