mirror of
https://github.com/NoRedInk/noredink-ui.git
synced 2024-11-27 13:02:42 +03:00
remove unused modules
This commit is contained in:
parent
afa37f1f20
commit
b309421d3d
31
elm.json
31
elm.json
@ -7,30 +7,17 @@
|
||||
"exposed-modules": [
|
||||
"Nri.Ui",
|
||||
"Nri.Ui.Accordion.V1",
|
||||
"Nri.Ui.Alert.V2",
|
||||
"Nri.Ui.Alert.V3",
|
||||
"Nri.Ui.Alert.V4",
|
||||
"Nri.Ui.AssetPath",
|
||||
"Nri.Ui.AssignmentIcon.V1",
|
||||
"Nri.Ui.BannerAlert.V2",
|
||||
"Nri.Ui.BannerAlert.V3",
|
||||
"Nri.Ui.BannerAlert.V4",
|
||||
"Nri.Ui.BannerAlert.V5",
|
||||
"Nri.Ui.BannerAlert.V6",
|
||||
"Nri.Ui.Button.V3",
|
||||
"Nri.Ui.Button.V4",
|
||||
"Nri.Ui.Button.V5",
|
||||
"Nri.Ui.Button.V6",
|
||||
"Nri.Ui.Button.V7",
|
||||
"Nri.Ui.Button.V8",
|
||||
"Nri.Ui.Button.V9",
|
||||
"Nri.Ui.Button.V10",
|
||||
"Nri.Ui.Callout.V1",
|
||||
"Nri.Ui.Checkbox.V3",
|
||||
"Nri.Ui.Checkbox.V4",
|
||||
"Nri.Ui.Checkbox.V5",
|
||||
"Nri.Ui.ClickableText.V1",
|
||||
"Nri.Ui.ClickableText.V2",
|
||||
"Nri.Ui.ClickableText.V3",
|
||||
"Nri.Ui.ClickableSvg.V1",
|
||||
"Nri.Ui.Colors.Extra",
|
||||
@ -42,7 +29,6 @@
|
||||
"Nri.Ui.Dropdown.V2",
|
||||
"Nri.Ui.Effects.V1",
|
||||
"Nri.Ui.Fonts.V1",
|
||||
"Nri.Ui.Heading.V1",
|
||||
"Nri.Ui.Heading.V2",
|
||||
"Nri.Ui.Html.Attributes.V2",
|
||||
"Nri.Ui.Html.V3",
|
||||
@ -52,26 +38,14 @@
|
||||
"Nri.Ui.InputStyles.V2",
|
||||
"Nri.Ui.Logo.V1",
|
||||
"Nri.Ui.MasteryIcon.V1",
|
||||
"Nri.Ui.Modal.V2",
|
||||
"Nri.Ui.Modal.V3",
|
||||
"Nri.Ui.Modal.V4",
|
||||
"Nri.Ui.Modal.V5",
|
||||
"Nri.Ui.Modal.V6",
|
||||
"Nri.Ui.Modal.V7",
|
||||
"Nri.Ui.Modal.V8",
|
||||
"Nri.Ui.Outline.V2",
|
||||
"Nri.Ui.Page.V2",
|
||||
"Nri.Ui.Page.V3",
|
||||
"Nri.Ui.Palette.V1",
|
||||
"Nri.Ui.Pennant.V1",
|
||||
"Nri.Ui.Pennant.V2",
|
||||
"Nri.Ui.PremiumCheckbox.V1",
|
||||
"Nri.Ui.PremiumCheckbox.V2",
|
||||
"Nri.Ui.PremiumCheckbox.V3",
|
||||
"Nri.Ui.PremiumCheckbox.V4",
|
||||
"Nri.Ui.PremiumCheckbox.V5",
|
||||
"Nri.Ui.PremiumCheckbox.V6",
|
||||
"Nri.Ui.SegmentedControl.V6",
|
||||
"Nri.Ui.SegmentedControl.V7",
|
||||
"Nri.Ui.SegmentedControl.V8",
|
||||
"Nri.Ui.Select.V5",
|
||||
@ -85,13 +59,10 @@
|
||||
"Nri.Ui.Svg.V1",
|
||||
"Nri.Ui.Table.V4",
|
||||
"Nri.Ui.Table.V5",
|
||||
"Nri.Ui.Tabs.V3",
|
||||
"Nri.Ui.Tabs.V4",
|
||||
"Nri.Ui.Text.V2",
|
||||
"Nri.Ui.Text.V3",
|
||||
"Nri.Ui.Text.V4",
|
||||
"Nri.Ui.Text.Writing.V1",
|
||||
"Nri.Ui.TextArea.V3",
|
||||
"Nri.Ui.TextArea.V4",
|
||||
"Nri.Ui.TextInput.V3",
|
||||
"Nri.Ui.TextInput.V4",
|
||||
@ -119,4 +90,4 @@
|
||||
"avh4/elm-program-test": "3.1.0 <= v < 4.0.0",
|
||||
"elm-explorations/test": "1.2.0 <= v < 2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,121 +0,0 @@
|
||||
module Nri.Ui.Alert.V2 exposing
|
||||
( error
|
||||
, success
|
||||
, tip
|
||||
, warning
|
||||
)
|
||||
|
||||
{-| UI components that highlight information to the user.
|
||||
|
||||
@docs error
|
||||
@docs success
|
||||
@docs tip
|
||||
@docs warning
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Html exposing (Html)
|
||||
import Css
|
||||
import Css.Global
|
||||
import Html.Styled exposing (fromUnstyled)
|
||||
import Html.Styled.Attributes exposing (css)
|
||||
import Markdown
|
||||
import Nri.Ui
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Fonts.V1 as Fonts
|
||||
import Nri.Ui.Icon.V3 as Icon
|
||||
|
||||
|
||||
{-| -}
|
||||
error : { r | exclamation : String } -> String -> Html msg
|
||||
error assets content =
|
||||
alert
|
||||
[ iconContainer [ Css.color Colors.purple ]
|
||||
(Icon.decorativeIcon (Icon.exclamation assets))
|
||||
, viewAlertContent Colors.purpleDark content
|
||||
]
|
||||
|
||||
|
||||
{-| -}
|
||||
success : { r | checkmark : String } -> String -> Html msg
|
||||
success assets content =
|
||||
alert
|
||||
[ iconContainer
|
||||
[ Css.color Colors.white
|
||||
, Css.backgroundColor Colors.green
|
||||
, Css.Global.children [ Css.Global.svg [ Css.maxWidth (Css.px 12) ] ]
|
||||
]
|
||||
(Icon.decorativeIcon (Icon.checkMarkSvg assets))
|
||||
, viewAlertContent Colors.greenDarkest content
|
||||
]
|
||||
|
||||
|
||||
{-| -}
|
||||
tip : { r | bulb : String } -> String -> Html msg
|
||||
tip assets content =
|
||||
alert
|
||||
[ iconContainer [ Css.color Colors.yellow ]
|
||||
(Icon.decorativeIcon (Icon.bulb assets))
|
||||
, viewAlertContent Colors.navy content
|
||||
]
|
||||
|
||||
|
||||
{-| -}
|
||||
warning : { r | exclamation : String } -> String -> Html msg
|
||||
warning assets content =
|
||||
alert
|
||||
[ iconContainer [ Css.color Colors.red ]
|
||||
(Icon.decorativeIcon (Icon.exclamation assets))
|
||||
, viewAlertContent Colors.red content
|
||||
]
|
||||
|
||||
|
||||
alert : List (Html msg) -> Html msg
|
||||
alert =
|
||||
Nri.Ui.styled Html.div
|
||||
"Nri-Ui-Alert-V2__alert"
|
||||
[ Css.displayFlex
|
||||
, Css.justifyContent Css.start
|
||||
, Css.alignItems Css.center
|
||||
, Css.paddingTop (Css.px 6)
|
||||
, Css.paddingBottom (Css.px 8)
|
||||
]
|
||||
[]
|
||||
|
||||
|
||||
iconContainer : List Css.Style -> Html msg -> Html msg
|
||||
iconContainer styles icon =
|
||||
Nri.Ui.styled Html.div
|
||||
"Nri-Ui-Alert-V2__iconContainer"
|
||||
(styles
|
||||
++ [ -- Content positioning
|
||||
Css.displayFlex
|
||||
, Css.justifyContent Css.center
|
||||
, Css.alignItems Css.center
|
||||
, Css.marginRight (Css.px 5)
|
||||
|
||||
-- Size
|
||||
, Css.borderRadius (Css.px 13)
|
||||
, Css.maxHeight (Css.px 20)
|
||||
, Css.maxWidth (Css.px 20)
|
||||
, Css.minHeight (Css.px 20)
|
||||
, Css.minWidth (Css.px 20)
|
||||
]
|
||||
)
|
||||
[]
|
||||
[ icon ]
|
||||
|
||||
|
||||
viewAlertContent : Css.ColorValue compatible -> String -> Html.Styled.Html msg
|
||||
viewAlertContent color content =
|
||||
Nri.Ui.styled Html.div
|
||||
"Nri-Ui-Alert-V2__viewAlertContent"
|
||||
[ Css.color color
|
||||
, Fonts.baseFont
|
||||
, Css.fontSize (Css.px 13)
|
||||
, Css.lineHeight (Css.num 1.2)
|
||||
, Css.listStyleType Css.none
|
||||
, Css.Global.descendants [ Css.Global.p [ Css.margin Css.zero ] ]
|
||||
]
|
||||
[]
|
||||
(Markdown.toHtml Nothing content |> List.map fromUnstyled)
|
@ -1,156 +0,0 @@
|
||||
module Nri.Ui.Alert.V3 exposing
|
||||
( error
|
||||
, success
|
||||
, tip
|
||||
, warning
|
||||
)
|
||||
|
||||
{-| UI components that highlight information to the user.
|
||||
|
||||
@docs error
|
||||
@docs success
|
||||
@docs tip
|
||||
@docs warning
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Html exposing (Html)
|
||||
import Css
|
||||
import Css.Global
|
||||
import Html.Styled exposing (fromUnstyled)
|
||||
import Html.Styled.Attributes exposing (css)
|
||||
import Markdown
|
||||
import Nri.Ui
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Fonts.V1 as Fonts
|
||||
import Nri.Ui.Icon.V3 as Icon
|
||||
import Nri.Ui.SpriteSheet exposing (bulb, checkmark, exclamationMark)
|
||||
import Nri.Ui.Svg.V1 as NriSvg exposing (Svg)
|
||||
|
||||
|
||||
{-| -}
|
||||
error : String -> Html msg
|
||||
error content =
|
||||
alert
|
||||
[ exclamation Colors.purple
|
||||
, viewAlertContent Colors.purpleDark content
|
||||
]
|
||||
|
||||
|
||||
{-| -}
|
||||
success : String -> Html msg
|
||||
success content =
|
||||
alert
|
||||
[ iconContainer
|
||||
[ Css.color Colors.white
|
||||
, Css.backgroundColor Colors.green
|
||||
]
|
||||
(Html.div
|
||||
[ css
|
||||
[ Css.width (Css.px 12)
|
||||
, Css.height (Css.px 12)
|
||||
, Css.margin Css.auto
|
||||
]
|
||||
]
|
||||
[ NriSvg.toHtml checkmark ]
|
||||
)
|
||||
, viewAlertContent Colors.greenDarkest content
|
||||
]
|
||||
|
||||
|
||||
{-| -}
|
||||
tip : String -> Html msg
|
||||
tip content =
|
||||
alert
|
||||
[ iconContainer [ Css.color Colors.yellow ] (NriSvg.toHtml bulb)
|
||||
, viewAlertContent Colors.navy content
|
||||
]
|
||||
|
||||
|
||||
{-| -}
|
||||
warning : String -> Html msg
|
||||
warning content =
|
||||
alert
|
||||
[ exclamation Colors.red
|
||||
, viewAlertContent Colors.red content
|
||||
]
|
||||
|
||||
|
||||
alert : List (Html msg) -> Html msg
|
||||
alert =
|
||||
Nri.Ui.styled Html.div
|
||||
"Nri-Ui-Alert-V3__alert"
|
||||
[ Css.displayFlex
|
||||
, Css.justifyContent Css.start
|
||||
, Css.alignItems Css.center
|
||||
, Css.paddingTop (Css.px 6)
|
||||
, Css.paddingBottom (Css.px 8)
|
||||
]
|
||||
[]
|
||||
|
||||
|
||||
exclamation : Css.Color -> Html msg
|
||||
exclamation backgroundColor =
|
||||
iconContainer
|
||||
[ Css.color Colors.white
|
||||
, Css.backgroundColor backgroundColor
|
||||
]
|
||||
(Html.div
|
||||
[ css [ Css.marginTop (Css.px 1), Css.height (Css.px 13) ] ]
|
||||
[ NriSvg.toHtml exclamationMark ]
|
||||
)
|
||||
|
||||
|
||||
iconContainer : List Css.Style -> Html msg -> Html msg
|
||||
iconContainer styles icon =
|
||||
Nri.Ui.styled Html.div
|
||||
"Nri-Ui-Alert-V3__iconContainer"
|
||||
(styles
|
||||
++ [ -- Content positioning
|
||||
Css.marginRight (Css.px 5)
|
||||
|
||||
-- Size
|
||||
, Css.borderRadius (Css.px 13)
|
||||
, Css.lineHeight iconContainerSize
|
||||
, Css.maxHeight iconContainerSize
|
||||
, Css.maxWidth iconContainerSize
|
||||
, Css.minHeight iconContainerSize
|
||||
, Css.minWidth iconContainerSize
|
||||
]
|
||||
)
|
||||
[]
|
||||
[ icon ]
|
||||
|
||||
|
||||
viewAlertContent : Css.ColorValue compatible -> String -> Html.Styled.Html msg
|
||||
viewAlertContent color content =
|
||||
Nri.Ui.styled Html.div
|
||||
"Nri-Ui-Alert-V3__viewAlertContent"
|
||||
[ Css.color color
|
||||
, Fonts.baseFont
|
||||
, Css.fontSize (Css.px 13)
|
||||
, Css.lineHeight iconContainerSize
|
||||
, Css.listStyleType Css.none
|
||||
|
||||
-- This global selector and overrides are necessary due to
|
||||
-- old stylesheets used on the monolith that set the
|
||||
-- `.txt p { font-size: 18px; }` -- without these overrides,
|
||||
-- we may see giant ugly alerts.
|
||||
-- Remove these if you want to! but be emotionally prepped
|
||||
-- to deal with visual regressions. 🙏
|
||||
, Css.Global.descendants
|
||||
[ Css.Global.p
|
||||
[ Css.margin Css.zero
|
||||
, Css.lineHeight iconContainerSize
|
||||
, Css.fontSize (Css.px 13)
|
||||
, Fonts.baseFont
|
||||
]
|
||||
]
|
||||
]
|
||||
[]
|
||||
(Markdown.toHtml Nothing content |> List.map fromUnstyled)
|
||||
|
||||
|
||||
iconContainerSize : Css.Px
|
||||
iconContainerSize =
|
||||
Css.px 20
|
@ -1,114 +0,0 @@
|
||||
module Nri.Ui.BannerAlert.V2 exposing
|
||||
( error
|
||||
, neutral
|
||||
, success
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
@docs error
|
||||
@docs neutral
|
||||
@docs success
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Accessibility
|
||||
import Css exposing (..)
|
||||
import Css.Global exposing (Snippet, children, descendants, everything, selector)
|
||||
import Html.Styled as Html exposing (Html)
|
||||
import Nri.Ui.Colors.V1
|
||||
import Nri.Ui.Fonts.V1
|
||||
|
||||
|
||||
{-| A banner to show error alerts
|
||||
-}
|
||||
error : String -> Html msg
|
||||
error =
|
||||
banner errorStyles
|
||||
|
||||
|
||||
{-| A banner to show neutral alerts
|
||||
-}
|
||||
neutral : String -> Html msg
|
||||
neutral =
|
||||
banner neutralStyles
|
||||
|
||||
|
||||
{-| A banner for success alerts
|
||||
-}
|
||||
success : String -> Html msg
|
||||
success =
|
||||
banner successStyles
|
||||
|
||||
|
||||
banner : Css.Style -> String -> Html msg
|
||||
banner bannerType alertMessage =
|
||||
Html.styled Accessibility.div
|
||||
[ bannerStyles, bannerType ]
|
||||
[]
|
||||
[ notification alertMessage ]
|
||||
|
||||
|
||||
notification : String -> Html msg
|
||||
notification message =
|
||||
Html.styled Html.div [ alertMessageStyles ] [] [ Accessibility.text message ]
|
||||
|
||||
|
||||
type CssClasses
|
||||
= AlertMessage
|
||||
| Banner
|
||||
| Error
|
||||
| Neutral
|
||||
| Success
|
||||
|
||||
|
||||
alertMessageStyles : Style
|
||||
alertMessageStyles =
|
||||
batch
|
||||
[ Css.fontSize (Css.px 20)
|
||||
, Css.fontWeight (Css.int 700)
|
||||
, Css.lineHeight (Css.px 25)
|
||||
, Css.maxWidth (Css.px 600)
|
||||
, Nri.Ui.Fonts.V1.baseFont
|
||||
]
|
||||
|
||||
|
||||
bannerStyles : Style
|
||||
bannerStyles =
|
||||
batch
|
||||
[ Css.alignItems Css.center
|
||||
, Css.displayFlex
|
||||
, Css.justifyContent Css.center
|
||||
, Css.padding (Css.px 20)
|
||||
, Css.width (Css.pct 100)
|
||||
, Css.Global.children
|
||||
[ Css.Global.button
|
||||
[ Css.position Css.absolute
|
||||
, Css.right (Css.px 15)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
errorStyles : Style
|
||||
errorStyles =
|
||||
batch
|
||||
[ Css.backgroundColor Nri.Ui.Colors.V1.purpleLight
|
||||
, Css.color Nri.Ui.Colors.V1.purpleDark
|
||||
]
|
||||
|
||||
|
||||
neutralStyles : Style
|
||||
neutralStyles =
|
||||
batch
|
||||
[ Css.backgroundColor Nri.Ui.Colors.V1.frost
|
||||
, Css.color Nri.Ui.Colors.V1.navy
|
||||
]
|
||||
|
||||
|
||||
successStyles : Style
|
||||
successStyles =
|
||||
batch
|
||||
[ Css.backgroundColor Nri.Ui.Colors.V1.greenLightest
|
||||
, Css.color Nri.Ui.Colors.V1.greenDarkest
|
||||
]
|
@ -1,138 +0,0 @@
|
||||
module Nri.Ui.BannerAlert.V3 exposing (error, neutral, success)
|
||||
|
||||
{-|
|
||||
|
||||
@docs error, neutral, success
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Html exposing (Html)
|
||||
import Css
|
||||
import Css.Global
|
||||
import Html.Styled.Attributes as Attributes exposing (css)
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Fonts.V1
|
||||
import Nri.Ui.SpriteSheet exposing (bulb, checkmark, exclamationMark)
|
||||
import Nri.Ui.Svg.V1 as NriSvg exposing (Svg)
|
||||
|
||||
|
||||
{-| A banner to show error alerts
|
||||
-}
|
||||
error : String -> Html msg
|
||||
error =
|
||||
banner
|
||||
{ backgroundColor = Colors.purpleLight
|
||||
, color = Colors.purpleDark
|
||||
, icon =
|
||||
{ backgroundColor = Colors.purple
|
||||
, height = Css.px 25
|
||||
, asset = exclamationMark
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
{-| A banner to show neutral alerts
|
||||
-}
|
||||
neutral : String -> Html msg
|
||||
neutral =
|
||||
banner
|
||||
{ backgroundColor = Colors.frost
|
||||
, color = Colors.navy
|
||||
, icon =
|
||||
{ backgroundColor = Colors.navy
|
||||
, height = Css.px 32
|
||||
, asset = bulb
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
{-| A banner for success alerts
|
||||
-}
|
||||
success : String -> Html msg
|
||||
success =
|
||||
banner
|
||||
{ backgroundColor = Colors.greenLightest
|
||||
, color = Colors.greenDarkest
|
||||
, icon =
|
||||
{ backgroundColor = Colors.green
|
||||
, height = Css.px 20
|
||||
, asset = checkmark
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type alias Config =
|
||||
{ color : Css.Color
|
||||
, backgroundColor : Css.Color
|
||||
, icon : IconConfig
|
||||
}
|
||||
|
||||
|
||||
banner : Config -> String -> Html msg
|
||||
banner config alertMessage =
|
||||
Html.div
|
||||
[ css
|
||||
[ Css.alignItems Css.center
|
||||
, Css.displayFlex
|
||||
, Css.justifyContent Css.center
|
||||
, Css.padding (Css.px 20)
|
||||
, Css.width (Css.pct 100)
|
||||
, Css.Global.children
|
||||
[ Css.Global.button
|
||||
[ Css.position Css.absolute
|
||||
, Css.right (Css.px 15)
|
||||
]
|
||||
]
|
||||
, Css.backgroundColor config.backgroundColor
|
||||
, Css.color config.color
|
||||
]
|
||||
]
|
||||
[ icon config.icon
|
||||
, notification alertMessage
|
||||
]
|
||||
|
||||
|
||||
type alias IconConfig =
|
||||
{ backgroundColor : Css.Color
|
||||
, height : Css.Px
|
||||
, asset : Svg
|
||||
}
|
||||
|
||||
|
||||
icon : IconConfig -> Html msg
|
||||
icon config =
|
||||
Html.div
|
||||
[ css
|
||||
[ Css.boxSizing Css.borderBox
|
||||
, Css.borderRadius (Css.pct 50)
|
||||
, Css.color Colors.white
|
||||
, Css.displayFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.justifyContent Css.center
|
||||
, Css.width (Css.px 50)
|
||||
, Css.height (Css.px 50)
|
||||
, Css.marginRight (Css.px 20)
|
||||
, Css.padding (Css.px 8)
|
||||
, Css.flexShrink (Css.num 0)
|
||||
, Css.backgroundColor config.backgroundColor
|
||||
]
|
||||
]
|
||||
[ Html.div
|
||||
[ css [ Css.height config.height ]
|
||||
]
|
||||
[ NriSvg.toHtml config.asset ]
|
||||
]
|
||||
|
||||
|
||||
notification : String -> Html msg
|
||||
notification message =
|
||||
Html.div
|
||||
[ css
|
||||
[ Css.fontSize (Css.px 20)
|
||||
, Css.fontWeight (Css.int 700)
|
||||
, Css.lineHeight (Css.px 25)
|
||||
, Css.maxWidth (Css.px 600)
|
||||
, Nri.Ui.Fonts.V1.baseFont
|
||||
]
|
||||
]
|
||||
[ Html.text message ]
|
@ -1,199 +0,0 @@
|
||||
module Nri.Ui.BannerAlert.V4 exposing (alert, error, neutral, success)
|
||||
|
||||
{-|
|
||||
|
||||
@docs alert, error, neutral, success
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Html exposing (Html)
|
||||
import Accessibility.Styled.Widget as Widget
|
||||
import Css
|
||||
import Css.Global
|
||||
import Html.Styled.Attributes as Attributes exposing (css)
|
||||
import Html.Styled.Events
|
||||
import Nri.Ui
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Fonts.V1
|
||||
import Nri.Ui.SpriteSheet exposing (bulb, checkmark, exclamationMark, xSvg)
|
||||
import Nri.Ui.Svg.V1 as NriSvg exposing (Svg)
|
||||
|
||||
|
||||
{-| A banner to show error alerts
|
||||
-}
|
||||
alert : String -> Maybe msg -> Html msg
|
||||
alert =
|
||||
banner
|
||||
{ backgroundColor = Colors.sunshine
|
||||
, color = Colors.navy
|
||||
, icon =
|
||||
{ backgroundColor = Colors.ochre
|
||||
, height = Css.px 25
|
||||
, asset = exclamationMark
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
{-| A banner to show error alerts
|
||||
-}
|
||||
error : String -> Maybe msg -> Html msg
|
||||
error =
|
||||
banner
|
||||
{ backgroundColor = Colors.purpleLight
|
||||
, color = Colors.purpleDark
|
||||
, icon =
|
||||
{ backgroundColor = Colors.purple
|
||||
, height = Css.px 25
|
||||
, asset = exclamationMark
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
{-| A banner to show neutral alerts
|
||||
-}
|
||||
neutral : String -> Maybe msg -> Html msg
|
||||
neutral =
|
||||
banner
|
||||
{ backgroundColor = Colors.frost
|
||||
, color = Colors.navy
|
||||
, icon =
|
||||
{ backgroundColor = Colors.navy
|
||||
, height = Css.px 32
|
||||
, asset = bulb
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
{-| A banner for success alerts
|
||||
-}
|
||||
success : String -> Maybe msg -> Html msg
|
||||
success =
|
||||
banner
|
||||
{ backgroundColor = Colors.greenLightest
|
||||
, color = Colors.greenDarkest
|
||||
, icon =
|
||||
{ backgroundColor = Colors.green
|
||||
, height = Css.px 20
|
||||
, asset = checkmark
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type alias Config =
|
||||
{ color : Css.Color
|
||||
, backgroundColor : Css.Color
|
||||
, icon : IconConfig
|
||||
}
|
||||
|
||||
|
||||
banner : Config -> String -> Maybe msg -> Html msg
|
||||
banner config alertMessage dismissMsg =
|
||||
let
|
||||
maybeDismissButton =
|
||||
case dismissMsg of
|
||||
Nothing ->
|
||||
Html.text ""
|
||||
|
||||
Just msg ->
|
||||
dismissButton msg
|
||||
in
|
||||
Html.div
|
||||
[ css
|
||||
[ Css.displayFlex
|
||||
, Css.justifyContent Css.center
|
||||
, Css.alignItems Css.center
|
||||
, Css.backgroundColor config.backgroundColor
|
||||
, Css.color config.color
|
||||
]
|
||||
]
|
||||
[ Html.span
|
||||
[ css
|
||||
[ Css.alignItems Css.center
|
||||
, Css.displayFlex
|
||||
, Css.justifyContent Css.center
|
||||
, Css.padding (Css.px 20)
|
||||
, Css.width (Css.pct 100)
|
||||
, Css.Global.children
|
||||
[ Css.Global.button
|
||||
[ Css.position Css.relative
|
||||
, Css.right (Css.px 15)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
[ icon config.icon
|
||||
, notification alertMessage
|
||||
]
|
||||
, maybeDismissButton
|
||||
]
|
||||
|
||||
|
||||
dismissButton : msg -> Html msg
|
||||
dismissButton msg =
|
||||
Nri.Ui.styled Html.div
|
||||
"dismiss-button-container"
|
||||
[ Css.padding (Css.px 25)
|
||||
]
|
||||
[]
|
||||
[ Html.button
|
||||
[ Html.Styled.Events.onClick msg
|
||||
, Widget.label "Dismiss banner"
|
||||
, css
|
||||
[ Css.borderWidth Css.zero
|
||||
, Css.backgroundColor Css.unset
|
||||
, Css.color Colors.azure
|
||||
, Css.width (Css.px 30)
|
||||
, Css.height (Css.px 30)
|
||||
, Css.padding2 Css.zero (Css.px 7)
|
||||
, Css.cursor Css.pointer
|
||||
]
|
||||
]
|
||||
[ NriSvg.toHtml xSvg
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
type alias IconConfig =
|
||||
{ backgroundColor : Css.Color
|
||||
, height : Css.Px
|
||||
, asset : Svg
|
||||
}
|
||||
|
||||
|
||||
icon : IconConfig -> Html msg
|
||||
icon config =
|
||||
Html.div
|
||||
[ css
|
||||
[ Css.boxSizing Css.borderBox
|
||||
, Css.borderRadius (Css.pct 50)
|
||||
, Css.color Colors.white
|
||||
, Css.displayFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.justifyContent Css.center
|
||||
, Css.width (Css.px 50)
|
||||
, Css.height (Css.px 50)
|
||||
, Css.marginRight (Css.px 20)
|
||||
, Css.padding (Css.px 8)
|
||||
, Css.flexShrink (Css.num 0)
|
||||
, Css.backgroundColor config.backgroundColor
|
||||
]
|
||||
]
|
||||
[ Html.div
|
||||
[ css [ Css.height config.height ]
|
||||
]
|
||||
[ NriSvg.toHtml config.asset ]
|
||||
]
|
||||
|
||||
|
||||
notification : String -> Html msg
|
||||
notification message =
|
||||
Nri.Ui.styled Html.div
|
||||
"banner-alert-notification"
|
||||
[ Css.fontSize (Css.px 20)
|
||||
, Css.fontWeight (Css.int 700)
|
||||
, Css.lineHeight (Css.px 27)
|
||||
, Css.maxWidth (Css.px 600)
|
||||
, Nri.Ui.Fonts.V1.baseFont
|
||||
]
|
||||
[]
|
||||
[ Html.text message ]
|
@ -1,258 +0,0 @@
|
||||
module Nri.Ui.BannerAlert.V5 exposing (alert, error, neutral, success, LinkConfig, BannerContent(..), Target(..))
|
||||
|
||||
{-|
|
||||
|
||||
@docs alert, error, neutral, success, LinkConfig, BannerContent, Target
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Html exposing (Html)
|
||||
import Accessibility.Styled.Widget as Widget
|
||||
import Css
|
||||
import Css.Global
|
||||
import Html.Styled.Attributes as Attributes exposing (css)
|
||||
import Html.Styled.Events
|
||||
import Nri.Ui
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Fonts.V1
|
||||
import Nri.Ui.SpriteSheet exposing (bulb, checkmark, exclamationMark, xSvg)
|
||||
import Nri.Ui.Svg.V1 as NriSvg exposing (Svg)
|
||||
|
||||
|
||||
{-| A type to capture either plain content, or a string which will include a url link.
|
||||
-}
|
||||
type BannerContent
|
||||
= Plain String
|
||||
| WithLink LinkConfig
|
||||
|
||||
|
||||
{-| A banner to show error alerts
|
||||
-}
|
||||
alert : BannerContent -> Maybe msg -> Html msg
|
||||
alert =
|
||||
banner
|
||||
{ backgroundColor = Colors.sunshine
|
||||
, color = Colors.navy
|
||||
, icon =
|
||||
{ backgroundColor = Colors.ochre
|
||||
, height = Css.px 25
|
||||
, asset = exclamationMark
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
{-| A banner to show error alerts
|
||||
-}
|
||||
error : BannerContent -> Maybe msg -> Html msg
|
||||
error =
|
||||
banner
|
||||
{ backgroundColor = Colors.purpleLight
|
||||
, color = Colors.purpleDark
|
||||
, icon =
|
||||
{ backgroundColor = Colors.purple
|
||||
, height = Css.px 25
|
||||
, asset = exclamationMark
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
{-| A banner to show neutral alerts
|
||||
-}
|
||||
neutral : BannerContent -> Maybe msg -> Html msg
|
||||
neutral =
|
||||
banner
|
||||
{ backgroundColor = Colors.frost
|
||||
, color = Colors.navy
|
||||
, icon =
|
||||
{ backgroundColor = Colors.navy
|
||||
, height = Css.px 32
|
||||
, asset = bulb
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
{-| A banner for success alerts
|
||||
-}
|
||||
success : BannerContent -> Maybe msg -> Html msg
|
||||
success =
|
||||
banner
|
||||
{ backgroundColor = Colors.greenLightest
|
||||
, color = Colors.greenDarkest
|
||||
, icon =
|
||||
{ backgroundColor = Colors.green
|
||||
, height = Css.px 20
|
||||
, asset = checkmark
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type alias StyleConfig =
|
||||
{ color : Css.Color
|
||||
, backgroundColor : Css.Color
|
||||
, icon : IconConfig
|
||||
}
|
||||
|
||||
|
||||
banner : StyleConfig -> BannerContent -> Maybe msg -> Html msg
|
||||
banner config bannerContent dismissMsg =
|
||||
let
|
||||
maybeDismissButton =
|
||||
case dismissMsg of
|
||||
Nothing ->
|
||||
Html.text ""
|
||||
|
||||
Just msg ->
|
||||
dismissButton msg
|
||||
|
||||
alertMessage =
|
||||
case bannerContent of
|
||||
Plain string ->
|
||||
Html.text string
|
||||
|
||||
WithLink linkConfig ->
|
||||
Html.div
|
||||
[ css
|
||||
[ Css.fontSize (Css.px 20)
|
||||
, Css.fontWeight (Css.int 700)
|
||||
, Css.lineHeight (Css.px 25)
|
||||
, Css.maxWidth (Css.px 600)
|
||||
, Nri.Ui.Fonts.V1.baseFont
|
||||
]
|
||||
]
|
||||
[ Html.text linkConfig.prefixText
|
||||
, Html.a
|
||||
[ Attributes.href <| linkConfig.linkUrl
|
||||
, targetToAttribute linkConfig.target
|
||||
]
|
||||
[ Html.text linkConfig.linkText ]
|
||||
, Html.text linkConfig.postfixText
|
||||
]
|
||||
in
|
||||
Html.div
|
||||
[ css
|
||||
[ Css.displayFlex
|
||||
, Css.justifyContent Css.center
|
||||
, Css.alignItems Css.center
|
||||
, Css.backgroundColor config.backgroundColor
|
||||
, Css.color config.color
|
||||
]
|
||||
]
|
||||
[ Html.span
|
||||
[ css
|
||||
[ Css.alignItems Css.center
|
||||
, Css.displayFlex
|
||||
, Css.justifyContent Css.center
|
||||
, Css.padding (Css.px 20)
|
||||
, Css.width (Css.pct 100)
|
||||
, Css.Global.children
|
||||
[ Css.Global.button
|
||||
[ Css.position Css.relative
|
||||
, Css.right (Css.px 15)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
[ icon config.icon
|
||||
, notification alertMessage
|
||||
]
|
||||
, maybeDismissButton
|
||||
]
|
||||
|
||||
|
||||
dismissButton : msg -> Html msg
|
||||
dismissButton msg =
|
||||
Nri.Ui.styled Html.div
|
||||
"dismiss-button-container"
|
||||
[ Css.padding (Css.px 25)
|
||||
]
|
||||
[]
|
||||
[ Html.button
|
||||
[ Html.Styled.Events.onClick msg
|
||||
, Widget.label "Dismiss banner"
|
||||
, css
|
||||
[ Css.borderWidth Css.zero
|
||||
, Css.backgroundColor Css.unset
|
||||
, Css.color Colors.azure
|
||||
, Css.width (Css.px 30)
|
||||
, Css.height (Css.px 30)
|
||||
, Css.padding2 Css.zero (Css.px 7)
|
||||
, Css.cursor Css.pointer
|
||||
]
|
||||
]
|
||||
[ NriSvg.toHtml xSvg
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
type alias IconConfig =
|
||||
{ backgroundColor : Css.Color
|
||||
, height : Css.Px
|
||||
, asset : Svg
|
||||
}
|
||||
|
||||
|
||||
icon : IconConfig -> Html msg
|
||||
icon config =
|
||||
Html.div
|
||||
[ css
|
||||
[ Css.boxSizing Css.borderBox
|
||||
, Css.borderRadius (Css.pct 50)
|
||||
, Css.color Colors.white
|
||||
, Css.displayFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.justifyContent Css.center
|
||||
, Css.width (Css.px 50)
|
||||
, Css.height (Css.px 50)
|
||||
, Css.marginRight (Css.px 20)
|
||||
, Css.padding (Css.px 8)
|
||||
, Css.flexShrink (Css.num 0)
|
||||
, Css.backgroundColor config.backgroundColor
|
||||
]
|
||||
]
|
||||
[ Html.div
|
||||
[ css [ Css.height config.height ]
|
||||
]
|
||||
[ NriSvg.toHtml config.asset ]
|
||||
]
|
||||
|
||||
|
||||
notification : Html msg -> Html msg
|
||||
notification message =
|
||||
Nri.Ui.styled Html.div
|
||||
"banner-alert-notification"
|
||||
[ Css.fontSize (Css.px 20)
|
||||
, Css.fontWeight (Css.int 700)
|
||||
, Css.lineHeight (Css.px 27)
|
||||
, Css.maxWidth (Css.px 600)
|
||||
, Nri.Ui.Fonts.V1.baseFont
|
||||
]
|
||||
[]
|
||||
[ message ]
|
||||
|
||||
|
||||
{-| Config describing a message containing an embedded link.
|
||||
-}
|
||||
type alias LinkConfig =
|
||||
{ prefixText : String
|
||||
, linkText : String
|
||||
, linkUrl : String
|
||||
, postfixText : String
|
||||
, target : Target
|
||||
}
|
||||
|
||||
|
||||
{-| The link target. Corresponds to values of "_blank" and "_self"
|
||||
-}
|
||||
type Target
|
||||
= Blank
|
||||
| Self
|
||||
|
||||
|
||||
targetToAttribute : Target -> Html.Attribute msg
|
||||
targetToAttribute linkTarget =
|
||||
case linkTarget of
|
||||
Blank ->
|
||||
Attributes.target "_blank"
|
||||
|
||||
Self ->
|
||||
Attributes.target "_self"
|
@ -1,780 +0,0 @@
|
||||
module Nri.Ui.Button.V3 exposing
|
||||
( ButtonSize(..), ButtonStyle(..), ButtonState(..), ButtonContent
|
||||
, ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
|
||||
, LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
|
||||
# Changes from V2:
|
||||
|
||||
- Uses Html.Styled
|
||||
- Removes buttonDeprecated
|
||||
- Removes Tiny size
|
||||
- Removes one-off Active hack
|
||||
- Removes "submit" button - we just used that for forms that were partially in Elm
|
||||
|
||||
|
||||
# About:
|
||||
|
||||
Common NoRedInk buttons. For accessibility purposes, buttons that perform an
|
||||
action on the current page should be HTML `<button>` elements and are created here
|
||||
with `*Button` functions. Buttons that take the user to a new page should be
|
||||
HTML `<a>` elements and are created here with `*Link` functions. Both versions
|
||||
should be able to use the same CSS class in all cases.
|
||||
|
||||
There will generally be a `*Button` and `*Link` version of each button style.
|
||||
(These will be created as they are needed.)
|
||||
|
||||
|
||||
## Common configs
|
||||
|
||||
@docs ButtonSize, ButtonStyle, ButtonState, ButtonContent
|
||||
|
||||
|
||||
## `<button>` Buttons
|
||||
|
||||
@docs ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
|
||||
|
||||
|
||||
## `<a>` Buttons
|
||||
|
||||
@docs LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Html exposing (Attribute, Html)
|
||||
import Accessibility.Styled.Role as Role
|
||||
import Accessibility.Styled.Widget as Widget
|
||||
import Css exposing (Style)
|
||||
import Css.Global
|
||||
import EventExtras.Styled as EventExtras
|
||||
import Html.Styled as Styled
|
||||
import Html.Styled.Attributes as Attributes
|
||||
import Html.Styled.Events as Events
|
||||
import Json.Decode
|
||||
import Markdown.Block
|
||||
import Markdown.Inline
|
||||
import Nri.Ui
|
||||
import Nri.Ui.AssetPath as AssetPath exposing (Asset)
|
||||
import Nri.Ui.Colors.Extra as ColorsExtra
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Fonts.V1
|
||||
import Nri.Ui.Icon.V3 as Icon exposing (IconType)
|
||||
|
||||
|
||||
{-| Sizes for buttons and links that have button classes
|
||||
-}
|
||||
type ButtonSize
|
||||
= Small
|
||||
| Medium
|
||||
| Large
|
||||
|
||||
|
||||
{-| Styleguide-approved styles for your buttons!
|
||||
|
||||
Note on borderless buttons:
|
||||
A borderless button that performs an action on the current page
|
||||
This button is intended to look like a link.
|
||||
Only use a borderless button when the clickable text in question follows the same layout/margin/padding as a bordered button
|
||||
|
||||
-}
|
||||
type ButtonStyle
|
||||
= Primary
|
||||
| Secondary
|
||||
| Borderless
|
||||
| Danger
|
||||
| Premium
|
||||
|
||||
|
||||
{-| Describes the state of a button. Has consequences for appearance and disabled attribute.
|
||||
|
||||
- Enabled: An enabled button. Takes the appearance of ButtonStyle
|
||||
- Unfulfilled: A button which appears with the InactiveColors palette but is not disabled.
|
||||
- Disabled: A button which appears with the InactiveColors palette and is disabled.
|
||||
- Error: A button which appears with the ErrorColors palette and is disabled.
|
||||
- Loading: A button which appears with the LoadingColors palette and is disabled
|
||||
- Success: A button which appears with the SuccessColors palette and is disabled
|
||||
|
||||
-}
|
||||
type ButtonState
|
||||
= Enabled
|
||||
| Unfulfilled
|
||||
| Disabled
|
||||
| Error
|
||||
| Loading
|
||||
| Success
|
||||
|
||||
|
||||
{-| The part of a button that remains constant through different button states
|
||||
-}
|
||||
type alias ButtonConfig msg =
|
||||
{ onClick : msg
|
||||
, size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, width : Maybe Int
|
||||
}
|
||||
|
||||
|
||||
{-| ButtonContent, often changes based on ButtonState. For example, a button in the "Success"
|
||||
state may have a different label than a button in the "Error" state
|
||||
-}
|
||||
type alias ButtonContent =
|
||||
{ label : String
|
||||
, state : ButtonState
|
||||
, icon : Maybe IconType
|
||||
}
|
||||
|
||||
|
||||
{-| A delightful button which can trigger an effect when clicked!
|
||||
|
||||
This button will trigger the passed-in message if the button state is:
|
||||
|
||||
- Enabled
|
||||
- Unfulfilled
|
||||
|
||||
This button will be Disabled if the button state is:
|
||||
|
||||
- Disabled
|
||||
- Error
|
||||
- Loading
|
||||
- Success
|
||||
|
||||
-}
|
||||
button : ButtonConfig msg -> ButtonContent -> Html msg
|
||||
button config content =
|
||||
customButton [] config content
|
||||
|
||||
|
||||
{-| Exactly the same as button but you can pass in a list of attributes
|
||||
-}
|
||||
customButton : List (Attribute msg) -> ButtonConfig msg -> ButtonContent -> Html msg
|
||||
customButton attributes config content =
|
||||
let
|
||||
buttonStyle_ =
|
||||
case content.state of
|
||||
Enabled ->
|
||||
styleToColorPalette config.style
|
||||
|
||||
Disabled ->
|
||||
InactiveColors
|
||||
|
||||
Error ->
|
||||
ErrorColors
|
||||
|
||||
Unfulfilled ->
|
||||
InactiveColors
|
||||
|
||||
Loading ->
|
||||
LoadingColors
|
||||
|
||||
Success ->
|
||||
SuccessColors
|
||||
|
||||
disabled =
|
||||
case content.state of
|
||||
Enabled ->
|
||||
False
|
||||
|
||||
Disabled ->
|
||||
True
|
||||
|
||||
Error ->
|
||||
True
|
||||
|
||||
Unfulfilled ->
|
||||
False
|
||||
|
||||
Loading ->
|
||||
True
|
||||
|
||||
Success ->
|
||||
True
|
||||
in
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "customButton")
|
||||
(buttonStyles config.size config.width buttonStyle_ Button)
|
||||
([ Events.onClick config.onClick
|
||||
, Attributes.disabled disabled
|
||||
, Attributes.type_ "button"
|
||||
]
|
||||
++ attributes
|
||||
)
|
||||
(viewLabel content.icon content.label)
|
||||
|
||||
|
||||
|
||||
-- COPY TO CLIPBOARD BUTTON
|
||||
|
||||
|
||||
{-| Config for copyToClipboard
|
||||
-}
|
||||
type alias CopyToClipboardConfig =
|
||||
{ size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, copyText : String
|
||||
, buttonLabel : String
|
||||
, withIcon : Bool
|
||||
, width : Maybe Int
|
||||
}
|
||||
|
||||
|
||||
{-| See ui/src/Page/Teach/Courses/Assignments/index.coffee
|
||||
You will need to hook this up to clipboard.js
|
||||
-}
|
||||
copyToClipboard : { r | teach_assignments_copyWhite_svg : Asset } -> CopyToClipboardConfig -> Html msg
|
||||
copyToClipboard assets config =
|
||||
let
|
||||
maybeIcon =
|
||||
if config.withIcon then
|
||||
Just (Icon.copy assets)
|
||||
|
||||
else
|
||||
Nothing
|
||||
in
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "copyToClipboard")
|
||||
(buttonStyles config.size config.width (styleToColorPalette config.style) Button)
|
||||
[ Widget.label "Copy URL to clipboard"
|
||||
, Attributes.attribute "data-clipboard-text" config.copyText
|
||||
]
|
||||
(viewLabel maybeIcon config.buttonLabel)
|
||||
|
||||
|
||||
|
||||
-- DELETE BUTTON
|
||||
|
||||
|
||||
type alias DeleteButtonConfig msg =
|
||||
{ label : String
|
||||
, onClick : msg
|
||||
}
|
||||
|
||||
|
||||
{-| A delete button (blue X)
|
||||
-}
|
||||
delete : { r | x : String } -> DeleteButtonConfig msg -> Html msg
|
||||
delete assets config =
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "delete")
|
||||
[ Css.display Css.inlineBlock
|
||||
, Css.backgroundRepeat Css.noRepeat
|
||||
, Css.backgroundColor Css.transparent
|
||||
, Css.backgroundPosition Css.center
|
||||
, Css.backgroundSize Css.contain
|
||||
, Css.border Css.zero
|
||||
, Css.width (Css.px 15)
|
||||
, Css.height (Css.px 15)
|
||||
, Css.padding Css.zero
|
||||
, Css.margin2 Css.zero (Css.px 6)
|
||||
, Css.cursor Css.pointer
|
||||
, Css.color Colors.azure
|
||||
]
|
||||
[ Events.onClick config.onClick
|
||||
, Attributes.type_ "button"
|
||||
, -- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||
Widget.label config.label
|
||||
]
|
||||
[ Icon.icon { alt = "Delete", icon = Icon.xSvg assets } ]
|
||||
|
||||
|
||||
|
||||
-- TOGGLE BUTTON
|
||||
|
||||
|
||||
{-| Buttons can be toggled into a pressed state and back again.
|
||||
-}
|
||||
type alias ToggleButtonConfig msg =
|
||||
{ label : String
|
||||
, onSelect : msg
|
||||
, onDeselect : msg
|
||||
, pressed : Bool
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
toggleButton : ToggleButtonConfig msg -> Html msg
|
||||
toggleButton config =
|
||||
let
|
||||
toggledStyles =
|
||||
if config.pressed then
|
||||
[ Css.color Colors.gray20
|
||||
, Css.backgroundColor Colors.glacier
|
||||
, Css.boxShadow5 Css.inset Css.zero (Css.px 3) Css.zero (ColorsExtra.withAlpha 0.2 Colors.gray20)
|
||||
, Css.border3 (Css.px 1) Css.solid Colors.azure
|
||||
, Css.fontWeight Css.bold
|
||||
]
|
||||
|
||||
else
|
||||
[]
|
||||
in
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "toggleButton")
|
||||
(buttonStyles Medium Nothing SecondaryColors Button
|
||||
++ toggledStyles
|
||||
)
|
||||
[ Events.onClick
|
||||
(if config.pressed then
|
||||
config.onDeselect
|
||||
|
||||
else
|
||||
config.onSelect
|
||||
)
|
||||
, Widget.pressed <| Just config.pressed
|
||||
|
||||
-- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||
, Role.button
|
||||
|
||||
-- Note: setting type: 'button' removes the default behavior of submit
|
||||
-- equivalent to preventDefaultBehavior = false
|
||||
-- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-name
|
||||
, Attributes.type_ "button"
|
||||
]
|
||||
(viewLabel Nothing config.label)
|
||||
|
||||
|
||||
{-| Inputs can be a clickable thing used in a form
|
||||
-}
|
||||
type alias InputConfig =
|
||||
{ content : Html Never
|
||||
, name : String
|
||||
, size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, value : String
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- LINKS THAT LOOK LIKE BUTTONS
|
||||
|
||||
|
||||
{-| Links are clickable things with a url.
|
||||
|
||||
NOTE: Links do not support two-line labels.
|
||||
|
||||
-}
|
||||
type alias LinkConfig =
|
||||
{ label : String
|
||||
, icon : Maybe IconType
|
||||
, url : String
|
||||
, size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, width : Maybe Int
|
||||
}
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||
some url
|
||||
-}
|
||||
link : LinkConfig -> Html msg
|
||||
link =
|
||||
linkBase "link" [ Attributes.target "_self" ]
|
||||
|
||||
|
||||
{-| Use this link for routing within a single page app.
|
||||
|
||||
This will make a normal <a> tag, but change the Events.onClick behavior to avoid reloading the page.
|
||||
|
||||
See <https://github.com/elm-lang/html/issues/110> for details on this implementation.
|
||||
|
||||
-}
|
||||
linkSpa :
|
||||
(route -> String)
|
||||
-> (route -> msg)
|
||||
->
|
||||
{ label : String
|
||||
, icon : Maybe IconType
|
||||
, size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, width : Maybe Int
|
||||
, route : route
|
||||
}
|
||||
-> Html msg
|
||||
linkSpa toUrl toMsg config =
|
||||
linkBase
|
||||
"linkSpa"
|
||||
[ EventExtras.onClickPreventDefaultForLinkWithHref (toMsg config.route)
|
||||
]
|
||||
{ label = config.label
|
||||
, icon = config.icon
|
||||
, size = config.size
|
||||
, style = config.style
|
||||
, width = config.width
|
||||
, url = toUrl config.route
|
||||
}
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||
some url and have it open to an external site
|
||||
-}
|
||||
linkExternal : LinkConfig -> Html msg
|
||||
linkExternal =
|
||||
linkBase "linkExternal" [ Attributes.target "_blank" ]
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||
some url, and it's an HTTP request (Rails includes JS to make this use the given HTTP method)
|
||||
-}
|
||||
linkWithMethod : String -> LinkConfig -> Html msg
|
||||
linkWithMethod method =
|
||||
linkBase "linkWithMethod" [ Attributes.attribute "data-method" method ]
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url.
|
||||
This should only take in messages that result in a Msg that triggers Analytics.trackAndRedirect. For buttons that trigger other effects on the page, please use Nri.Button.button instead
|
||||
-}
|
||||
linkWithTracking : msg -> LinkConfig -> Html msg
|
||||
linkWithTracking onTrack =
|
||||
linkBase
|
||||
"linkWithTracking"
|
||||
[ Events.preventDefaultOn "click"
|
||||
(Json.Decode.succeed ( onTrack, True ))
|
||||
]
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url and have it open to an external site
|
||||
|
||||
This should only take in messages that result in tracking events. For buttons that trigger other effects on the page, please use Nri.Ui.Button.V2.button instead
|
||||
|
||||
-}
|
||||
linkExternalWithTracking : msg -> LinkConfig -> Html msg
|
||||
linkExternalWithTracking onTrack =
|
||||
linkBase
|
||||
"linkExternalWithTracking"
|
||||
[ Attributes.target "_blank"
|
||||
, EventExtras.onClickForLinkWithHref onTrack
|
||||
]
|
||||
|
||||
|
||||
{-| Helper function for building links with an arbitrary number of Attributes
|
||||
-}
|
||||
linkBase : String -> List (Attribute msg) -> LinkConfig -> Html msg
|
||||
linkBase linkFunctionName extraAttrs config =
|
||||
Nri.Ui.styled Styled.a
|
||||
(styledName linkFunctionName)
|
||||
(Css.whiteSpace Css.noWrap
|
||||
:: buttonStyles config.size config.width (styleToColorPalette config.style) Anchor
|
||||
)
|
||||
(Attributes.href config.url
|
||||
:: extraAttrs
|
||||
)
|
||||
(viewLabel config.icon config.label)
|
||||
|
||||
|
||||
|
||||
-- HELPERS
|
||||
|
||||
|
||||
type ColorPalette
|
||||
= PrimaryColors
|
||||
| SecondaryColors
|
||||
| BorderlessColors
|
||||
| DangerColors
|
||||
| PremiumColors
|
||||
| InactiveColors
|
||||
| LoadingColors
|
||||
| SuccessColors
|
||||
| ErrorColors
|
||||
|
||||
|
||||
styleToColorPalette : ButtonStyle -> ColorPalette
|
||||
styleToColorPalette style =
|
||||
case style of
|
||||
Primary ->
|
||||
PrimaryColors
|
||||
|
||||
Secondary ->
|
||||
SecondaryColors
|
||||
|
||||
Borderless ->
|
||||
BorderlessColors
|
||||
|
||||
Danger ->
|
||||
DangerColors
|
||||
|
||||
Premium ->
|
||||
PremiumColors
|
||||
|
||||
|
||||
buttonStyles : ButtonSize -> Maybe Int -> ColorPalette -> ElementType -> List Style
|
||||
buttonStyles size width colorPalette elementType =
|
||||
List.concat
|
||||
[ buttonStyle
|
||||
, colorStyle colorPalette
|
||||
, sizeStyle size width elementType
|
||||
]
|
||||
|
||||
|
||||
viewLabel : Maybe IconType -> String -> List (Html msg)
|
||||
viewLabel icn label =
|
||||
case icn of
|
||||
Nothing ->
|
||||
renderMarkdown label
|
||||
|
||||
Just iconType ->
|
||||
[ Html.span [] (Icon.decorativeIcon iconType :: renderMarkdown label) ]
|
||||
|
||||
|
||||
renderMarkdown : String -> List (Html msg)
|
||||
renderMarkdown markdown =
|
||||
case Markdown.Block.parse Nothing markdown of
|
||||
-- It seems to be always first wrapped in a `Paragraph` and never directly a `PlainInline`
|
||||
[ Markdown.Block.Paragraph _ inlines ] ->
|
||||
List.map (Markdown.Inline.toHtml >> Styled.fromUnstyled) inlines
|
||||
|
||||
_ ->
|
||||
[ Html.text markdown ]
|
||||
|
||||
|
||||
|
||||
-- STYLES
|
||||
|
||||
|
||||
buttonStyle : List Style
|
||||
buttonStyle =
|
||||
[ Css.cursor Css.pointer
|
||||
, Css.display Css.inlineBlock
|
||||
, -- Specifying the font can and should go away after bootstrap is removed from application.css
|
||||
Nri.Ui.Fonts.V1.baseFont
|
||||
, Css.textOverflow Css.ellipsis
|
||||
, Css.overflow Css.hidden
|
||||
, Css.textDecoration Css.none
|
||||
, Css.backgroundImage Css.none
|
||||
, Css.textShadow Css.none
|
||||
, Css.property "transition" "all 0.2s"
|
||||
, Css.boxShadow Css.none
|
||||
, Css.border Css.zero
|
||||
, Css.marginBottom Css.zero
|
||||
, Css.hover [ Css.textDecoration Css.none ]
|
||||
, Css.disabled [ Css.cursor Css.notAllowed ]
|
||||
]
|
||||
|
||||
|
||||
colorStyle : ColorPalette -> List Style
|
||||
colorStyle colorPalette =
|
||||
let
|
||||
( config, additionalStyles ) =
|
||||
case colorPalette of
|
||||
PrimaryColors ->
|
||||
( { background = Colors.azure
|
||||
, hover = Colors.azureDark
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.azureDark
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
SecondaryColors ->
|
||||
( { background = Colors.white
|
||||
, hover = Colors.glacier
|
||||
, text = Colors.azure
|
||||
, border = Just <| Colors.azure
|
||||
, shadow = Colors.azure
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
BorderlessColors ->
|
||||
( { background = Css.rgba 0 0 0 0
|
||||
, hover = Css.rgba 0 0 0 0
|
||||
, text = Colors.azure
|
||||
, border = Nothing
|
||||
, shadow = Css.rgba 0 0 0 0
|
||||
}
|
||||
, [ Css.hover
|
||||
[ Css.textDecoration Css.underline
|
||||
, Css.disabled [ Css.textDecoration Css.none ]
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
DangerColors ->
|
||||
( { background = Colors.red
|
||||
, hover = Colors.redDark
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.redDark
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
PremiumColors ->
|
||||
( { background = Colors.yellow
|
||||
, hover = Colors.ochre
|
||||
, text = Colors.navy
|
||||
, border = Nothing
|
||||
, shadow = Colors.ochre
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
InactiveColors ->
|
||||
( { background = Colors.gray92
|
||||
, hover = Colors.gray92
|
||||
, text = Colors.gray45
|
||||
, border = Nothing
|
||||
, shadow = Colors.gray92
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
LoadingColors ->
|
||||
( { background = Colors.glacier
|
||||
, hover = Colors.glacier
|
||||
, text = Colors.navy
|
||||
, border = Nothing
|
||||
, shadow = Colors.glacier
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
SuccessColors ->
|
||||
( { background = Colors.greenDark
|
||||
, hover = Colors.greenDark
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.greenDark
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
ErrorColors ->
|
||||
( { background = Colors.purple
|
||||
, hover = Colors.purple
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.purple
|
||||
}
|
||||
, []
|
||||
)
|
||||
in
|
||||
[ Css.batch additionalStyles
|
||||
, Css.color config.text
|
||||
, Css.backgroundColor config.background
|
||||
, Css.fontWeight (Css.int 700)
|
||||
, Css.textAlign Css.center
|
||||
, case config.border of
|
||||
Nothing ->
|
||||
Css.borderStyle Css.none
|
||||
|
||||
Just color ->
|
||||
Css.batch
|
||||
[ Css.borderColor color
|
||||
, Css.borderStyle Css.solid
|
||||
]
|
||||
, Css.borderBottomStyle Css.solid
|
||||
, Css.borderBottomColor config.shadow
|
||||
, Css.fontStyle Css.normal
|
||||
, Css.hover
|
||||
[ Css.color config.text
|
||||
, Css.backgroundColor config.hover
|
||||
, Css.disabled [ Css.backgroundColor config.background ]
|
||||
]
|
||||
, Css.visited [ Css.color config.text ]
|
||||
]
|
||||
|
||||
|
||||
type ElementType
|
||||
= Anchor
|
||||
| Button
|
||||
|
||||
|
||||
sizeStyle : ButtonSize -> Maybe Int -> ElementType -> List Style
|
||||
sizeStyle size width elementType =
|
||||
let
|
||||
config =
|
||||
case size of
|
||||
Small ->
|
||||
{ fontSize = 15
|
||||
, height = 36
|
||||
, imageHeight = 15
|
||||
, shadowHeight = 2
|
||||
, minWidth = 75
|
||||
}
|
||||
|
||||
Medium ->
|
||||
{ fontSize = 17
|
||||
, height = 45
|
||||
, imageHeight = 15
|
||||
, shadowHeight = 3
|
||||
, minWidth = 100
|
||||
}
|
||||
|
||||
Large ->
|
||||
{ fontSize = 20
|
||||
, height = 56
|
||||
, imageHeight = 20
|
||||
, shadowHeight = 4
|
||||
, minWidth = 200
|
||||
}
|
||||
|
||||
widthAttributes =
|
||||
case width of
|
||||
Just pxWidth ->
|
||||
[ Css.maxWidth (Css.pct 100)
|
||||
, Css.width (Css.px <| toFloat pxWidth)
|
||||
]
|
||||
|
||||
Nothing ->
|
||||
[ Css.padding2 Css.zero (Css.px 16)
|
||||
, Css.minWidth (Css.px config.minWidth)
|
||||
]
|
||||
|
||||
lineHeightPx =
|
||||
case elementType of
|
||||
Anchor ->
|
||||
config.height
|
||||
|
||||
Button ->
|
||||
case size of
|
||||
Small ->
|
||||
15
|
||||
|
||||
Medium ->
|
||||
19
|
||||
|
||||
Large ->
|
||||
22
|
||||
in
|
||||
[ Css.fontSize (Css.px config.fontSize)
|
||||
, Css.borderRadius (Css.px 8)
|
||||
, Css.height (Css.px config.height)
|
||||
, Css.lineHeight (Css.px lineHeightPx)
|
||||
, Css.boxSizing Css.borderBox
|
||||
, Css.borderWidth (Css.px 1)
|
||||
, Css.borderBottomWidth (Css.px config.shadowHeight)
|
||||
, Css.batch widthAttributes
|
||||
, Css.Global.descendants
|
||||
[ Css.Global.img
|
||||
[ Css.height (Css.px config.imageHeight)
|
||||
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||
, Css.position Css.relative
|
||||
, Css.bottom (Css.px 2)
|
||||
, Css.verticalAlign Css.middle
|
||||
]
|
||||
, Css.Global.svg
|
||||
[ Css.height (Css.px config.imageHeight) |> Css.important
|
||||
, Css.width (Css.px config.imageHeight) |> Css.important
|
||||
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||
, Css.position Css.relative
|
||||
, Css.bottom (Css.px 2)
|
||||
, Css.verticalAlign Css.middle
|
||||
]
|
||||
, Css.Global.svg
|
||||
[ Css.important <| Css.height (Css.px config.imageHeight)
|
||||
, Css.important <| Css.width Css.auto
|
||||
, Css.maxWidth (Css.px (config.imageHeight * 1.25))
|
||||
, Css.paddingRight (Css.px <| config.imageHeight / 6)
|
||||
, Css.position Css.relative
|
||||
, Css.bottom (Css.px 2)
|
||||
, Css.verticalAlign Css.middle
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
styledName : String -> String
|
||||
styledName suffix =
|
||||
"Nri-Ui-Button-V3-" ++ suffix
|
@ -1,811 +0,0 @@
|
||||
module Nri.Ui.Button.V4 exposing
|
||||
( ButtonSize(..), ButtonWidth(..), ButtonStyle(..), ButtonState(..), ButtonContent
|
||||
, ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
|
||||
, LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
|
||||
# Changes from V3:
|
||||
|
||||
- Adds `ButtonWidth`.
|
||||
- Button now grows vertically to fit content.
|
||||
To limit the height use attributes on its container or consider truncating content before rendering.
|
||||
|
||||
|
||||
# About:
|
||||
|
||||
Common NoRedInk buttons. For accessibility purposes, buttons that perform an
|
||||
action on the current page should be HTML `<button>` elements and are created here
|
||||
with `*Button` functions. Buttons that take the user to a new page should be
|
||||
HTML `<a>` elements and are created here with `*Link` functions. Both versions
|
||||
should be able to use the same CSS class in all cases.
|
||||
|
||||
There will generally be a `*Button` and `*Link` version of each button style.
|
||||
(These will be created as they are needed.)
|
||||
|
||||
In general a button should never truncate or obscure its contents. This could
|
||||
make it difficult or impossible for a student or teacher to use the site, so in
|
||||
general choose buttons that grow to fit their contents. It is better to risk
|
||||
weird layout than to block users. Might this be a golden rule? Of course there
|
||||
may be exceptions, for example if button content is supplied by an end-user.
|
||||
|
||||
|
||||
## Common configs
|
||||
|
||||
@docs ButtonSize, ButtonWidth, ButtonStyle, ButtonState, ButtonContent
|
||||
|
||||
|
||||
## `<button>` Buttons
|
||||
|
||||
@docs ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
|
||||
|
||||
|
||||
## `<a>` Buttons
|
||||
|
||||
@docs LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Html exposing (Attribute, Html)
|
||||
import Accessibility.Styled.Role as Role
|
||||
import Accessibility.Styled.Widget as Widget
|
||||
import Css exposing (Style)
|
||||
import Css.Global
|
||||
import EventExtras.Styled as EventExtras
|
||||
import Html.Styled as Styled
|
||||
import Html.Styled.Attributes as Attributes
|
||||
import Html.Styled.Events as Events
|
||||
import Json.Decode
|
||||
import Markdown.Block
|
||||
import Markdown.Inline
|
||||
import Nri.Ui
|
||||
import Nri.Ui.AssetPath as AssetPath exposing (Asset)
|
||||
import Nri.Ui.Colors.Extra as ColorsExtra
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Fonts.V1
|
||||
import Nri.Ui.Icon.V3 as Icon exposing (IconType)
|
||||
|
||||
|
||||
{-| Sizes for buttons and links that have button classes
|
||||
-}
|
||||
type ButtonSize
|
||||
= Small
|
||||
| Medium
|
||||
| Large
|
||||
|
||||
|
||||
{-| Width sizing behavior for buttons.
|
||||
|
||||
`WidthExact Int` defines a size in `px` for the button's total width, and
|
||||
`WidthUnbounded` leaves the maxiumum width unbounded (there is a minimum width).
|
||||
|
||||
-}
|
||||
type ButtonWidth
|
||||
= WidthExact Int
|
||||
| WidthUnbounded
|
||||
|
||||
|
||||
{-| Styleguide-approved styles for your buttons!
|
||||
|
||||
Note on borderless buttons:
|
||||
A borderless button that performs an action on the current page
|
||||
This button is intended to look like a link.
|
||||
Only use a borderless button when the clickable text in question follows the same layout/margin/padding as a bordered button
|
||||
|
||||
-}
|
||||
type ButtonStyle
|
||||
= Primary
|
||||
| Secondary
|
||||
| Borderless
|
||||
| Danger
|
||||
| Premium
|
||||
|
||||
|
||||
{-| Describes the state of a button. Has consequences for appearance and disabled attribute.
|
||||
|
||||
- Enabled: An enabled button. Takes the appearance of ButtonStyle
|
||||
- Unfulfilled: A button which appears with the InactiveColors palette but is not disabled.
|
||||
- Disabled: A button which appears with the InactiveColors palette and is disabled.
|
||||
- Error: A button which appears with the ErrorColors palette and is disabled.
|
||||
- Loading: A button which appears with the LoadingColors palette and is disabled
|
||||
- Success: A button which appears with the SuccessColors palette and is disabled
|
||||
|
||||
-}
|
||||
type ButtonState
|
||||
= Enabled
|
||||
| Unfulfilled
|
||||
| Disabled
|
||||
| Error
|
||||
| Loading
|
||||
| Success
|
||||
|
||||
|
||||
{-| The part of a button that remains constant through different button states
|
||||
-}
|
||||
type alias ButtonConfig msg =
|
||||
{ onClick : msg
|
||||
, size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, width : ButtonWidth
|
||||
}
|
||||
|
||||
|
||||
{-| ButtonContent, often changes based on ButtonState. For example, a button in the "Success"
|
||||
state may have a different label than a button in the "Error" state
|
||||
-}
|
||||
type alias ButtonContent =
|
||||
{ label : String
|
||||
, state : ButtonState
|
||||
, icon : Maybe IconType
|
||||
}
|
||||
|
||||
|
||||
{-| A delightful button which can trigger an effect when clicked!
|
||||
|
||||
This button will trigger the passed-in message if the button state is:
|
||||
|
||||
- Enabled
|
||||
- Unfulfilled
|
||||
|
||||
This button will be Disabled if the button state is:
|
||||
|
||||
- Disabled
|
||||
- Error
|
||||
- Loading
|
||||
- Success
|
||||
|
||||
-}
|
||||
button : ButtonConfig msg -> ButtonContent -> Html msg
|
||||
button config content =
|
||||
customButton [] config content
|
||||
|
||||
|
||||
{-| Exactly the same as button but you can pass in a list of attributes
|
||||
-}
|
||||
customButton : List (Attribute msg) -> ButtonConfig msg -> ButtonContent -> Html msg
|
||||
customButton attributes config content =
|
||||
let
|
||||
buttonStyle_ =
|
||||
case content.state of
|
||||
Enabled ->
|
||||
styleToColorPalette config.style
|
||||
|
||||
Disabled ->
|
||||
InactiveColors
|
||||
|
||||
Error ->
|
||||
ErrorColors
|
||||
|
||||
Unfulfilled ->
|
||||
InactiveColors
|
||||
|
||||
Loading ->
|
||||
LoadingColors
|
||||
|
||||
Success ->
|
||||
SuccessColors
|
||||
|
||||
disabled =
|
||||
case content.state of
|
||||
Enabled ->
|
||||
False
|
||||
|
||||
Disabled ->
|
||||
True
|
||||
|
||||
Error ->
|
||||
True
|
||||
|
||||
Unfulfilled ->
|
||||
False
|
||||
|
||||
Loading ->
|
||||
True
|
||||
|
||||
Success ->
|
||||
True
|
||||
in
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "customButton")
|
||||
(buttonStyles config.size config.width buttonStyle_ Button)
|
||||
([ Events.onClick config.onClick
|
||||
, Attributes.disabled disabled
|
||||
, Attributes.type_ "button"
|
||||
]
|
||||
++ attributes
|
||||
)
|
||||
(viewLabel content.icon content.label)
|
||||
|
||||
|
||||
|
||||
-- COPY TO CLIPBOARD BUTTON
|
||||
|
||||
|
||||
{-| Config for copyToClipboard
|
||||
-}
|
||||
type alias CopyToClipboardConfig =
|
||||
{ size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, copyText : String
|
||||
, buttonLabel : String
|
||||
, withIcon : Bool
|
||||
, width : ButtonWidth
|
||||
}
|
||||
|
||||
|
||||
{-| See ui/src/Page/Teach/Courses/Assignments/index.coffee
|
||||
You will need to hook this up to clipboard.js
|
||||
-}
|
||||
copyToClipboard : { r | teach_assignments_copyWhite_svg : Asset } -> CopyToClipboardConfig -> Html msg
|
||||
copyToClipboard assets config =
|
||||
let
|
||||
maybeIcon =
|
||||
if config.withIcon then
|
||||
Just (Icon.copy assets)
|
||||
|
||||
else
|
||||
Nothing
|
||||
in
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "copyToClipboard")
|
||||
(buttonStyles config.size config.width (styleToColorPalette config.style) Button)
|
||||
[ Widget.label "Copy URL to clipboard"
|
||||
, Attributes.attribute "data-clipboard-text" config.copyText
|
||||
]
|
||||
(viewLabel maybeIcon config.buttonLabel)
|
||||
|
||||
|
||||
|
||||
-- DELETE BUTTON
|
||||
|
||||
|
||||
type alias DeleteButtonConfig msg =
|
||||
{ label : String
|
||||
, onClick : msg
|
||||
}
|
||||
|
||||
|
||||
{-| A delete button (blue X)
|
||||
-}
|
||||
delete : { r | x : String } -> DeleteButtonConfig msg -> Html msg
|
||||
delete assets config =
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "delete")
|
||||
[ Css.display Css.inlineBlock
|
||||
, Css.backgroundRepeat Css.noRepeat
|
||||
, Css.backgroundColor Css.transparent
|
||||
, Css.backgroundPosition Css.center
|
||||
, Css.backgroundSize Css.contain
|
||||
, Css.border Css.zero
|
||||
, Css.width (Css.px 15)
|
||||
, Css.height (Css.px 15)
|
||||
, Css.padding Css.zero
|
||||
, Css.margin2 Css.zero (Css.px 6)
|
||||
, Css.cursor Css.pointer
|
||||
, Css.color Colors.azure
|
||||
]
|
||||
[ Events.onClick config.onClick
|
||||
, Attributes.type_ "button"
|
||||
, -- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||
Widget.label config.label
|
||||
]
|
||||
[ Icon.icon { alt = "Delete", icon = Icon.xSvg assets } ]
|
||||
|
||||
|
||||
|
||||
-- TOGGLE BUTTON
|
||||
|
||||
|
||||
{-| Buttons can be toggled into a pressed state and back again.
|
||||
-}
|
||||
type alias ToggleButtonConfig msg =
|
||||
{ label : String
|
||||
, onSelect : msg
|
||||
, onDeselect : msg
|
||||
, pressed : Bool
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
toggleButton : ToggleButtonConfig msg -> Html msg
|
||||
toggleButton config =
|
||||
let
|
||||
toggledStyles =
|
||||
if config.pressed then
|
||||
[ Css.color Colors.gray20
|
||||
, Css.backgroundColor Colors.glacier
|
||||
, Css.boxShadow5 Css.inset Css.zero (Css.px 3) Css.zero (ColorsExtra.withAlpha 0.2 Colors.gray20)
|
||||
, Css.border3 (Css.px 1) Css.solid Colors.azure
|
||||
, Css.fontWeight Css.bold
|
||||
]
|
||||
|
||||
else
|
||||
[]
|
||||
in
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "toggleButton")
|
||||
(buttonStyles Medium WidthUnbounded SecondaryColors Button
|
||||
++ toggledStyles
|
||||
)
|
||||
[ Events.onClick
|
||||
(if config.pressed then
|
||||
config.onDeselect
|
||||
|
||||
else
|
||||
config.onSelect
|
||||
)
|
||||
, Widget.pressed <| Just config.pressed
|
||||
|
||||
-- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||
, Role.button
|
||||
|
||||
-- Note: setting type: 'button' removes the default behavior of submit
|
||||
-- equivalent to preventDefaultBehavior = false
|
||||
-- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-name
|
||||
, Attributes.type_ "button"
|
||||
]
|
||||
(viewLabel Nothing config.label)
|
||||
|
||||
|
||||
{-| Inputs can be a clickable thing used in a form
|
||||
-}
|
||||
type alias InputConfig =
|
||||
{ content : Html Never
|
||||
, name : String
|
||||
, size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, value : String
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- LINKS THAT LOOK LIKE BUTTONS
|
||||
|
||||
|
||||
{-| Links are clickable things with a url.
|
||||
|
||||
NOTE: Links do not support two-line labels.
|
||||
|
||||
-}
|
||||
type alias LinkConfig =
|
||||
{ label : String
|
||||
, icon : Maybe IconType
|
||||
, url : String
|
||||
, size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, width : ButtonWidth
|
||||
}
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||
some url
|
||||
-}
|
||||
link : LinkConfig -> Html msg
|
||||
link =
|
||||
linkBase "link" [ Attributes.target "_self" ]
|
||||
|
||||
|
||||
{-| Use this link for routing within a single page app.
|
||||
|
||||
This will make a normal <a> tag, but change the Events.onClick behavior to avoid reloading the page.
|
||||
|
||||
See <https://github.com/elm-lang/html/issues/110> for details on this implementation.
|
||||
|
||||
-}
|
||||
linkSpa :
|
||||
(route -> String)
|
||||
-> (route -> msg)
|
||||
->
|
||||
{ label : String
|
||||
, icon : Maybe IconType
|
||||
, size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, width : ButtonWidth
|
||||
, route : route
|
||||
}
|
||||
-> Html msg
|
||||
linkSpa toUrl toMsg config =
|
||||
linkBase
|
||||
"linkSpa"
|
||||
[ EventExtras.onClickPreventDefaultForLinkWithHref (toMsg config.route)
|
||||
]
|
||||
{ label = config.label
|
||||
, icon = config.icon
|
||||
, size = config.size
|
||||
, style = config.style
|
||||
, width = config.width
|
||||
, url = toUrl config.route
|
||||
}
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||
some url and have it open to an external site
|
||||
-}
|
||||
linkExternal : LinkConfig -> Html msg
|
||||
linkExternal =
|
||||
linkBase "linkExternal" [ Attributes.target "_blank" ]
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||
some url, and it's an HTTP request (Rails includes JS to make this use the given HTTP method)
|
||||
-}
|
||||
linkWithMethod : String -> LinkConfig -> Html msg
|
||||
linkWithMethod method =
|
||||
linkBase "linkWithMethod" [ Attributes.attribute "data-method" method ]
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url.
|
||||
This should only take in messages that result in a Msg that triggers Analytics.trackAndRedirect. For buttons that trigger other effects on the page, please use Nri.Button.button instead
|
||||
-}
|
||||
linkWithTracking : msg -> LinkConfig -> Html msg
|
||||
linkWithTracking onTrack =
|
||||
linkBase
|
||||
"linkWithTracking"
|
||||
[ Events.preventDefaultOn "click"
|
||||
(Json.Decode.succeed ( onTrack, True ))
|
||||
]
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url and have it open to an external site
|
||||
|
||||
This should only take in messages that result in tracking events. For buttons that trigger other effects on the page, please use Nri.Ui.Button.V2.button instead
|
||||
|
||||
-}
|
||||
linkExternalWithTracking : msg -> LinkConfig -> Html msg
|
||||
linkExternalWithTracking onTrack =
|
||||
linkBase
|
||||
"linkExternalWithTracking"
|
||||
[ Attributes.target "_blank"
|
||||
, EventExtras.onClickForLinkWithHref onTrack
|
||||
]
|
||||
|
||||
|
||||
{-| Helper function for building links with an arbitrary number of Attributes
|
||||
-}
|
||||
linkBase : String -> List (Attribute msg) -> LinkConfig -> Html msg
|
||||
linkBase linkFunctionName extraAttrs config =
|
||||
Nri.Ui.styled Styled.a
|
||||
(styledName linkFunctionName)
|
||||
(Css.whiteSpace Css.noWrap
|
||||
:: buttonStyles config.size config.width (styleToColorPalette config.style) Anchor
|
||||
)
|
||||
(Attributes.href config.url
|
||||
:: extraAttrs
|
||||
)
|
||||
(viewLabel config.icon config.label)
|
||||
|
||||
|
||||
|
||||
-- HELPERS
|
||||
|
||||
|
||||
type ColorPalette
|
||||
= PrimaryColors
|
||||
| SecondaryColors
|
||||
| BorderlessColors
|
||||
| DangerColors
|
||||
| PremiumColors
|
||||
| InactiveColors
|
||||
| LoadingColors
|
||||
| SuccessColors
|
||||
| ErrorColors
|
||||
|
||||
|
||||
styleToColorPalette : ButtonStyle -> ColorPalette
|
||||
styleToColorPalette style =
|
||||
case style of
|
||||
Primary ->
|
||||
PrimaryColors
|
||||
|
||||
Secondary ->
|
||||
SecondaryColors
|
||||
|
||||
Borderless ->
|
||||
BorderlessColors
|
||||
|
||||
Danger ->
|
||||
DangerColors
|
||||
|
||||
Premium ->
|
||||
PremiumColors
|
||||
|
||||
|
||||
buttonStyles : ButtonSize -> ButtonWidth -> ColorPalette -> ElementType -> List Style
|
||||
buttonStyles size width colorPalette elementType =
|
||||
List.concat
|
||||
[ buttonStyle
|
||||
, colorStyle colorPalette
|
||||
, sizeStyle size width elementType
|
||||
]
|
||||
|
||||
|
||||
viewLabel : Maybe IconType -> String -> List (Html msg)
|
||||
viewLabel icn label =
|
||||
case icn of
|
||||
Nothing ->
|
||||
renderMarkdown label
|
||||
|
||||
Just iconType ->
|
||||
[ Html.span [] (Icon.decorativeIcon iconType :: renderMarkdown label) ]
|
||||
|
||||
|
||||
renderMarkdown : String -> List (Html msg)
|
||||
renderMarkdown markdown =
|
||||
case Markdown.Block.parse Nothing markdown of
|
||||
-- It seems to be always first wrapped in a `Paragraph` and never directly a `PlainInline`
|
||||
[ Markdown.Block.Paragraph _ inlines ] ->
|
||||
List.map (Markdown.Inline.toHtml >> Styled.fromUnstyled) inlines
|
||||
|
||||
_ ->
|
||||
[ Html.text markdown ]
|
||||
|
||||
|
||||
|
||||
-- STYLES
|
||||
|
||||
|
||||
buttonStyle : List Style
|
||||
buttonStyle =
|
||||
[ Css.cursor Css.pointer
|
||||
, Css.display Css.inlineBlock
|
||||
, -- Specifying the font can and should go away after bootstrap is removed from application.css
|
||||
Nri.Ui.Fonts.V1.baseFont
|
||||
, Css.textOverflow Css.ellipsis
|
||||
, Css.overflow Css.hidden
|
||||
, Css.textDecoration Css.none
|
||||
, Css.backgroundImage Css.none
|
||||
, Css.textShadow Css.none
|
||||
, Css.property "transition" "all 0.2s"
|
||||
, Css.boxShadow Css.none
|
||||
, Css.border Css.zero
|
||||
, Css.marginBottom Css.zero
|
||||
, Css.hover [ Css.textDecoration Css.none ]
|
||||
, Css.disabled [ Css.cursor Css.notAllowed ]
|
||||
]
|
||||
|
||||
|
||||
colorStyle : ColorPalette -> List Style
|
||||
colorStyle colorPalette =
|
||||
let
|
||||
( config, additionalStyles ) =
|
||||
case colorPalette of
|
||||
PrimaryColors ->
|
||||
( { background = Colors.azure
|
||||
, hover = Colors.azureDark
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.azureDark
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
SecondaryColors ->
|
||||
( { background = Colors.white
|
||||
, hover = Colors.glacier
|
||||
, text = Colors.azure
|
||||
, border = Just <| Colors.azure
|
||||
, shadow = Colors.azure
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
BorderlessColors ->
|
||||
( { background = Css.rgba 0 0 0 0
|
||||
, hover = Css.rgba 0 0 0 0
|
||||
, text = Colors.azure
|
||||
, border = Nothing
|
||||
, shadow = Css.rgba 0 0 0 0
|
||||
}
|
||||
, [ Css.hover
|
||||
[ Css.textDecoration Css.underline
|
||||
, Css.disabled [ Css.textDecoration Css.none ]
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
DangerColors ->
|
||||
( { background = Colors.red
|
||||
, hover = Colors.redDark
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.redDark
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
PremiumColors ->
|
||||
( { background = Colors.yellow
|
||||
, hover = Colors.ochre
|
||||
, text = Colors.navy
|
||||
, border = Nothing
|
||||
, shadow = Colors.ochre
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
InactiveColors ->
|
||||
( { background = Colors.gray92
|
||||
, hover = Colors.gray92
|
||||
, text = Colors.gray45
|
||||
, border = Nothing
|
||||
, shadow = Colors.gray92
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
LoadingColors ->
|
||||
( { background = Colors.glacier
|
||||
, hover = Colors.glacier
|
||||
, text = Colors.navy
|
||||
, border = Nothing
|
||||
, shadow = Colors.glacier
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
SuccessColors ->
|
||||
( { background = Colors.greenDark
|
||||
, hover = Colors.greenDark
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.greenDark
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
ErrorColors ->
|
||||
( { background = Colors.purple
|
||||
, hover = Colors.purple
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.purple
|
||||
}
|
||||
, []
|
||||
)
|
||||
in
|
||||
[ Css.batch additionalStyles
|
||||
, Css.color config.text
|
||||
, Css.backgroundColor config.background
|
||||
, Css.fontWeight (Css.int 700)
|
||||
, Css.textAlign Css.center
|
||||
, case config.border of
|
||||
Nothing ->
|
||||
Css.borderStyle Css.none
|
||||
|
||||
Just color ->
|
||||
Css.batch
|
||||
[ Css.borderColor color
|
||||
, Css.borderStyle Css.solid
|
||||
]
|
||||
, Css.borderBottomStyle Css.solid
|
||||
, Css.borderBottomColor config.shadow
|
||||
, Css.fontStyle Css.normal
|
||||
, Css.hover
|
||||
[ Css.color config.text
|
||||
, Css.backgroundColor config.hover
|
||||
, Css.disabled [ Css.backgroundColor config.background ]
|
||||
]
|
||||
, Css.visited [ Css.color config.text ]
|
||||
]
|
||||
|
||||
|
||||
type ElementType
|
||||
= Anchor
|
||||
| Button
|
||||
|
||||
|
||||
sizeStyle : ButtonSize -> ButtonWidth -> ElementType -> List Style
|
||||
sizeStyle size width elementType =
|
||||
let
|
||||
config =
|
||||
case size of
|
||||
Small ->
|
||||
{ fontSize = 15
|
||||
, height = 36
|
||||
, imageHeight = 15
|
||||
, shadowHeight = 2
|
||||
, minWidth = 75
|
||||
}
|
||||
|
||||
Medium ->
|
||||
{ fontSize = 17
|
||||
, height = 45
|
||||
, imageHeight = 15
|
||||
, shadowHeight = 3
|
||||
, minWidth = 100
|
||||
}
|
||||
|
||||
Large ->
|
||||
{ fontSize = 20
|
||||
, height = 56
|
||||
, imageHeight = 20
|
||||
, shadowHeight = 4
|
||||
, minWidth = 200
|
||||
}
|
||||
|
||||
sizingAttributes =
|
||||
case elementType of
|
||||
Button ->
|
||||
let
|
||||
verticalPaddingPx =
|
||||
4
|
||||
in
|
||||
[ Css.minHeight (Css.px config.height)
|
||||
, Css.paddingTop (Css.px verticalPaddingPx)
|
||||
, Css.paddingBottom (Css.px verticalPaddingPx)
|
||||
]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
|
||||
widthAttributes =
|
||||
case width of
|
||||
WidthExact pxWidth ->
|
||||
[ Css.maxWidth (Css.pct 100)
|
||||
, Css.width (Css.px <| toFloat pxWidth)
|
||||
]
|
||||
|
||||
WidthUnbounded ->
|
||||
[ Css.paddingLeft (Css.px 16)
|
||||
, Css.paddingRight (Css.px 16)
|
||||
, Css.minWidth (Css.px config.minWidth)
|
||||
]
|
||||
|
||||
lineHeightPx =
|
||||
case elementType of
|
||||
Anchor ->
|
||||
config.height
|
||||
|
||||
Button ->
|
||||
case size of
|
||||
Small ->
|
||||
15
|
||||
|
||||
Medium ->
|
||||
19
|
||||
|
||||
Large ->
|
||||
22
|
||||
in
|
||||
[ Css.fontSize (Css.px config.fontSize)
|
||||
, Css.borderRadius (Css.px 8)
|
||||
, Css.lineHeight (Css.px lineHeightPx)
|
||||
, Css.boxSizing Css.borderBox
|
||||
, Css.borderWidth (Css.px 1)
|
||||
, Css.borderBottomWidth (Css.px config.shadowHeight)
|
||||
, Css.batch sizingAttributes
|
||||
, Css.batch widthAttributes
|
||||
, Css.Global.descendants
|
||||
[ Css.Global.img
|
||||
[ Css.height (Css.px config.imageHeight)
|
||||
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||
, Css.position Css.relative
|
||||
, Css.bottom (Css.px 2)
|
||||
, Css.verticalAlign Css.middle
|
||||
]
|
||||
, Css.Global.svg
|
||||
[ Css.height (Css.px config.imageHeight) |> Css.important
|
||||
, Css.width (Css.px config.imageHeight) |> Css.important
|
||||
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||
, Css.position Css.relative
|
||||
, Css.bottom (Css.px 2)
|
||||
, Css.verticalAlign Css.middle
|
||||
]
|
||||
, Css.Global.svg
|
||||
[ Css.important <| Css.height (Css.px config.imageHeight)
|
||||
, Css.important <| Css.width Css.auto
|
||||
, Css.maxWidth (Css.px (config.imageHeight * 1.25))
|
||||
, Css.paddingRight (Css.px <| config.imageHeight / 6)
|
||||
, Css.position Css.relative
|
||||
, Css.bottom (Css.px 2)
|
||||
, Css.verticalAlign Css.middle
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
styledName : String -> String
|
||||
styledName suffix =
|
||||
"Nri-Ui-Button-V4-" ++ suffix
|
@ -1,810 +0,0 @@
|
||||
module Nri.Ui.Button.V6 exposing
|
||||
( ButtonSize(..), ButtonWidth(..), ButtonStyle(..), ButtonState(..), ButtonContent
|
||||
, ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
|
||||
, LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
|
||||
# Changes from V5:
|
||||
|
||||
- Update version of Nri.Ui.Icon to V4
|
||||
|
||||
|
||||
# About:
|
||||
|
||||
Common NoRedInk buttons. For accessibility purposes, buttons that perform an
|
||||
action on the current page should be HTML `<button>` elements and are created here
|
||||
with `*Button` functions. Buttons that take the user to a new page should be
|
||||
HTML `<a>` elements and are created here with `*Link` functions. Both versions
|
||||
should be able to use the same CSS class in all cases.
|
||||
|
||||
There will generally be a `*Button` and `*Link` version of each button style.
|
||||
(These will be created as they are needed.)
|
||||
|
||||
In general a button should never truncate or obscure its contents. This could
|
||||
make it difficult or impossible for a student or teacher to use the site, so in
|
||||
general choose buttons that grow to fit their contents. It is better to risk
|
||||
weird layout than to block users. Might this be a golden rule? Of course there
|
||||
may be exceptions, for example if button content is supplied by an end-user.
|
||||
|
||||
|
||||
## Common configs
|
||||
|
||||
@docs ButtonSize, ButtonWidth, ButtonStyle, ButtonState, ButtonContent
|
||||
|
||||
|
||||
## `<button>` Buttons
|
||||
|
||||
@docs ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
|
||||
|
||||
|
||||
## `<a>` Buttons
|
||||
|
||||
@docs LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Html exposing (Attribute, Html)
|
||||
import Accessibility.Styled.Role as Role
|
||||
import Accessibility.Styled.Widget as Widget
|
||||
import Css exposing (Style)
|
||||
import Css.Global
|
||||
import EventExtras.Styled as EventExtras
|
||||
import Html.Styled as Styled
|
||||
import Html.Styled.Attributes as Attributes
|
||||
import Html.Styled.Events as Events
|
||||
import Json.Decode
|
||||
import Markdown.Block
|
||||
import Markdown.Inline
|
||||
import Nri.Ui
|
||||
import Nri.Ui.AssetPath as AssetPath exposing (Asset)
|
||||
import Nri.Ui.Colors.Extra as ColorsExtra
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Fonts.V1
|
||||
import Nri.Ui.Icon.V4 as Icon exposing (IconType)
|
||||
|
||||
|
||||
{-| Sizes for buttons and links that have button classes
|
||||
-}
|
||||
type ButtonSize
|
||||
= Small
|
||||
| Medium
|
||||
| Large
|
||||
|
||||
|
||||
{-| Width sizing behavior for buttons.
|
||||
|
||||
`WidthExact Int` defines a size in `px` for the button's total width, and
|
||||
`WidthUnbounded` leaves the maxiumum width unbounded (there is a minimum width).
|
||||
|
||||
-}
|
||||
type ButtonWidth
|
||||
= WidthExact Int
|
||||
| WidthUnbounded
|
||||
|
||||
|
||||
{-| Styleguide-approved styles for your buttons!
|
||||
|
||||
Note on borderless buttons:
|
||||
A borderless button that performs an action on the current page
|
||||
This button is intended to look like a link.
|
||||
Only use a borderless button when the clickable text in question follows the same layout/margin/padding as a bordered button
|
||||
|
||||
-}
|
||||
type ButtonStyle
|
||||
= Primary
|
||||
| Secondary
|
||||
| Borderless
|
||||
| Danger
|
||||
| Premium
|
||||
|
||||
|
||||
{-| Describes the state of a button. Has consequences for appearance and disabled attribute.
|
||||
|
||||
- Enabled: An enabled button. Takes the appearance of ButtonStyle
|
||||
- Unfulfilled: A button which appears with the InactiveColors palette but is not disabled.
|
||||
- Disabled: A button which appears with the InactiveColors palette and is disabled.
|
||||
- Error: A button which appears with the ErrorColors palette and is disabled.
|
||||
- Loading: A button which appears with the LoadingColors palette and is disabled
|
||||
- Success: A button which appears with the SuccessColors palette and is disabled
|
||||
|
||||
-}
|
||||
type ButtonState
|
||||
= Enabled
|
||||
| Unfulfilled
|
||||
| Disabled
|
||||
| Error
|
||||
| Loading
|
||||
| Success
|
||||
|
||||
|
||||
{-| The part of a button that remains constant through different button states
|
||||
-}
|
||||
type alias ButtonConfig msg =
|
||||
{ onClick : msg
|
||||
, size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, width : ButtonWidth
|
||||
}
|
||||
|
||||
|
||||
{-| ButtonContent, often changes based on ButtonState. For example, a button in the "Success"
|
||||
state may have a different label than a button in the "Error" state
|
||||
-}
|
||||
type alias ButtonContent =
|
||||
{ label : String
|
||||
, state : ButtonState
|
||||
, icon : Maybe IconType
|
||||
}
|
||||
|
||||
|
||||
{-| A delightful button which can trigger an effect when clicked!
|
||||
|
||||
This button will trigger the passed-in message if the button state is:
|
||||
|
||||
- Enabled
|
||||
- Unfulfilled
|
||||
|
||||
This button will be Disabled if the button state is:
|
||||
|
||||
- Disabled
|
||||
- Error
|
||||
- Loading
|
||||
- Success
|
||||
|
||||
-}
|
||||
button : ButtonConfig msg -> ButtonContent -> Html msg
|
||||
button config content =
|
||||
customButton [] config content
|
||||
|
||||
|
||||
{-| Exactly the same as button but you can pass in a list of attributes
|
||||
-}
|
||||
customButton : List (Attribute msg) -> ButtonConfig msg -> ButtonContent -> Html msg
|
||||
customButton attributes config content =
|
||||
let
|
||||
buttonStyle_ =
|
||||
case content.state of
|
||||
Enabled ->
|
||||
styleToColorPalette config.style
|
||||
|
||||
Disabled ->
|
||||
InactiveColors
|
||||
|
||||
Error ->
|
||||
ErrorColors
|
||||
|
||||
Unfulfilled ->
|
||||
InactiveColors
|
||||
|
||||
Loading ->
|
||||
LoadingColors
|
||||
|
||||
Success ->
|
||||
SuccessColors
|
||||
|
||||
disabled =
|
||||
case content.state of
|
||||
Enabled ->
|
||||
False
|
||||
|
||||
Disabled ->
|
||||
True
|
||||
|
||||
Error ->
|
||||
True
|
||||
|
||||
Unfulfilled ->
|
||||
False
|
||||
|
||||
Loading ->
|
||||
True
|
||||
|
||||
Success ->
|
||||
True
|
||||
in
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "customButton")
|
||||
[ buttonStyles config.size config.width buttonStyle_ ]
|
||||
([ Events.onClick config.onClick
|
||||
, Attributes.disabled disabled
|
||||
, Attributes.type_ "button"
|
||||
]
|
||||
++ attributes
|
||||
)
|
||||
[ viewLabel content.icon content.label ]
|
||||
|
||||
|
||||
|
||||
-- COPY TO CLIPBOARD BUTTON
|
||||
|
||||
|
||||
{-| Config for copyToClipboard
|
||||
-}
|
||||
type alias CopyToClipboardConfig =
|
||||
{ size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, copyText : String
|
||||
, buttonLabel : String
|
||||
, withIcon : Bool
|
||||
, width : ButtonWidth
|
||||
}
|
||||
|
||||
|
||||
{-| See ui/src/Page/Teach/Courses/Assignments/index.coffee
|
||||
You will need to hook this up to clipboard.js
|
||||
-}
|
||||
copyToClipboard : { r | teach_assignments_copyWhite_svg : Asset } -> CopyToClipboardConfig -> Html msg
|
||||
copyToClipboard assets config =
|
||||
let
|
||||
maybeIcon =
|
||||
if config.withIcon then
|
||||
Just (Icon.copy assets)
|
||||
|
||||
else
|
||||
Nothing
|
||||
in
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "copyToClipboard")
|
||||
[ buttonStyles config.size config.width (styleToColorPalette config.style) ]
|
||||
[ Widget.label "Copy URL to clipboard"
|
||||
, Attributes.attribute "data-clipboard-text" config.copyText
|
||||
]
|
||||
[ viewLabel maybeIcon config.buttonLabel ]
|
||||
|
||||
|
||||
|
||||
-- DELETE BUTTON
|
||||
|
||||
|
||||
type alias DeleteButtonConfig msg =
|
||||
{ label : String
|
||||
, onClick : msg
|
||||
}
|
||||
|
||||
|
||||
{-| A delete button (blue X)
|
||||
-}
|
||||
delete : { r | x : String } -> DeleteButtonConfig msg -> Html msg
|
||||
delete assets config =
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "delete")
|
||||
[ Css.display Css.inlineBlock
|
||||
, Css.backgroundRepeat Css.noRepeat
|
||||
, Css.backgroundColor Css.transparent
|
||||
, Css.backgroundPosition Css.center
|
||||
, Css.backgroundSize Css.contain
|
||||
, Css.border Css.zero
|
||||
, Css.width (Css.px 15)
|
||||
, Css.height (Css.px 15)
|
||||
, Css.padding Css.zero
|
||||
, Css.margin2 Css.zero (Css.px 6)
|
||||
, Css.cursor Css.pointer
|
||||
, Css.color Colors.azure
|
||||
]
|
||||
[ Events.onClick config.onClick
|
||||
, Attributes.type_ "button"
|
||||
, -- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||
Widget.label config.label
|
||||
]
|
||||
[ Icon.icon { alt = "Delete", icon = Icon.xSvg assets } ]
|
||||
|
||||
|
||||
|
||||
-- TOGGLE BUTTON
|
||||
|
||||
|
||||
{-| Buttons can be toggled into a pressed state and back again.
|
||||
-}
|
||||
type alias ToggleButtonConfig msg =
|
||||
{ label : String
|
||||
, onSelect : msg
|
||||
, onDeselect : msg
|
||||
, pressed : Bool
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
toggleButton : ToggleButtonConfig msg -> Html msg
|
||||
toggleButton config =
|
||||
let
|
||||
toggledStyles =
|
||||
if config.pressed then
|
||||
Css.batch
|
||||
[ Css.color Colors.gray20
|
||||
, Css.backgroundColor Colors.glacier
|
||||
, Css.boxShadow5 Css.inset Css.zero (Css.px 3) Css.zero (ColorsExtra.withAlpha 0.2 Colors.gray20)
|
||||
, Css.border3 (Css.px 1) Css.solid Colors.azure
|
||||
, Css.fontWeight Css.bold
|
||||
]
|
||||
|
||||
else
|
||||
Css.batch
|
||||
[]
|
||||
in
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "toggleButton")
|
||||
[ buttonStyles Medium WidthUnbounded SecondaryColors
|
||||
, toggledStyles
|
||||
]
|
||||
[ Events.onClick
|
||||
(if config.pressed then
|
||||
config.onDeselect
|
||||
|
||||
else
|
||||
config.onSelect
|
||||
)
|
||||
, Widget.pressed <| Just config.pressed
|
||||
|
||||
-- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||
, Role.button
|
||||
|
||||
-- Note: setting type: 'button' removes the default behavior of submit
|
||||
-- equivalent to preventDefaultBehavior = false
|
||||
-- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-name
|
||||
, Attributes.type_ "button"
|
||||
]
|
||||
[ viewLabel Nothing config.label ]
|
||||
|
||||
|
||||
{-| Inputs can be a clickable thing used in a form
|
||||
-}
|
||||
type alias InputConfig =
|
||||
{ content : Html Never
|
||||
, name : String
|
||||
, size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, value : String
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- LINKS THAT LOOK LIKE BUTTONS
|
||||
|
||||
|
||||
{-| Links are clickable things with a url.
|
||||
|
||||
NOTE: Links do not support two-line labels.
|
||||
|
||||
-}
|
||||
type alias LinkConfig =
|
||||
{ label : String
|
||||
, icon : Maybe IconType
|
||||
, url : String
|
||||
, size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, width : ButtonWidth
|
||||
}
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||
some url
|
||||
-}
|
||||
link : LinkConfig -> Html msg
|
||||
link =
|
||||
linkBase "link" [ Attributes.target "_self" ]
|
||||
|
||||
|
||||
{-| Use this link for routing within a single page app.
|
||||
|
||||
This will make a normal <a> tag, but change the Events.onClick behavior to avoid reloading the page.
|
||||
|
||||
See <https://github.com/elm-lang/html/issues/110> for details on this implementation.
|
||||
|
||||
-}
|
||||
linkSpa :
|
||||
(route -> String)
|
||||
-> (route -> msg)
|
||||
->
|
||||
{ label : String
|
||||
, icon : Maybe IconType
|
||||
, size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, width : ButtonWidth
|
||||
, route : route
|
||||
}
|
||||
-> Html msg
|
||||
linkSpa toUrl toMsg config =
|
||||
linkBase
|
||||
"linkSpa"
|
||||
[ EventExtras.onClickPreventDefaultForLinkWithHref (toMsg config.route)
|
||||
]
|
||||
{ label = config.label
|
||||
, icon = config.icon
|
||||
, size = config.size
|
||||
, style = config.style
|
||||
, width = config.width
|
||||
, url = toUrl config.route
|
||||
}
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||
some url and have it open to an external site
|
||||
-}
|
||||
linkExternal : LinkConfig -> Html msg
|
||||
linkExternal =
|
||||
linkBase "linkExternal" [ Attributes.target "_blank" ]
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||
some url, and it's an HTTP request (Rails includes JS to make this use the given HTTP method)
|
||||
-}
|
||||
linkWithMethod : String -> LinkConfig -> Html msg
|
||||
linkWithMethod method =
|
||||
linkBase "linkWithMethod" [ Attributes.attribute "data-method" method ]
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url.
|
||||
This should only take in messages that result in a Msg that triggers Analytics.trackAndRedirect. For buttons that trigger other effects on the page, please use Nri.Button.button instead
|
||||
-}
|
||||
linkWithTracking : msg -> LinkConfig -> Html msg
|
||||
linkWithTracking onTrack =
|
||||
linkBase
|
||||
"linkWithTracking"
|
||||
[ Events.preventDefaultOn "click"
|
||||
(Json.Decode.succeed ( onTrack, True ))
|
||||
]
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url and have it open to an external site
|
||||
|
||||
This should only take in messages that result in tracking events. For buttons that trigger other effects on the page, please use Nri.Ui.Button.V2.button instead
|
||||
|
||||
-}
|
||||
linkExternalWithTracking : msg -> LinkConfig -> Html msg
|
||||
linkExternalWithTracking onTrack =
|
||||
linkBase
|
||||
"linkExternalWithTracking"
|
||||
[ Attributes.target "_blank"
|
||||
, EventExtras.onClickForLinkWithHref onTrack
|
||||
]
|
||||
|
||||
|
||||
{-| Helper function for building links with an arbitrary number of Attributes
|
||||
-}
|
||||
linkBase : String -> List (Attribute msg) -> LinkConfig -> Html msg
|
||||
linkBase linkFunctionName extraAttrs config =
|
||||
Nri.Ui.styled Styled.a
|
||||
(styledName linkFunctionName)
|
||||
[ buttonStyles config.size config.width (styleToColorPalette config.style) ]
|
||||
(Attributes.href config.url
|
||||
:: extraAttrs
|
||||
)
|
||||
[ viewLabel config.icon config.label ]
|
||||
|
||||
|
||||
|
||||
-- HELPERS
|
||||
|
||||
|
||||
type ColorPalette
|
||||
= PrimaryColors
|
||||
| SecondaryColors
|
||||
| BorderlessColors
|
||||
| DangerColors
|
||||
| PremiumColors
|
||||
| InactiveColors
|
||||
| LoadingColors
|
||||
| SuccessColors
|
||||
| ErrorColors
|
||||
|
||||
|
||||
styleToColorPalette : ButtonStyle -> ColorPalette
|
||||
styleToColorPalette style =
|
||||
case style of
|
||||
Primary ->
|
||||
PrimaryColors
|
||||
|
||||
Secondary ->
|
||||
SecondaryColors
|
||||
|
||||
Borderless ->
|
||||
BorderlessColors
|
||||
|
||||
Danger ->
|
||||
DangerColors
|
||||
|
||||
Premium ->
|
||||
PremiumColors
|
||||
|
||||
|
||||
buttonStyles : ButtonSize -> ButtonWidth -> ColorPalette -> Style
|
||||
buttonStyles size width colorPalette =
|
||||
Css.batch
|
||||
[ buttonStyle
|
||||
, colorStyle colorPalette
|
||||
, sizeStyle size width
|
||||
]
|
||||
|
||||
|
||||
viewLabel : Maybe IconType -> String -> Html msg
|
||||
viewLabel icn label =
|
||||
Nri.Ui.styled Html.span
|
||||
"button-label-span"
|
||||
[ Css.overflow Css.hidden -- Keep scrollbars out of our button
|
||||
, Css.overflowWrap Css.breakWord -- Ensure that words that exceed the button width break instead of disappearing
|
||||
, Css.padding2 (Css.px 2) Css.zero -- Without a bit of bottom padding, text that extends below the baseline, like "g" gets cut off
|
||||
]
|
||||
[]
|
||||
(case icn of
|
||||
Nothing ->
|
||||
renderMarkdown label
|
||||
|
||||
Just iconType ->
|
||||
Icon.decorativeIcon iconType :: renderMarkdown label
|
||||
)
|
||||
|
||||
|
||||
renderMarkdown : String -> List (Html msg)
|
||||
renderMarkdown markdown =
|
||||
case Markdown.Block.parse Nothing markdown of
|
||||
-- It seems to be always first wrapped in a `Paragraph` and never directly a `PlainInline`
|
||||
[ Markdown.Block.Paragraph _ inlines ] ->
|
||||
List.map (Markdown.Inline.toHtml >> Styled.fromUnstyled) inlines
|
||||
|
||||
_ ->
|
||||
[ Html.text markdown ]
|
||||
|
||||
|
||||
|
||||
-- STYLES
|
||||
|
||||
|
||||
buttonStyle : Style
|
||||
buttonStyle =
|
||||
Css.batch
|
||||
[ Css.cursor Css.pointer
|
||||
, Css.display Css.inlineBlock
|
||||
, -- Specifying the font can and should go away after bootstrap is removed from application.css
|
||||
Nri.Ui.Fonts.V1.baseFont
|
||||
, Css.textOverflow Css.ellipsis
|
||||
, Css.overflow Css.hidden
|
||||
, Css.textDecoration Css.none
|
||||
, Css.backgroundImage Css.none
|
||||
, Css.textShadow Css.none
|
||||
, Css.property "transition" "background-color 0.2s, color 0.2s, box-shadow 0.2s, border 0.2s, border-width 0s"
|
||||
, Css.boxShadow Css.none
|
||||
, Css.border Css.zero
|
||||
, Css.marginBottom Css.zero
|
||||
, Css.hover [ Css.textDecoration Css.none ]
|
||||
, Css.disabled [ Css.cursor Css.notAllowed ]
|
||||
, Css.displayFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.justifyContent Css.center
|
||||
]
|
||||
|
||||
|
||||
colorStyle : ColorPalette -> Style
|
||||
colorStyle colorPalette =
|
||||
let
|
||||
( config, additionalStyles ) =
|
||||
case colorPalette of
|
||||
PrimaryColors ->
|
||||
( { background = Colors.azure
|
||||
, hover = Colors.azureDark
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.azureDark
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
SecondaryColors ->
|
||||
( { background = Colors.white
|
||||
, hover = Colors.glacier
|
||||
, text = Colors.azure
|
||||
, border = Just <| Colors.azure
|
||||
, shadow = Colors.azure
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
BorderlessColors ->
|
||||
( { background = Css.rgba 0 0 0 0
|
||||
, hover = Css.rgba 0 0 0 0
|
||||
, text = Colors.azure
|
||||
, border = Nothing
|
||||
, shadow = Css.rgba 0 0 0 0
|
||||
}
|
||||
, [ Css.hover
|
||||
[ Css.textDecoration Css.underline
|
||||
, Css.disabled [ Css.textDecoration Css.none ]
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
DangerColors ->
|
||||
( { background = Colors.red
|
||||
, hover = Colors.redDark
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.redDark
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
PremiumColors ->
|
||||
( { background = Colors.yellow
|
||||
, hover = Colors.ochre
|
||||
, text = Colors.navy
|
||||
, border = Nothing
|
||||
, shadow = Colors.ochre
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
InactiveColors ->
|
||||
( { background = Colors.gray92
|
||||
, hover = Colors.gray92
|
||||
, text = Colors.gray45
|
||||
, border = Nothing
|
||||
, shadow = Colors.gray92
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
LoadingColors ->
|
||||
( { background = Colors.glacier
|
||||
, hover = Colors.glacier
|
||||
, text = Colors.navy
|
||||
, border = Nothing
|
||||
, shadow = Colors.glacier
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
SuccessColors ->
|
||||
( { background = Colors.greenDark
|
||||
, hover = Colors.greenDark
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.greenDark
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
ErrorColors ->
|
||||
( { background = Colors.purple
|
||||
, hover = Colors.purple
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.purple
|
||||
}
|
||||
, []
|
||||
)
|
||||
in
|
||||
Css.batch
|
||||
[ Css.batch additionalStyles
|
||||
, Css.color config.text
|
||||
, Css.backgroundColor config.background
|
||||
, Css.fontWeight (Css.int 700)
|
||||
, Css.textAlign Css.center
|
||||
, case config.border of
|
||||
Nothing ->
|
||||
Css.borderStyle Css.none
|
||||
|
||||
Just color ->
|
||||
Css.batch
|
||||
[ Css.borderColor color
|
||||
, Css.borderStyle Css.solid
|
||||
]
|
||||
, Css.borderBottomStyle Css.solid
|
||||
, Css.borderBottomColor config.shadow
|
||||
, Css.fontStyle Css.normal
|
||||
, Css.hover
|
||||
[ Css.color config.text
|
||||
, Css.backgroundColor config.hover
|
||||
, Css.disabled [ Css.backgroundColor config.background ]
|
||||
]
|
||||
, Css.visited [ Css.color config.text ]
|
||||
]
|
||||
|
||||
|
||||
sizeStyle : ButtonSize -> ButtonWidth -> Style
|
||||
sizeStyle size width =
|
||||
let
|
||||
config =
|
||||
case size of
|
||||
Small ->
|
||||
{ fontSize = 15
|
||||
, height = 36
|
||||
, imageHeight = 15
|
||||
, shadowHeight = 2
|
||||
, minWidth = 75
|
||||
}
|
||||
|
||||
Medium ->
|
||||
{ fontSize = 17
|
||||
, height = 45
|
||||
, imageHeight = 15
|
||||
, shadowHeight = 3
|
||||
, minWidth = 100
|
||||
}
|
||||
|
||||
Large ->
|
||||
{ fontSize = 20
|
||||
, height = 56
|
||||
, imageHeight = 20
|
||||
, shadowHeight = 4
|
||||
, minWidth = 200
|
||||
}
|
||||
|
||||
sizingAttributes =
|
||||
let
|
||||
verticalPaddingPx =
|
||||
2
|
||||
in
|
||||
[ Css.minHeight (Css.px config.height)
|
||||
, Css.paddingTop (Css.px verticalPaddingPx)
|
||||
, Css.paddingBottom (Css.px verticalPaddingPx)
|
||||
]
|
||||
|
||||
widthAttributes =
|
||||
case width of
|
||||
WidthExact pxWidth ->
|
||||
[ Css.maxWidth (Css.pct 100)
|
||||
, Css.width (Css.px <| toFloat pxWidth)
|
||||
, Css.paddingRight (Css.px 4)
|
||||
, Css.paddingLeft (Css.px 4)
|
||||
]
|
||||
|
||||
WidthUnbounded ->
|
||||
[ Css.paddingLeft (Css.px 16)
|
||||
, Css.paddingRight (Css.px 16)
|
||||
, Css.minWidth (Css.px config.minWidth)
|
||||
]
|
||||
|
||||
lineHeightPx =
|
||||
case size of
|
||||
Small ->
|
||||
15
|
||||
|
||||
Medium ->
|
||||
19
|
||||
|
||||
Large ->
|
||||
22
|
||||
in
|
||||
Css.batch
|
||||
[ Css.fontSize (Css.px config.fontSize)
|
||||
, Css.borderRadius (Css.px 8)
|
||||
, Css.lineHeight (Css.px lineHeightPx)
|
||||
, Css.boxSizing Css.borderBox
|
||||
, Css.borderWidth (Css.px 1)
|
||||
, Css.borderBottomWidth (Css.px config.shadowHeight)
|
||||
, Css.batch sizingAttributes
|
||||
, Css.batch widthAttributes
|
||||
, Css.Global.descendants
|
||||
[ Css.Global.img
|
||||
[ Css.height (Css.px config.imageHeight)
|
||||
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||
, Css.position Css.relative
|
||||
, Css.bottom (Css.px 2)
|
||||
, Css.verticalAlign Css.middle
|
||||
]
|
||||
, Css.Global.svg
|
||||
[ Css.height (Css.px config.imageHeight) |> Css.important
|
||||
, Css.width (Css.px config.imageHeight) |> Css.important
|
||||
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||
, Css.position Css.relative
|
||||
, Css.bottom (Css.px 2)
|
||||
, Css.verticalAlign Css.middle
|
||||
]
|
||||
, Css.Global.svg
|
||||
[ Css.important <| Css.height (Css.px config.imageHeight)
|
||||
, Css.important <| Css.width Css.auto
|
||||
, Css.maxWidth (Css.px (config.imageHeight * 1.25))
|
||||
, Css.paddingRight (Css.px <| config.imageHeight / 6)
|
||||
, Css.position Css.relative
|
||||
, Css.bottom (Css.px 2)
|
||||
, Css.verticalAlign Css.middle
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
styledName : String -> String
|
||||
styledName suffix =
|
||||
"Nri-Ui-Button-V6-" ++ suffix
|
@ -1,818 +0,0 @@
|
||||
module Nri.Ui.Button.V7 exposing
|
||||
( ButtonSize(..), ButtonWidth(..), ButtonStyle(..), ButtonState(..), ButtonContent
|
||||
, ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
|
||||
, LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
|
||||
# Changes from V6:
|
||||
|
||||
- WidthUnbounded button displays inlineFlex, so that it doesn't accidentally take up the entire width
|
||||
- Add WidthFillContainer, for when we intend to take up the full container
|
||||
|
||||
|
||||
# About:
|
||||
|
||||
Common NoRedInk buttons. For accessibility purposes, buttons that perform an
|
||||
action on the current page should be HTML `<button>` elements and are created here
|
||||
with `*Button` functions. Buttons that take the user to a new page should be
|
||||
HTML `<a>` elements and are created here with `*Link` functions. Both versions
|
||||
should be able to use the same CSS class in all cases.
|
||||
|
||||
There will generally be a `*Button` and `*Link` version of each button style.
|
||||
(These will be created as they are needed.)
|
||||
|
||||
In general a button should never truncate or obscure its contents. This could
|
||||
make it difficult or impossible for a student or teacher to use the site, so in
|
||||
general choose buttons that grow to fit their contents. It is better to risk
|
||||
weird layout than to block users. Might this be a golden rule? Of course there
|
||||
may be exceptions, for example if button content is supplied by an end-user.
|
||||
|
||||
|
||||
## Common configs
|
||||
|
||||
@docs ButtonSize, ButtonWidth, ButtonStyle, ButtonState, ButtonContent
|
||||
|
||||
|
||||
## `<button>` Buttons
|
||||
|
||||
@docs ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
|
||||
|
||||
|
||||
## `<a>` Buttons
|
||||
|
||||
@docs LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Html exposing (Attribute, Html)
|
||||
import Accessibility.Styled.Role as Role
|
||||
import Accessibility.Styled.Widget as Widget
|
||||
import Css exposing (Style)
|
||||
import Css.Global
|
||||
import EventExtras.Styled as EventExtras
|
||||
import Html.Styled as Styled
|
||||
import Html.Styled.Attributes as Attributes
|
||||
import Html.Styled.Events as Events
|
||||
import Json.Decode
|
||||
import Markdown.Block
|
||||
import Markdown.Inline
|
||||
import Nri.Ui
|
||||
import Nri.Ui.AssetPath as AssetPath exposing (Asset)
|
||||
import Nri.Ui.Colors.Extra as ColorsExtra
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Fonts.V1
|
||||
import Nri.Ui.Icon.V4 as Icon exposing (IconType)
|
||||
|
||||
|
||||
{-| Sizes for buttons and links that have button classes
|
||||
-}
|
||||
type ButtonSize
|
||||
= Small
|
||||
| Medium
|
||||
| Large
|
||||
|
||||
|
||||
{-| Width sizing behavior for buttons.
|
||||
|
||||
`WidthExact Int` defines a size in `px` for the button's total width, and
|
||||
`WidthUnbounded` leaves the maxiumum width unbounded (there is a minimum width).
|
||||
|
||||
-}
|
||||
type ButtonWidth
|
||||
= WidthExact Int
|
||||
| WidthUnbounded
|
||||
| WidthFillContainer
|
||||
|
||||
|
||||
{-| Styleguide-approved styles for your buttons!
|
||||
|
||||
Note on borderless buttons:
|
||||
A borderless button that performs an action on the current page
|
||||
This button is intended to look like a link.
|
||||
Only use a borderless button when the clickable text in question follows the same layout/margin/padding as a bordered button
|
||||
|
||||
-}
|
||||
type ButtonStyle
|
||||
= Primary
|
||||
| Secondary
|
||||
| Borderless
|
||||
| Danger
|
||||
| Premium
|
||||
|
||||
|
||||
{-| Describes the state of a button. Has consequences for appearance and disabled attribute.
|
||||
|
||||
- Enabled: An enabled button. Takes the appearance of ButtonStyle
|
||||
- Unfulfilled: A button which appears with the InactiveColors palette but is not disabled.
|
||||
- Disabled: A button which appears with the InactiveColors palette and is disabled.
|
||||
- Error: A button which appears with the ErrorColors palette and is disabled.
|
||||
- Loading: A button which appears with the LoadingColors palette and is disabled
|
||||
- Success: A button which appears with the SuccessColors palette and is disabled
|
||||
|
||||
-}
|
||||
type ButtonState
|
||||
= Enabled
|
||||
| Unfulfilled
|
||||
| Disabled
|
||||
| Error
|
||||
| Loading
|
||||
| Success
|
||||
|
||||
|
||||
{-| The part of a button that remains constant through different button states
|
||||
-}
|
||||
type alias ButtonConfig msg =
|
||||
{ onClick : msg
|
||||
, size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, width : ButtonWidth
|
||||
}
|
||||
|
||||
|
||||
{-| ButtonContent, often changes based on ButtonState. For example, a button in the "Success"
|
||||
state may have a different label than a button in the "Error" state
|
||||
-}
|
||||
type alias ButtonContent =
|
||||
{ label : String
|
||||
, state : ButtonState
|
||||
, icon : Maybe IconType
|
||||
}
|
||||
|
||||
|
||||
{-| A delightful button which can trigger an effect when clicked!
|
||||
|
||||
This button will trigger the passed-in message if the button state is:
|
||||
|
||||
- Enabled
|
||||
- Unfulfilled
|
||||
|
||||
This button will be Disabled if the button state is:
|
||||
|
||||
- Disabled
|
||||
- Error
|
||||
- Loading
|
||||
- Success
|
||||
|
||||
-}
|
||||
button : ButtonConfig msg -> ButtonContent -> Html msg
|
||||
button config content =
|
||||
customButton [] config content
|
||||
|
||||
|
||||
{-| Exactly the same as button but you can pass in a list of attributes
|
||||
-}
|
||||
customButton : List (Attribute msg) -> ButtonConfig msg -> ButtonContent -> Html msg
|
||||
customButton attributes config content =
|
||||
let
|
||||
buttonStyle_ =
|
||||
case content.state of
|
||||
Enabled ->
|
||||
styleToColorPalette config.style
|
||||
|
||||
Disabled ->
|
||||
InactiveColors
|
||||
|
||||
Error ->
|
||||
ErrorColors
|
||||
|
||||
Unfulfilled ->
|
||||
InactiveColors
|
||||
|
||||
Loading ->
|
||||
LoadingColors
|
||||
|
||||
Success ->
|
||||
SuccessColors
|
||||
|
||||
disabled =
|
||||
case content.state of
|
||||
Enabled ->
|
||||
False
|
||||
|
||||
Disabled ->
|
||||
True
|
||||
|
||||
Error ->
|
||||
True
|
||||
|
||||
Unfulfilled ->
|
||||
False
|
||||
|
||||
Loading ->
|
||||
True
|
||||
|
||||
Success ->
|
||||
True
|
||||
in
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "customButton")
|
||||
[ buttonStyles config.size config.width buttonStyle_ ]
|
||||
([ Events.onClick config.onClick
|
||||
, Attributes.disabled disabled
|
||||
, Attributes.type_ "button"
|
||||
]
|
||||
++ attributes
|
||||
)
|
||||
[ viewLabel content.icon content.label ]
|
||||
|
||||
|
||||
|
||||
-- COPY TO CLIPBOARD BUTTON
|
||||
|
||||
|
||||
{-| Config for copyToClipboard
|
||||
-}
|
||||
type alias CopyToClipboardConfig =
|
||||
{ size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, copyText : String
|
||||
, buttonLabel : String
|
||||
, withIcon : Bool
|
||||
, width : ButtonWidth
|
||||
}
|
||||
|
||||
|
||||
{-| See ui/src/Page/Teach/Courses/Assignments/index.coffee
|
||||
You will need to hook this up to clipboard.js
|
||||
-}
|
||||
copyToClipboard : { r | teach_assignments_copyWhite_svg : Asset } -> CopyToClipboardConfig -> Html msg
|
||||
copyToClipboard assets config =
|
||||
let
|
||||
maybeIcon =
|
||||
if config.withIcon then
|
||||
Just (Icon.copy assets)
|
||||
|
||||
else
|
||||
Nothing
|
||||
in
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "copyToClipboard")
|
||||
[ buttonStyles config.size config.width (styleToColorPalette config.style) ]
|
||||
[ Widget.label "Copy URL to clipboard"
|
||||
, Attributes.attribute "data-clipboard-text" config.copyText
|
||||
]
|
||||
[ viewLabel maybeIcon config.buttonLabel ]
|
||||
|
||||
|
||||
|
||||
-- DELETE BUTTON
|
||||
|
||||
|
||||
type alias DeleteButtonConfig msg =
|
||||
{ label : String
|
||||
, onClick : msg
|
||||
}
|
||||
|
||||
|
||||
{-| A delete button (blue X)
|
||||
-}
|
||||
delete : { r | x : String } -> DeleteButtonConfig msg -> Html msg
|
||||
delete assets config =
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "delete")
|
||||
[ Css.display Css.inlineBlock
|
||||
, Css.backgroundRepeat Css.noRepeat
|
||||
, Css.backgroundColor Css.transparent
|
||||
, Css.backgroundPosition Css.center
|
||||
, Css.backgroundSize Css.contain
|
||||
, Css.border Css.zero
|
||||
, Css.width (Css.px 15)
|
||||
, Css.height (Css.px 15)
|
||||
, Css.padding Css.zero
|
||||
, Css.margin2 Css.zero (Css.px 6)
|
||||
, Css.cursor Css.pointer
|
||||
, Css.color Colors.azure
|
||||
]
|
||||
[ Events.onClick config.onClick
|
||||
, Attributes.type_ "button"
|
||||
, -- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||
Widget.label config.label
|
||||
]
|
||||
[ Icon.icon { alt = "Delete", icon = Icon.xSvg assets } ]
|
||||
|
||||
|
||||
|
||||
-- TOGGLE BUTTON
|
||||
|
||||
|
||||
{-| Buttons can be toggled into a pressed state and back again.
|
||||
-}
|
||||
type alias ToggleButtonConfig msg =
|
||||
{ label : String
|
||||
, onSelect : msg
|
||||
, onDeselect : msg
|
||||
, pressed : Bool
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
toggleButton : ToggleButtonConfig msg -> Html msg
|
||||
toggleButton config =
|
||||
let
|
||||
toggledStyles =
|
||||
if config.pressed then
|
||||
Css.batch
|
||||
[ Css.color Colors.gray20
|
||||
, Css.backgroundColor Colors.glacier
|
||||
, Css.boxShadow5 Css.inset Css.zero (Css.px 3) Css.zero (ColorsExtra.withAlpha 0.2 Colors.gray20)
|
||||
, Css.border3 (Css.px 1) Css.solid Colors.azure
|
||||
, Css.fontWeight Css.bold
|
||||
]
|
||||
|
||||
else
|
||||
Css.batch
|
||||
[]
|
||||
in
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "toggleButton")
|
||||
[ buttonStyles Medium WidthUnbounded SecondaryColors
|
||||
, toggledStyles
|
||||
]
|
||||
[ Events.onClick
|
||||
(if config.pressed then
|
||||
config.onDeselect
|
||||
|
||||
else
|
||||
config.onSelect
|
||||
)
|
||||
, Widget.pressed <| Just config.pressed
|
||||
|
||||
-- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||
, Role.button
|
||||
|
||||
-- Note: setting type: 'button' removes the default behavior of submit
|
||||
-- equivalent to preventDefaultBehavior = false
|
||||
-- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-name
|
||||
, Attributes.type_ "button"
|
||||
]
|
||||
[ viewLabel Nothing config.label ]
|
||||
|
||||
|
||||
{-| Inputs can be a clickable thing used in a form
|
||||
-}
|
||||
type alias InputConfig =
|
||||
{ content : Html Never
|
||||
, name : String
|
||||
, size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, value : String
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- LINKS THAT LOOK LIKE BUTTONS
|
||||
|
||||
|
||||
{-| Links are clickable things with a url.
|
||||
|
||||
NOTE: Links do not support two-line labels.
|
||||
|
||||
-}
|
||||
type alias LinkConfig =
|
||||
{ label : String
|
||||
, icon : Maybe IconType
|
||||
, url : String
|
||||
, size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, width : ButtonWidth
|
||||
}
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||
some url
|
||||
-}
|
||||
link : LinkConfig -> Html msg
|
||||
link =
|
||||
linkBase "link" [ Attributes.target "_self" ]
|
||||
|
||||
|
||||
{-| Use this link for routing within a single page app.
|
||||
|
||||
This will make a normal <a> tag, but change the Events.onClick behavior to avoid reloading the page.
|
||||
|
||||
See <https://github.com/elm-lang/html/issues/110> for details on this implementation.
|
||||
|
||||
-}
|
||||
linkSpa :
|
||||
(route -> String)
|
||||
-> (route -> msg)
|
||||
->
|
||||
{ label : String
|
||||
, icon : Maybe IconType
|
||||
, size : ButtonSize
|
||||
, style : ButtonStyle
|
||||
, width : ButtonWidth
|
||||
, route : route
|
||||
}
|
||||
-> Html msg
|
||||
linkSpa toUrl toMsg config =
|
||||
linkBase
|
||||
"linkSpa"
|
||||
[ EventExtras.onClickPreventDefaultForLinkWithHref (toMsg config.route)
|
||||
]
|
||||
{ label = config.label
|
||||
, icon = config.icon
|
||||
, size = config.size
|
||||
, style = config.style
|
||||
, width = config.width
|
||||
, url = toUrl config.route
|
||||
}
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||
some url and have it open to an external site
|
||||
-}
|
||||
linkExternal : LinkConfig -> Html msg
|
||||
linkExternal =
|
||||
linkBase "linkExternal" [ Attributes.target "_blank" ]
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||
some url, and it's an HTTP request (Rails includes JS to make this use the given HTTP method)
|
||||
-}
|
||||
linkWithMethod : String -> LinkConfig -> Html msg
|
||||
linkWithMethod method =
|
||||
linkBase "linkWithMethod" [ Attributes.attribute "data-method" method ]
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url.
|
||||
This should only take in messages that result in a Msg that triggers Analytics.trackAndRedirect. For buttons that trigger other effects on the page, please use Nri.Button.button instead
|
||||
-}
|
||||
linkWithTracking : msg -> LinkConfig -> Html msg
|
||||
linkWithTracking onTrack =
|
||||
linkBase
|
||||
"linkWithTracking"
|
||||
[ Events.preventDefaultOn "click"
|
||||
(Json.Decode.succeed ( onTrack, True ))
|
||||
]
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url and have it open to an external site
|
||||
|
||||
This should only take in messages that result in tracking events. For buttons that trigger other effects on the page, please use Nri.Ui.Button.V2.button instead
|
||||
|
||||
-}
|
||||
linkExternalWithTracking : msg -> LinkConfig -> Html msg
|
||||
linkExternalWithTracking onTrack =
|
||||
linkBase
|
||||
"linkExternalWithTracking"
|
||||
[ Attributes.target "_blank"
|
||||
, EventExtras.onClickForLinkWithHref onTrack
|
||||
]
|
||||
|
||||
|
||||
{-| Helper function for building links with an arbitrary number of Attributes
|
||||
-}
|
||||
linkBase : String -> List (Attribute msg) -> LinkConfig -> Html msg
|
||||
linkBase linkFunctionName extraAttrs config =
|
||||
Nri.Ui.styled Styled.a
|
||||
(styledName linkFunctionName)
|
||||
[ buttonStyles config.size config.width (styleToColorPalette config.style) ]
|
||||
(Attributes.href config.url
|
||||
:: extraAttrs
|
||||
)
|
||||
[ viewLabel config.icon config.label ]
|
||||
|
||||
|
||||
|
||||
-- HELPERS
|
||||
|
||||
|
||||
type ColorPalette
|
||||
= PrimaryColors
|
||||
| SecondaryColors
|
||||
| BorderlessColors
|
||||
| DangerColors
|
||||
| PremiumColors
|
||||
| InactiveColors
|
||||
| LoadingColors
|
||||
| SuccessColors
|
||||
| ErrorColors
|
||||
|
||||
|
||||
styleToColorPalette : ButtonStyle -> ColorPalette
|
||||
styleToColorPalette style =
|
||||
case style of
|
||||
Primary ->
|
||||
PrimaryColors
|
||||
|
||||
Secondary ->
|
||||
SecondaryColors
|
||||
|
||||
Borderless ->
|
||||
BorderlessColors
|
||||
|
||||
Danger ->
|
||||
DangerColors
|
||||
|
||||
Premium ->
|
||||
PremiumColors
|
||||
|
||||
|
||||
buttonStyles : ButtonSize -> ButtonWidth -> ColorPalette -> Style
|
||||
buttonStyles size width colorPalette =
|
||||
Css.batch
|
||||
[ buttonStyle
|
||||
, colorStyle colorPalette
|
||||
, sizeStyle size width
|
||||
]
|
||||
|
||||
|
||||
viewLabel : Maybe IconType -> String -> Html msg
|
||||
viewLabel icn label =
|
||||
Nri.Ui.styled Html.span
|
||||
"button-label-span"
|
||||
[ Css.overflow Css.hidden -- Keep scrollbars out of our button
|
||||
, Css.overflowWrap Css.breakWord -- Ensure that words that exceed the button width break instead of disappearing
|
||||
, Css.padding2 (Css.px 2) Css.zero -- Without a bit of bottom padding, text that extends below the baseline, like "g" gets cut off
|
||||
]
|
||||
[]
|
||||
(case icn of
|
||||
Nothing ->
|
||||
renderMarkdown label
|
||||
|
||||
Just iconType ->
|
||||
Icon.decorativeIcon iconType :: renderMarkdown label
|
||||
)
|
||||
|
||||
|
||||
renderMarkdown : String -> List (Html msg)
|
||||
renderMarkdown markdown =
|
||||
case Markdown.Block.parse Nothing markdown of
|
||||
-- It seems to be always first wrapped in a `Paragraph` and never directly a `PlainInline`
|
||||
[ Markdown.Block.Paragraph _ inlines ] ->
|
||||
List.map (Markdown.Inline.toHtml >> Styled.fromUnstyled) inlines
|
||||
|
||||
_ ->
|
||||
[ Html.text markdown ]
|
||||
|
||||
|
||||
|
||||
-- STYLES
|
||||
|
||||
|
||||
buttonStyle : Style
|
||||
buttonStyle =
|
||||
Css.batch
|
||||
[ Css.cursor Css.pointer
|
||||
, -- Specifying the font can and should go away after bootstrap is removed from application.css
|
||||
Nri.Ui.Fonts.V1.baseFont
|
||||
, Css.textOverflow Css.ellipsis
|
||||
, Css.overflow Css.hidden
|
||||
, Css.textDecoration Css.none
|
||||
, Css.backgroundImage Css.none
|
||||
, Css.textShadow Css.none
|
||||
, Css.property "transition" "background-color 0.2s, color 0.2s, box-shadow 0.2s, border 0.2s, border-width 0s"
|
||||
, Css.boxShadow Css.none
|
||||
, Css.border Css.zero
|
||||
, Css.marginBottom Css.zero
|
||||
, Css.hover [ Css.textDecoration Css.none ]
|
||||
, Css.disabled [ Css.cursor Css.notAllowed ]
|
||||
, Css.display Css.inlineFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.justifyContent Css.center
|
||||
]
|
||||
|
||||
|
||||
colorStyle : ColorPalette -> Style
|
||||
colorStyle colorPalette =
|
||||
let
|
||||
( config, additionalStyles ) =
|
||||
case colorPalette of
|
||||
PrimaryColors ->
|
||||
( { background = Colors.azure
|
||||
, hover = Colors.azureDark
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.azureDark
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
SecondaryColors ->
|
||||
( { background = Colors.white
|
||||
, hover = Colors.glacier
|
||||
, text = Colors.azure
|
||||
, border = Just <| Colors.azure
|
||||
, shadow = Colors.azure
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
BorderlessColors ->
|
||||
( { background = Css.rgba 0 0 0 0
|
||||
, hover = Css.rgba 0 0 0 0
|
||||
, text = Colors.azure
|
||||
, border = Nothing
|
||||
, shadow = Css.rgba 0 0 0 0
|
||||
}
|
||||
, [ Css.hover
|
||||
[ Css.textDecoration Css.underline
|
||||
, Css.disabled [ Css.textDecoration Css.none ]
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
DangerColors ->
|
||||
( { background = Colors.red
|
||||
, hover = Colors.redDark
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.redDark
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
PremiumColors ->
|
||||
( { background = Colors.yellow
|
||||
, hover = Colors.ochre
|
||||
, text = Colors.navy
|
||||
, border = Nothing
|
||||
, shadow = Colors.ochre
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
InactiveColors ->
|
||||
( { background = Colors.gray92
|
||||
, hover = Colors.gray92
|
||||
, text = Colors.gray45
|
||||
, border = Nothing
|
||||
, shadow = Colors.gray92
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
LoadingColors ->
|
||||
( { background = Colors.glacier
|
||||
, hover = Colors.glacier
|
||||
, text = Colors.navy
|
||||
, border = Nothing
|
||||
, shadow = Colors.glacier
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
SuccessColors ->
|
||||
( { background = Colors.greenDark
|
||||
, hover = Colors.greenDark
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.greenDark
|
||||
}
|
||||
, []
|
||||
)
|
||||
|
||||
ErrorColors ->
|
||||
( { background = Colors.purple
|
||||
, hover = Colors.purple
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.purple
|
||||
}
|
||||
, []
|
||||
)
|
||||
in
|
||||
Css.batch
|
||||
[ Css.batch additionalStyles
|
||||
, Css.color config.text
|
||||
, Css.backgroundColor config.background
|
||||
, Css.fontWeight (Css.int 700)
|
||||
, Css.textAlign Css.center
|
||||
, case config.border of
|
||||
Nothing ->
|
||||
Css.borderStyle Css.none
|
||||
|
||||
Just color ->
|
||||
Css.batch
|
||||
[ Css.borderColor color
|
||||
, Css.borderStyle Css.solid
|
||||
]
|
||||
, Css.borderBottomStyle Css.solid
|
||||
, Css.borderBottomColor config.shadow
|
||||
, Css.fontStyle Css.normal
|
||||
, Css.hover
|
||||
[ Css.color config.text
|
||||
, Css.backgroundColor config.hover
|
||||
, Css.disabled [ Css.backgroundColor config.background ]
|
||||
]
|
||||
, Css.visited [ Css.color config.text ]
|
||||
]
|
||||
|
||||
|
||||
sizeStyle : ButtonSize -> ButtonWidth -> Style
|
||||
sizeStyle size width =
|
||||
let
|
||||
config =
|
||||
case size of
|
||||
Small ->
|
||||
{ fontSize = 15
|
||||
, height = 36
|
||||
, imageHeight = 15
|
||||
, shadowHeight = 2
|
||||
, minWidth = 75
|
||||
}
|
||||
|
||||
Medium ->
|
||||
{ fontSize = 17
|
||||
, height = 45
|
||||
, imageHeight = 15
|
||||
, shadowHeight = 3
|
||||
, minWidth = 100
|
||||
}
|
||||
|
||||
Large ->
|
||||
{ fontSize = 20
|
||||
, height = 56
|
||||
, imageHeight = 20
|
||||
, shadowHeight = 4
|
||||
, minWidth = 200
|
||||
}
|
||||
|
||||
sizingAttributes =
|
||||
let
|
||||
verticalPaddingPx =
|
||||
2
|
||||
in
|
||||
[ Css.minHeight (Css.px config.height)
|
||||
, Css.paddingTop (Css.px verticalPaddingPx)
|
||||
, Css.paddingBottom (Css.px verticalPaddingPx)
|
||||
]
|
||||
|
||||
widthAttributes =
|
||||
case width of
|
||||
WidthExact pxWidth ->
|
||||
[ Css.maxWidth (Css.pct 100)
|
||||
, Css.width (Css.px <| toFloat pxWidth)
|
||||
, Css.paddingRight (Css.px 4)
|
||||
, Css.paddingLeft (Css.px 4)
|
||||
]
|
||||
|
||||
WidthUnbounded ->
|
||||
[ Css.paddingLeft (Css.px 16)
|
||||
, Css.paddingRight (Css.px 16)
|
||||
, Css.minWidth (Css.px config.minWidth)
|
||||
]
|
||||
|
||||
WidthFillContainer ->
|
||||
[ Css.paddingLeft (Css.px 16)
|
||||
, Css.paddingRight (Css.px 16)
|
||||
, Css.minWidth (Css.px config.minWidth)
|
||||
, Css.width (Css.pct 100)
|
||||
]
|
||||
|
||||
lineHeightPx =
|
||||
case size of
|
||||
Small ->
|
||||
15
|
||||
|
||||
Medium ->
|
||||
19
|
||||
|
||||
Large ->
|
||||
22
|
||||
in
|
||||
Css.batch
|
||||
[ Css.fontSize (Css.px config.fontSize)
|
||||
, Css.borderRadius (Css.px 8)
|
||||
, Css.lineHeight (Css.px lineHeightPx)
|
||||
, Css.boxSizing Css.borderBox
|
||||
, Css.borderWidth (Css.px 1)
|
||||
, Css.borderBottomWidth (Css.px config.shadowHeight)
|
||||
, Css.batch sizingAttributes
|
||||
, Css.batch widthAttributes
|
||||
, Css.Global.descendants
|
||||
[ Css.Global.img
|
||||
[ Css.height (Css.px config.imageHeight)
|
||||
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||
, Css.position Css.relative
|
||||
, Css.bottom (Css.px 2)
|
||||
, Css.verticalAlign Css.middle
|
||||
]
|
||||
, Css.Global.svg
|
||||
[ Css.height (Css.px config.imageHeight) |> Css.important
|
||||
, Css.width (Css.px config.imageHeight) |> Css.important
|
||||
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||
, Css.position Css.relative
|
||||
, Css.bottom (Css.px 2)
|
||||
, Css.verticalAlign Css.middle
|
||||
]
|
||||
, Css.Global.svg
|
||||
[ Css.important <| Css.height (Css.px config.imageHeight)
|
||||
, Css.important <| Css.width Css.auto
|
||||
, Css.maxWidth (Css.px (config.imageHeight * 1.25))
|
||||
, Css.paddingRight (Css.px <| config.imageHeight / 6)
|
||||
, Css.position Css.relative
|
||||
, Css.bottom (Css.px 2)
|
||||
, Css.verticalAlign Css.middle
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
styledName : String -> String
|
||||
styledName suffix =
|
||||
"Nri-Ui-Button-V7-" ++ suffix
|
@ -1,947 +0,0 @@
|
||||
module Nri.Ui.Button.V9 exposing
|
||||
( button, link
|
||||
, Attribute
|
||||
, icon, custom
|
||||
, onClick
|
||||
, href, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||
, small, medium, large
|
||||
, exactWidth, unboundedWidth, fillContainerWidth
|
||||
, primary, secondary, danger, premium
|
||||
, enabled, unfulfilled, disabled, error, loading, success
|
||||
, delete
|
||||
, toggleButton
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
|
||||
# Changes from V8:
|
||||
|
||||
- Changes API to be attribute-based, rather than config-based
|
||||
|
||||
|
||||
# Create a button or link
|
||||
|
||||
@docs button, link
|
||||
@docs Attribute
|
||||
@docs icon, custom
|
||||
|
||||
|
||||
## Behavior
|
||||
|
||||
@docs onClick
|
||||
@docs href, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||
|
||||
|
||||
## Sizing
|
||||
|
||||
@docs small, medium, large
|
||||
@docs exactWidth, unboundedWidth, fillContainerWidth
|
||||
|
||||
|
||||
## Change the color scheme
|
||||
|
||||
@docs primary, secondary, danger, premium
|
||||
|
||||
|
||||
## Change the state (buttons only)
|
||||
|
||||
@docs enabled, unfulfilled, disabled, error, loading, success
|
||||
|
||||
|
||||
# Commonly-used buttons
|
||||
|
||||
@docs delete
|
||||
@docs toggleButton
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Html exposing (Html)
|
||||
import Accessibility.Styled.Role as Role
|
||||
import Accessibility.Styled.Widget as Widget
|
||||
import AttributeExtras exposing (targetBlank)
|
||||
import Css exposing (Style)
|
||||
import Css.Global
|
||||
import EventExtras.Styled as EventExtras
|
||||
import Html as RootHtml
|
||||
import Html.Styled as Styled
|
||||
import Html.Styled.Attributes as Attributes
|
||||
import Html.Styled.Events as Events
|
||||
import Json.Decode
|
||||
import Markdown.Block
|
||||
import Markdown.Inline
|
||||
import Nri.Ui
|
||||
import Nri.Ui.AssetPath as AssetPath exposing (Asset)
|
||||
import Nri.Ui.Colors.Extra as ColorsExtra
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Fonts.V1
|
||||
import Nri.Ui.Html.Attributes.V2 as AttributesExtra
|
||||
import Nri.Ui.Svg.V1 as NriSvg exposing (Svg)
|
||||
import Svg
|
||||
import Svg.Attributes
|
||||
|
||||
|
||||
styledName : String -> String
|
||||
styledName suffix =
|
||||
"Nri-Ui-Button-V9-" ++ suffix
|
||||
|
||||
|
||||
{-|
|
||||
|
||||
Button.button "My great button!"
|
||||
[ Button.onClick ()
|
||||
, Button.enabled
|
||||
]
|
||||
|
||||
By default, the button is enabled, Medium sized, with primary colors, and an unbounded width.
|
||||
|
||||
-}
|
||||
button : String -> List (Attribute msg) -> Html msg
|
||||
button name attributes =
|
||||
(label name :: attributes)
|
||||
|> List.foldl (\(Attribute attribute) b -> attribute b) build
|
||||
|> renderButton
|
||||
|
||||
|
||||
{-|
|
||||
|
||||
Button.link "My great link!"
|
||||
[ Button.href "My href"
|
||||
, Button.secondary
|
||||
]
|
||||
|
||||
By default, the link is Medium sized, with primary colors, and an unbounded width.
|
||||
|
||||
-}
|
||||
link : String -> List (Attribute msg) -> Html msg
|
||||
link name attributes =
|
||||
(label name :: attributes)
|
||||
|> List.foldl (\(Attribute attribute) l -> attribute l) build
|
||||
|> renderLink
|
||||
|
||||
|
||||
{-| -}
|
||||
label : String -> Attribute msg
|
||||
label label_ =
|
||||
set (\attributes -> { attributes | label = label_ })
|
||||
|
||||
|
||||
{-| -}
|
||||
icon : Svg -> Attribute msg
|
||||
icon icon_ =
|
||||
set (\attributes -> { attributes | icon = Just icon_ })
|
||||
|
||||
|
||||
{-| -}
|
||||
custom : List (Html.Attribute msg) -> Attribute msg
|
||||
custom attributes =
|
||||
set
|
||||
(\config ->
|
||||
{ config
|
||||
| customAttributes = List.append config.customAttributes attributes
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
-- LINKING, CLICKING, and TRACKING BEHAVIOR
|
||||
|
||||
|
||||
{-| -}
|
||||
onClick : msg -> Attribute msg
|
||||
onClick msg =
|
||||
set (\attributes -> { attributes | onClick = Just msg })
|
||||
|
||||
|
||||
type Link
|
||||
= Default
|
||||
| WithTracking
|
||||
| SinglePageApp
|
||||
| WithMethod String
|
||||
| External
|
||||
| ExternalWithTracking
|
||||
|
||||
|
||||
{-| -}
|
||||
href : String -> Attribute msg
|
||||
href url =
|
||||
set (\attributes -> { attributes | url = url })
|
||||
|
||||
|
||||
{-| Use this link for routing within a single page app.
|
||||
|
||||
This will make a normal <a> tag, but change the Events.onClick behavior to avoid reloading the page.
|
||||
|
||||
See <https://github.com/elm-lang/html/issues/110> for details on this implementation.
|
||||
|
||||
-}
|
||||
linkSpa : String -> Attribute msg
|
||||
linkSpa url =
|
||||
set
|
||||
(\attributes ->
|
||||
{ attributes
|
||||
| linkType = SinglePageApp
|
||||
, url = url
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||
some url, and it's an HTTP request (Rails includes JS to make this use the given HTTP method)
|
||||
-}
|
||||
linkWithMethod : { method : String, url : String } -> Attribute msg
|
||||
linkWithMethod { method, url } =
|
||||
set
|
||||
(\attributes ->
|
||||
{ attributes
|
||||
| linkType = WithMethod method
|
||||
, url = url
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url.
|
||||
This should only take in messages that result in a Msg that triggers Analytics.trackAndRedirect.
|
||||
For buttons that trigger other effects on the page, please use Nri.Button.button instead.
|
||||
-}
|
||||
linkWithTracking : { track : msg, url : String } -> Attribute msg
|
||||
linkWithTracking { track, url } =
|
||||
set
|
||||
(\attributes ->
|
||||
{ attributes
|
||||
| linkType = WithTracking
|
||||
, url = url
|
||||
, onClick = Just track
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||
some url and have it open to an external site
|
||||
-}
|
||||
linkExternal : String -> Attribute msg
|
||||
linkExternal url =
|
||||
set
|
||||
(\attributes ->
|
||||
{ attributes
|
||||
| linkType = External
|
||||
, url = url
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url and have it open to an external site.
|
||||
-}
|
||||
linkExternalWithTracking : { track : msg, url : String } -> Attribute msg
|
||||
linkExternalWithTracking { track, url } =
|
||||
set
|
||||
(\attributes ->
|
||||
{ attributes
|
||||
| linkType = ExternalWithTracking
|
||||
, url = url
|
||||
, onClick = Just track
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
-- BUTTON SIZING
|
||||
|
||||
|
||||
{-| -}
|
||||
small : Attribute msg
|
||||
small =
|
||||
set (\attributes -> { attributes | size = Small })
|
||||
|
||||
|
||||
{-| -}
|
||||
medium : Attribute msg
|
||||
medium =
|
||||
set (\attributes -> { attributes | size = Medium })
|
||||
|
||||
|
||||
{-| -}
|
||||
large : Attribute msg
|
||||
large =
|
||||
set (\attributes -> { attributes | size = Large })
|
||||
|
||||
|
||||
|
||||
-- BUTTON WIDTH
|
||||
|
||||
|
||||
type ButtonWidth
|
||||
= WidthExact Int
|
||||
| WidthUnbounded
|
||||
| WidthFillContainer
|
||||
|
||||
|
||||
{-| Sizes for buttons and links that have button classes
|
||||
-}
|
||||
type ButtonSize
|
||||
= Small
|
||||
| Medium
|
||||
| Large
|
||||
|
||||
|
||||
{-| Define a size in `px` for the button's total width.
|
||||
-}
|
||||
exactWidth : Int -> Attribute msg
|
||||
exactWidth inPx =
|
||||
set (\attributes -> { attributes | width = WidthExact inPx })
|
||||
|
||||
|
||||
{-| Leave the maxiumum width unbounded (there is a minimum width).
|
||||
-}
|
||||
unboundedWidth : Attribute msg
|
||||
unboundedWidth =
|
||||
set (\attributes -> { attributes | width = WidthUnbounded })
|
||||
|
||||
|
||||
{-| -}
|
||||
fillContainerWidth : Attribute msg
|
||||
fillContainerWidth =
|
||||
set (\attributes -> { attributes | width = WidthFillContainer })
|
||||
|
||||
|
||||
|
||||
-- COLOR SCHEMES
|
||||
|
||||
|
||||
{-| -}
|
||||
primary : Attribute msg
|
||||
primary =
|
||||
set
|
||||
(\attributes ->
|
||||
{ attributes | style = primaryColors }
|
||||
)
|
||||
|
||||
|
||||
{-| -}
|
||||
secondary : Attribute msg
|
||||
secondary =
|
||||
set
|
||||
(\attributes ->
|
||||
{ attributes | style = secondaryColors }
|
||||
)
|
||||
|
||||
|
||||
{-| -}
|
||||
danger : Attribute msg
|
||||
danger =
|
||||
set
|
||||
(\attributes ->
|
||||
{ attributes
|
||||
| style =
|
||||
{ background = Colors.red
|
||||
, hover = Colors.redDark
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.redDark
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
{-| -}
|
||||
premium : Attribute msg
|
||||
premium =
|
||||
set
|
||||
(\attributes ->
|
||||
{ attributes
|
||||
| style =
|
||||
{ background = Colors.yellow
|
||||
, hover = Colors.ochre
|
||||
, text = Colors.navy
|
||||
, border = Nothing
|
||||
, shadow = Colors.ochre
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
-- BUTTON STATE
|
||||
|
||||
|
||||
type ButtonState
|
||||
= Enabled
|
||||
| Unfulfilled
|
||||
| Disabled
|
||||
| Error
|
||||
| Loading
|
||||
| Success
|
||||
|
||||
|
||||
{-| -}
|
||||
enabled : Attribute msg
|
||||
enabled =
|
||||
set (\attributes -> { attributes | state = Enabled })
|
||||
|
||||
|
||||
{-| Shows inactive styles.
|
||||
-}
|
||||
unfulfilled : Attribute msg
|
||||
unfulfilled =
|
||||
set (\attributes -> { attributes | state = Unfulfilled })
|
||||
|
||||
|
||||
{-| Shows inactive styling. If a button, this attribute will disable it.
|
||||
-}
|
||||
disabled : Attribute msg
|
||||
disabled =
|
||||
set (\attributes -> { attributes | state = Disabled })
|
||||
|
||||
|
||||
{-| Shows error styling. If a button, this attribute will disable it.
|
||||
-}
|
||||
error : Attribute msg
|
||||
error =
|
||||
set (\attributes -> { attributes | state = Error })
|
||||
|
||||
|
||||
{-| Shows loading styling. If a button, this attribute will disable it.
|
||||
-}
|
||||
loading : Attribute msg
|
||||
loading =
|
||||
set (\attributes -> { attributes | state = Loading })
|
||||
|
||||
|
||||
{-| Shows success styling. If a button, this attribute will disable it.
|
||||
-}
|
||||
success : Attribute msg
|
||||
success =
|
||||
set (\attributes -> { attributes | state = Success })
|
||||
|
||||
|
||||
{-| -}
|
||||
type Attribute msg
|
||||
= Attribute (ButtonOrLink msg -> ButtonOrLink msg)
|
||||
|
||||
|
||||
|
||||
-- INTERNALS
|
||||
|
||||
|
||||
set :
|
||||
(ButtonOrLinkAttributes msg -> ButtonOrLinkAttributes msg)
|
||||
-> Attribute msg
|
||||
set with =
|
||||
Attribute (\(ButtonOrLink config) -> ButtonOrLink (with config))
|
||||
|
||||
|
||||
build : ButtonOrLink msg
|
||||
build =
|
||||
ButtonOrLink
|
||||
{ onClick = Nothing
|
||||
, url = "#"
|
||||
, linkType = Default
|
||||
, size = Medium
|
||||
, style = primaryColors
|
||||
, width = WidthUnbounded
|
||||
, label = ""
|
||||
, state = Enabled
|
||||
, icon = Nothing
|
||||
, customAttributes = []
|
||||
}
|
||||
|
||||
|
||||
type ButtonOrLink msg
|
||||
= ButtonOrLink (ButtonOrLinkAttributes msg)
|
||||
|
||||
|
||||
type alias ButtonOrLinkAttributes msg =
|
||||
{ onClick : Maybe msg
|
||||
, url : String
|
||||
, linkType : Link
|
||||
, size : ButtonSize
|
||||
, style : ColorPalette
|
||||
, width : ButtonWidth
|
||||
, label : String
|
||||
, state : ButtonState
|
||||
, icon : Maybe Svg
|
||||
, customAttributes : List (Html.Attribute msg)
|
||||
}
|
||||
|
||||
|
||||
renderButton : ButtonOrLink msg -> Html msg
|
||||
renderButton ((ButtonOrLink config) as button_) =
|
||||
let
|
||||
buttonStyle_ =
|
||||
getColorPalette button_
|
||||
|
||||
isDisabled =
|
||||
case config.state of
|
||||
Enabled ->
|
||||
False
|
||||
|
||||
Disabled ->
|
||||
True
|
||||
|
||||
Error ->
|
||||
True
|
||||
|
||||
Unfulfilled ->
|
||||
False
|
||||
|
||||
Loading ->
|
||||
True
|
||||
|
||||
Success ->
|
||||
True
|
||||
in
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "customButton")
|
||||
[ buttonStyles config.size config.width buttonStyle_ ]
|
||||
((Maybe.map Events.onClick config.onClick
|
||||
|> Maybe.withDefault AttributesExtra.none
|
||||
)
|
||||
:: Attributes.disabled isDisabled
|
||||
:: Attributes.type_ "button"
|
||||
:: config.customAttributes
|
||||
)
|
||||
[ viewLabel config.icon config.label ]
|
||||
|
||||
|
||||
renderLink : ButtonOrLink msg -> Html msg
|
||||
renderLink ((ButtonOrLink config) as link_) =
|
||||
let
|
||||
colorPalette =
|
||||
getColorPalette link_
|
||||
|
||||
linkBase linkFunctionName extraAttrs =
|
||||
Nri.Ui.styled Styled.a
|
||||
(styledName linkFunctionName)
|
||||
[ buttonStyles config.size config.width colorPalette ]
|
||||
(Attributes.href config.url :: extraAttrs)
|
||||
[ viewLabel config.icon config.label ]
|
||||
in
|
||||
case config.linkType of
|
||||
Default ->
|
||||
linkBase "link"
|
||||
(Attributes.target "_self" :: config.customAttributes)
|
||||
|
||||
SinglePageApp ->
|
||||
linkBase "linkSpa"
|
||||
((Maybe.map EventExtras.onClickPreventDefaultForLinkWithHref config.onClick
|
||||
|> Maybe.withDefault AttributesExtra.none
|
||||
)
|
||||
:: config.customAttributes
|
||||
)
|
||||
|
||||
WithMethod method ->
|
||||
linkBase "linkWithMethod"
|
||||
(Attributes.attribute "data-method" method
|
||||
:: config.customAttributes
|
||||
)
|
||||
|
||||
WithTracking ->
|
||||
linkBase
|
||||
"linkWithTracking"
|
||||
((Maybe.map
|
||||
(\msg ->
|
||||
Events.preventDefaultOn "click"
|
||||
(Json.Decode.succeed ( msg, True ))
|
||||
)
|
||||
config.onClick
|
||||
|> Maybe.withDefault AttributesExtra.none
|
||||
)
|
||||
:: config.customAttributes
|
||||
)
|
||||
|
||||
External ->
|
||||
linkBase "linkExternal"
|
||||
(targetBlank ++ config.customAttributes)
|
||||
|
||||
ExternalWithTracking ->
|
||||
linkBase "linkExternalWithTracking"
|
||||
(List.concat
|
||||
[ targetBlank
|
||||
, config.onClick
|
||||
|> Maybe.map
|
||||
(\onClickMsg ->
|
||||
[ Events.onClick onClickMsg
|
||||
, Events.on "auxclick" (Json.Decode.succeed onClickMsg)
|
||||
]
|
||||
)
|
||||
|> Maybe.withDefault []
|
||||
, config.customAttributes
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
|
||||
-- DELETE BUTTON
|
||||
|
||||
|
||||
{-| A delete button (blue X)
|
||||
-}
|
||||
delete : { label : String, onClick : msg } -> Html msg
|
||||
delete config =
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "delete")
|
||||
[ Css.display Css.inlineBlock
|
||||
, Css.backgroundRepeat Css.noRepeat
|
||||
, Css.backgroundColor Css.transparent
|
||||
, Css.backgroundPosition Css.center
|
||||
, Css.backgroundSize Css.contain
|
||||
, Css.border Css.zero
|
||||
, Css.width (Css.px 15)
|
||||
, Css.height (Css.px 15)
|
||||
, Css.padding Css.zero
|
||||
, Css.margin2 Css.zero (Css.px 6)
|
||||
, Css.cursor Css.pointer
|
||||
, Css.color Colors.azure
|
||||
]
|
||||
[ Events.onClick config.onClick
|
||||
, Attributes.type_ "button"
|
||||
, -- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||
Widget.label config.label
|
||||
]
|
||||
[ Svg.svg [ Svg.Attributes.viewBox "0 0 25 25" ]
|
||||
[ Svg.title [] [ RootHtml.text "Delete" ]
|
||||
, Svg.path
|
||||
[ Svg.Attributes.fill "#146aff" -- TODO: this should be azure, but css colors aren't extractable afaik
|
||||
, Svg.Attributes.d "M1.067 6.015c-1.423-1.422-1.423-3.526 0-4.948 1.422-1.423 3.526-1.423 4.948 0l6.371 6.37 6.371-6.37c1.422-1.423 3.783-1.423 5.176 0 1.423 1.422 1.423 3.782 0 5.176l-6.37 6.37 6.37 6.372c1.423 1.422 1.423 3.526 0 4.948-1.422 1.423-3.526 1.423-4.948 0l-6.371-6.37-6.371 6.37c-1.422 1.423-3.783 1.423-5.176 0-1.423-1.422-1.423-3.782 0-5.176l6.37-6.143-6.37-6.599z"
|
||||
]
|
||||
[]
|
||||
]
|
||||
|> Styled.fromUnstyled
|
||||
]
|
||||
|
||||
|
||||
|
||||
-- TOGGLE BUTTON
|
||||
|
||||
|
||||
{-| A button that can be toggled into a pressed state and back again.
|
||||
-}
|
||||
toggleButton :
|
||||
{ label : String
|
||||
, onSelect : msg
|
||||
, onDeselect : msg
|
||||
, pressed : Bool
|
||||
}
|
||||
-> Html msg
|
||||
toggleButton config =
|
||||
let
|
||||
toggledStyles =
|
||||
if config.pressed then
|
||||
Css.batch
|
||||
[ Css.color Colors.gray20
|
||||
, Css.backgroundColor Colors.glacier
|
||||
, Css.boxShadow5 Css.inset Css.zero (Css.px 3) Css.zero (ColorsExtra.withAlpha 0.2 Colors.gray20)
|
||||
, Css.border3 (Css.px 1) Css.solid Colors.azure
|
||||
, Css.fontWeight Css.bold
|
||||
]
|
||||
|
||||
else
|
||||
Css.batch
|
||||
[]
|
||||
in
|
||||
Nri.Ui.styled Html.button
|
||||
(styledName "toggleButton")
|
||||
[ buttonStyles Medium WidthUnbounded secondaryColors
|
||||
, toggledStyles
|
||||
]
|
||||
[ Events.onClick
|
||||
(if config.pressed then
|
||||
config.onDeselect
|
||||
|
||||
else
|
||||
config.onSelect
|
||||
)
|
||||
, Widget.pressed <| Just config.pressed
|
||||
|
||||
-- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||
, Role.button
|
||||
|
||||
-- Note: setting type: 'button' removes the default behavior of submit
|
||||
-- equivalent to preventDefaultBehavior = false
|
||||
-- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-name
|
||||
, Attributes.type_ "button"
|
||||
]
|
||||
[ viewLabel Nothing config.label ]
|
||||
|
||||
|
||||
buttonStyles : ButtonSize -> ButtonWidth -> ColorPalette -> Style
|
||||
buttonStyles size width colors =
|
||||
Css.batch
|
||||
[ buttonStyle
|
||||
, sizeStyle size width
|
||||
, colorStyle colors
|
||||
]
|
||||
|
||||
|
||||
viewLabel : Maybe Svg -> String -> Html msg
|
||||
viewLabel maybeSvg label_ =
|
||||
Nri.Ui.styled Html.span
|
||||
"button-label-span"
|
||||
[ Css.overflow Css.hidden -- Keep scrollbars out of our button
|
||||
, Css.overflowWrap Css.breakWord -- Ensure that words that exceed the button width break instead of disappearing
|
||||
, Css.padding2 (Css.px 2) Css.zero -- Without a bit of bottom padding, text that extends below the baseline, like "g" gets cut off
|
||||
]
|
||||
[]
|
||||
(case maybeSvg of
|
||||
Nothing ->
|
||||
renderMarkdown label_
|
||||
|
||||
Just svg ->
|
||||
NriSvg.toHtml svg :: renderMarkdown label_
|
||||
)
|
||||
|
||||
|
||||
renderMarkdown : String -> List (Html msg)
|
||||
renderMarkdown markdown =
|
||||
case Markdown.Block.parse Nothing markdown of
|
||||
-- It seems to be always first wrapped in a `Paragraph` and never directly a `PlainInline`
|
||||
[ Markdown.Block.Paragraph _ inlines ] ->
|
||||
List.map (Markdown.Inline.toHtml >> Styled.fromUnstyled) inlines
|
||||
|
||||
_ ->
|
||||
[ Html.text markdown ]
|
||||
|
||||
|
||||
|
||||
-- STYLES
|
||||
|
||||
|
||||
buttonStyle : Style
|
||||
buttonStyle =
|
||||
Css.batch
|
||||
[ Css.cursor Css.pointer
|
||||
, -- Specifying the font can and should go away after bootstrap is removed from application.css
|
||||
Nri.Ui.Fonts.V1.baseFont
|
||||
, Css.textOverflow Css.ellipsis
|
||||
, Css.overflow Css.hidden
|
||||
, Css.textDecoration Css.none
|
||||
, Css.backgroundImage Css.none
|
||||
, Css.textShadow Css.none
|
||||
, Css.property "transition" "background-color 0.2s, color 0.2s, box-shadow 0.2s, border 0.2s, border-width 0s"
|
||||
, Css.boxShadow Css.none
|
||||
, Css.border Css.zero
|
||||
, Css.marginBottom Css.zero
|
||||
, Css.hover [ Css.textDecoration Css.none ]
|
||||
, Css.disabled [ Css.cursor Css.notAllowed ]
|
||||
, Css.display Css.inlineFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.justifyContent Css.center
|
||||
]
|
||||
|
||||
|
||||
|
||||
-- COLORS
|
||||
|
||||
|
||||
type alias ColorPalette =
|
||||
{ background : Css.Color
|
||||
, hover : Css.Color
|
||||
, text : Css.Color
|
||||
, border : Maybe Css.Color
|
||||
, shadow : Css.Color
|
||||
}
|
||||
|
||||
|
||||
primaryColors : ColorPalette
|
||||
primaryColors =
|
||||
{ background = Colors.azure
|
||||
, hover = Colors.azureDark
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.azureDark
|
||||
}
|
||||
|
||||
|
||||
secondaryColors : ColorPalette
|
||||
secondaryColors =
|
||||
{ background = Colors.white
|
||||
, hover = Colors.glacier
|
||||
, text = Colors.azure
|
||||
, border = Just <| Colors.azure
|
||||
, shadow = Colors.azure
|
||||
}
|
||||
|
||||
|
||||
getColorPalette : ButtonOrLink msg -> ColorPalette
|
||||
getColorPalette (ButtonOrLink config) =
|
||||
case config.state of
|
||||
Enabled ->
|
||||
config.style
|
||||
|
||||
Disabled ->
|
||||
{ background = Colors.gray92
|
||||
, hover = Colors.gray92
|
||||
, text = Colors.gray45
|
||||
, border = Nothing
|
||||
, shadow = Colors.gray92
|
||||
}
|
||||
|
||||
Error ->
|
||||
{ background = Colors.purple
|
||||
, hover = Colors.purple
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.purple
|
||||
}
|
||||
|
||||
Unfulfilled ->
|
||||
{ background = Colors.gray92
|
||||
, hover = Colors.gray92
|
||||
, text = Colors.gray45
|
||||
, border = Nothing
|
||||
, shadow = Colors.gray92
|
||||
}
|
||||
|
||||
Loading ->
|
||||
{ background = Colors.glacier
|
||||
, hover = Colors.glacier
|
||||
, text = Colors.navy
|
||||
, border = Nothing
|
||||
, shadow = Colors.glacier
|
||||
}
|
||||
|
||||
Success ->
|
||||
{ background = Colors.greenDark
|
||||
, hover = Colors.greenDark
|
||||
, text = Colors.white
|
||||
, border = Nothing
|
||||
, shadow = Colors.greenDark
|
||||
}
|
||||
|
||||
|
||||
colorStyle : ColorPalette -> Style
|
||||
colorStyle colorPalette =
|
||||
Css.batch
|
||||
[ Css.color colorPalette.text
|
||||
, Css.backgroundColor colorPalette.background
|
||||
, Css.fontWeight (Css.int 700)
|
||||
, Css.textAlign Css.center
|
||||
, case colorPalette.border of
|
||||
Nothing ->
|
||||
Css.borderStyle Css.none
|
||||
|
||||
Just color ->
|
||||
Css.batch
|
||||
[ Css.borderColor color
|
||||
, Css.borderStyle Css.solid
|
||||
]
|
||||
, Css.borderBottomStyle Css.solid
|
||||
, Css.borderBottomColor colorPalette.shadow
|
||||
, Css.fontStyle Css.normal
|
||||
, Css.hover
|
||||
[ Css.color colorPalette.text
|
||||
, Css.backgroundColor colorPalette.hover
|
||||
, Css.disabled [ Css.backgroundColor colorPalette.background ]
|
||||
]
|
||||
, Css.visited [ Css.color colorPalette.text ]
|
||||
]
|
||||
|
||||
|
||||
sizeStyle : ButtonSize -> ButtonWidth -> Style
|
||||
sizeStyle size width =
|
||||
let
|
||||
config =
|
||||
case size of
|
||||
Small ->
|
||||
{ fontSize = 15
|
||||
, height = 36
|
||||
, imageHeight = 15
|
||||
, shadowHeight = 2
|
||||
, minWidth = 75
|
||||
}
|
||||
|
||||
Medium ->
|
||||
{ fontSize = 17
|
||||
, height = 45
|
||||
, imageHeight = 15
|
||||
, shadowHeight = 3
|
||||
, minWidth = 100
|
||||
}
|
||||
|
||||
Large ->
|
||||
{ fontSize = 20
|
||||
, height = 56
|
||||
, imageHeight = 20
|
||||
, shadowHeight = 4
|
||||
, minWidth = 200
|
||||
}
|
||||
|
||||
sizingAttributes =
|
||||
let
|
||||
verticalPaddingPx =
|
||||
2
|
||||
in
|
||||
[ Css.minHeight (Css.px config.height)
|
||||
, Css.paddingTop (Css.px verticalPaddingPx)
|
||||
, Css.paddingBottom (Css.px verticalPaddingPx)
|
||||
]
|
||||
|
||||
widthAttributes =
|
||||
case width of
|
||||
WidthExact pxWidth ->
|
||||
[ Css.maxWidth (Css.pct 100)
|
||||
, Css.width (Css.px <| toFloat pxWidth)
|
||||
, Css.paddingRight (Css.px 4)
|
||||
, Css.paddingLeft (Css.px 4)
|
||||
]
|
||||
|
||||
WidthUnbounded ->
|
||||
[ Css.paddingLeft (Css.px 16)
|
||||
, Css.paddingRight (Css.px 16)
|
||||
, Css.minWidth (Css.px config.minWidth)
|
||||
]
|
||||
|
||||
WidthFillContainer ->
|
||||
[ Css.paddingLeft (Css.px 16)
|
||||
, Css.paddingRight (Css.px 16)
|
||||
, Css.minWidth (Css.px config.minWidth)
|
||||
, Css.width (Css.pct 100)
|
||||
]
|
||||
|
||||
lineHeightPx =
|
||||
case size of
|
||||
Small ->
|
||||
15
|
||||
|
||||
Medium ->
|
||||
19
|
||||
|
||||
Large ->
|
||||
22
|
||||
in
|
||||
Css.batch
|
||||
[ Css.fontSize (Css.px config.fontSize)
|
||||
, Css.borderRadius (Css.px 8)
|
||||
, Css.lineHeight (Css.px lineHeightPx)
|
||||
, Css.boxSizing Css.borderBox
|
||||
, Css.borderWidth (Css.px 1)
|
||||
, Css.borderBottomWidth (Css.px config.shadowHeight)
|
||||
, Css.batch sizingAttributes
|
||||
, Css.batch widthAttributes
|
||||
, Css.Global.descendants
|
||||
[ Css.Global.img
|
||||
[ Css.height (Css.px config.imageHeight)
|
||||
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||
, Css.position Css.relative
|
||||
, Css.bottom (Css.px 2)
|
||||
, Css.verticalAlign Css.middle
|
||||
]
|
||||
, Css.Global.svg
|
||||
[ Css.height (Css.px config.imageHeight) |> Css.important
|
||||
, Css.width (Css.px config.imageHeight) |> Css.important
|
||||
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||
, Css.position Css.relative
|
||||
, Css.bottom (Css.px 2)
|
||||
, Css.verticalAlign Css.middle
|
||||
]
|
||||
, Css.Global.svg
|
||||
[ Css.important <| Css.height (Css.px config.imageHeight)
|
||||
, Css.important <| Css.width Css.auto
|
||||
, Css.maxWidth (Css.px (config.imageHeight * 1.25))
|
||||
, Css.paddingRight (Css.px <| config.imageHeight / 6)
|
||||
, Css.position Css.relative
|
||||
, Css.bottom (Css.px 2)
|
||||
, Css.verticalAlign Css.middle
|
||||
]
|
||||
]
|
||||
]
|
@ -1,168 +0,0 @@
|
||||
module Nri.Ui.ClickableText.V1 exposing
|
||||
( ButtonConfig, button
|
||||
, LinkConfig, link
|
||||
, Size(..)
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
|
||||
# About:
|
||||
|
||||
ClickableText looks different from Nri.Ui.Button in that it displays without margin or padding.
|
||||
ClickableText has the suave, traditional look of a "link"!
|
||||
|
||||
For accessibility purposes, buttons that perform an action on the current page should be HTML `<button>`
|
||||
elements and are created here with `*Button` functions. Buttons that take the user to a new page should be
|
||||
HTML `<a>` elements and are created here with `*Link` functions.
|
||||
|
||||
|
||||
# `<button>` creators
|
||||
|
||||
@docs ButtonConfig, button
|
||||
|
||||
|
||||
# `<a>` creators
|
||||
|
||||
@docs LinkConfig, link
|
||||
|
||||
|
||||
# Config
|
||||
|
||||
@docs Size
|
||||
|
||||
-}
|
||||
|
||||
import Css
|
||||
import Css.Global
|
||||
import Html.Styled as Html exposing (..)
|
||||
import Html.Styled.Attributes as Attributes
|
||||
import Html.Styled.Events as Events
|
||||
import Nri.Ui
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Fonts.V1
|
||||
import Nri.Ui.Icon.V4 as Icon exposing (IconType)
|
||||
|
||||
|
||||
{-| Sizes for the button
|
||||
-}
|
||||
type Size
|
||||
= Small
|
||||
| Medium
|
||||
| Large
|
||||
|
||||
|
||||
{-| Config for the button
|
||||
-}
|
||||
type alias ButtonConfig msg =
|
||||
{ label : String
|
||||
, size : Size
|
||||
, icon : Maybe IconType
|
||||
, onClick : msg
|
||||
}
|
||||
|
||||
|
||||
{-| Creates a `<button>` element
|
||||
-}
|
||||
button : ButtonConfig msg -> Html msg
|
||||
button config =
|
||||
let
|
||||
fontSize =
|
||||
sizeToPx config.size
|
||||
in
|
||||
Nri.Ui.styled Html.button
|
||||
(dataDescriptor "button")
|
||||
(clickableTextStyles fontSize)
|
||||
[ Events.onClick config.onClick
|
||||
]
|
||||
[ icon fontSize config.icon
|
||||
, text config.label
|
||||
]
|
||||
|
||||
|
||||
{-| Config for the link
|
||||
-}
|
||||
type alias LinkConfig =
|
||||
{ label : String
|
||||
, size : Size
|
||||
, icon : Maybe IconType
|
||||
, url : String
|
||||
}
|
||||
|
||||
|
||||
{-| Creates a `<a>` element
|
||||
-}
|
||||
link : LinkConfig -> List (Attribute msg) -> Html msg
|
||||
link config additionalAttributes =
|
||||
let
|
||||
fontSize =
|
||||
sizeToPx config.size
|
||||
in
|
||||
Nri.Ui.styled Html.a
|
||||
(dataDescriptor "link")
|
||||
(clickableTextStyles fontSize)
|
||||
(Attributes.href config.url
|
||||
:: additionalAttributes
|
||||
)
|
||||
[ icon fontSize config.icon
|
||||
, text config.label
|
||||
]
|
||||
|
||||
|
||||
clickableTextStyles : Css.Px -> List Css.Style
|
||||
clickableTextStyles fontSize =
|
||||
[ Css.cursor Css.pointer
|
||||
, -- Specifying the font can and should go away after bootstrap is removed from application.css
|
||||
Nri.Ui.Fonts.V1.baseFont
|
||||
, Css.backgroundImage Css.none
|
||||
, Css.textShadow Css.none
|
||||
, Css.boxShadow Css.none
|
||||
, Css.border Css.zero
|
||||
, Css.disabled [ Css.cursor Css.notAllowed ]
|
||||
, Css.color Colors.azure
|
||||
, Css.backgroundColor Css.transparent
|
||||
, Css.fontWeight (Css.int 600)
|
||||
, Css.textAlign Css.left
|
||||
, Css.borderStyle Css.none
|
||||
, Css.textDecoration Css.none
|
||||
, Css.hover [ Css.textDecoration Css.underline ]
|
||||
, Css.fontSize fontSize
|
||||
, Css.padding Css.zero
|
||||
]
|
||||
|
||||
|
||||
icon : Css.Px -> Maybe IconType -> Html msg
|
||||
icon fontSize maybeIcon =
|
||||
case maybeIcon of
|
||||
Just iconType ->
|
||||
-- TODO: We should never use an image here, only SVG
|
||||
Nri.Ui.styled Html.span
|
||||
(dataDescriptor "icon-holder")
|
||||
[ Css.height fontSize
|
||||
, Css.width fontSize
|
||||
, Css.display Css.inlineBlock
|
||||
, Css.marginRight (Css.px 5)
|
||||
]
|
||||
[]
|
||||
[ Icon.decorativeIcon iconType ]
|
||||
|
||||
Nothing ->
|
||||
text ""
|
||||
|
||||
|
||||
sizeToPx : Size -> Css.Px
|
||||
sizeToPx size =
|
||||
case size of
|
||||
Small ->
|
||||
Css.px 15
|
||||
|
||||
Medium ->
|
||||
Css.px 17
|
||||
|
||||
Large ->
|
||||
Css.px 20
|
||||
|
||||
|
||||
dataDescriptor : String -> String
|
||||
dataDescriptor descriptor =
|
||||
"clickable-text-v1-" ++ descriptor
|
@ -1,176 +0,0 @@
|
||||
module Nri.Ui.ClickableText.V2 exposing
|
||||
( button
|
||||
, link
|
||||
, Size(..)
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
|
||||
# Changes from V1
|
||||
|
||||
- Removes dependency on Icon that makes versioned assets hard to work with
|
||||
- Fixes vertical alignment of icons
|
||||
- Inlines configs to make parsing documentation easier
|
||||
|
||||
|
||||
# About:
|
||||
|
||||
ClickableText looks different from Nri.Ui.Button in that it displays without margin or padding.
|
||||
ClickableText has the suave, traditional look of a "link"!
|
||||
|
||||
For accessibility purposes, buttons that perform an action on the current page should be HTML `<button>`
|
||||
elements and are created here with `*Button` functions. Buttons that take the user to a new page should be
|
||||
HTML `<a>` elements and are created here with `*Link` functions.
|
||||
|
||||
|
||||
# `<button>` creators
|
||||
|
||||
@docs button
|
||||
|
||||
|
||||
# `<a>` creators
|
||||
|
||||
@docs link
|
||||
|
||||
|
||||
# Config
|
||||
|
||||
@docs Size
|
||||
|
||||
-}
|
||||
|
||||
import Css
|
||||
import Html.Styled as Html exposing (..)
|
||||
import Html.Styled.Attributes as Attributes
|
||||
import Html.Styled.Events as Events
|
||||
import Nri.Ui
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Fonts.V1
|
||||
import Nri.Ui.Svg.V1 as NriSvg exposing (Svg)
|
||||
|
||||
|
||||
{-| Sizes for the button
|
||||
-}
|
||||
type Size
|
||||
= Small
|
||||
| Medium
|
||||
| Large
|
||||
|
||||
|
||||
{-| Creates a `<button>` element
|
||||
-}
|
||||
button :
|
||||
{ label : String
|
||||
, size : Size
|
||||
, icon : Maybe Svg
|
||||
, onClick : msg
|
||||
}
|
||||
-> Html msg
|
||||
button config =
|
||||
Nri.Ui.styled Html.button
|
||||
(dataDescriptor "button")
|
||||
clickableTextStyles
|
||||
[ Events.onClick config.onClick
|
||||
]
|
||||
[ viewContent config ]
|
||||
|
||||
|
||||
{-| Creates a `<a>` element
|
||||
-}
|
||||
link :
|
||||
{ label : String
|
||||
, size : Size
|
||||
, icon : Maybe Svg
|
||||
, url : String
|
||||
}
|
||||
-> List (Attribute msg)
|
||||
-> Html msg
|
||||
link config additionalAttributes =
|
||||
Nri.Ui.styled Html.a
|
||||
(dataDescriptor "link")
|
||||
clickableTextStyles
|
||||
(Attributes.href config.url :: additionalAttributes)
|
||||
[ viewContent config ]
|
||||
|
||||
|
||||
viewContent : { a | label : String, size : Size, icon : Maybe Svg } -> Html msg
|
||||
viewContent config =
|
||||
let
|
||||
fontSize =
|
||||
sizeToPx config.size
|
||||
in
|
||||
span [ Attributes.css [ Css.fontSize fontSize ] ]
|
||||
(case config.icon of
|
||||
Just icon ->
|
||||
[ div
|
||||
[ Attributes.css
|
||||
[ Css.displayFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.property "line-height" "normal"
|
||||
]
|
||||
]
|
||||
[ div
|
||||
[ Attributes.css
|
||||
[ Css.height fontSize
|
||||
, Css.maxWidth fontSize
|
||||
, Css.minWidth fontSize -- so it doesn't shrink when the label is long
|
||||
, case config.size of
|
||||
Small ->
|
||||
Css.marginRight (Css.px 3)
|
||||
|
||||
Medium ->
|
||||
Css.marginRight (Css.px 3)
|
||||
|
||||
Large ->
|
||||
Css.marginRight (Css.px 4)
|
||||
]
|
||||
]
|
||||
[ NriSvg.toHtml icon ]
|
||||
, span [] [ text config.label ]
|
||||
]
|
||||
]
|
||||
|
||||
Nothing ->
|
||||
[ text config.label ]
|
||||
)
|
||||
|
||||
|
||||
clickableTextStyles : List Css.Style
|
||||
clickableTextStyles =
|
||||
[ Css.cursor Css.pointer
|
||||
, Nri.Ui.Fonts.V1.baseFont
|
||||
, Css.backgroundImage Css.none
|
||||
, Css.textShadow Css.none
|
||||
, Css.boxShadow Css.none
|
||||
, Css.border Css.zero
|
||||
, Css.disabled [ Css.cursor Css.notAllowed ]
|
||||
, Css.color Colors.azure
|
||||
, Css.backgroundColor Css.transparent
|
||||
, Css.fontWeight (Css.int 600)
|
||||
, Css.textAlign Css.left
|
||||
, Css.borderStyle Css.none
|
||||
, Css.textDecoration Css.none
|
||||
, Css.hover [ Css.textDecoration Css.underline ]
|
||||
, Css.padding Css.zero
|
||||
, Css.display Css.inlineBlock
|
||||
, Css.verticalAlign Css.textBottom
|
||||
]
|
||||
|
||||
|
||||
sizeToPx : Size -> Css.Px
|
||||
sizeToPx size =
|
||||
case size of
|
||||
Small ->
|
||||
Css.px 15
|
||||
|
||||
Medium ->
|
||||
Css.px 17
|
||||
|
||||
Large ->
|
||||
Css.px 20
|
||||
|
||||
|
||||
dataDescriptor : String -> String
|
||||
dataDescriptor descriptor =
|
||||
"clickable-text-v2-" ++ descriptor
|
@ -1,199 +0,0 @@
|
||||
module Nri.Ui.Heading.V1 exposing
|
||||
( Heading, heading
|
||||
, withVisualLevel, VisualLevel(..)
|
||||
, withDocumentLevel, DocumentLevel(..)
|
||||
, view
|
||||
)
|
||||
|
||||
{-| Headings with customization options for accessibility.
|
||||
|
||||
|
||||
## Understanding spacing
|
||||
|
||||
- All text styles have a specific line-height. This is set so that when text
|
||||
in the given style is long enough to wrap, the spacing between wrapped lines
|
||||
looks good.
|
||||
- No heading styles have padding.
|
||||
- **Heading styles** do not have margin. It is up to the caller to add
|
||||
appropriate margin to the layout.
|
||||
|
||||
@docs Heading, heading
|
||||
|
||||
@docs withVisualLevel, VisualLevel
|
||||
|
||||
@docs withDocumentLevel, DocumentLevel
|
||||
|
||||
@docs view
|
||||
|
||||
-}
|
||||
|
||||
import Css exposing (..)
|
||||
import Html.Styled exposing (..)
|
||||
import Html.Styled.Attributes exposing (css)
|
||||
import Nri.Ui.Colors.V1 exposing (..)
|
||||
import Nri.Ui.Fonts.V1 as Fonts
|
||||
|
||||
|
||||
{-| -}
|
||||
type Heading msg
|
||||
= Heading (List (Html msg)) VisualLevel DocumentLevel
|
||||
|
||||
|
||||
{-| start a heading. Render it with `view`.
|
||||
-}
|
||||
heading : List (Html msg) -> Heading msg
|
||||
heading content =
|
||||
Heading content Top H1
|
||||
|
||||
|
||||
|
||||
-- VISUAL LEVEL
|
||||
|
||||
|
||||
{-| -}
|
||||
type VisualLevel
|
||||
= Top
|
||||
| Tagline
|
||||
| Subhead
|
||||
| Small
|
||||
|
||||
|
||||
{-| Customize how the heading looks. The visual hierarchy should go Top ->
|
||||
Tagline -> Subhead -> Small.
|
||||
|
||||
heading [ text "Hello, World!" ]
|
||||
|> withVisualLevel Top
|
||||
|> view
|
||||
|
||||
-}
|
||||
withVisualLevel : VisualLevel -> Heading msg -> Heading msg
|
||||
withVisualLevel visualLevel (Heading content _ documentLevel) =
|
||||
Heading content visualLevel documentLevel
|
||||
|
||||
|
||||
getStyles : VisualLevel -> Style
|
||||
getStyles visualLevel =
|
||||
case visualLevel of
|
||||
Top ->
|
||||
headingStyles
|
||||
{ font = Fonts.baseFont
|
||||
, color = navy
|
||||
, size = 30
|
||||
, lineHeight = 38
|
||||
, weight = 700
|
||||
}
|
||||
|
||||
Tagline ->
|
||||
headingStyles
|
||||
{ font = Fonts.baseFont
|
||||
, color = gray45
|
||||
, size = 20
|
||||
, lineHeight = 30
|
||||
, weight = 400
|
||||
}
|
||||
|
||||
Subhead ->
|
||||
headingStyles
|
||||
{ font = Fonts.baseFont
|
||||
, color = navy
|
||||
, size = 20
|
||||
, lineHeight = 26
|
||||
, weight = 700
|
||||
}
|
||||
|
||||
Small ->
|
||||
Css.batch
|
||||
[ headingStyles
|
||||
{ font = Fonts.baseFont
|
||||
, color = gray20
|
||||
, size = 16
|
||||
, lineHeight = 21
|
||||
, weight = 700
|
||||
}
|
||||
, letterSpacing (px -0.13)
|
||||
]
|
||||
|
||||
|
||||
headingStyles :
|
||||
{ color : Color
|
||||
, font : Style
|
||||
, lineHeight : Float
|
||||
, size : Float
|
||||
, weight : Int
|
||||
}
|
||||
-> Style
|
||||
headingStyles config =
|
||||
Css.batch
|
||||
[ config.font
|
||||
, fontSize (px config.size)
|
||||
, color config.color
|
||||
, lineHeight (px config.lineHeight)
|
||||
, fontWeight (int config.weight)
|
||||
, padding zero
|
||||
, textAlign left
|
||||
, margin zero
|
||||
]
|
||||
|
||||
|
||||
|
||||
-- DOCUMENT LEVEL
|
||||
|
||||
|
||||
{-| -}
|
||||
type DocumentLevel
|
||||
= H1
|
||||
| H2
|
||||
| H3
|
||||
| H4
|
||||
| H5
|
||||
| H6
|
||||
|
||||
|
||||
{-| Customize the document level of the heading. For accessibility reasons, you
|
||||
should have exactly one H1, and only increase the level by one. You can use a
|
||||
tool like [axe](https://www.deque.com/axe/) to check this.
|
||||
|
||||
heading [ text "Hello, World!" ]
|
||||
|> withDocumentLevel H1
|
||||
|> view
|
||||
|
||||
-}
|
||||
withDocumentLevel : DocumentLevel -> Heading msg -> Heading msg
|
||||
withDocumentLevel documentLevel (Heading content visualLevel _) =
|
||||
Heading content visualLevel documentLevel
|
||||
|
||||
|
||||
getTag : DocumentLevel -> (List (Attribute msg) -> List (Html msg) -> Html msg)
|
||||
getTag documentLevel =
|
||||
case documentLevel of
|
||||
H1 ->
|
||||
h1
|
||||
|
||||
H2 ->
|
||||
h2
|
||||
|
||||
H3 ->
|
||||
h3
|
||||
|
||||
H4 ->
|
||||
h4
|
||||
|
||||
H5 ->
|
||||
h5
|
||||
|
||||
H6 ->
|
||||
h6
|
||||
|
||||
|
||||
|
||||
-- VIEW
|
||||
|
||||
|
||||
{-| render a Heading to Html. See the other docs in this module for
|
||||
customizations.
|
||||
-}
|
||||
view : Heading msg -> Html msg
|
||||
view (Heading content visualLevel documentLevel) =
|
||||
getTag documentLevel
|
||||
[ css [ getStyles visualLevel ] ]
|
||||
content
|
@ -1,213 +0,0 @@
|
||||
module Nri.Ui.Modal.V2 exposing
|
||||
( Model
|
||||
, info
|
||||
, warning
|
||||
)
|
||||
|
||||
{-| Changes from V1:
|
||||
|
||||
- Use Styled Html
|
||||
|
||||
@docs Model
|
||||
@docs info
|
||||
@docs warning
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Html exposing (..)
|
||||
import Accessibility.Styled.Role as Role
|
||||
import Accessibility.Styled.Widget as Widget
|
||||
import Css
|
||||
import Css.Global exposing (Snippet, body, children, descendants, everything, selector)
|
||||
import Html.Styled
|
||||
import Html.Styled.Events exposing (onClick)
|
||||
import Nri.Ui
|
||||
import Nri.Ui.Colors.Extra
|
||||
import Nri.Ui.Colors.V1
|
||||
import Nri.Ui.Fonts.V1 as Fonts
|
||||
|
||||
|
||||
{-|
|
||||
|
||||
- `onDismiss`: If `Nothing`, the modal will not be dismissable
|
||||
- `visibleTitle`: If `False`, the title will still be used for screen readers
|
||||
- `content`: This will be placed in a `width:100%` div in the main area of the modal
|
||||
- `footerContent`: The optional items here will be stacked below the main content area and center-aligned.
|
||||
Commonly you will either give a list of Nri.Ui.Buttons,
|
||||
or an empty list.
|
||||
|
||||
-}
|
||||
type alias Model msg =
|
||||
{ title : String
|
||||
, visibleTitle : Bool
|
||||
, content : Html msg
|
||||
, footerContent : List (Html msg)
|
||||
, onDismiss : Maybe msg
|
||||
, width : Maybe Int
|
||||
}
|
||||
|
||||
|
||||
type ModalType
|
||||
= Info
|
||||
| Warning
|
||||
|
||||
|
||||
{-| -}
|
||||
info : Model msg -> Html msg
|
||||
info =
|
||||
view Info
|
||||
|
||||
|
||||
{-| -}
|
||||
warning : Model msg -> Html msg
|
||||
warning =
|
||||
view Warning
|
||||
|
||||
|
||||
view : ModalType -> Model msg -> Html msg
|
||||
view modalType { title, visibleTitle, content, onDismiss, footerContent, width } =
|
||||
Nri.Ui.styled div
|
||||
"modal-backdrop-container"
|
||||
((case modalType of
|
||||
Info ->
|
||||
Css.backgroundColor (Nri.Ui.Colors.Extra.withAlpha 0.9 Nri.Ui.Colors.V1.navy)
|
||||
|
||||
Warning ->
|
||||
Css.backgroundColor (Nri.Ui.Colors.Extra.withAlpha 0.9 Nri.Ui.Colors.V1.gray20)
|
||||
)
|
||||
:: [ Css.height (Css.vh 100)
|
||||
, Css.left Css.zero
|
||||
, Css.overflow Css.hidden
|
||||
, Css.position Css.fixed
|
||||
, Css.top Css.zero
|
||||
, Css.width (Css.pct 100)
|
||||
, Css.zIndex (Css.int 200)
|
||||
, Css.displayFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.justifyContent Css.center
|
||||
]
|
||||
)
|
||||
[ Role.dialog
|
||||
, Widget.label title
|
||||
, Widget.modal True
|
||||
]
|
||||
[ Nri.Ui.styled Html.Styled.div
|
||||
"modal-click-catcher"
|
||||
[ Css.bottom Css.zero
|
||||
, Css.left Css.zero
|
||||
, Css.position Css.absolute
|
||||
, Css.right Css.zero
|
||||
, Css.top Css.zero
|
||||
]
|
||||
(case onDismiss of
|
||||
Nothing ->
|
||||
[]
|
||||
|
||||
Just msg ->
|
||||
[ onClick msg ]
|
||||
)
|
||||
[]
|
||||
, Nri.Ui.styled div
|
||||
"modal-container"
|
||||
[ Css.width (Css.px 600)
|
||||
, Css.maxHeight <| Css.calc (Css.vh 100) Css.minus (Css.px 100)
|
||||
, Css.padding4 (Css.px 35) Css.zero (Css.px 25) Css.zero
|
||||
, Css.margin2 (Css.px 75) Css.auto
|
||||
, Css.backgroundColor Nri.Ui.Colors.V1.white
|
||||
, Css.borderRadius (Css.px 20)
|
||||
, Css.property "box-shadow" "0 1px 10px 0 rgba(0, 0, 0, 0.35)"
|
||||
, Css.position Css.relative -- required for closeButtonContainer
|
||||
, Css.displayFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.flexDirection Css.column
|
||||
, Css.flexWrap Css.noWrap
|
||||
, Fonts.baseFont
|
||||
]
|
||||
[]
|
||||
[ -- This global <style> node sets overflow to hidden on the body element,
|
||||
-- thereby preventing the page from scrolling behind the backdrop when the modal is
|
||||
-- open (and this node is present on the page).
|
||||
Css.Global.global
|
||||
[ Css.Global.body
|
||||
[ Css.overflow Css.hidden ]
|
||||
]
|
||||
, if visibleTitle then
|
||||
viewHeader modalType title
|
||||
|
||||
else
|
||||
text ""
|
||||
, viewContent modalType content
|
||||
, viewFooter footerContent
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
viewHeader : ModalType -> String -> Html msg
|
||||
viewHeader modalType title =
|
||||
Nri.Ui.styled Html.h3
|
||||
"modal-header"
|
||||
((case modalType of
|
||||
Info ->
|
||||
Css.color Nri.Ui.Colors.V1.navy
|
||||
|
||||
Warning ->
|
||||
Css.color Nri.Ui.Colors.V1.red
|
||||
)
|
||||
:: [ Css.fontWeight (Css.int 700)
|
||||
, Css.lineHeight (Css.px 27)
|
||||
, Css.margin2 Css.zero (Css.px 65)
|
||||
, Css.fontSize (Css.px 20)
|
||||
, Fonts.baseFont
|
||||
]
|
||||
)
|
||||
[]
|
||||
[ Html.text title
|
||||
]
|
||||
|
||||
|
||||
viewContent : ModalType -> Html msg -> Html msg
|
||||
viewContent modalType content =
|
||||
Nri.Ui.styled div
|
||||
"modal-content"
|
||||
[ Css.overflowY Css.scroll
|
||||
, Css.padding2 (Css.px 30) (Css.px 45)
|
||||
, Css.width (Css.pct 100)
|
||||
, Css.minHeight (Css.px 150)
|
||||
, Css.boxSizing Css.borderBox
|
||||
]
|
||||
[]
|
||||
[ content ]
|
||||
|
||||
|
||||
viewFooter : List (Html msg) -> Html msg
|
||||
viewFooter footerContent =
|
||||
case footerContent of
|
||||
[] ->
|
||||
Html.text ""
|
||||
|
||||
_ ->
|
||||
Nri.Ui.styled div
|
||||
"modal-footer"
|
||||
[ Css.alignItems Css.center
|
||||
, Css.displayFlex
|
||||
, Css.flexDirection Css.column
|
||||
, Css.flexGrow (Css.int 2)
|
||||
, Css.flexWrap Css.noWrap
|
||||
, Css.margin4 (Css.px 20) Css.zero Css.zero Css.zero
|
||||
, Css.width (Css.pct 100)
|
||||
]
|
||||
[]
|
||||
(List.map
|
||||
(\x ->
|
||||
Nri.Ui.styled div
|
||||
"modal-footer-item"
|
||||
[ Css.margin4 (Css.px 10) Css.zero Css.zero Css.zero
|
||||
, Css.firstChild
|
||||
[ Css.margin Css.zero
|
||||
]
|
||||
]
|
||||
[]
|
||||
[ x ]
|
||||
)
|
||||
footerContent
|
||||
)
|
@ -1,246 +0,0 @@
|
||||
module Nri.Ui.Modal.V3 exposing
|
||||
( Model
|
||||
, info
|
||||
, warning
|
||||
)
|
||||
|
||||
{-| Changes from V2:
|
||||
|
||||
- Add assets for close button
|
||||
|
||||
@docs Model
|
||||
@docs info
|
||||
@docs warning
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Html exposing (..)
|
||||
import Accessibility.Styled.Role as Role
|
||||
import Accessibility.Styled.Widget as Widget
|
||||
import Css
|
||||
import Css.Global exposing (Snippet, body, children, descendants, everything, selector)
|
||||
import Html.Styled
|
||||
import Html.Styled.Events exposing (onClick)
|
||||
import Nri.Ui
|
||||
import Nri.Ui.AssetPath exposing (Asset(..))
|
||||
import Nri.Ui.Colors.Extra
|
||||
import Nri.Ui.Colors.V1
|
||||
import Nri.Ui.Fonts.V1 as Fonts
|
||||
import Nri.Ui.Icon.V3 as Icon
|
||||
|
||||
|
||||
{-|
|
||||
|
||||
- `onDismiss`: If `Nothing`, the modal will not be dismissable
|
||||
- `visibleTitle`: If `False`, the title will still be used for screen readers
|
||||
- `content`: This will be placed in a `width:100%` div in the main area of the modal
|
||||
- `footerContent`: The optional items here will be stacked below the main content area and center-aligned.
|
||||
Commonly you will either give a list of Nri.Ui.Buttons,
|
||||
or an empty list.
|
||||
|
||||
-}
|
||||
type alias Model msg =
|
||||
{ title : String
|
||||
, visibleTitle : Bool
|
||||
, content : Html msg
|
||||
, footerContent : List (Html msg)
|
||||
, onDismiss : Maybe msg
|
||||
, width : Maybe Int
|
||||
}
|
||||
|
||||
|
||||
type alias Assets r =
|
||||
{ r | icons_xBlue_svg : Asset }
|
||||
|
||||
|
||||
type ModalType
|
||||
= Info
|
||||
| Warning
|
||||
|
||||
|
||||
{-| -}
|
||||
info : Assets r -> Model msg -> Html msg
|
||||
info assets =
|
||||
view assets Info
|
||||
|
||||
|
||||
{-| -}
|
||||
warning : Assets r -> Model msg -> Html msg
|
||||
warning assets =
|
||||
view assets Warning
|
||||
|
||||
|
||||
view : Assets r -> ModalType -> Model msg -> Html msg
|
||||
view assets modalType { title, visibleTitle, content, onDismiss, footerContent, width } =
|
||||
Nri.Ui.styled div
|
||||
"modal-backdrop-container"
|
||||
((case modalType of
|
||||
Info ->
|
||||
Css.backgroundColor (Nri.Ui.Colors.Extra.withAlpha 0.9 Nri.Ui.Colors.V1.navy)
|
||||
|
||||
Warning ->
|
||||
Css.backgroundColor (Nri.Ui.Colors.Extra.withAlpha 0.9 Nri.Ui.Colors.V1.gray20)
|
||||
)
|
||||
:: [ Css.height (Css.vh 100)
|
||||
, Css.left Css.zero
|
||||
, Css.overflow Css.hidden
|
||||
, Css.position Css.fixed
|
||||
, Css.top Css.zero
|
||||
, Css.width (Css.pct 100)
|
||||
, Css.zIndex (Css.int 200)
|
||||
, Css.displayFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.justifyContent Css.center
|
||||
]
|
||||
)
|
||||
[ Role.dialog
|
||||
, Widget.label title
|
||||
, Widget.modal True
|
||||
]
|
||||
[ Nri.Ui.styled Html.Styled.div
|
||||
"modal-click-catcher"
|
||||
[ Css.bottom Css.zero
|
||||
, Css.left Css.zero
|
||||
, Css.position Css.absolute
|
||||
, Css.right Css.zero
|
||||
, Css.top Css.zero
|
||||
]
|
||||
(case onDismiss of
|
||||
Nothing ->
|
||||
[]
|
||||
|
||||
Just msg ->
|
||||
[ onClick msg ]
|
||||
)
|
||||
[]
|
||||
, Nri.Ui.styled div
|
||||
"modal-container"
|
||||
[ Css.width (Css.px 600)
|
||||
, Css.maxHeight <| Css.calc (Css.vh 100) Css.minus (Css.px 100)
|
||||
, Css.padding4 (Css.px 40) Css.zero (Css.px 40) Css.zero
|
||||
, Css.margin2 (Css.px 75) Css.auto
|
||||
, Css.backgroundColor Nri.Ui.Colors.V1.white
|
||||
, Css.borderRadius (Css.px 20)
|
||||
, Css.property "box-shadow" "0 1px 10px 0 rgba(0, 0, 0, 0.35)"
|
||||
, Css.position Css.relative -- required for closeButtonContainer
|
||||
, Css.displayFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.flexDirection Css.column
|
||||
, Css.flexWrap Css.noWrap
|
||||
, Fonts.baseFont
|
||||
]
|
||||
[]
|
||||
[ -- This global <style> node sets overflow to hidden on the body element,
|
||||
-- thereby preventing the page from scrolling behind the backdrop when the modal is
|
||||
-- open (and this node is present on the page).
|
||||
Css.Global.global
|
||||
[ Css.Global.body
|
||||
[ Css.overflow Css.hidden ]
|
||||
]
|
||||
, case onDismiss of
|
||||
Just msg ->
|
||||
closeButton assets msg
|
||||
|
||||
Nothing ->
|
||||
text ""
|
||||
, if visibleTitle then
|
||||
viewHeader modalType title
|
||||
|
||||
else
|
||||
text ""
|
||||
, viewContent modalType content
|
||||
, viewFooter footerContent
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
closeButton : Assets r -> msg -> Html msg
|
||||
closeButton assets msg =
|
||||
Nri.Ui.styled div
|
||||
"close-button-container"
|
||||
[ Css.position Css.absolute
|
||||
, Css.top Css.zero
|
||||
, Css.right Css.zero
|
||||
, Css.padding (Css.px 25)
|
||||
]
|
||||
[]
|
||||
[ Icon.button
|
||||
{ alt = "Close"
|
||||
, msg = msg
|
||||
, icon = Icon.close assets
|
||||
, disabled = False
|
||||
, size = Icon.Medium
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
viewHeader : ModalType -> String -> Html msg
|
||||
viewHeader modalType title =
|
||||
Nri.Ui.styled Html.h3
|
||||
"modal-header"
|
||||
((case modalType of
|
||||
Info ->
|
||||
Css.color Nri.Ui.Colors.V1.navy
|
||||
|
||||
Warning ->
|
||||
Css.color Nri.Ui.Colors.V1.red
|
||||
)
|
||||
:: [ Css.fontWeight (Css.int 700)
|
||||
, Css.lineHeight (Css.px 27)
|
||||
, Css.margin2 Css.zero (Css.px 49)
|
||||
, Css.fontSize (Css.px 20)
|
||||
, Fonts.baseFont
|
||||
, Css.textAlign Css.center
|
||||
]
|
||||
)
|
||||
[]
|
||||
[ Html.text title
|
||||
]
|
||||
|
||||
|
||||
viewContent : ModalType -> Html msg -> Html msg
|
||||
viewContent modalType content =
|
||||
Nri.Ui.styled div
|
||||
"modal-content"
|
||||
[ Css.overflowY Css.auto
|
||||
, Css.padding2 (Css.px 30) (Css.px 40)
|
||||
, Css.width (Css.pct 100)
|
||||
, Css.minHeight (Css.px 150)
|
||||
, Css.boxSizing Css.borderBox
|
||||
]
|
||||
[]
|
||||
[ content ]
|
||||
|
||||
|
||||
viewFooter : List (Html msg) -> Html msg
|
||||
viewFooter footerContent =
|
||||
case footerContent of
|
||||
[] ->
|
||||
Html.text ""
|
||||
|
||||
_ ->
|
||||
Nri.Ui.styled div
|
||||
"modal-footer"
|
||||
[ Css.alignItems Css.center
|
||||
, Css.displayFlex
|
||||
, Css.flexDirection Css.column
|
||||
, Css.flexGrow (Css.int 2)
|
||||
, Css.flexWrap Css.noWrap
|
||||
, Css.margin4 (Css.px 20) Css.zero Css.zero Css.zero
|
||||
, Css.width (Css.pct 100)
|
||||
]
|
||||
[]
|
||||
(List.map
|
||||
(\x ->
|
||||
Nri.Ui.styled div
|
||||
"modal-footer-item"
|
||||
[ Css.margin4 (Css.px 10) Css.zero Css.zero Css.zero
|
||||
, Css.firstChild
|
||||
[ Css.margin Css.zero
|
||||
]
|
||||
]
|
||||
[]
|
||||
[ x ]
|
||||
)
|
||||
footerContent
|
||||
)
|
@ -1,270 +0,0 @@
|
||||
module Nri.Ui.Modal.V4 exposing
|
||||
( Model, Dismissibility(..)
|
||||
, info
|
||||
, warning
|
||||
)
|
||||
|
||||
{-| Changes from V3:
|
||||
|
||||
- changed `onDismiss` from `Maybe msg` to `Dismissibility msg`
|
||||
|
||||
Changes from V2:
|
||||
|
||||
- Add assets for close button
|
||||
|
||||
@docs Model, Dismissibility
|
||||
@docs info
|
||||
@docs warning
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Html exposing (..)
|
||||
import Accessibility.Styled.Role as Role
|
||||
import Accessibility.Styled.Widget as Widget
|
||||
import Css
|
||||
import Css.Global exposing (Snippet, body, children, descendants, everything, selector)
|
||||
import Html.Styled
|
||||
import Html.Styled.Events exposing (onClick)
|
||||
import Nri.Ui
|
||||
import Nri.Ui.AssetPath exposing (Asset(..))
|
||||
import Nri.Ui.Colors.Extra
|
||||
import Nri.Ui.Colors.V1
|
||||
import Nri.Ui.Fonts.V1 as Fonts
|
||||
import Nri.Ui.Icon.V3 as Icon
|
||||
|
||||
|
||||
{-|
|
||||
|
||||
- `onDismiss`: If `Nothing`, the modal will not be dismissable
|
||||
- `visibleTitle`: If `False`, the title will still be used for screen readers
|
||||
- `content`: This will be placed in a `width:100%` div in the main area of the modal
|
||||
- `footerContent`: The optional items here will be stacked below the main content area and center-aligned.
|
||||
Commonly you will either give a list of Nri.Ui.Buttons,
|
||||
or an empty list.
|
||||
|
||||
-}
|
||||
type alias Model msg =
|
||||
{ title : String
|
||||
, visibleTitle : Bool
|
||||
, content : Html msg
|
||||
, footerContent : List (Html msg)
|
||||
, onDismiss : Dismissibility msg
|
||||
, width : Maybe Int
|
||||
}
|
||||
|
||||
|
||||
{-|
|
||||
|
||||
- `NotDismissible`
|
||||
- `WithBackgroundOrX`: user can dismiss by clicking "X" (top right) or modal backdrop
|
||||
- `WithOnlyX`: user can dismiss by clicking "X" (top right)
|
||||
|
||||
-}
|
||||
type Dismissibility msg
|
||||
= NotDismissible
|
||||
| WithBackgroundOrX msg
|
||||
| WithOnlyX msg
|
||||
|
||||
|
||||
type alias Assets r =
|
||||
{ r | icons_xBlue_svg : Asset }
|
||||
|
||||
|
||||
type ModalType
|
||||
= Info
|
||||
| Warning
|
||||
|
||||
|
||||
{-| -}
|
||||
info : Assets r -> Model msg -> Html msg
|
||||
info assets =
|
||||
view assets Info
|
||||
|
||||
|
||||
{-| -}
|
||||
warning : Assets r -> Model msg -> Html msg
|
||||
warning assets =
|
||||
view assets Warning
|
||||
|
||||
|
||||
view : Assets r -> ModalType -> Model msg -> Html msg
|
||||
view assets modalType { title, visibleTitle, content, onDismiss, footerContent, width } =
|
||||
Nri.Ui.styled div
|
||||
"modal-backdrop-container"
|
||||
((case modalType of
|
||||
Info ->
|
||||
Css.backgroundColor (Nri.Ui.Colors.Extra.withAlpha 0.9 Nri.Ui.Colors.V1.navy)
|
||||
|
||||
Warning ->
|
||||
Css.backgroundColor (Nri.Ui.Colors.Extra.withAlpha 0.9 Nri.Ui.Colors.V1.gray20)
|
||||
)
|
||||
:: [ Css.height (Css.vh 100)
|
||||
, Css.left Css.zero
|
||||
, Css.overflow Css.hidden
|
||||
, Css.position Css.fixed
|
||||
, Css.top Css.zero
|
||||
, Css.width (Css.pct 100)
|
||||
, Css.zIndex (Css.int 200)
|
||||
, Css.displayFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.justifyContent Css.center
|
||||
]
|
||||
)
|
||||
[ Role.dialog
|
||||
, Widget.label title
|
||||
, Widget.modal True
|
||||
]
|
||||
[ Nri.Ui.styled Html.Styled.div
|
||||
"modal-click-catcher"
|
||||
[ Css.bottom Css.zero
|
||||
, Css.left Css.zero
|
||||
, Css.position Css.absolute
|
||||
, Css.right Css.zero
|
||||
, Css.top Css.zero
|
||||
]
|
||||
(case onDismiss of
|
||||
NotDismissible ->
|
||||
[]
|
||||
|
||||
WithBackgroundOrX msg ->
|
||||
[ onClick msg ]
|
||||
|
||||
WithOnlyX msg ->
|
||||
[]
|
||||
)
|
||||
[]
|
||||
, Nri.Ui.styled div
|
||||
"modal-container"
|
||||
[ Css.width (Css.px 600)
|
||||
, Css.maxHeight <| Css.calc (Css.vh 100) Css.minus (Css.px 100)
|
||||
, Css.padding4 (Css.px 40) Css.zero (Css.px 40) Css.zero
|
||||
, Css.margin2 (Css.px 75) Css.auto
|
||||
, Css.backgroundColor Nri.Ui.Colors.V1.white
|
||||
, Css.borderRadius (Css.px 20)
|
||||
, Css.property "box-shadow" "0 1px 10px 0 rgba(0, 0, 0, 0.35)"
|
||||
, Css.position Css.relative -- required for closeButtonContainer
|
||||
, Css.displayFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.flexDirection Css.column
|
||||
, Css.flexWrap Css.noWrap
|
||||
, Fonts.baseFont
|
||||
]
|
||||
[]
|
||||
[ -- This global <style> node sets overflow to hidden on the body element,
|
||||
-- thereby preventing the page from scrolling behind the backdrop when the modal is
|
||||
-- open (and this node is present on the page).
|
||||
Css.Global.global
|
||||
[ Css.Global.body
|
||||
[ Css.overflow Css.hidden ]
|
||||
]
|
||||
, case onDismiss of
|
||||
NotDismissible ->
|
||||
text ""
|
||||
|
||||
WithBackgroundOrX msg ->
|
||||
closeButton assets msg
|
||||
|
||||
WithOnlyX msg ->
|
||||
closeButton assets msg
|
||||
, if visibleTitle then
|
||||
viewHeader modalType title
|
||||
|
||||
else
|
||||
text ""
|
||||
, viewContent modalType content
|
||||
, viewFooter footerContent
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
closeButton : Assets r -> msg -> Html msg
|
||||
closeButton assets msg =
|
||||
Nri.Ui.styled div
|
||||
"close-button-container"
|
||||
[ Css.position Css.absolute
|
||||
, Css.top Css.zero
|
||||
, Css.right Css.zero
|
||||
, Css.padding (Css.px 25)
|
||||
]
|
||||
[]
|
||||
[ Icon.button
|
||||
{ alt = "Close"
|
||||
, msg = msg
|
||||
, icon = Icon.close assets
|
||||
, disabled = False
|
||||
, size = Icon.Medium
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
viewHeader : ModalType -> String -> Html msg
|
||||
viewHeader modalType title =
|
||||
Nri.Ui.styled Html.h3
|
||||
"modal-header"
|
||||
((case modalType of
|
||||
Info ->
|
||||
Css.color Nri.Ui.Colors.V1.navy
|
||||
|
||||
Warning ->
|
||||
Css.color Nri.Ui.Colors.V1.red
|
||||
)
|
||||
:: [ Css.fontWeight (Css.int 700)
|
||||
, Css.lineHeight (Css.px 27)
|
||||
, Css.margin2 Css.zero (Css.px 49)
|
||||
, Css.fontSize (Css.px 20)
|
||||
, Fonts.baseFont
|
||||
, Css.textAlign Css.center
|
||||
]
|
||||
)
|
||||
[]
|
||||
[ Html.text title
|
||||
]
|
||||
|
||||
|
||||
viewContent : ModalType -> Html msg -> Html msg
|
||||
viewContent modalType content =
|
||||
Nri.Ui.styled div
|
||||
"modal-content"
|
||||
[ Css.overflowY Css.auto
|
||||
, Css.padding2 (Css.px 30) (Css.px 40)
|
||||
, Css.width (Css.pct 100)
|
||||
, Css.minHeight (Css.px 150)
|
||||
, Css.boxSizing Css.borderBox
|
||||
]
|
||||
[]
|
||||
[ content ]
|
||||
|
||||
|
||||
viewFooter : List (Html msg) -> Html msg
|
||||
viewFooter footerContent =
|
||||
case footerContent of
|
||||
[] ->
|
||||
Html.text ""
|
||||
|
||||
_ ->
|
||||
Nri.Ui.styled div
|
||||
"modal-footer"
|
||||
[ Css.alignItems Css.center
|
||||
, Css.displayFlex
|
||||
, Css.flexDirection Css.column
|
||||
, Css.flexGrow (Css.int 2)
|
||||
, Css.flexWrap Css.noWrap
|
||||
, Css.margin4 (Css.px 20) Css.zero Css.zero Css.zero
|
||||
, Css.width (Css.pct 100)
|
||||
, Css.minHeight (Css.px 125) -- so the footer doesn't compress on Safari
|
||||
]
|
||||
[]
|
||||
(List.map
|
||||
(\x ->
|
||||
Nri.Ui.styled div
|
||||
"modal-footer-item"
|
||||
[ Css.margin4 (Css.px 10) Css.zero Css.zero Css.zero
|
||||
, Css.firstChild
|
||||
[ Css.margin Css.zero
|
||||
]
|
||||
]
|
||||
[]
|
||||
[ x ]
|
||||
)
|
||||
footerContent
|
||||
)
|
@ -1,421 +0,0 @@
|
||||
module Nri.Ui.Modal.V5 exposing
|
||||
( Model, init
|
||||
, Msg, update, subscriptions
|
||||
, info, warning, FocusableElementAttrs
|
||||
, viewContent, viewFooter
|
||||
, launchButton, closeButton
|
||||
, primaryButton, secondaryButton, dangerButton
|
||||
)
|
||||
|
||||
{-| Changes from V4:
|
||||
|
||||
- Remove dependence on Assets
|
||||
- Adds keyboard support (escape key to exit, tabs contained within the modal)
|
||||
|
||||
These changes have required major API changes. Be sure to wire up subscriptions!
|
||||
|
||||
import Html.Styled exposing (..)
|
||||
import Nri.Ui.Modal.V5 as Modal
|
||||
|
||||
view : Modal.State -> Html Msg
|
||||
view state =
|
||||
Modal.info
|
||||
{ title = { title = "Modal Header", visibleTitle = True }
|
||||
, wrapMsg = ModalMsg
|
||||
, content =
|
||||
\{ onlyFocusableElement } ->
|
||||
div []
|
||||
[ Modal.viewContent [ text "Content goes here!" ]
|
||||
, Modal.viewFooter
|
||||
[ Modal.primaryButton DoSomething "Continue" onlyFocusableElement
|
||||
, text "`onlyFocusableElement` will trap the focus on the 'Continue' button."
|
||||
]
|
||||
]
|
||||
}
|
||||
state
|
||||
|
||||
subscriptions : Modal.State -> Sub Msg
|
||||
subscriptions state =
|
||||
Modal.subscriptions state
|
||||
|
||||
|
||||
## State and updates
|
||||
|
||||
@docs Model, init
|
||||
@docs Msg, update, subscriptions
|
||||
|
||||
|
||||
## Views
|
||||
|
||||
|
||||
### Modals
|
||||
|
||||
@docs info, warning, FocusableElementAttrs
|
||||
|
||||
|
||||
### View containers
|
||||
|
||||
@docs viewContent, viewFooter
|
||||
|
||||
|
||||
### Buttons
|
||||
|
||||
@docs launchButton, closeButton
|
||||
@docs primaryButton, secondaryButton, dangerButton
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Modal as Modal
|
||||
import Accessibility.Style
|
||||
import Accessibility.Styled as Html exposing (..)
|
||||
import Accessibility.Styled.Style
|
||||
import Accessibility.Styled.Widget as Widget
|
||||
import Color
|
||||
import Css
|
||||
import Css.Global
|
||||
import Html as Root
|
||||
import Html.Attributes exposing (style)
|
||||
import Html.Styled.Attributes exposing (css)
|
||||
import Html.Styled.Events exposing (onClick)
|
||||
import Nri.Ui
|
||||
import Nri.Ui.Colors.Extra
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Fonts.V1 as Fonts
|
||||
import Nri.Ui.SpriteSheet
|
||||
import Nri.Ui.Svg.V1
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Model =
|
||||
Modal.Model
|
||||
|
||||
|
||||
{-| -}
|
||||
init : Model
|
||||
init =
|
||||
Modal.init
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Msg =
|
||||
Modal.Msg
|
||||
|
||||
|
||||
{-| Include the subscription if you want the modal to dismiss on `Esc`.
|
||||
-}
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions =
|
||||
Modal.subscriptions
|
||||
|
||||
|
||||
{-| -}
|
||||
update : { dismissOnEscAndOverlayClick : Bool } -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update config msg model =
|
||||
Modal.update config msg model
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias FocusableElementAttrs msg =
|
||||
{ onlyFocusableElement : List (Root.Attribute msg)
|
||||
, firstFocusableElement : List (Root.Attribute msg)
|
||||
, lastFocusableElement : List (Root.Attribute msg)
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
info :
|
||||
{ title : { visibleTitle : Bool, title : String }
|
||||
, content : FocusableElementAttrs msg -> Html msg
|
||||
, wrapMsg : Msg -> msg
|
||||
}
|
||||
-> Model
|
||||
-> Html msg
|
||||
info config model =
|
||||
Modal.view
|
||||
{ overlayColor = toOverlayColor Colors.navy
|
||||
, wrapMsg = config.wrapMsg
|
||||
, modalAttributes = modalStyles
|
||||
, title = viewTitle Colors.navy config.title
|
||||
, content = config.content >> toUnstyled
|
||||
}
|
||||
model
|
||||
|> fromUnstyled
|
||||
|
||||
|
||||
{-| -}
|
||||
warning :
|
||||
{ title : { visibleTitle : Bool, title : String }
|
||||
, content : FocusableElementAttrs msg -> Html msg
|
||||
, wrapMsg : Msg -> msg
|
||||
}
|
||||
-> Model
|
||||
-> Html msg
|
||||
warning config model =
|
||||
Modal.view
|
||||
{ overlayColor = toOverlayColor Colors.gray20
|
||||
, wrapMsg = config.wrapMsg
|
||||
, modalAttributes = modalStyles
|
||||
, title = viewTitle Colors.red config.title
|
||||
, content = config.content >> toUnstyled
|
||||
}
|
||||
model
|
||||
|> fromUnstyled
|
||||
|
||||
|
||||
toOverlayColor : Css.Color -> String
|
||||
toOverlayColor color =
|
||||
toCssString (Nri.Ui.Colors.Extra.withAlpha 0.9 color)
|
||||
|
||||
|
||||
modalStyles : List (Root.Attribute Never)
|
||||
modalStyles =
|
||||
[ style "width" "600px"
|
||||
, style "max-height" "calc(100vh - 100px)"
|
||||
, style "padding" "40px 0 40px 0"
|
||||
, style "margin" "75px auto"
|
||||
, style "background-color" (toCssString Colors.white)
|
||||
, style "border-radius" "20px"
|
||||
, style "box-shadow" "0 1px 10px 0 rgba(0, 0, 0, 0.35)"
|
||||
, style "position" "relative" -- required for closeButtonContainer
|
||||
]
|
||||
|
||||
|
||||
{-| -}
|
||||
viewTitle : Css.Color -> { visibleTitle : Bool, title : String } -> ( String, List (Root.Attribute Never) )
|
||||
viewTitle color { visibleTitle, title } =
|
||||
( title
|
||||
, if visibleTitle then
|
||||
[ style "font-weight" "700"
|
||||
, style "line-height" "27px"
|
||||
, style "margin" "0 49px"
|
||||
, style "font-size" "20px"
|
||||
, style "text-align" "center"
|
||||
, style "color" (toCssString color)
|
||||
]
|
||||
|
||||
else
|
||||
Accessibility.Style.invisible
|
||||
)
|
||||
|
||||
|
||||
toCssString : Css.Color -> String
|
||||
toCssString =
|
||||
Nri.Ui.Colors.Extra.fromCssColor >> Color.toRGBString
|
||||
|
||||
|
||||
{-| -}
|
||||
viewContent : List (Html msg) -> Html msg
|
||||
viewContent =
|
||||
Nri.Ui.styled div
|
||||
"modal-content"
|
||||
[ Css.overflowY Css.auto
|
||||
, Css.padding2 (Css.px 30) (Css.px 40)
|
||||
, Css.width (Css.pct 100)
|
||||
, Css.minHeight (Css.px 150)
|
||||
, Css.boxSizing Css.borderBox
|
||||
]
|
||||
[]
|
||||
|
||||
|
||||
{-| -}
|
||||
viewFooter : List (Html msg) -> Html msg
|
||||
viewFooter =
|
||||
Nri.Ui.styled div
|
||||
"modal-footer"
|
||||
[ Css.alignItems Css.center
|
||||
, Css.displayFlex
|
||||
, Css.flexDirection Css.column
|
||||
, Css.flexGrow (Css.int 2)
|
||||
, Css.flexWrap Css.noWrap
|
||||
, Css.margin4 (Css.px 20) Css.zero Css.zero Css.zero
|
||||
, Css.width (Css.pct 100)
|
||||
]
|
||||
[]
|
||||
|
||||
|
||||
|
||||
--BUTTONS
|
||||
|
||||
|
||||
{-| -}
|
||||
launchButton : (Msg -> msg) -> List Css.Style -> String -> Html msg
|
||||
launchButton wrapMsg styles label =
|
||||
button
|
||||
(css styles
|
||||
:: List.map Html.Styled.Attributes.fromUnstyled
|
||||
(Modal.openOnClick wrapMsg (String.replace " " "-" label))
|
||||
)
|
||||
[ text label ]
|
||||
|
||||
|
||||
{-| -}
|
||||
closeButton : (Msg -> msg) -> List (Root.Attribute msg) -> Html msg
|
||||
closeButton wrapMsg focusableElementAttrs =
|
||||
Nri.Ui.styled button
|
||||
"close-button-container"
|
||||
[ Css.position Css.absolute
|
||||
, Css.top Css.zero
|
||||
, Css.right Css.zero
|
||||
, Css.padding (Css.px 25)
|
||||
, Css.borderWidth Css.zero
|
||||
, Css.width (Css.px 75)
|
||||
, Css.backgroundColor Css.transparent
|
||||
, Css.cursor Css.pointer
|
||||
, Css.color Colors.azure
|
||||
, Css.hover [ Css.color Colors.azureDark ]
|
||||
, Css.property "transition" "color 0.1s"
|
||||
]
|
||||
(Widget.label "Close modal"
|
||||
:: Html.Styled.Attributes.map wrapMsg (onClick Modal.close)
|
||||
:: List.map Html.Styled.Attributes.fromUnstyled focusableElementAttrs
|
||||
)
|
||||
[ Nri.Ui.Svg.V1.toHtml Nri.Ui.SpriteSheet.xSvg
|
||||
]
|
||||
|
||||
|
||||
{-| -}
|
||||
primaryButton : msg -> String -> List (Root.Attribute msg) -> Html msg
|
||||
primaryButton msg label focusableElementAttrs =
|
||||
Nri.Ui.styled button
|
||||
"modal__primary-button"
|
||||
[ buttonStyle, colorStyle PrimaryColors, sizeStyle ]
|
||||
(onClick msg :: List.map Html.Styled.Attributes.fromUnstyled focusableElementAttrs)
|
||||
[ text label ]
|
||||
|
||||
|
||||
{-| -}
|
||||
secondaryButton : msg -> String -> List (Root.Attribute msg) -> Html msg
|
||||
secondaryButton msg label focusableElementAttrs =
|
||||
Nri.Ui.styled button
|
||||
"modal__secondary-button"
|
||||
[ buttonStyle
|
||||
, colorStyle SecondaryColors
|
||||
, Css.fontSize (Css.px 20)
|
||||
, Css.marginTop (Css.px 30)
|
||||
]
|
||||
(onClick msg :: List.map Html.Styled.Attributes.fromUnstyled focusableElementAttrs)
|
||||
[ text label ]
|
||||
|
||||
|
||||
{-| -}
|
||||
dangerButton : msg -> String -> List (Root.Attribute msg) -> Html msg
|
||||
dangerButton msg label focusableElementAttrs =
|
||||
Nri.Ui.styled button
|
||||
"modal__warning-button"
|
||||
[ buttonStyle, colorStyle DangerColors, sizeStyle ]
|
||||
(onClick msg :: List.map Html.Styled.Attributes.fromUnstyled focusableElementAttrs)
|
||||
[ text label ]
|
||||
|
||||
|
||||
buttonStyle : Css.Style
|
||||
buttonStyle =
|
||||
Css.batch
|
||||
[ Css.cursor Css.pointer
|
||||
, -- Specifying the font can and should go away after bootstrap is removed from application.css
|
||||
Fonts.baseFont
|
||||
, Css.textOverflow Css.ellipsis
|
||||
, Css.overflow Css.hidden
|
||||
, Css.textDecoration Css.none
|
||||
, Css.backgroundImage Css.none
|
||||
, Css.textShadow Css.none
|
||||
, Css.property "transition" "background-color 0.2s, color 0.2s, box-shadow 0.2s, border 0.2s, border-width 0s"
|
||||
, Css.boxShadow Css.none
|
||||
, Css.border Css.zero
|
||||
, Css.marginBottom Css.zero
|
||||
, Css.hover [ Css.textDecoration Css.none ]
|
||||
, Css.display Css.inlineFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.justifyContent Css.center
|
||||
]
|
||||
|
||||
|
||||
type ColorPalette
|
||||
= PrimaryColors
|
||||
| SecondaryColors
|
||||
| DangerColors
|
||||
|
||||
|
||||
colorStyle : ColorPalette -> Css.Style
|
||||
colorStyle colorPalette =
|
||||
let
|
||||
config =
|
||||
case colorPalette of
|
||||
PrimaryColors ->
|
||||
{ background = Colors.azure
|
||||
, hover = Colors.azureDark
|
||||
, text = Colors.white
|
||||
, shadow = Colors.azureDark
|
||||
}
|
||||
|
||||
SecondaryColors ->
|
||||
{ background = Colors.white
|
||||
, hover = Colors.white
|
||||
, text = Colors.azure
|
||||
, shadow = Colors.white
|
||||
}
|
||||
|
||||
DangerColors ->
|
||||
{ background = Colors.red
|
||||
, hover = Colors.redDark
|
||||
, text = Colors.white
|
||||
, shadow = Colors.redDark
|
||||
}
|
||||
in
|
||||
Css.batch
|
||||
[ Css.color config.text
|
||||
, Css.backgroundColor config.background
|
||||
, Css.fontWeight (Css.int 700)
|
||||
, Css.textAlign Css.center
|
||||
, Css.borderStyle Css.none
|
||||
, Css.borderBottomStyle Css.solid
|
||||
, Css.borderBottomColor config.shadow
|
||||
, Css.fontStyle Css.normal
|
||||
, Css.hover
|
||||
[ Css.color config.text
|
||||
, Css.backgroundColor config.hover
|
||||
, Css.disabled [ Css.backgroundColor config.background ]
|
||||
]
|
||||
, Css.visited [ Css.color config.text ]
|
||||
]
|
||||
|
||||
|
||||
sizeStyle : Css.Style
|
||||
sizeStyle =
|
||||
let
|
||||
config =
|
||||
{ fontSize = 20
|
||||
, height = 56
|
||||
, imageHeight = 20
|
||||
, shadowHeight = 4
|
||||
, minWidth = 200
|
||||
}
|
||||
|
||||
sizingAttributes =
|
||||
let
|
||||
verticalPaddingPx =
|
||||
2
|
||||
in
|
||||
[ Css.minHeight (Css.px config.height)
|
||||
, Css.paddingTop (Css.px verticalPaddingPx)
|
||||
, Css.paddingBottom (Css.px verticalPaddingPx)
|
||||
]
|
||||
|
||||
widthAttributes =
|
||||
[ Css.paddingLeft (Css.px 16)
|
||||
, Css.paddingRight (Css.px 16)
|
||||
, Css.minWidth (Css.px 230)
|
||||
]
|
||||
|
||||
lineHeightPx =
|
||||
22
|
||||
in
|
||||
Css.batch
|
||||
[ Css.fontSize (Css.px config.fontSize)
|
||||
, Css.borderRadius (Css.px 8)
|
||||
, Css.lineHeight (Css.px lineHeightPx)
|
||||
, Css.boxSizing Css.borderBox
|
||||
, Css.borderWidth (Css.px 1)
|
||||
, Css.borderBottomWidth (Css.px config.shadowHeight)
|
||||
, Css.batch sizingAttributes
|
||||
, Css.batch widthAttributes
|
||||
]
|
@ -1,302 +0,0 @@
|
||||
module Nri.Ui.Modal.V6 exposing
|
||||
( Model, init
|
||||
, Msg, update, subscriptions
|
||||
, open, close
|
||||
, info, warning, FocusableElementAttrs
|
||||
, viewContent, viewFooter
|
||||
, closeButton
|
||||
)
|
||||
|
||||
{-| Changes from V5:
|
||||
|
||||
- Removes button helpers, now that we can use Nri.Ui.Button.V9 directly
|
||||
|
||||
These changes have required major API changes. Be sure to wire up subscriptions!
|
||||
|
||||
import Html.Styled exposing (..)
|
||||
import Nri.Ui.Button.V10 as Button
|
||||
import Nri.Ui.Modal.V6 as Modal
|
||||
|
||||
type Msg
|
||||
= ModalMsg Modal.Msg
|
||||
| DoSomthing
|
||||
|
||||
view : Modal.Model -> Html Msg
|
||||
view state =
|
||||
Modal.info
|
||||
{ title = "Modal Header"
|
||||
, visibleTitle = True
|
||||
, wrapMsg = ModalMsg
|
||||
, content =
|
||||
\{ onlyFocusableElement } ->
|
||||
div []
|
||||
[ Modal.viewContent [ text "Content goes here!" ]
|
||||
, Modal.viewFooter
|
||||
[ Button.button "Continue"
|
||||
[ Button.primary
|
||||
, Button.onClick DoSomthing
|
||||
, Button.custom onlyFocusableElement
|
||||
]
|
||||
, text "`onlyFocusableElement` will trap the focus on the 'Continue' button."
|
||||
]
|
||||
]
|
||||
}
|
||||
state
|
||||
|
||||
subscriptions : Modal.Model -> Sub Msg
|
||||
subscriptions state =
|
||||
Modal.subscriptions state
|
||||
|
||||
view init
|
||||
--> text "" -- a closed modal
|
||||
|
||||
|
||||
## State and updates
|
||||
|
||||
@docs Model, init
|
||||
@docs Msg, update, subscriptions
|
||||
|
||||
@docs open, close
|
||||
|
||||
|
||||
## Views
|
||||
|
||||
|
||||
### Modals
|
||||
|
||||
@docs info, warning, FocusableElementAttrs
|
||||
|
||||
|
||||
### View containers
|
||||
|
||||
@docs viewContent, viewFooter
|
||||
|
||||
|
||||
## X icon
|
||||
|
||||
@docs closeButton
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Modal as Modal
|
||||
import Accessibility.Style
|
||||
import Accessibility.Styled as Html exposing (..)
|
||||
import Accessibility.Styled.Style
|
||||
import Accessibility.Styled.Widget as Widget
|
||||
import Color
|
||||
import Color.Transparent
|
||||
import Css
|
||||
import Css.Global
|
||||
import Html as Root
|
||||
import Html.Attributes exposing (style)
|
||||
import Html.Styled.Attributes exposing (css)
|
||||
import Html.Styled.Events exposing (onClick)
|
||||
import Nri.Ui
|
||||
import Nri.Ui.Colors.Extra
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Fonts.V1 as Fonts
|
||||
import Nri.Ui.SpriteSheet
|
||||
import Nri.Ui.Svg.V1
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Model =
|
||||
Modal.Model
|
||||
|
||||
|
||||
{-| -}
|
||||
init : Model
|
||||
init =
|
||||
Modal.init
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Msg =
|
||||
Modal.Msg
|
||||
|
||||
|
||||
{-| Include the subscription if you want the modal to dismiss on `Esc`.
|
||||
-}
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions =
|
||||
Modal.subscriptions
|
||||
|
||||
|
||||
{-| -}
|
||||
update : { dismissOnEscAndOverlayClick : Bool } -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update config msg model =
|
||||
Modal.update config msg model
|
||||
|
||||
|
||||
{-| -}
|
||||
close : Msg
|
||||
close =
|
||||
Modal.close
|
||||
|
||||
|
||||
{-| Pass the id of the element that focus should return to when the modal closes.
|
||||
-}
|
||||
open : String -> Msg
|
||||
open =
|
||||
Modal.open
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias FocusableElementAttrs msg =
|
||||
{ onlyFocusableElement : List (Attribute msg)
|
||||
, firstFocusableElement : List (Attribute msg)
|
||||
, lastFocusableElement : List (Attribute msg)
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
info :
|
||||
{ visibleTitle : Bool
|
||||
, title : String
|
||||
, content : FocusableElementAttrs msg -> Html msg
|
||||
, wrapMsg : Msg -> msg
|
||||
}
|
||||
-> Model
|
||||
-> Html msg
|
||||
info config model =
|
||||
view { overlayColor = Colors.navy, titleColor = Colors.navy } config model
|
||||
|
||||
|
||||
{-| -}
|
||||
warning :
|
||||
{ visibleTitle : Bool
|
||||
, title : String
|
||||
, content : FocusableElementAttrs msg -> Html msg
|
||||
, wrapMsg : Msg -> msg
|
||||
}
|
||||
-> Model
|
||||
-> Html msg
|
||||
warning config model =
|
||||
view { overlayColor = Colors.gray20, titleColor = Colors.red } config model
|
||||
|
||||
|
||||
view :
|
||||
{ overlayColor : Css.Color, titleColor : Css.Color }
|
||||
->
|
||||
{ visibleTitle : Bool
|
||||
, title : String
|
||||
, content : FocusableElementAttrs msg -> Html msg
|
||||
, wrapMsg : Msg -> msg
|
||||
}
|
||||
-> Model
|
||||
-> Html msg
|
||||
view { overlayColor, titleColor } config model =
|
||||
Modal.view
|
||||
{ overlayColor = toOverlayColor overlayColor
|
||||
, wrapMsg = config.wrapMsg
|
||||
, modalAttributes = modalStyles
|
||||
, title = viewTitle titleColor { title = config.title, visibleTitle = config.visibleTitle }
|
||||
, content =
|
||||
\{ onlyFocusableElement, firstFocusableElement, lastFocusableElement } ->
|
||||
{ onlyFocusableElement = List.map Html.Styled.Attributes.fromUnstyled onlyFocusableElement
|
||||
, firstFocusableElement = List.map Html.Styled.Attributes.fromUnstyled firstFocusableElement
|
||||
, lastFocusableElement = List.map Html.Styled.Attributes.fromUnstyled lastFocusableElement
|
||||
}
|
||||
|> config.content
|
||||
|> toUnstyled
|
||||
}
|
||||
model
|
||||
|> fromUnstyled
|
||||
|
||||
|
||||
toOverlayColor : Css.Color -> String
|
||||
toOverlayColor color =
|
||||
color
|
||||
|> Nri.Ui.Colors.Extra.fromCssColor
|
||||
|> Color.Transparent.fromColor (Color.Transparent.customOpacity 0.9)
|
||||
|> Color.Transparent.toRGBAString
|
||||
|
||||
|
||||
modalStyles : List (Root.Attribute Never)
|
||||
modalStyles =
|
||||
[ style "width" "600px"
|
||||
, style "max-height" "calc(100vh - 100px)"
|
||||
, style "padding" "40px 0 40px 0"
|
||||
, style "margin" "75px auto"
|
||||
, style "background-color" ((Color.toRGBString << Nri.Ui.Colors.Extra.fromCssColor) Colors.white)
|
||||
, style "border-radius" "20px"
|
||||
, style "box-shadow" "0 1px 10px 0 rgba(0, 0, 0, 0.35)"
|
||||
, style "position" "relative" -- required for closeButtonContainer
|
||||
]
|
||||
|
||||
|
||||
{-| -}
|
||||
viewTitle : Css.Color -> { visibleTitle : Bool, title : String } -> ( String, List (Root.Attribute Never) )
|
||||
viewTitle color { visibleTitle, title } =
|
||||
( title
|
||||
, if visibleTitle then
|
||||
[ style "font-weight" "700"
|
||||
, style "line-height" "27px"
|
||||
, style "margin" "0 49px"
|
||||
, style "font-size" "20px"
|
||||
, style "text-align" "center"
|
||||
, style "color" ((Color.toRGBString << Nri.Ui.Colors.Extra.fromCssColor) color)
|
||||
]
|
||||
|
||||
else
|
||||
Accessibility.Style.invisible
|
||||
)
|
||||
|
||||
|
||||
{-| -}
|
||||
viewContent : List (Html msg) -> Html msg
|
||||
viewContent =
|
||||
Nri.Ui.styled div
|
||||
"modal-content"
|
||||
[ Css.overflowY Css.auto
|
||||
, Css.padding2 (Css.px 30) (Css.px 40)
|
||||
, Css.width (Css.pct 100)
|
||||
, Css.minHeight (Css.px 150)
|
||||
, Css.boxSizing Css.borderBox
|
||||
]
|
||||
[]
|
||||
|
||||
|
||||
{-| -}
|
||||
viewFooter : List (Html msg) -> Html msg
|
||||
viewFooter =
|
||||
Nri.Ui.styled div
|
||||
"modal-footer"
|
||||
[ Css.alignItems Css.center
|
||||
, Css.displayFlex
|
||||
, Css.flexDirection Css.column
|
||||
, Css.flexGrow (Css.int 2)
|
||||
, Css.flexWrap Css.noWrap
|
||||
, Css.margin4 (Css.px 20) Css.zero Css.zero Css.zero
|
||||
, Css.width (Css.pct 100)
|
||||
]
|
||||
[]
|
||||
|
||||
|
||||
|
||||
--BUTTONS
|
||||
|
||||
|
||||
{-| -}
|
||||
closeButton : (Msg -> msg) -> List (Attribute msg) -> Html msg
|
||||
closeButton wrapMsg focusableElementAttrs =
|
||||
Nri.Ui.styled button
|
||||
"close-button-container"
|
||||
[ Css.position Css.absolute
|
||||
, Css.top Css.zero
|
||||
, Css.right Css.zero
|
||||
, Css.padding (Css.px 25)
|
||||
, Css.borderWidth Css.zero
|
||||
, Css.width (Css.px 75)
|
||||
, Css.backgroundColor Css.transparent
|
||||
, Css.cursor Css.pointer
|
||||
, Css.color Colors.azure
|
||||
, Css.hover [ Css.color Colors.azureDark ]
|
||||
, Css.property "transition" "color 0.1s"
|
||||
]
|
||||
(Widget.label "Close modal"
|
||||
:: Html.Styled.Attributes.map wrapMsg (onClick Modal.close)
|
||||
:: focusableElementAttrs
|
||||
)
|
||||
[ Nri.Ui.Svg.V1.toHtml Nri.Ui.SpriteSheet.xSvg
|
||||
]
|
@ -1,352 +0,0 @@
|
||||
module Nri.Ui.Modal.V7 exposing
|
||||
( Model, init
|
||||
, Msg, update, subscriptions
|
||||
, open, close
|
||||
, info, warning
|
||||
, viewContent, viewFooter
|
||||
, Attribute
|
||||
, multipleFocusableElementView, onlyFocusableElementView
|
||||
, autofocusOnLastElement
|
||||
, closeButton
|
||||
)
|
||||
|
||||
{-| Changes from V6:
|
||||
|
||||
- Modal starts a new stacking context, to prevent non-normal-flow elements from showing through the backdrop
|
||||
- Scrollable content shows a shadow
|
||||
|
||||
```
|
||||
import Html.Styled exposing (..)
|
||||
import Nri.Ui.Button.V10 as Button
|
||||
import Nri.Ui.Modal.V7 as Modal
|
||||
|
||||
type Msg
|
||||
= ModalMsg Modal.Msg
|
||||
| DoSomthing
|
||||
|
||||
view : Modal.Model -> Html Msg
|
||||
view state =
|
||||
Modal.info
|
||||
{ title = "Modal Header"
|
||||
, visibleTitle = True
|
||||
, wrapMsg = ModalMsg
|
||||
}
|
||||
[ Modal.onlyFocusableElementView
|
||||
(\{ onlyFocusableElement } ->
|
||||
div []
|
||||
[ Modal.viewContent [ text "Content goes here!" ]
|
||||
, Modal.viewFooter
|
||||
[ Button.button "Continue"
|
||||
[ Button.primary
|
||||
, Button.onClick DoSomthing
|
||||
, Button.custom onlyFocusableElement
|
||||
]
|
||||
, text "`onlyFocusableElement` will trap the focus on the 'Continue' button."
|
||||
]
|
||||
]
|
||||
)
|
||||
]
|
||||
state
|
||||
|
||||
subscriptions : Modal.Model -> Sub Msg
|
||||
subscriptions state =
|
||||
Modal.subscriptions state
|
||||
|
||||
view init
|
||||
--> text "" -- a closed modal
|
||||
```
|
||||
|
||||
|
||||
## State and updates
|
||||
|
||||
@docs Model, init
|
||||
@docs Msg, update, subscriptions
|
||||
|
||||
@docs open, close
|
||||
|
||||
|
||||
## Views
|
||||
|
||||
|
||||
### Modals
|
||||
|
||||
@docs info, warning
|
||||
|
||||
|
||||
### View containers
|
||||
|
||||
@docs viewContent, viewFooter
|
||||
|
||||
|
||||
### Attributes
|
||||
|
||||
@docs Attribute
|
||||
@docs multipleFocusableElementView, onlyFocusableElementView
|
||||
@docs autofocusOnLastElement
|
||||
|
||||
|
||||
## X icon
|
||||
|
||||
@docs closeButton
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Modal.Copy as Modal
|
||||
import Accessibility.Style
|
||||
import Accessibility.Styled as Html exposing (..)
|
||||
import Accessibility.Styled.Widget as Widget
|
||||
import Color
|
||||
import Color.Transparent
|
||||
import Css
|
||||
import Css.Global
|
||||
import Html as Root
|
||||
import Html.Attributes exposing (style)
|
||||
import Html.Styled.Attributes as Attributes exposing (css)
|
||||
import Html.Styled.Events exposing (onClick)
|
||||
import Nri.Ui
|
||||
import Nri.Ui.Colors.Extra
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Fonts.V1 as Fonts
|
||||
import Nri.Ui.SpriteSheet
|
||||
import Nri.Ui.Svg.V1
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Model =
|
||||
Modal.Model
|
||||
|
||||
|
||||
{-| -}
|
||||
init : Model
|
||||
init =
|
||||
Modal.init
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Msg =
|
||||
Modal.Msg
|
||||
|
||||
|
||||
{-| Include the subscription if you want the modal to dismiss on `Esc`.
|
||||
-}
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions =
|
||||
Modal.subscriptions
|
||||
|
||||
|
||||
{-| -}
|
||||
update : { dismissOnEscAndOverlayClick : Bool } -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update config msg model =
|
||||
Modal.update config msg model
|
||||
|
||||
|
||||
{-| -}
|
||||
close : Msg
|
||||
close =
|
||||
Modal.close
|
||||
|
||||
|
||||
{-| Pass the id of the element that focus should return to when the modal closes.
|
||||
-}
|
||||
open : String -> Msg
|
||||
open =
|
||||
Modal.open
|
||||
|
||||
|
||||
{-| -}
|
||||
info :
|
||||
{ visibleTitle : Bool
|
||||
, title : String
|
||||
, wrapMsg : Msg -> msg
|
||||
}
|
||||
-> List (Modal.Attribute msg)
|
||||
-> Model
|
||||
-> Html msg
|
||||
info config model =
|
||||
view { overlayColor = Colors.navy, titleColor = Colors.navy } config model
|
||||
|
||||
|
||||
{-| -}
|
||||
warning :
|
||||
{ visibleTitle : Bool
|
||||
, title : String
|
||||
, wrapMsg : Msg -> msg
|
||||
}
|
||||
-> List (Modal.Attribute msg)
|
||||
-> Model
|
||||
-> Html msg
|
||||
warning config model =
|
||||
view { overlayColor = Colors.gray20, titleColor = Colors.red } config model
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Attribute msg =
|
||||
Modal.Attribute msg
|
||||
|
||||
|
||||
{-| -}
|
||||
autofocusOnLastElement : Modal.Attribute msg
|
||||
autofocusOnLastElement =
|
||||
Modal.autofocusOnLastElement
|
||||
|
||||
|
||||
{-| -}
|
||||
multipleFocusableElementView :
|
||||
({ firstFocusableElement : List (Html.Attribute msg)
|
||||
, lastFocusableElement : List (Html.Attribute msg)
|
||||
, autofocusElement : Html.Attribute msg
|
||||
}
|
||||
-> Html msg
|
||||
)
|
||||
-> Modal.Attribute msg
|
||||
multipleFocusableElementView =
|
||||
Modal.multipleFocusableElementView
|
||||
|
||||
|
||||
{-| -}
|
||||
onlyFocusableElementView : (List (Html.Attribute msg) -> Html msg) -> Modal.Attribute msg
|
||||
onlyFocusableElementView =
|
||||
Modal.onlyFocusableElementView
|
||||
|
||||
|
||||
view :
|
||||
{ overlayColor : Css.Color, titleColor : Css.Color }
|
||||
->
|
||||
{ visibleTitle : Bool
|
||||
, title : String
|
||||
, wrapMsg : Msg -> msg
|
||||
}
|
||||
-> List (Modal.Attribute msg)
|
||||
-> Model
|
||||
-> Html msg
|
||||
view { overlayColor, titleColor } config attributes model =
|
||||
Modal.view config.wrapMsg
|
||||
config.title
|
||||
([ Modal.overlayColor (Nri.Ui.Colors.Extra.withAlpha 0.9 overlayColor)
|
||||
, Modal.titleStyles
|
||||
(if config.visibleTitle then
|
||||
titleStyles titleColor
|
||||
|
||||
else
|
||||
invisibleTitleStyles
|
||||
)
|
||||
, Modal.custom modalStyles
|
||||
]
|
||||
++ attributes
|
||||
)
|
||||
model
|
||||
|> List.singleton
|
||||
|> div [ css [ Css.position Css.relative, Css.zIndex (Css.int 1) ] ]
|
||||
|
||||
|
||||
modalStyles : List Css.Style
|
||||
modalStyles =
|
||||
[ Css.property "width" "600px"
|
||||
, Css.property "padding" "40px 0 40px 0"
|
||||
, Css.property "margin" "75px auto"
|
||||
, Css.property "background-color" ((Color.toRGBString << Nri.Ui.Colors.Extra.fromCssColor) Colors.white)
|
||||
, Css.property "border-radius" "20px"
|
||||
, Css.property "box-shadow" "0 1px 10px 0 rgba(0, 0, 0, 0.35)"
|
||||
, Css.property "position" "relative" -- required for closeButtonContainer
|
||||
]
|
||||
|
||||
|
||||
titleStyles : Css.Color -> List Css.Style
|
||||
titleStyles color =
|
||||
[ Fonts.baseFont
|
||||
, Css.property "font-weight" "700"
|
||||
, Css.property "line-height" "27px"
|
||||
, Css.property "margin" "0 49px"
|
||||
, Css.property "font-size" "20px"
|
||||
, Css.property "text-align" "center"
|
||||
, Css.property "color" ((Color.toRGBString << Nri.Ui.Colors.Extra.fromCssColor) color)
|
||||
]
|
||||
|
||||
|
||||
invisibleTitleStyles : List Css.Style
|
||||
invisibleTitleStyles =
|
||||
[ Css.property "property" "clip rect(1px, 1px, 1px, 1px)"
|
||||
, Css.property "position" "absolute"
|
||||
, Css.property "height" "1px"
|
||||
, Css.property "width" "1px"
|
||||
, Css.property "overflow" "hidden"
|
||||
, Css.property "margin" "-1px"
|
||||
, Css.property "padding" "0"
|
||||
, Css.property "border" "0"
|
||||
]
|
||||
|
||||
|
||||
{-| -}
|
||||
viewContent : List (Html msg) -> Html msg
|
||||
viewContent =
|
||||
Nri.Ui.styled div
|
||||
"modal-content"
|
||||
[ Css.overflowY Css.auto
|
||||
, Css.minHeight (Css.px 150)
|
||||
, Css.maxHeight (Css.calc (Css.vh 100) Css.minus (Css.px 360))
|
||||
, Css.padding2 (Css.px 30) (Css.px 40)
|
||||
, Css.width (Css.pct 100)
|
||||
, Css.boxSizing Css.borderBox
|
||||
|
||||
-- Shadows for indicating that the content is scrollable
|
||||
, Css.property "background"
|
||||
"""
|
||||
/* TOP shadow */
|
||||
|
||||
top linear-gradient(to top, rgb(255, 255, 255), rgb(255, 255, 255)) local,
|
||||
top linear-gradient(to top, rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.15)) scroll,
|
||||
|
||||
/* BOTTOM shadow */
|
||||
|
||||
bottom linear-gradient(to bottom, rgb(255, 255, 255), rgb(255, 255, 255)) local,
|
||||
bottom linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.15)) scroll
|
||||
"""
|
||||
, Css.backgroundSize2 (Css.pct 100) (Css.px 10)
|
||||
, Css.backgroundRepeat Css.noRepeat
|
||||
]
|
||||
[]
|
||||
|
||||
|
||||
{-| -}
|
||||
viewFooter : List (Html msg) -> Html msg
|
||||
viewFooter =
|
||||
Nri.Ui.styled div
|
||||
"modal-footer"
|
||||
[ Css.alignItems Css.center
|
||||
, Css.displayFlex
|
||||
, Css.flexDirection Css.column
|
||||
, Css.flexGrow (Css.int 2)
|
||||
, Css.flexWrap Css.noWrap
|
||||
, Css.margin4 (Css.px 20) Css.zero Css.zero Css.zero
|
||||
, Css.width (Css.pct 100)
|
||||
]
|
||||
[]
|
||||
|
||||
|
||||
|
||||
--BUTTONS
|
||||
|
||||
|
||||
{-| -}
|
||||
closeButton : (Msg -> msg) -> List (Html.Attribute msg) -> Html msg
|
||||
closeButton wrapMsg focusableElementAttrs =
|
||||
Nri.Ui.styled button
|
||||
"close-button-container"
|
||||
[ Css.position Css.absolute
|
||||
, Css.top Css.zero
|
||||
, Css.right Css.zero
|
||||
, Css.padding (Css.px 25)
|
||||
, Css.borderWidth Css.zero
|
||||
, Css.width (Css.px 75)
|
||||
, Css.backgroundColor Css.transparent
|
||||
, Css.cursor Css.pointer
|
||||
, Css.color Colors.azure
|
||||
, Css.hover [ Css.color Colors.azureDark ]
|
||||
, Css.property "transition" "color 0.1s"
|
||||
]
|
||||
(Widget.label "Close modal"
|
||||
:: Attributes.map wrapMsg (onClick Modal.close)
|
||||
:: focusableElementAttrs
|
||||
)
|
||||
[ Nri.Ui.Svg.V1.toHtml Nri.Ui.SpriteSheet.xSvg
|
||||
]
|
@ -1,408 +0,0 @@
|
||||
module Nri.Ui.Outline.V2 exposing
|
||||
( segment
|
||||
, node
|
||||
, NodeLayout
|
||||
, NodeConfig
|
||||
, config
|
||||
, html
|
||||
)
|
||||
|
||||
{-| A module for rendering outline layouts.
|
||||
|
||||
@docs segment
|
||||
@docs node
|
||||
@docs NodeLayout
|
||||
@docs NodeConfig
|
||||
@docs config
|
||||
@docs html
|
||||
|
||||
-}
|
||||
|
||||
import Css
|
||||
import Css.Global exposing (Snippet, children, descendants, everything, selector)
|
||||
import Html.Styled as Html exposing (Attribute, Html)
|
||||
import Html.Styled.Attributes as Attributes
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Effects.V1
|
||||
import Nri.Ui.Fonts.V1 as Fonts
|
||||
import Nri.Ui.Palette.V1 exposing (Palette)
|
||||
|
||||
|
||||
{-| A wrapper for a node rendered into Html. This type exists to prevent us
|
||||
from accidentally wrapping a node in a container element before passing it as a
|
||||
child to another node or segment. Such wrapping would break some of our
|
||||
styling, which assumes nodes of the same level are sibblings in the Html tree.
|
||||
-}
|
||||
type NodeLayout msg
|
||||
= NodeLayout (Html msg)
|
||||
|
||||
|
||||
{-| A container to draw nodes in.
|
||||
|
||||
segment
|
||||
[ node { config | label = "First Node" }
|
||||
, node { config | label = "Second Node" }
|
||||
]
|
||||
|
||||
-}
|
||||
segment : List (NodeLayout msg) -> Html msg
|
||||
segment children =
|
||||
Html.styled Html.div
|
||||
[ segmentStyles ]
|
||||
[ Attributes.attribute "data-is-segment" "" ]
|
||||
(List.map unlayout children)
|
||||
|
||||
|
||||
{-| Wrap any html in a NodeLayout so you can use it as sibling content to nodes.
|
||||
|
||||
segment
|
||||
[ node { config | label = "This is a node" }
|
||||
, html (Html.text "This is some random Html content!")
|
||||
]
|
||||
|
||||
-}
|
||||
html : Html msg -> NodeLayout msg
|
||||
html child =
|
||||
Html.div
|
||||
[ Attributes.attribute "data-is-custom-html" "" ]
|
||||
[ child ]
|
||||
|> NodeLayout
|
||||
|
||||
|
||||
{-| Defines how a node should look.
|
||||
-}
|
||||
type alias NodeConfig msg =
|
||||
-- The node's label.
|
||||
{ label : Html msg
|
||||
|
||||
-- The content of the node (the part in the colored area).
|
||||
, contents : Html msg
|
||||
|
||||
-- Child nodes (and other content) to be placed below the content.
|
||||
, children : List (NodeLayout msg)
|
||||
|
||||
-- The node is selected. Draw a selection shadow around it.
|
||||
, selected : Bool
|
||||
|
||||
-- The node is ghosted. Fade it out.
|
||||
, ghosted : Bool
|
||||
|
||||
-- Addition attributes to be set on the top level node element.
|
||||
, attrs : List (Html.Attribute msg)
|
||||
}
|
||||
|
||||
|
||||
{-| A default node configuration, allowing you to only set the properties you care about.
|
||||
|
||||
node { config | label = "Claim" }
|
||||
|
||||
-}
|
||||
config : NodeConfig msg
|
||||
config =
|
||||
{ label = Html.text ""
|
||||
, contents = Html.text ""
|
||||
, children = []
|
||||
, selected = False
|
||||
, ghosted = False
|
||||
, attrs = []
|
||||
}
|
||||
|
||||
|
||||
{-| Draw a node of an outline structure. You can draw other nodes inside it and
|
||||
connecting lines will appear.
|
||||
|
||||
node { config | label = "Claim" }
|
||||
|
||||
-}
|
||||
node : NodeConfig msg -> NodeLayout msg
|
||||
node { label, contents, children, selected, attrs, ghosted } =
|
||||
NodeLayout <|
|
||||
-- We use a custom Html tag name here, to ensure we can find the first and
|
||||
-- last node in a last-of-type selector.
|
||||
Html.styled (Html.node "outline-node")
|
||||
[ nodeStyles
|
||||
, if ghosted then
|
||||
ghostedNodeStyles
|
||||
|
||||
else
|
||||
Css.batch []
|
||||
]
|
||||
(Attributes.attribute "data-is-node" "" :: attrs)
|
||||
[ Html.styled Html.div
|
||||
[ innerNodeStyles
|
||||
, if selected then
|
||||
selectedNodeStyles
|
||||
|
||||
else
|
||||
Css.batch []
|
||||
]
|
||||
[ Attributes.attribute "data-is-inner-node" "" ]
|
||||
(Html.styled Html.div [ labelStyles ] [ Attributes.attribute "data-is-label" "" ] [ label ]
|
||||
:: Html.styled Html.div [ contentsStyles ] [ Attributes.attribute "data-is-contents" "" ] [ contents ]
|
||||
:: List.map unlayout children
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
unlayout : NodeLayout msg -> Html msg
|
||||
unlayout (NodeLayout html_) =
|
||||
html_
|
||||
|
||||
|
||||
type Style
|
||||
= Segment
|
||||
| Node
|
||||
| InnerNode
|
||||
| SelectedNode
|
||||
| GhostedNode
|
||||
| Label
|
||||
| Contents
|
||||
| CustomHtml
|
||||
|
||||
|
||||
labelHeight : Float
|
||||
labelHeight =
|
||||
35
|
||||
|
||||
|
||||
segmentStyles : Css.Style
|
||||
segmentStyles =
|
||||
Css.batch
|
||||
[ Css.position Css.relative
|
||||
, Css.zIndex (Css.int 0)
|
||||
|
||||
-- The overflow property cuts of connecting lines extending from
|
||||
-- top level nodes.
|
||||
, Css.overflow Css.auto
|
||||
]
|
||||
|
||||
|
||||
nodeStyles : Css.Style
|
||||
nodeStyles =
|
||||
Css.batch
|
||||
-- The node's relative positioning allows the connecting line to
|
||||
-- point upward relative from the node's bounding box.
|
||||
[ Css.position Css.relative
|
||||
, Css.display Css.block
|
||||
|
||||
-- This selects all nodes on a level but the first.
|
||||
, Css.Global.generalSiblings
|
||||
[ Css.Global.selector "[data-is-node]"
|
||||
-- Add some spacing between nodes of the same level.
|
||||
[ Css.marginTop (Css.px 20)
|
||||
, Css.before
|
||||
[ Css.property "content" "''"
|
||||
, Css.borderLeft2 (Css.px 1) Css.solid
|
||||
, Css.batch lineStyles
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
-- Child nodes have a connecting line and are indented.
|
||||
, Css.Global.descendants
|
||||
[ Css.Global.selector "[data-is-node]"
|
||||
[ Css.marginTop (Css.px 20)
|
||||
, Css.before
|
||||
-- Draw the connect line. It is like an antenna pointing
|
||||
-- upward in the direction of the parent node.
|
||||
[ Css.property "content" "''"
|
||||
, Css.borderLeft2 (Css.px 1) Css.solid
|
||||
, Css.borderBottom2 (Css.px 1) Css.solid
|
||||
, Css.batch lineStyles
|
||||
]
|
||||
, Css.Global.children
|
||||
-- Indent this node relative to the parent.
|
||||
[ Css.Global.selector "[data-is-inner-node]"
|
||||
[ Css.marginLeft (Css.px 50)
|
||||
]
|
||||
]
|
||||
]
|
||||
, Css.Global.selector "[data-is-custom-html]"
|
||||
[ Css.marginLeft (Css.px 50)
|
||||
]
|
||||
]
|
||||
|
||||
-- 1. Root level nodes are connected with one another using straight lines,
|
||||
-- so they are never curved.
|
||||
--
|
||||
-- Root Node 1
|
||||
-- │
|
||||
-- Root Node 2
|
||||
--
|
||||
-- 2. Second level nodes have a curved line when they are the last node of
|
||||
-- their level, and if their parent is the last root node. If either
|
||||
-- case is not true, they are connected to a straight line that
|
||||
-- continues beneath then. This is illustrated in the diagram below. As
|
||||
-- you can see only 'Child Node 3' should have a curved connecting line.
|
||||
--
|
||||
-- Root Node 1
|
||||
-- │
|
||||
-- ├─ Child Node 1
|
||||
-- │
|
||||
-- Root Node 2
|
||||
-- │
|
||||
-- ├─ Child Node 2
|
||||
-- │
|
||||
-- ╰─ Child Node 3
|
||||
--
|
||||
, Css.lastOfType
|
||||
[ Css.Global.descendants
|
||||
[ Css.Global.selector "[data-is-node]"
|
||||
[ Css.lastOfType
|
||||
[ Css.before
|
||||
[ Css.borderRadius (Css.px 8)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
-- 3. Third and lower level nodes always have a curved line when they are
|
||||
-- the last node on their level. This is illustrated in the diagram
|
||||
-- below. Sub-Child Node 2 has a curved connecting line, even though it's
|
||||
-- parent node is not the last node on its level.
|
||||
--
|
||||
-- Root Node
|
||||
-- │
|
||||
-- ├─ Child Node 1
|
||||
-- │ │
|
||||
-- │ ├─ Sub-Child Node 1
|
||||
-- │ │
|
||||
-- │ ╰─ Sub-Child Node 2
|
||||
-- │
|
||||
-- ╰─ Child Node 2
|
||||
--
|
||||
, Css.Global.descendants
|
||||
[ Css.Global.selector "[data-is-node]"
|
||||
[ Css.Global.descendants
|
||||
[ Css.Global.selector "[data-is-node]"
|
||||
[ Css.lastOfType
|
||||
[ Css.before
|
||||
[ Css.borderRadius (Css.px 8)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
innerNodeStyles : Css.Style
|
||||
innerNodeStyles =
|
||||
Css.batch
|
||||
[ Css.overflow Css.auto
|
||||
|
||||
-- The position and zIndex create a new stacking context. Connecting
|
||||
-- lines in child nodes of this one will be drawn in this context.
|
||||
, Css.position Css.relative
|
||||
, Css.zIndex (Css.int 0)
|
||||
|
||||
-- Recursively assign color styles to the different nested levels of
|
||||
-- the outline structure.
|
||||
, Css.Global.descendants
|
||||
(colorStyles
|
||||
[ Nri.Ui.Palette.V1.cornflower
|
||||
, Nri.Ui.Palette.V1.aqua
|
||||
, Nri.Ui.Palette.V1.turquoise
|
||||
, Nri.Ui.Palette.V1.green
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
ghostedNodeStyles : Css.Style
|
||||
ghostedNodeStyles =
|
||||
Css.batch
|
||||
[ Css.opacity (Css.num 0.5)
|
||||
, Css.zIndex (Css.int -1)
|
||||
, Css.position Css.relative
|
||||
]
|
||||
|
||||
|
||||
labelStyles : Css.Style
|
||||
labelStyles =
|
||||
Css.batch
|
||||
[ Css.border2 (Css.px 1) Css.solid
|
||||
, Css.padding2 Css.zero (Css.px 15)
|
||||
, Css.fontSize (Css.px 15)
|
||||
, Css.borderRadius (Css.px labelHeight)
|
||||
, Css.lineHeight (Css.px (labelHeight - 3))
|
||||
, Css.height (Css.px labelHeight)
|
||||
, Css.backgroundColor Colors.white
|
||||
, Css.position Css.absolute
|
||||
, Css.boxSizing Css.borderBox
|
||||
, Css.top Css.zero
|
||||
, Css.left Css.zero
|
||||
, Css.fontSize (Css.px 15)
|
||||
, Fonts.baseFont
|
||||
, Css.color Colors.gray20
|
||||
, Css.fontWeight Css.bold
|
||||
]
|
||||
|
||||
|
||||
contentsStyles : Css.Style
|
||||
contentsStyles =
|
||||
Css.batch
|
||||
[ Css.borderRadius (Css.px 8)
|
||||
, Css.marginTop (Css.px (labelHeight / 2))
|
||||
, Css.marginLeft (Css.px (labelHeight / 2))
|
||||
, Css.minHeight (Css.px 70)
|
||||
|
||||
-- Ensure there's some margin on all sides, so we have the option of
|
||||
-- drawing a border around selected contents without it being cut of
|
||||
-- by the surrounding inner node.
|
||||
, Css.marginRight (Css.px 5)
|
||||
, Css.marginBottom (Css.px 5)
|
||||
]
|
||||
|
||||
|
||||
selectedNodeStyles : Css.Style
|
||||
selectedNodeStyles =
|
||||
Css.batch
|
||||
[ Css.Global.children
|
||||
[ Css.Global.selector "[data-is-contents]"
|
||||
[ Css.batch Nri.Ui.Effects.V1.selectionShadow
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
colorStyles : List Palette -> List Snippet
|
||||
colorStyles palettes =
|
||||
case palettes of
|
||||
[] ->
|
||||
[]
|
||||
|
||||
palette :: rest ->
|
||||
[ Css.Global.selector "[data-is-inner-node]"
|
||||
[ Css.Global.descendants (colorStyles rest)
|
||||
]
|
||||
, Css.Global.selector "[data-is-contents]"
|
||||
[ Css.backgroundColor palette.background
|
||||
]
|
||||
, Css.Global.selector "[data-is-label]"
|
||||
[ Css.color palette.primary
|
||||
, Css.borderColor palette.border
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
lineStyles : List Css.Style
|
||||
lineStyles =
|
||||
[ Css.display Css.block
|
||||
, Css.borderColor Colors.gray75
|
||||
, Css.position Css.absolute
|
||||
, Css.width (Css.px 30)
|
||||
, Css.left (Css.px 30)
|
||||
, Css.bottom (Css.calc (Css.pct 100) Css.minus (Css.px (labelHeight / 2)))
|
||||
|
||||
-- Ensure the connecting line is long enough. The containing element will
|
||||
-- cut it to size.
|
||||
, Css.height (Css.vh 10000)
|
||||
|
||||
-- Make the connecting line go beneath the parent node,
|
||||
-- giving the impression it stops when touching the
|
||||
-- parent.
|
||||
, Css.zIndex (Css.int -100)
|
||||
]
|
@ -1,201 +0,0 @@
|
||||
module Nri.Ui.Page.V2 exposing (DefaultPage, broken, blocked, notFound, noPermission)
|
||||
|
||||
{-| A styled NRI issue page!
|
||||
|
||||
@docs DefaultPage, broken, blocked, notFound, noPermission
|
||||
|
||||
-}
|
||||
|
||||
import Css exposing (..)
|
||||
import Html.Styled as Html exposing (Html)
|
||||
import Html.Styled.Attributes as Attributes
|
||||
import Nri.Ui.Button.V5 as Button
|
||||
import Nri.Ui.Text.V2 as Text
|
||||
|
||||
|
||||
{-| The default page information is for the button
|
||||
which will direct the user back to the main page of
|
||||
the SPA. Specify it's name and the message which will
|
||||
navigate to the page.
|
||||
-}
|
||||
type alias DefaultPage msg =
|
||||
{ link : msg
|
||||
, name : String
|
||||
}
|
||||
|
||||
|
||||
{-| For the not found page.
|
||||
-}
|
||||
notFound : DefaultPage msg -> Html msg
|
||||
notFound defaultPage =
|
||||
view
|
||||
{ emoji = "\u{1F914}"
|
||||
, title = "We couldn’t find that!"
|
||||
, subtitle = "Feel free to browse around, or check out our help center."
|
||||
, defaultPage = Just defaultPage
|
||||
, details = Nothing
|
||||
}
|
||||
|
||||
|
||||
{-| For HTTP errors and other broken states.
|
||||
-}
|
||||
broken : DefaultPage msg -> Html msg
|
||||
broken defaultPage =
|
||||
view
|
||||
{ emoji = "😵"
|
||||
, title = "There was a problem!"
|
||||
, subtitle = "You can try again, or check out our help center."
|
||||
, defaultPage = Just defaultPage
|
||||
, details = Nothing
|
||||
}
|
||||
|
||||
|
||||
{-| For HTTP errors and other broken states, where link goes to "/".
|
||||
-}
|
||||
blocked : String -> Html msg
|
||||
blocked details =
|
||||
view
|
||||
{ emoji = "😵"
|
||||
, title = "There was a problem!"
|
||||
, subtitle = "You can try again, or check out our help center."
|
||||
, defaultPage = Nothing
|
||||
, details = Just details
|
||||
}
|
||||
|
||||
|
||||
{-| For pages the user does not have access to.
|
||||
-}
|
||||
noPermission : DefaultPage msg -> Html msg
|
||||
noPermission defaultPage =
|
||||
view
|
||||
{ emoji = "\u{1F910}"
|
||||
, title = "You do not have access to this page!"
|
||||
, subtitle = "Talk to a site administrator if you believe you should have access to this page."
|
||||
, defaultPage = Just defaultPage
|
||||
, details = Nothing
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- INTERNAL
|
||||
|
||||
|
||||
type alias Config msg =
|
||||
{ emoji : String
|
||||
, title : String
|
||||
, subtitle : String
|
||||
, defaultPage : Maybe (DefaultPage msg)
|
||||
, details : Maybe String
|
||||
}
|
||||
|
||||
|
||||
view : Config msg -> Html msg
|
||||
view config =
|
||||
viewContainer
|
||||
[ viewEmoji [ Html.text config.emoji ]
|
||||
, Text.heading [ Html.text config.title ]
|
||||
, Text.tagline [ Html.text config.subtitle ]
|
||||
, viewButton
|
||||
[ viewExit config ]
|
||||
, viewButton
|
||||
[ Button.linkExternal
|
||||
{ label = "Get help!"
|
||||
, icon = Nothing
|
||||
, url = "https://noredink.zendesk.com/hc/en-us"
|
||||
, size = Button.Large
|
||||
, style = Button.Secondary
|
||||
, width = Button.WidthExact 260
|
||||
}
|
||||
]
|
||||
, case config.details of
|
||||
Just details ->
|
||||
viewButton [ viewDetails details ]
|
||||
|
||||
Nothing ->
|
||||
Html.text ""
|
||||
]
|
||||
|
||||
|
||||
viewExit : Config msg -> Html msg
|
||||
viewExit config =
|
||||
case config.defaultPage of
|
||||
Just defaultPage ->
|
||||
Button.button
|
||||
{ onClick = defaultPage.link
|
||||
, size = Button.Large
|
||||
, style = Button.Primary
|
||||
, width = Button.WidthExact 260
|
||||
}
|
||||
{ label = "Return to " ++ defaultPage.name
|
||||
, state = Button.Enabled
|
||||
, icon = Nothing
|
||||
}
|
||||
|
||||
Nothing ->
|
||||
Button.link
|
||||
{ label = "Return to dashboard"
|
||||
, icon = Nothing
|
||||
, url = "/"
|
||||
, size = Button.Large
|
||||
, style = Button.Primary
|
||||
, width = Button.WidthExact 260
|
||||
}
|
||||
|
||||
|
||||
viewDetails : String -> Html msg
|
||||
viewDetails detailsForEngineers =
|
||||
Html.div []
|
||||
[ Html.styled Html.details
|
||||
[ margin (px 10)
|
||||
, maxWidth (px 700)
|
||||
]
|
||||
[]
|
||||
[ Html.styled Html.summary
|
||||
[ color (hex "8F8F8F") ]
|
||||
[]
|
||||
[ Html.text "Details for NoRedInk engineers" ]
|
||||
, Html.styled Html.code
|
||||
[ display block
|
||||
, whiteSpace normal
|
||||
, overflowWrap breakWord
|
||||
, textAlign left
|
||||
, marginTop (px 10)
|
||||
]
|
||||
[]
|
||||
[ Html.text detailsForEngineers ]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
viewContainer : List (Html msg) -> Html msg
|
||||
viewContainer =
|
||||
Html.styled Html.div
|
||||
[ marginTop (px 80)
|
||||
, displayFlex
|
||||
, flexDirection column
|
||||
, alignItems center
|
||||
]
|
||||
[ Attributes.attribute "data-page-container" "" ]
|
||||
|
||||
|
||||
viewButton : List (Html msg) -> Html msg
|
||||
viewButton children =
|
||||
Html.styled Html.div
|
||||
[ marginTop (px 15)
|
||||
]
|
||||
[]
|
||||
[ Html.styled Html.div
|
||||
[ textAlign center ]
|
||||
[]
|
||||
children
|
||||
]
|
||||
|
||||
|
||||
viewEmoji : List (Html msg) -> Html msg
|
||||
viewEmoji =
|
||||
Html.styled Html.div
|
||||
[ fontSize (px 75)
|
||||
, height (px 98)
|
||||
, lineHeight (px 98)
|
||||
]
|
||||
[]
|
@ -1,51 +0,0 @@
|
||||
module Nri.Ui.Pennant.V1 exposing (premiumFlag)
|
||||
|
||||
{-| Used for indicating Premium content
|
||||
|
||||
@docs premiumFlag
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled exposing (Html)
|
||||
import Svg.Styled as Svg exposing (..)
|
||||
import Svg.Styled.Attributes as SvgAttr exposing (..)
|
||||
|
||||
|
||||
{-| -}
|
||||
premiumFlag : Html msg
|
||||
premiumFlag =
|
||||
svg
|
||||
[ version "1.1"
|
||||
, id "Layer_1"
|
||||
, SvgAttr.width "25"
|
||||
, SvgAttr.height "19"
|
||||
, SvgAttr.viewBox "0 0 25 19"
|
||||
, SvgAttr.style "margin-left: 8px;"
|
||||
]
|
||||
[ Svg.title [] [ text "Premium" ]
|
||||
, Svg.style [] [ text " .premium-flag-st0{fill:#FEC709;} .premium-flag-st1{fill:#146AFF;} " ]
|
||||
, g [ id "Page-1" ]
|
||||
[ g
|
||||
[ id "icon_x2F_p-mini-pennant-yellow"
|
||||
, SvgAttr.transform "translate(0.000000, -3.000000)"
|
||||
]
|
||||
[ g
|
||||
[ id "Group"
|
||||
, SvgAttr.transform "translate(0.000000, 3.750000)"
|
||||
]
|
||||
[ polygon
|
||||
[ id "Fill-2"
|
||||
, class "premium-flag-st0"
|
||||
, points "12.7,0 0,0 0,13.8 0,15.8 0,17.5 7.3,17.5 24.8,17.5 19.4,8.1 24.8,0 "
|
||||
]
|
||||
[]
|
||||
, Svg.path
|
||||
[ id "P"
|
||||
, class "premium-flag-st1"
|
||||
, d "M7.5,3.8h4.2c1.1,0,1.9,0.3,2.5,0.8s0.9,1.2,0.9,2.1s-0.3,1.6-0.9,2.1c-0.6,0.5-1.4,0.8-2.5,0.8H9.3 v4.1H7.5V3.8z M11.5,8.1c0.6,0,1.1-0.1,1.4-0.4c0.3-0.3,0.5-0.6,0.5-1.1c0-0.5-0.2-0.9-0.5-1.1c-0.3-0.3-0.8-0.4-1.4-0.4H9.3v3 H11.5z"
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
@ -1,113 +0,0 @@
|
||||
module Nri.Ui.PremiumCheckbox.V4 exposing (PremiumConfig, premium, Pennant(..))
|
||||
|
||||
{-|
|
||||
|
||||
@docs PremiumConfig, premium, Pennant
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Html
|
||||
import Css exposing (..)
|
||||
import Html.Styled.Attributes as Attributes exposing (css)
|
||||
import Nri.Ui.AssetPath exposing (Asset(..))
|
||||
import Nri.Ui.AssetPath.Css
|
||||
import Nri.Ui.Checkbox.V5 as Checkbox
|
||||
|
||||
|
||||
{-|
|
||||
|
||||
- `onChange`: A message for when the user toggles the checkbox
|
||||
- `onLockedClick`: A message for when the user clicks a checkbox they don't have PremiumLevel for.
|
||||
If you get this message, you should show an `Nri.Ui.Premium.Model.view`
|
||||
|
||||
-}
|
||||
type alias PremiumConfig msg =
|
||||
{ label : String
|
||||
, id : String
|
||||
, selected : Checkbox.IsSelected
|
||||
, disabled : Bool
|
||||
, isLocked : Bool
|
||||
, pennant : Maybe Pennant
|
||||
, onChange : Bool -> msg
|
||||
, onLockedClick : msg
|
||||
}
|
||||
|
||||
|
||||
{-| Premium is the yellow "P" pennant
|
||||
PremiumWithWriting is the yellow "P+" pennant
|
||||
-}
|
||||
type Pennant
|
||||
= Premium
|
||||
| PremiumWithWriting
|
||||
|
||||
|
||||
{-| A checkbox that should be used for premium content
|
||||
-}
|
||||
premium : Assets a -> PremiumConfig msg -> Html.Html msg
|
||||
premium assets config =
|
||||
Html.div
|
||||
[ css
|
||||
[ displayFlex
|
||||
, alignItems center
|
||||
]
|
||||
]
|
||||
[ Checkbox.viewWithLabel
|
||||
{ identifier = config.id
|
||||
, label = config.label
|
||||
, setterMsg =
|
||||
if config.isLocked then
|
||||
\_ -> config.onLockedClick
|
||||
|
||||
else
|
||||
config.onChange
|
||||
, selected = config.selected
|
||||
, disabled = config.disabled
|
||||
, theme =
|
||||
if config.isLocked then
|
||||
Checkbox.Locked
|
||||
|
||||
else
|
||||
Checkbox.Square
|
||||
}
|
||||
, case config.pennant of
|
||||
Just pennant ->
|
||||
Html.div
|
||||
[ Attributes.class "premium-checkbox-V1__PremiumClass"
|
||||
, css
|
||||
[ property "content" "''"
|
||||
, display inlineBlock
|
||||
, width (px 26)
|
||||
, height (px 24)
|
||||
, marginLeft (px 8)
|
||||
, backgroundImage
|
||||
(case pennant of
|
||||
Premium ->
|
||||
assets.iconPremiumFlag_svg
|
||||
|
||||
PremiumWithWriting ->
|
||||
assets.iconPremiumWithWritingFlag_svg
|
||||
)
|
||||
, backgroundRepeat noRepeat
|
||||
, backgroundPosition center
|
||||
]
|
||||
]
|
||||
[]
|
||||
|
||||
Nothing ->
|
||||
Html.text ""
|
||||
]
|
||||
|
||||
|
||||
{-| The assets used in this module.
|
||||
-}
|
||||
type alias Assets r =
|
||||
{ r
|
||||
| iconPremiumFlag_svg : Asset
|
||||
, iconPremiumWithWritingFlag_svg : Asset
|
||||
}
|
||||
|
||||
|
||||
backgroundImage : Asset -> Style
|
||||
backgroundImage =
|
||||
Nri.Ui.AssetPath.Css.url
|
||||
>> property "background-image"
|
@ -1,150 +0,0 @@
|
||||
module Nri.Ui.PremiumCheckbox.V5 exposing (view, Pennant(..))
|
||||
|
||||
{-|
|
||||
|
||||
@docs view, Pennant
|
||||
|
||||
|
||||
# Changes from V4
|
||||
|
||||
- inlines the Config
|
||||
- renames render function from `premium` to `view`
|
||||
- removes dependency on Assets
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Html exposing (Html)
|
||||
import Css exposing (..)
|
||||
import Html.Styled exposing (fromUnstyled)
|
||||
import Html.Styled.Attributes as Attributes exposing (css)
|
||||
import Nri.Ui.Checkbox.V5 as Checkbox
|
||||
import Svg exposing (..)
|
||||
import Svg.Attributes exposing (..)
|
||||
|
||||
|
||||
{-| Premium is the yellow "P" pennant
|
||||
PremiumWithWriting is the yellow "P+" pennant
|
||||
-}
|
||||
type Pennant
|
||||
= Premium
|
||||
| PremiumWithWriting
|
||||
|
||||
|
||||
{-| A checkbox that should be used for premium content
|
||||
|
||||
- `onChange`: A message for when the user toggles the checkbox
|
||||
- `onLockedClick`: A message for when the user clicks a checkbox they don't have PremiumLevel for.
|
||||
If you get this message, you should show an `Nri.Ui.Premium.Model.view`
|
||||
|
||||
-}
|
||||
view :
|
||||
{ label : String
|
||||
, id : String
|
||||
, selected : Checkbox.IsSelected
|
||||
, disabled : Bool
|
||||
, isLocked : Bool
|
||||
, pennant : Pennant
|
||||
, onChange : Bool -> msg
|
||||
, onLockedClick : msg
|
||||
}
|
||||
-> Html msg
|
||||
view config =
|
||||
Html.div
|
||||
[ css
|
||||
[ displayFlex
|
||||
, alignItems center
|
||||
]
|
||||
]
|
||||
[ Checkbox.viewWithLabel
|
||||
{ identifier = config.id
|
||||
, label = config.label
|
||||
, setterMsg =
|
||||
if config.isLocked then
|
||||
\_ -> config.onLockedClick
|
||||
|
||||
else
|
||||
config.onChange
|
||||
, selected = config.selected
|
||||
, disabled = config.disabled
|
||||
, theme =
|
||||
if config.isLocked then
|
||||
Checkbox.Locked
|
||||
|
||||
else
|
||||
Checkbox.Square
|
||||
}
|
||||
, case config.pennant of
|
||||
Premium ->
|
||||
premiumFlag
|
||||
|
||||
PremiumWithWriting ->
|
||||
premiumWithWritingFlag
|
||||
]
|
||||
|
||||
|
||||
premiumFlag : Html msg
|
||||
premiumFlag =
|
||||
svg
|
||||
[ version "1.1"
|
||||
, id "Layer_1"
|
||||
, Svg.Attributes.width "25"
|
||||
, Svg.Attributes.height "19"
|
||||
, Svg.Attributes.viewBox "0 0 25 19"
|
||||
, Svg.Attributes.style "margin-left: 8px;"
|
||||
]
|
||||
[ Svg.title [] [ text "Premium" ]
|
||||
, Svg.style [] [ text " .premium-flag-st0{fill:#FEC709;} .premium-flag-st1{fill:#146AFF;} " ]
|
||||
, g [ id "Page-1" ]
|
||||
[ g
|
||||
[ id "icon_x2F_p-mini-pennant-yellow"
|
||||
, Svg.Attributes.transform "translate(0.000000, -3.000000)"
|
||||
]
|
||||
[ g
|
||||
[ id "Group"
|
||||
, Svg.Attributes.transform "translate(0.000000, 3.750000)"
|
||||
]
|
||||
[ polygon
|
||||
[ id "Fill-2"
|
||||
, class "premium-flag-st0"
|
||||
, points "12.7,0 0,0 0,13.8 0,15.8 0,17.5 7.3,17.5 24.8,17.5 19.4,8.1 24.8,0 "
|
||||
]
|
||||
[]
|
||||
, Svg.path
|
||||
[ id "P"
|
||||
, class "premium-flag-st1"
|
||||
, d "M7.5,3.8h4.2c1.1,0,1.9,0.3,2.5,0.8s0.9,1.2,0.9,2.1s-0.3,1.6-0.9,2.1c-0.6,0.5-1.4,0.8-2.5,0.8H9.3 v4.1H7.5V3.8z M11.5,8.1c0.6,0,1.1-0.1,1.4-0.4c0.3-0.3,0.5-0.6,0.5-1.1c0-0.5-0.2-0.9-0.5-1.1c-0.3-0.3-0.8-0.4-1.4-0.4H9.3v3 H11.5z"
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|> fromUnstyled
|
||||
|
||||
|
||||
premiumWithWritingFlag : Html msg
|
||||
premiumWithWritingFlag =
|
||||
svg
|
||||
[ Svg.Attributes.width "25"
|
||||
, Svg.Attributes.height "18"
|
||||
, Svg.Attributes.viewBox "0 0 25 18"
|
||||
, Svg.Attributes.style "margin-left: 8px;"
|
||||
]
|
||||
[ Svg.title [] [ text "Premium with Writing" ]
|
||||
, g
|
||||
[ Svg.Attributes.fill "none"
|
||||
, fillRule "evenodd"
|
||||
]
|
||||
[ Svg.path
|
||||
[ Svg.Attributes.fill "#FEC709"
|
||||
, d "M12.662 0H0v17.5h24.777l-5.384-9.423L24.777 0z"
|
||||
]
|
||||
[]
|
||||
, Svg.path
|
||||
[ Svg.Attributes.fill "#146AFF"
|
||||
, d "M2.5 3.75h4.249c1.054 0 1.874.254 2.461.763.587.509.88 1.203.88 2.083 0 .88-.296 1.577-.887 2.09-.591.514-1.41.77-2.454.77H4.274v4.084H2.5V3.75zm4.043 4.331c.614 0 1.079-.126 1.395-.378.316-.252.474-.616.474-1.093 0-.486-.155-.855-.467-1.107-.312-.252-.78-.378-1.402-.378h-2.27v2.956h2.27zM12.85 7h1.31c.152 0 .278.127.278.288V9.57h2.283c.152 0 .279.127.279.28v1.31a.281.281 0 0 1-.279.278h-2.283v2.283a.281.281 0 0 1-.278.279h-1.31a.281.281 0 0 1-.28-.279v-2.283h-2.282A.283.283 0 0 1 10 11.16V9.85c0-.153.127-.28.288-.28h2.282V7.288c0-.161.127-.288.28-.288z"
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
|> fromUnstyled
|
@ -1,150 +0,0 @@
|
||||
module Nri.Ui.SegmentedControl.V6 exposing (Config, Icon, Option, Width(..), view)
|
||||
|
||||
{-|
|
||||
|
||||
@docs Config, Icon, Option, Width, view
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled exposing (..)
|
||||
import Accessibility.Styled.Role as Role
|
||||
import Css exposing (..)
|
||||
import Html.Styled as Html exposing (Html)
|
||||
import Html.Styled.Attributes as Attr exposing (css)
|
||||
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.V3 as Icon
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Config a msg =
|
||||
{ onClick : a -> msg
|
||||
, options : List (Option a)
|
||||
, selected : a
|
||||
, width : Width
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Option a =
|
||||
{ value : a
|
||||
, icon : Maybe Icon
|
||||
, label : String
|
||||
, id : String
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
type Width
|
||||
= FitContent
|
||||
| FillContainer
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Icon =
|
||||
{ alt : String
|
||||
, icon : Icon.IconType
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
view : Config a msg -> Html.Html msg
|
||||
view config =
|
||||
tabList <|
|
||||
List.map (viewTab config) config.options
|
||||
|
||||
|
||||
tabList : List (Html.Html msg) -> Html.Html msg
|
||||
tabList =
|
||||
Nri.Ui.styled div
|
||||
"Nri-Ui-SegmentedControl-tabList"
|
||||
[ displayFlex, cursor pointer ]
|
||||
[ Role.tabList ]
|
||||
|
||||
|
||||
viewTab : Config a msg -> Option a -> Html.Html msg
|
||||
viewTab config option =
|
||||
Html.div
|
||||
[ Attr.id option.id
|
||||
, Role.tab
|
||||
, Events.onClick (config.onClick option.value)
|
||||
, css sharedTabStyles
|
||||
, css <|
|
||||
if option.value == config.selected then
|
||||
focusedTabStyles
|
||||
|
||||
else
|
||||
unFocusedTabStyles
|
||||
, css <|
|
||||
case config.width of
|
||||
FitContent ->
|
||||
[]
|
||||
|
||||
FillContainer ->
|
||||
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)
|
||||
, color Colors.azure
|
||||
, 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
|
||||
]
|
||||
|
||||
|
||||
focusedTabStyles : List Style
|
||||
focusedTabStyles =
|
||||
[ backgroundColor Colors.glacier
|
||||
, boxShadow5 inset zero (px 3) zero (withAlpha 0.2 Colors.gray20)
|
||||
, color Colors.gray20
|
||||
]
|
||||
|
||||
|
||||
unFocusedTabStyles : List Style
|
||||
unFocusedTabStyles =
|
||||
[ backgroundColor Colors.white
|
||||
, boxShadow5 inset zero (px -2) zero Colors.azure
|
||||
, color Colors.azure
|
||||
]
|
||||
|
||||
|
||||
expandingTabStyles : List Style
|
||||
expandingTabStyles =
|
||||
[ flexGrow (int 1)
|
||||
, textAlign center
|
||||
]
|
@ -1,368 +0,0 @@
|
||||
module Nri.Ui.Tabs.V3 exposing
|
||||
( Alignment(..)
|
||||
, Config
|
||||
, LinkConfig
|
||||
, Tab
|
||||
, TabLink
|
||||
, links
|
||||
, view
|
||||
, viewCustom
|
||||
, viewTabDefault
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
@docs Alignment
|
||||
@docs Config
|
||||
@docs LinkConfig
|
||||
@docs Tab
|
||||
@docs TabLink
|
||||
@docs links
|
||||
@docs view
|
||||
@docs viewCustom
|
||||
|
||||
|
||||
## Defaults
|
||||
|
||||
@docs viewTabDefault
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Aria
|
||||
import Accessibility.Key
|
||||
import Accessibility.Role
|
||||
import Accessibility.Widget
|
||||
import Css exposing (Style)
|
||||
import Html.Styled as Html exposing (Attribute, Html)
|
||||
import Html.Styled.Attributes as Attributes
|
||||
import Html.Styled.Events as Events
|
||||
import Json.Decode
|
||||
import List.Zipper exposing (Zipper)
|
||||
import List.Zipper.Extra
|
||||
import Nri.Ui.Colors.Extra
|
||||
import Nri.Ui.Colors.V1
|
||||
import Nri.Ui.Fonts.V1
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Config id msg =
|
||||
{ title : Maybe String
|
||||
, onSelect : id -> msg
|
||||
, tabs : Zipper (Tab id)
|
||||
, content : id -> Html msg
|
||||
, alignment : Alignment
|
||||
}
|
||||
|
||||
|
||||
{-| Determines whether tabs are centered or floating to the left or right.
|
||||
-}
|
||||
type Alignment
|
||||
= Left
|
||||
| Center
|
||||
| Right
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Tab id =
|
||||
{ label : String
|
||||
, id : id
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
view : Config id msg -> Html msg
|
||||
view config =
|
||||
viewCustom config viewTabDefault
|
||||
|
||||
|
||||
{-| -}
|
||||
viewTabDefault : Tab id -> Html msg
|
||||
viewTabDefault tab =
|
||||
Html.text tab.label
|
||||
|
||||
|
||||
{-| -}
|
||||
viewCustom : Config id msg -> (Tab id -> Html msg) -> Html msg
|
||||
viewCustom config viewInnerTab =
|
||||
let
|
||||
selected =
|
||||
List.Zipper.current config.tabs
|
||||
|
||||
viewTabs =
|
||||
List.Zipper.toList config.tabs
|
||||
|> List.map (viewTab config viewInnerTab selected)
|
||||
in
|
||||
Html.div
|
||||
[]
|
||||
[ Html.styled Html.div
|
||||
[ Css.displayFlex
|
||||
, Css.alignItems Css.flexEnd
|
||||
, Css.borderBottom (Css.px 1)
|
||||
, Css.borderBottomStyle Css.solid
|
||||
, Css.borderBottomColor Nri.Ui.Colors.V1.navy
|
||||
, Nri.Ui.Fonts.V1.baseFont
|
||||
]
|
||||
[]
|
||||
[ config.title
|
||||
|> Maybe.map viewTitle
|
||||
|> Maybe.withDefault (Html.text "")
|
||||
, Html.styled Html.ul
|
||||
(stylesTabsAligned config.alignment)
|
||||
[ Attributes.fromUnstyled <| Accessibility.Role.tabList
|
||||
]
|
||||
viewTabs
|
||||
]
|
||||
, Html.div
|
||||
[ Attributes.fromUnstyled <| Accessibility.Role.tabPanel
|
||||
, Attributes.fromUnstyled <| Accessibility.Aria.labelledBy (tabToId selected)
|
||||
, Attributes.fromUnstyled <| Accessibility.Widget.hidden False
|
||||
, Attributes.id (tabToBodyId selected)
|
||||
]
|
||||
[ config.content selected.id ]
|
||||
]
|
||||
|
||||
|
||||
viewTitle : String -> Html msg
|
||||
viewTitle title =
|
||||
Html.styled Html.h1
|
||||
[ Css.flexGrow (Css.int 2)
|
||||
, Css.fontSize (Css.px 30)
|
||||
, Css.fontWeight Css.bold
|
||||
, Css.margin Css.zero
|
||||
, Css.marginTop (Css.px 5)
|
||||
, Css.marginBottom (Css.px 10)
|
||||
, Css.color Nri.Ui.Colors.V1.navy
|
||||
, Css.width (Css.px 430)
|
||||
]
|
||||
[]
|
||||
[ Html.text title ]
|
||||
|
||||
|
||||
viewTab : Config id msg -> (Tab id -> Html msg) -> Tab id -> Tab id -> Html msg
|
||||
viewTab { onSelect, tabs } viewInnerTab selected tab =
|
||||
let
|
||||
isSelected =
|
||||
selected.id == tab.id
|
||||
in
|
||||
Html.styled Html.li
|
||||
(stylesTabSelectable isSelected)
|
||||
[ Events.onClick (onSelect tab.id)
|
||||
, Attributes.fromUnstyled <| Accessibility.Key.onKeyDown [ Accessibility.Key.enter (onSelect tab.id) ]
|
||||
, Events.onFocus (onSelect tab.id)
|
||||
, Attributes.tabindex 0
|
||||
, Attributes.fromUnstyled <| Accessibility.Role.presentation
|
||||
, Attributes.id (tabToId tab)
|
||||
, Attributes.fromUnstyled <| Accessibility.Aria.controls (tabToBodyId tab)
|
||||
, Attributes.fromUnstyled <| Accessibility.Widget.selected (selected.id == tab.id)
|
||||
, Events.on "keyup" <|
|
||||
Json.Decode.andThen
|
||||
(\keyCode ->
|
||||
if keyCode == 39 then
|
||||
tabs
|
||||
|> List.Zipper.next
|
||||
|> Maybe.map (List.Zipper.current >> .id >> onSelect >> Json.Decode.succeed)
|
||||
|> Maybe.withDefault (Json.Decode.fail "No next tab")
|
||||
|
||||
else if keyCode == 37 then
|
||||
tabs
|
||||
|> List.Zipper.previous
|
||||
|> Maybe.map (List.Zipper.current >> .id >> onSelect >> Json.Decode.succeed)
|
||||
|> Maybe.withDefault (Json.Decode.fail "No previous tab")
|
||||
|
||||
else
|
||||
Json.Decode.fail "Wrong key code"
|
||||
)
|
||||
Events.keyCode
|
||||
]
|
||||
[ Html.styled Html.div
|
||||
[ Css.color Nri.Ui.Colors.V1.navy
|
||||
, Css.hover [ Css.textDecoration Css.none ]
|
||||
, Css.focus [ Css.textDecoration Css.none ]
|
||||
, Css.display Css.inlineBlock
|
||||
, Css.padding4 (Css.px 14) (Css.px 20) (Css.px 12) (Css.px 20)
|
||||
, Css.position Css.relative
|
||||
, Css.textDecoration Css.none
|
||||
, Css.property "background" "none"
|
||||
, Css.fontFamily Css.inherit
|
||||
, Css.fontSize Css.inherit
|
||||
, Css.cursor Css.pointer
|
||||
]
|
||||
[ Attributes.fromUnstyled <| Accessibility.Role.tab
|
||||
, Attributes.tabindex -1
|
||||
]
|
||||
[ viewInnerTab tab ]
|
||||
]
|
||||
|
||||
|
||||
{-| Describe a tab that is meant to link to another page
|
||||
-}
|
||||
type alias TabLink =
|
||||
{ label : String
|
||||
, href : Maybe String
|
||||
}
|
||||
|
||||
|
||||
{-| Configure a set a tab links
|
||||
-}
|
||||
type alias LinkConfig msg =
|
||||
{ title : Maybe String
|
||||
, tabs : Zipper TabLink
|
||||
, content : Html msg
|
||||
, alignment : Alignment
|
||||
}
|
||||
|
||||
|
||||
{-| View a set of tab links
|
||||
-}
|
||||
links : LinkConfig msg -> Html msg
|
||||
links config =
|
||||
Html.div []
|
||||
[ Html.styled Html.nav
|
||||
[ Css.displayFlex
|
||||
, Css.alignItems Css.flexEnd
|
||||
, Css.borderBottom (Css.px 1)
|
||||
, Css.borderBottomStyle Css.solid
|
||||
, Css.borderBottomColor Nri.Ui.Colors.V1.navy
|
||||
, Nri.Ui.Fonts.V1.baseFont
|
||||
]
|
||||
[]
|
||||
[ config.title
|
||||
|> Maybe.map viewTitle
|
||||
|> Maybe.withDefault (Html.text "")
|
||||
, Html.styled Html.ul
|
||||
(stylesTabsAligned config.alignment)
|
||||
[]
|
||||
(config.tabs
|
||||
|> mapWithCurrent (viewTabLink config)
|
||||
|> List.Zipper.toList
|
||||
)
|
||||
]
|
||||
, Html.div [] [ config.content ]
|
||||
]
|
||||
|
||||
|
||||
viewTabLink : LinkConfig msg -> Bool -> TabLink -> Html msg
|
||||
viewTabLink config isSelected tabLink =
|
||||
Html.styled Html.li
|
||||
(stylesTabSelectable isSelected)
|
||||
[ Attributes.fromUnstyled <| Accessibility.Role.presentation
|
||||
, Attributes.id (tabToId tabLink)
|
||||
]
|
||||
[ case tabLink.href of
|
||||
Just href ->
|
||||
Html.styled Html.a
|
||||
[ Css.color Nri.Ui.Colors.V1.navy
|
||||
, Css.display Css.inlineBlock
|
||||
, Css.padding4 (Css.px 14) (Css.px 20) (Css.px 12) (Css.px 20)
|
||||
, Css.textDecoration Css.none
|
||||
]
|
||||
[ Attributes.href href ]
|
||||
[ Html.text tabLink.label ]
|
||||
|
||||
Nothing ->
|
||||
Html.styled Html.button
|
||||
[ Css.color Nri.Ui.Colors.V1.navy
|
||||
, Css.display Css.inlineBlock
|
||||
, Css.padding4 (Css.px 14) (Css.px 20) (Css.px 12) (Css.px 20)
|
||||
, Css.textDecoration Css.none
|
||||
, Css.fontFamily Css.inherit
|
||||
, Css.fontSize Css.inherit
|
||||
, Css.border Css.zero
|
||||
, Css.property "background" "none"
|
||||
, Css.lineHeight (Css.num 1)
|
||||
]
|
||||
[]
|
||||
[ Html.text tabLink.label ]
|
||||
]
|
||||
|
||||
|
||||
|
||||
-- HELP
|
||||
|
||||
|
||||
tabToId : { a | label : String } -> String
|
||||
tabToId tab =
|
||||
tab.label
|
||||
|
||||
|
||||
tabToBodyId : { a | label : String } -> String
|
||||
tabToBodyId tab =
|
||||
"tab-body-" ++ tab.label
|
||||
|
||||
|
||||
mapWithCurrent : (Bool -> a -> b) -> Zipper a -> Zipper b
|
||||
mapWithCurrent fn zipper =
|
||||
List.Zipper.Extra.from
|
||||
(List.map (fn False) (List.Zipper.before zipper))
|
||||
(fn True (List.Zipper.current zipper))
|
||||
(List.map (fn False) (List.Zipper.after zipper))
|
||||
|
||||
|
||||
|
||||
-- STYLES
|
||||
|
||||
|
||||
stylesTabsAligned : Alignment -> List Style
|
||||
stylesTabsAligned alignment =
|
||||
let
|
||||
alignmentStyles =
|
||||
case alignment of
|
||||
Left ->
|
||||
[ Css.justifyContent Css.flexStart ]
|
||||
|
||||
Center ->
|
||||
[ Css.justifyContent Css.center ]
|
||||
|
||||
Right ->
|
||||
[ Css.justifyContent Css.flexEnd ]
|
||||
in
|
||||
stylesTabs ++ alignmentStyles
|
||||
|
||||
|
||||
stylesTabs : List Style
|
||||
stylesTabs =
|
||||
[ Css.listStyle Css.none
|
||||
, Css.margin Css.zero
|
||||
, Css.fontSize (Css.px 19)
|
||||
, Css.displayFlex
|
||||
, Css.flexGrow (Css.int 1)
|
||||
, Css.marginRight (Css.px 10)
|
||||
]
|
||||
|
||||
|
||||
stylesTabSelectable : Bool -> List Style
|
||||
stylesTabSelectable isSelected =
|
||||
let
|
||||
stylesDynamic =
|
||||
if isSelected then
|
||||
[ Css.backgroundColor Nri.Ui.Colors.V1.white
|
||||
, Css.borderBottom (Css.px 1)
|
||||
, Css.borderBottomStyle Css.solid
|
||||
, Css.borderBottomColor Nri.Ui.Colors.V1.white
|
||||
]
|
||||
|
||||
else
|
||||
[ Css.backgroundColor Nri.Ui.Colors.V1.frost
|
||||
, Css.backgroundImage <|
|
||||
Css.linearGradient2 Css.toTop
|
||||
(Css.stop2 (Nri.Ui.Colors.Extra.withAlpha 0.25 Nri.Ui.Colors.V1.azure) (Css.pct 0))
|
||||
(Css.stop2 (Nri.Ui.Colors.Extra.withAlpha 0 Nri.Ui.Colors.V1.azure) (Css.pct 25))
|
||||
[ Css.stop2 (Nri.Ui.Colors.Extra.withAlpha 0 Nri.Ui.Colors.V1.azure) (Css.pct 100) ]
|
||||
]
|
||||
in
|
||||
stylesTab ++ stylesDynamic
|
||||
|
||||
|
||||
stylesTab : List Style
|
||||
stylesTab =
|
||||
[ Css.display Css.inlineBlock
|
||||
, Css.borderTopLeftRadius (Css.px 10)
|
||||
, Css.borderTopRightRadius (Css.px 10)
|
||||
, Css.border3 (Css.px 1) Css.solid Nri.Ui.Colors.V1.navy
|
||||
, Css.marginBottom (Css.px -1)
|
||||
, Css.marginLeft (Css.px 10)
|
||||
, Css.cursor Css.pointer
|
||||
, Css.firstChild
|
||||
[ Css.marginLeft Css.zero
|
||||
]
|
||||
]
|
@ -1,130 +0,0 @@
|
||||
module Nri.Ui.Text.V3 exposing
|
||||
( heading, subHeading, smallHeading, tagline
|
||||
, caption, mediumBody, smallBody, smallBodyGray
|
||||
, ugMediumBody, ugSmallBody
|
||||
, noWidow
|
||||
)
|
||||
|
||||
{-| Changes from V2:
|
||||
|
||||
- Normalizes how margin is applied (see "Understanding spacing", below).
|
||||
|
||||
|
||||
## Understanding spacing
|
||||
|
||||
- All text styles have a specific line-height. This is set so that when text in the given style
|
||||
is long enough to wrap, the spacing between wrapped lines looks good.
|
||||
- No text styles have padding.
|
||||
- **Heading styles** do not have margin. It is up to the caller to add appropriate margin to the layout.
|
||||
- **Paragraph styles** only have bottom margin, but with **:last-child bottom margin set to zero**.
|
||||
This bottom margin is set to look good when multiple paragraphs of the same style follow one another.
|
||||
- If you want content after the paragraph and don't want the margin, put the paragraph in a `div` so that it will be the last-child, which will get rid of the bottom margin.
|
||||
- **User-authored content blocks** preserve line breaks and do not have margin.
|
||||
|
||||
|
||||
## Heading styles
|
||||
|
||||
Please use `Nri.Ui.Heading.V2` instead of these in new code. If you're here to
|
||||
make a new Text version, please remove them.
|
||||
|
||||
@docs heading, subHeading, smallHeading, tagline
|
||||
|
||||
|
||||
## Paragraph styles
|
||||
|
||||
@docs caption, mediumBody, smallBody, smallBodyGray
|
||||
|
||||
|
||||
## User-authored content blocks:
|
||||
|
||||
@docs ugMediumBody, ugSmallBody
|
||||
|
||||
|
||||
## Modifying strings to display nicely:
|
||||
|
||||
@docs noWidow
|
||||
|
||||
-}
|
||||
|
||||
import Html.Styled exposing (Html)
|
||||
import Nri.Ui.Heading.V2 as Heading
|
||||
import Nri.Ui.Text.V4 as V4
|
||||
|
||||
|
||||
{-| This is a Page Heading.
|
||||
-}
|
||||
heading : List (Html msg) -> Html msg
|
||||
heading content =
|
||||
Heading.h1 [] content
|
||||
|
||||
|
||||
{-| This is a tagline for a page heading.
|
||||
-}
|
||||
tagline : List (Html msg) -> Html msg
|
||||
tagline content =
|
||||
Heading.h2 [] content
|
||||
|
||||
|
||||
{-| This is a subhead.
|
||||
-}
|
||||
subHeading : List (Html msg) -> Html msg
|
||||
subHeading content =
|
||||
Heading.h3 [] content
|
||||
|
||||
|
||||
{-| This is a small Page Heading.
|
||||
-}
|
||||
smallHeading : List (Html msg) -> Html msg
|
||||
smallHeading content =
|
||||
Heading.h4 [] content
|
||||
|
||||
|
||||
{-| This is some medium body copy.
|
||||
-}
|
||||
mediumBody : List (Html msg) -> Html msg
|
||||
mediumBody =
|
||||
V4.mediumBody
|
||||
|
||||
|
||||
{-| This is some small body copy.
|
||||
-}
|
||||
smallBody : List (Html msg) -> Html msg
|
||||
smallBody =
|
||||
V4.smallBody
|
||||
|
||||
|
||||
{-| This is some small body copy but it's gray.
|
||||
-}
|
||||
smallBodyGray : List (Html msg) -> Html msg
|
||||
smallBodyGray =
|
||||
V4.smallBodyGray
|
||||
|
||||
|
||||
{-| This is a little note or caption.
|
||||
-}
|
||||
caption : List (Html msg) -> Html msg
|
||||
caption =
|
||||
V4.caption
|
||||
|
||||
|
||||
{-| User-generated text.
|
||||
-}
|
||||
ugMediumBody : List (Html msg) -> Html msg
|
||||
ugMediumBody =
|
||||
V4.ugMediumBody
|
||||
|
||||
|
||||
{-| User-generated text.
|
||||
-}
|
||||
ugSmallBody : List (Html msg) -> Html msg
|
||||
ugSmallBody =
|
||||
V4.ugSmallBody
|
||||
|
||||
|
||||
{-| Eliminate widows (single words on their own line caused by
|
||||
wrapping) by inserting a non-breaking space if there are at least two
|
||||
words.
|
||||
-}
|
||||
noWidow : String -> String
|
||||
noWidow =
|
||||
V4.noWidow
|
@ -1,202 +0,0 @@
|
||||
module Nri.Ui.TextArea.V3 exposing (view, writing, contentCreation, Height(..), HeightBehavior(..), Model, generateId)
|
||||
|
||||
{-|
|
||||
|
||||
|
||||
## Upgrading to V3
|
||||
|
||||
- Do nothing! (This just uses new elm-css styles)
|
||||
|
||||
|
||||
## The Nri styleguide-specified textarea with overlapping label
|
||||
|
||||
@docs view, writing, contentCreation, Height, HeightBehavior, Model, generateId
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled.Style
|
||||
import Css exposing (plus, px)
|
||||
import Html.Styled as Html exposing (Html)
|
||||
import Html.Styled.Attributes as Attributes
|
||||
import Html.Styled.Events as Events
|
||||
import Nri.Ui.InputStyles.V2 as InputStyles
|
||||
exposing
|
||||
( Theme(..)
|
||||
, input
|
||||
, label
|
||||
)
|
||||
import Nri.Ui.Util exposing (dashify, removePunctuation)
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Model msg =
|
||||
{ value : String
|
||||
, autofocus : Bool
|
||||
, onInput : String -> msg
|
||||
, isInError : Bool
|
||||
, height : HeightBehavior
|
||||
, placeholder : String
|
||||
, label : String
|
||||
, showLabel : Bool
|
||||
}
|
||||
|
||||
|
||||
{-| Control whether to auto-expand the height.
|
||||
-}
|
||||
type HeightBehavior
|
||||
= Fixed
|
||||
| AutoResize Height
|
||||
|
||||
|
||||
{-| For specifying the actual height.
|
||||
-}
|
||||
type Height
|
||||
= DefaultHeight
|
||||
| SingleLine
|
||||
|
||||
|
||||
{-| -}
|
||||
view : Model msg -> Html msg
|
||||
view model =
|
||||
view_ Standard model
|
||||
|
||||
|
||||
{-| Used for Writing Cycles
|
||||
-}
|
||||
writing : Model msg -> Html msg
|
||||
writing model =
|
||||
view_ Writing model
|
||||
|
||||
|
||||
{-| Used for Content Creation
|
||||
-}
|
||||
contentCreation : Model msg -> Html msg
|
||||
contentCreation model =
|
||||
view_ ContentCreation model
|
||||
|
||||
|
||||
{-| -}
|
||||
view_ : Theme -> Model msg -> Html msg
|
||||
view_ theme model =
|
||||
let
|
||||
autoresizeAttrs =
|
||||
case model.height of
|
||||
AutoResize _ ->
|
||||
[ Attributes.attribute "data-autoresize" "" ]
|
||||
|
||||
Fixed ->
|
||||
[]
|
||||
|
||||
heightForStyle =
|
||||
case theme of
|
||||
Standard ->
|
||||
InputStyles.textAreaHeight
|
||||
|
||||
ContentCreation ->
|
||||
InputStyles.textAreaHeight
|
||||
|
||||
Writing ->
|
||||
InputStyles.writingMinHeight
|
||||
in
|
||||
Html.styled Html.div
|
||||
[ Css.position Css.relative ]
|
||||
[]
|
||||
[ Html.styled (Html.node "nri-textarea-v3")
|
||||
[ Css.display Css.block ]
|
||||
autoresizeAttrs
|
||||
[ Html.styled Html.textarea
|
||||
[ InputStyles.input theme model.isInError
|
||||
, Css.boxSizing Css.borderBox
|
||||
, case model.height of
|
||||
AutoResize minimumHeight ->
|
||||
Css.minHeight (calculateMinHeight theme minimumHeight)
|
||||
|
||||
Fixed ->
|
||||
Css.minHeight heightForStyle
|
||||
]
|
||||
[ Events.onInput model.onInput
|
||||
, Attributes.value model.value
|
||||
, Attributes.id (generateId model.label)
|
||||
, Attributes.autofocus model.autofocus
|
||||
, Attributes.placeholder model.placeholder
|
||||
, Attributes.attribute "data-gramm" "false" -- disables grammarly to prevent https://github.com/NoRedInk/NoRedInk/issues/14859
|
||||
, Attributes.class "override-sass-styles"
|
||||
, Attributes.attribute "aria-invalid" <|
|
||||
if model.isInError then
|
||||
"true"
|
||||
|
||||
else
|
||||
"false"
|
||||
]
|
||||
[]
|
||||
, if not model.showLabel then
|
||||
Html.label
|
||||
([ Attributes.for (generateId model.label)
|
||||
]
|
||||
++ Accessibility.Styled.Style.invisible
|
||||
)
|
||||
[ Html.text model.label ]
|
||||
|
||||
else
|
||||
Html.label
|
||||
[ Attributes.for (generateId model.label)
|
||||
, Attributes.css [ InputStyles.label theme model.isInError ]
|
||||
]
|
||||
[ Html.text model.label ]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
calculateMinHeight : Theme -> Height -> Css.Px
|
||||
calculateMinHeight textAreaStyle specifiedHeight =
|
||||
{- On including padding in this calculation:
|
||||
|
||||
When the textarea is autoresized, TextArea.js updates the textarea's
|
||||
height by taking its scrollHeight. Because scrollHeight's calculation
|
||||
includes the element's padding no matter what [1], we need to set the
|
||||
textarea's box-sizing to border-box in order to use the same measurement
|
||||
for its height as scrollHeight.
|
||||
|
||||
So, min-height also needs to be specified in terms of padding + content
|
||||
height.
|
||||
|
||||
[1] https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
|
||||
-}
|
||||
case specifiedHeight of
|
||||
SingleLine ->
|
||||
case textAreaStyle of
|
||||
Standard ->
|
||||
singleLineHeight
|
||||
|
||||
Writing ->
|
||||
writingSingleLineHeight
|
||||
|
||||
ContentCreation ->
|
||||
singleLineHeight
|
||||
|
||||
DefaultHeight ->
|
||||
case textAreaStyle of
|
||||
Standard ->
|
||||
InputStyles.textAreaHeight
|
||||
|
||||
Writing ->
|
||||
InputStyles.writingMinHeight
|
||||
|
||||
ContentCreation ->
|
||||
InputStyles.textAreaHeight
|
||||
|
||||
|
||||
singleLineHeight : Css.Px
|
||||
singleLineHeight =
|
||||
px (.numericValue InputStyles.inputPaddingVertical + .numericValue InputStyles.inputLineHeight + .numericValue InputStyles.inputPaddingVertical)
|
||||
|
||||
|
||||
writingSingleLineHeight : Css.Px
|
||||
writingSingleLineHeight =
|
||||
px (.numericValue InputStyles.writingPaddingTop + .numericValue InputStyles.writingLineHeight + .numericValue InputStyles.writingPadding)
|
||||
|
||||
|
||||
{-| -}
|
||||
generateId : String -> String
|
||||
generateId labelText =
|
||||
"nri-ui-text-area-" ++ (dashify <| removePunctuation labelText)
|
Loading…
Reference in New Issue
Block a user