From 9f65f823f9c391d1cc2afd991c89aee57bc185b8 Mon Sep 17 00:00:00 2001 From: charbelrami Date: Tue, 26 Jul 2022 17:21:38 -0300 Subject: [PATCH] Add ability to set custom CSS on checkboxes --- deprecated-modules.csv | 1 + elm.json | 3 +- forbidden-imports.toml | 3 + src/Nri/Ui/Checkbox/V6.elm | 333 +++++++++++++++++++++++ src/Nri/Ui/PremiumCheckbox/V8.elm | 43 ++- styleguide-app/Examples/Checkbox.elm | 71 ++++- styleguide-app/Examples/IconExamples.elm | 8 +- styleguide-app/Examples/TextArea.elm | 11 +- tests/elm-verify-examples.json | 1 + 9 files changed, 468 insertions(+), 6 deletions(-) create mode 100644 src/Nri/Ui/Checkbox/V6.elm diff --git a/deprecated-modules.csv b/deprecated-modules.csv index 230b2143..e6914b5c 100644 --- a/deprecated-modules.csv +++ b/deprecated-modules.csv @@ -1,4 +1,5 @@ Nri.Ui.Accordion.V1,upgrade to V3 +Nri.Ui.Checkbox.V5,upgrade to V6 Nri.Ui.Menu.V1,upgrade to V3 Nri.Ui.SortableTable.V2,upgrade to V3 Nri.Ui.Tabs.V6,upgrade to V7 diff --git a/elm.json b/elm.json index 8e12cd01..3c56b3a9 100644 --- a/elm.json +++ b/elm.json @@ -15,6 +15,7 @@ "Nri.Ui.Button.V10", "Nri.Ui.Carousel.V1", "Nri.Ui.Checkbox.V5", + "Nri.Ui.Checkbox.V6", "Nri.Ui.ClickableSvg.V2", "Nri.Ui.ClickableText.V3", "Nri.Ui.Container.V2", @@ -90,4 +91,4 @@ "elm-explorations/test": "1.2.2 <= v < 2.0.0", "tesk9/accessible-html": "5.0.0 <= v < 6.0.0" } -} +} \ No newline at end of file diff --git a/forbidden-imports.toml b/forbidden-imports.toml index 3979ea69..bc5d173d 100644 --- a/forbidden-imports.toml +++ b/forbidden-imports.toml @@ -42,6 +42,9 @@ hint = 'upgrade to V3' hint = 'upgrade to V10' usages = ['styleguide-app/../src/Nri/Ui/SlideModal/V2.elm'] +[forbidden."Nri.Ui.Checkbox.V5"] +hint = 'upgrade to V6' + [forbidden."Nri.Ui.ClickableSvg.V1"] hint = 'upgrade to V2' usages = ['styleguide-app/Examples/Tooltip.elm'] diff --git a/src/Nri/Ui/Checkbox/V6.elm b/src/Nri/Ui/Checkbox/V6.elm new file mode 100644 index 00000000..e0511e40 --- /dev/null +++ b/src/Nri/Ui/Checkbox/V6.elm @@ -0,0 +1,333 @@ +module Nri.Ui.Checkbox.V6 exposing + ( Model, Theme(..), IsSelected(..) + , view, viewWithLabel + , selectedFromBool + , viewIcon, checkboxLockOnInside + ) + +{-| + + +# Patch changes + + - Use Nri.Ui.Svg.V1 rather than a custom Icon type specific to this module + - Make the filter ids within the svg unique (now the id depends on the checkbox identifier) + - Explicitly box-sizing content-box on the label () + + +# Changes from V5: + + - Adds `containerCss` + - Adds `enabledLabelCss` + - Adds `disabledLabelCss` + +@docs Model, Theme, IsSelected + +@docs view, viewWithLabel + +@docs selectedFromBool + +@docs viewIcon, checkboxLockOnInside + +-} + +import Accessibility.Styled as Html +import Accessibility.Styled.Aria as Aria +import Accessibility.Styled.Style +import CheckboxIcons +import Css exposing (..) +import Css.Global +import Html.Styled +import Html.Styled.Attributes as Attributes exposing (css) +import Html.Styled.Events as Events +import Json.Decode +import Nri.Ui.Colors.V1 as Colors +import Nri.Ui.Fonts.V1 as Fonts +import Nri.Ui.Svg.V1 exposing (Svg) + + +{-| -} +type alias Model msg = + { identifier : String + , label : String + , setterMsg : Bool -> msg + , selected : IsSelected + , disabled : Bool + , theme : Theme + , containerCss : List Css.Style + , enabledLabelCss : List Css.Style + , disabledLabelCss : List Css.Style + } + + +{-| + + = Selected -- Checked (rendered with a checkmark) + | NotSelected -- Not Checked (rendered blank) + | PartiallySelected -- Indeterminate (rendered dash) + +-} +type IsSelected + = Selected + | NotSelected + | PartiallySelected + + +{-| -} +type Theme + = Square + | Locked + + +{-| If your selectedness is always selected or not selected, +you will likely store that state as a `Bool` in your model. +`selectedFromBool` lets you easily convert that into an `IsSelected` value +for use with `Nri.Ui.Checkbox`. +-} +selectedFromBool : Bool -> IsSelected +selectedFromBool isSelected = + case isSelected of + True -> + Selected + + False -> + NotSelected + + +selectedToMaybe : IsSelected -> Maybe Bool +selectedToMaybe selected = + case selected of + Selected -> + Just True + + NotSelected -> + Just False + + PartiallySelected -> + Nothing + + +{-| Shows a checkbox (the label is only used for accessibility hints) +-} +view : Model msg -> Html.Html msg +view model = + buildCheckbox model + (\label -> + Html.span Accessibility.Styled.Style.invisible + [ Html.text label ] + ) + + +{-| Shows a checkbox and its label text +-} +viewWithLabel : Model msg -> Html.Html msg +viewWithLabel model = + buildCheckbox model <| + \label -> Html.span [] [ Html.text label ] + + +buildCheckbox : Model msg -> (String -> Html.Html msg) -> Html.Html msg +buildCheckbox model labelView = + checkboxContainer model + [ viewCheckbox model + , case model.theme of + Square -> + let + icon = + case model.selected of + Selected -> + CheckboxIcons.checked model.identifier + + NotSelected -> + CheckboxIcons.unchecked model.identifier + + PartiallySelected -> + CheckboxIcons.checkedPartially model.identifier + in + if model.disabled then + viewDisabledLabel model labelView icon + + else + viewEnabledLabel model labelView icon + + Locked -> + if model.disabled then + viewDisabledLabel model labelView (checkboxLockOnInside model.identifier) + + else + viewEnabledLabel model labelView (checkboxLockOnInside model.identifier) + ] + + +checkboxContainer : { a | identifier : String, containerCss : List Style } -> List (Html.Html msg) -> Html.Html msg +checkboxContainer model = + Html.Styled.span + [ css + [ display block + , height inherit + , position relative + , marginLeft (px -4) + , pseudoClass "focus-within" + [ Css.Global.descendants + [ Css.Global.class "checkbox-icon-container" + [ borderColor (rgb 0 95 204) + ] + ] + ] + , Css.Global.descendants + [ Css.Global.input [ position absolute, top (calc (pct 50) minus (px 10)), left (px 10) ] + ] + , Css.batch model.containerCss + ] + , Attributes.id (model.identifier ++ "-container") + , Events.stopPropagationOn "click" (Json.Decode.fail "stop click propagation") + ] + + +viewCheckbox : + { a + | identifier : String + , setterMsg : Bool -> msg + , selected : IsSelected + , disabled : Bool + } + -> Html.Html msg +viewCheckbox model = + Html.checkbox model.identifier + (selectedToMaybe model.selected) + [ Attributes.id model.identifier + , if model.disabled then + Aria.disabled True + + else + Events.onCheck (\_ -> onCheck model) + ] + + +viewEnabledLabel : + { a + | identifier : String + , setterMsg : Bool -> msg + , selected : IsSelected + , label : String + , enabledLabelCss : List Style + } + -> (String -> Html.Html msg) + -> Svg + -> Html.Html msg +viewEnabledLabel model labelView icon = + Html.Styled.label + [ Attributes.for model.identifier + , labelClass model.selected + , css + [ positioning + , textStyle + , cursor pointer + , Css.batch model.enabledLabelCss + ] + ] + [ viewIcon [] icon + , labelView model.label + ] + + +onCheck : { a | setterMsg : Bool -> msg, selected : IsSelected } -> msg +onCheck model = + selectedToMaybe model.selected + |> Maybe.withDefault False + |> not + |> model.setterMsg + + +viewDisabledLabel : + { a | identifier : String, selected : IsSelected, label : String, disabledLabelCss : List Style } + -> (String -> Html.Html msg) + -> Svg + -> Html.Html msg +viewDisabledLabel model labelView icon = + Html.Styled.label + [ Attributes.for model.identifier + , labelClass model.selected + , css + [ positioning + , textStyle + , outline none + , cursor auto + , Css.batch model.disabledLabelCss + ] + ] + [ viewIcon [ opacity (num 0.4) ] icon + , labelView model.label + ] + + +labelClass : IsSelected -> Html.Styled.Attribute msg +labelClass isSelected = + case isSelected of + Selected -> + toClassList [ "Label", "Checked" ] + + NotSelected -> + toClassList [ "Label", "Unchecked" ] + + PartiallySelected -> + toClassList [ "Label", "Indeterminate" ] + + +toClassList : List String -> Html.Styled.Attribute msg +toClassList = + List.map (\a -> ( "checkbox-V5__" ++ a, True )) >> Attributes.classList + + +positioning : Style +positioning = + batch + [ display inlineBlock + , padding4 (px 13) zero (px 13) (px 40) + , position relative + ] + + +textStyle : Style +textStyle = + batch + [ Fonts.baseFont + , fontSize (px 15) + , fontWeight (int 600) + , color Colors.navy + ] + + +{-| -} +viewIcon : List Style -> Svg -> Html.Html msg +viewIcon styles icon = + Html.div + [ css + [ position absolute + , left zero + , top (calc (pct 50) minus (px 18)) + , border3 (px 2) solid transparent + , padding (px 2) + , borderRadius (px 3) + , height (Css.px 27) + , boxSizing contentBox + ] + , Attributes.class "checkbox-icon-container" + ] + [ Html.div + [ css + [ display inlineBlock + , backgroundColor Colors.white + , height (Css.px 27) + , borderRadius (px 4) + ] + ] + [ Nri.Ui.Svg.V1.toHtml (Nri.Ui.Svg.V1.withCss styles icon) + ] + ] + + +{-| -} +checkboxLockOnInside : String -> Svg +checkboxLockOnInside = + CheckboxIcons.lockOnInside diff --git a/src/Nri/Ui/PremiumCheckbox/V8.elm b/src/Nri/Ui/PremiumCheckbox/V8.elm index 5bfa40a1..20938c6c 100644 --- a/src/Nri/Ui/PremiumCheckbox/V8.elm +++ b/src/Nri/Ui/PremiumCheckbox/V8.elm @@ -5,6 +5,9 @@ module Nri.Ui.PremiumCheckbox.V8 exposing , Attribute , disabled, enabled , id + , setCheckboxContainerCss + , setCheckboxEnabledLabelCss + , setCheckboxDisabledLabelCss ) {-| Changes from V7: @@ -12,6 +15,7 @@ module Nri.Ui.PremiumCheckbox.V8 exposing - Use PremiumDisplay instead of PremiumLevel - Rename showPennant to onLockedClick - Fix clicking on locked checkbox to send a onLockedClick + - Exposes checkbox custom styling @docs view @@ -29,13 +33,20 @@ module Nri.Ui.PremiumCheckbox.V8 exposing @docs disabled, enabled @docs id + +### Custom CSS + +@docs setCheckboxContainerCss +@docs setCheckboxEnabledLabelCss +@docs setCheckboxDisabledLabelCss + -} import Accessibility.Styled as Html exposing (Html) import Css exposing (..) import Html.Styled.Attributes as Attributes exposing (class, css) import Html.Styled.Events as Events -import Nri.Ui.Checkbox.V5 as Checkbox +import Nri.Ui.Checkbox.V6 as Checkbox import Nri.Ui.Colors.V1 as Colors import Nri.Ui.Data.PremiumDisplay as PremiumDisplay exposing (PremiumDisplay) import Nri.Ui.Fonts.V1 as Fonts @@ -93,6 +104,27 @@ setSelectionStatus status = Attribute (\config -> { config | selected = status }) +{-| Set custom CSS for the checkbox container +-} +setCheckboxContainerCss : List Css.Style -> Attribute msg +setCheckboxContainerCss checkboxContainerCss = + Attribute <| \config -> { config | checkboxContainerCss = checkboxContainerCss } + + +{-| Set custom CSS for the enabled checkbox label +-} +setCheckboxEnabledLabelCss : List Css.Style -> Attribute msg +setCheckboxEnabledLabelCss checkboxEnabledLabelCss = + Attribute <| \config -> { config | checkboxEnabledLabelCss = checkboxEnabledLabelCss } + + +{-| Set custom CSS for the disabled checkbox label +-} +setCheckboxDisabledLabelCss : List Css.Style -> Attribute msg +setCheckboxDisabledLabelCss checkboxDisabledLabelCss = + Attribute <| \config -> { config | checkboxDisabledLabelCss = checkboxDisabledLabelCss } + + {-| -} selected : Bool -> Attribute msg selected isSelected = @@ -125,6 +157,9 @@ type alias Config msg = , containerCss : List Css.Style , selected : Checkbox.IsSelected , onLockedMsg : Maybe msg + , checkboxContainerCss : List Css.Style + , checkboxEnabledLabelCss : List Css.Style + , checkboxDisabledLabelCss : List Css.Style } @@ -139,6 +174,9 @@ emptyConfig = ] , selected = Checkbox.NotSelected , onLockedMsg = Nothing + , checkboxContainerCss = [] + , checkboxEnabledLabelCss = [] + , checkboxDisabledLabelCss = [] } @@ -203,6 +241,9 @@ view { label, onChange } attributes = else Checkbox.Square + , containerCss = config.checkboxContainerCss + , enabledLabelCss = config.checkboxEnabledLabelCss + , disabledLabelCss = config.checkboxDisabledLabelCss } ] diff --git a/styleguide-app/Examples/Checkbox.elm b/styleguide-app/Examples/Checkbox.elm index 7a62547d..d4a5017d 100644 --- a/styleguide-app/Examples/Checkbox.elm +++ b/styleguide-app/Examples/Checkbox.elm @@ -13,7 +13,7 @@ import Example exposing (Example) import Html.Styled as Html exposing (..) import Html.Styled.Attributes exposing (css) import KeyboardSupport exposing (Key(..)) -import Nri.Ui.Checkbox.V5 as Checkbox +import Nri.Ui.Checkbox.V6 as Checkbox import Nri.Ui.Colors.V1 as Colors import Nri.Ui.Data.PremiumDisplay as PremiumDisplay import Nri.Ui.Fonts.V1 as Fonts @@ -39,7 +39,7 @@ type alias State = example : Example State Msg example = { name = "Checkbox" - , version = 5 + , version = 6 , state = init , update = update , subscriptions = \_ -> Sub.none @@ -53,6 +53,8 @@ example = , viewMultilineCheckboxes state , Heading.h2 [ Heading.style Heading.Subhead ] [ text "Premium Checkboxes" ] , viewPremiumCheckboxes state + , viewCustomStyledCheckbox state + , viewCustomStyledPremiumCheckboxes state ] , categories = [ Inputs ] , keyboardSupport = @@ -128,6 +130,9 @@ viewInteractableCheckbox id state = , selected = isSelected id state , disabled = False , theme = Checkbox.Square + , containerCss = [] + , enabledLabelCss = [] + , disabledLabelCss = [] } @@ -140,6 +145,9 @@ viewIndeterminateCheckbox id state = , selected = Checkbox.PartiallySelected , disabled = True , theme = Checkbox.Square + , containerCss = [] + , enabledLabelCss = [] + , disabledLabelCss = [] } @@ -152,6 +160,9 @@ viewLockedOnInsideCheckbox id state = , selected = Checkbox.NotSelected , disabled = True , theme = Checkbox.Locked + , containerCss = [] + , enabledLabelCss = [] + , disabledLabelCss = [] } @@ -164,6 +175,9 @@ viewDisabledCheckbox id state = , selected = isSelected id state , disabled = True , theme = Checkbox.Square + , containerCss = [] + , enabledLabelCss = [] + , disabledLabelCss = [] } @@ -183,6 +197,9 @@ viewMultilineCheckboxes state = , selected = isSelected id state , disabled = False , theme = Checkbox.Square + , containerCss = [] + , enabledLabelCss = [] + , disabledLabelCss = [] } , Checkbox.viewWithLabel { identifier = "fake-partially-selected" @@ -191,6 +208,9 @@ viewMultilineCheckboxes state = , selected = Checkbox.PartiallySelected , disabled = True , theme = Checkbox.Square + , containerCss = [] + , enabledLabelCss = [] + , disabledLabelCss = [] } , Checkbox.viewWithLabel { identifier = "fake-not-selected-locked" @@ -199,6 +219,9 @@ viewMultilineCheckboxes state = , selected = Checkbox.NotSelected , disabled = True , theme = Checkbox.Locked + , containerCss = [] + , enabledLabelCss = [] + , disabledLabelCss = [] } , Checkbox.viewWithLabel { identifier = "fake-not-selected-square" @@ -207,6 +230,9 @@ viewMultilineCheckboxes state = , selected = Checkbox.NotSelected , disabled = True , theme = Checkbox.Square + , containerCss = [] + , enabledLabelCss = [] + , disabledLabelCss = [] } ] @@ -241,6 +267,47 @@ viewPremiumCheckboxes state = ] +viewCustomStyledCheckbox : State -> Html Msg +viewCustomStyledCheckbox state = + Html.section + [ css [ Css.width (Css.px 500) ] ] + [ Heading.h2 [ Heading.style Heading.Subhead ] [ Html.text "Custom-styled Checkboxes" ] + , let + id = + "styleguide-checkbox-custom-style" + in + Checkbox.viewWithLabel + { identifier = id + , label = "This is a custom-styled Checkbox" + , setterMsg = ToggleCheck id + , selected = isSelected id state + , disabled = False + , theme = Checkbox.Square + , containerCss = [ Css.backgroundColor Colors.navy ] + , enabledLabelCss = [ Css.color Colors.white ] + , disabledLabelCss = [] + } + ] + + +viewCustomStyledPremiumCheckboxes : State -> Html Msg +viewCustomStyledPremiumCheckboxes state = + Html.section + [ css [ Css.width (Css.px 500) ] ] + [ Heading.h2 [ Heading.style Heading.Subhead ] [ Html.text "Custom-styled Premium Checkboxes" ] + , PremiumCheckbox.view + { label = "This is a custom-styled Premium Checkbox" + , onChange = ToggleCheck "premium-custom" + } + [ PremiumCheckbox.premium PremiumDisplay.PremiumUnlocked + , PremiumCheckbox.onLockedClick NoOp + , PremiumCheckbox.selected (Set.member "premium-custom" state.isChecked) + , PremiumCheckbox.setCheckboxContainerCss [ Css.backgroundColor Colors.navy ] + , PremiumCheckbox.setCheckboxEnabledLabelCss [ Css.color Colors.white ] + ] + ] + + type alias Id = String diff --git a/styleguide-app/Examples/IconExamples.elm b/styleguide-app/Examples/IconExamples.elm index f5793892..f2f1d133 100644 --- a/styleguide-app/Examples/IconExamples.elm +++ b/styleguide-app/Examples/IconExamples.elm @@ -21,7 +21,7 @@ import Example exposing (Example) import Html.Styled as Html exposing (Html) import Html.Styled.Attributes as Attributes exposing (css) import Html.Styled.Events as Events -import Nri.Ui.Checkbox.V5 as Checkbox +import Nri.Ui.Checkbox.V6 as Checkbox import Nri.Ui.Colors.Extra exposing (fromCssColor, toCssColor) import Nri.Ui.Colors.V1 as Colors import Nri.Ui.Heading.V2 as Heading @@ -167,6 +167,9 @@ viewSettings { showIconName } = , selected = Checkbox.selectedFromBool showIconName , disabled = False , theme = Checkbox.Square + , containerCss = [] + , enabledLabelCss = [] + , disabledLabelCss = [] } @@ -306,6 +309,9 @@ viewSingularExampleSettings groups state = , selected = Checkbox.selectedFromBool state.showBorder , disabled = False , theme = Checkbox.Square + , containerCss = [] + , enabledLabelCss = [] + , disabledLabelCss = [] } , Html.label [] [ Html.text "Color: " diff --git a/styleguide-app/Examples/TextArea.elm b/styleguide-app/Examples/TextArea.elm index 4ae9841e..ea292662 100644 --- a/styleguide-app/Examples/TextArea.elm +++ b/styleguide-app/Examples/TextArea.elm @@ -12,7 +12,7 @@ import Dict exposing (Dict) import Example exposing (Example) import Html.Styled as Html import Html.Styled.Attributes as Attributes exposing (css) -import Nri.Ui.Checkbox.V5 as Checkbox +import Nri.Ui.Checkbox.V6 as Checkbox import Nri.Ui.Colors.V1 as Colors import Nri.Ui.Heading.V2 as Heading import Nri.Ui.InputStyles.V3 as InputStyles exposing (Theme(..)) @@ -80,6 +80,9 @@ example = , selected = state.showLabel , disabled = False , theme = Checkbox.Square + , containerCss = [] + , enabledLabelCss = [] + , disabledLabelCss = [] } , Checkbox.viewWithLabel { identifier = "textarea-autoresize" @@ -88,6 +91,9 @@ example = , selected = state.autoResize , disabled = False , theme = Checkbox.Square + , containerCss = [] + , enabledLabelCss = [] + , disabledLabelCss = [] } , Checkbox.viewWithLabel { identifier = "textarea-isInError" @@ -96,6 +102,9 @@ example = , selected = state.isInError , disabled = False , theme = Checkbox.Square + , containerCss = [] + , enabledLabelCss = [] + , disabledLabelCss = [] } ] , TextArea.view diff --git a/tests/elm-verify-examples.json b/tests/elm-verify-examples.json index 554341a3..bedeebac 100644 --- a/tests/elm-verify-examples.json +++ b/tests/elm-verify-examples.json @@ -11,6 +11,7 @@ "Nri.Ui.Button.V10", "Nri.Ui.Carousel.V1", "Nri.Ui.Checkbox.V5", + "Nri.Ui.Checkbox.V6", "Nri.Ui.ClickableSvg.V2", "Nri.Ui.ClickableText.V3", "Nri.Ui.Container.V2",