Merge branch 'master' into tessa/kill-sortable-table-v1

This commit is contained in:
Tessa 2020-08-04 11:18:44 -07:00 committed by GitHub
commit 327b49837d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1 additions and 1599 deletions

View File

@ -11,7 +11,6 @@
"Nri.Ui.AssignmentIcon.V2",
"Nri.Ui.Button.V10",
"Nri.Ui.Button.V8",
"Nri.Ui.Button.V5",
"Nri.Ui.Callout.V1",
"Nri.Ui.Checkbox.V5",
"Nri.Ui.ClickableSvg.V1",
@ -37,16 +36,13 @@
"Nri.Ui.MasteryIcon.V1",
"Nri.Ui.Menu.V1",
"Nri.Ui.Message.V1",
"Nri.Ui.Modal.V2",
"Nri.Ui.Modal.V3",
"Nri.Ui.Modal.V8",
"Nri.Ui.Modal.V9",
"Nri.Ui.Page.V3",
"Nri.Ui.Palette.V1",
"Nri.Ui.Pennant.V2",
"Nri.Ui.PremiumCheckbox.V6",
"Nri.Ui.RadioButton.V1",
"Nri.Ui.SegmentedControl.V6",
"Nri.Ui.SegmentedControl.V9",
"Nri.Ui.Select.V5",
"Nri.Ui.Select.V7",

View File

@ -3,8 +3,6 @@ src/Nri/Ui/Page/V3.elm,Nri.Ui.Text,2
src/Nri/Ui/SlideModal/V2.elm,Nri.Ui.Button,8
src/Nri/Ui/SlideModal/V2.elm,Nri.Ui.Text,2
src/Nri/Ui/Button/V8.elm,Html,DEPRECATED
src/Nri/Ui/Button/V5.elm,Nri.Ui.Icon,3
src/Nri/Ui/SegmentedControl/V6.elm,Nri.Ui.Icon,3
src/Nri/Ui/Icon/V3.elm,Accessibility.Role,DEPRECATED
src/Nri/Ui/Icon/V3.elm,Html,DEPRECATED
src/Nri/Ui/Icon/V5.elm,Accessibility.Role,DEPRECATED

1 filename name version
3 src/Nri/Ui/SlideModal/V2.elm Nri.Ui.Button 8
4 src/Nri/Ui/SlideModal/V2.elm Nri.Ui.Text 2
5 src/Nri/Ui/Button/V8.elm Html DEPRECATED
src/Nri/Ui/Button/V5.elm Nri.Ui.Icon 3
src/Nri/Ui/SegmentedControl/V6.elm Nri.Ui.Icon 3
6 src/Nri/Ui/Icon/V3.elm Accessibility.Role DEPRECATED
7 src/Nri/Ui/Icon/V3.elm Html DEPRECATED
8 src/Nri/Ui/Icon/V5.elm Accessibility.Role DEPRECATED

View File

@ -1,813 +0,0 @@
module Nri.Ui.Button.V5 exposing
( ButtonSize(..), ButtonWidth(..), ButtonStyle(..), ButtonState(..), ButtonContent
, ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
, LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
)
{-|
# Changes from V4:
- Allows links with words exceeding button-width to wrap
- Standardizes vertical text alignment between buttons and links
- Limit CSS transitions to a subset of properties to avoid unintended "zoom"
effects.
# 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
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_ ]
([ 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-V5-" ++ suffix

View File

@ -85,7 +85,7 @@ spinningDots =
]
[ Svg.circle [ SvgAttributes.fill "#004e95", SvgAttributes.cx "6.13", SvgAttributes.cy "0.98", SvgAttributes.r "0.98" ] []
, Svg.circle [ SvgAttributes.fill "#004cc9", SvgAttributes.cx "9.95", SvgAttributes.cy "2.47", SvgAttributes.r "0.98", SvgAttributes.transform "translate(1.12 7.67) rotate(-44.43)" ] []
, Svg.circle [ SvgAttributes.fill "#146aff", SvgAttributes.x "11.56", SvgAttributes.cy "6.24", SvgAttributes.r "0.98", SvgAttributes.transform "translate(5.09 17.67) rotate(-88.86)" ] []
, Svg.circle [ SvgAttributes.fill "#146aff", SvgAttributes.cx "11.56", SvgAttributes.cy "6.24", SvgAttributes.r "0.98", SvgAttributes.transform "translate(5.09 17.67) rotate(-88.86)" ] []
, Svg.circle [ SvgAttributes.fill "#0af", SvgAttributes.cx "10", SvgAttributes.cy "10.02", SvgAttributes.r "0.98", SvgAttributes.transform "translate(-4.15 9.58) rotate(-43.29)" ] []
, Svg.circle [ SvgAttributes.fill "#d4f0ff", SvgAttributes.cx "6.2", SvgAttributes.cy "11.56", SvgAttributes.r "0.98", SvgAttributes.transform "translate(-5.6 17.29) rotate(-87.71)" ] []
, Svg.circle [ SvgAttributes.fill "#eef9ff", SvgAttributes.cx "2.44", SvgAttributes.cy "9.92", SvgAttributes.r "0.98", SvgAttributes.transform "translate(-6.03 4.21) rotate(-42.14)" ] []

View File

@ -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
)

View File

@ -1,412 +0,0 @@
module Nri.Ui.Modal.V8 exposing
( Model, init
, Msg, update, subscriptions
, open, close
, info, warning
, ViewFuncs
, Focusable
, multipleFocusableElementView, onlyFocusableElementView
)
{-| Changes from V7:
- More customizable attributes
- Rather than accepting any number of attributes, Modal provides one callback that returns a focusable
- viewFooter has been merged into viewContent
- viewContent and closeButton are now callbacks that are pre-configured with settings
(previously you passed config through)
## State and updates
@docs Model, init
@docs Msg, update, subscriptions
@docs open, close
## Views
### Modals
@docs info, warning
@docs ViewFuncs
### Focusable
@docs Focusable
@docs multipleFocusableElementView, onlyFocusableElementView
-}
import Accessibility.Modal.Copy as Modal
import Accessibility.Styled as Html exposing (..)
import Accessibility.Styled.Widget as Widget
import Color.Transparent as Transparent
import Css
import Css.Transitions
import Html.Styled.Attributes as Attributes exposing (css)
import Html.Styled.Events exposing (onClick)
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
type alias Config msg =
{ visibleTitle : Bool
, title : String
, wrapMsg : Msg -> msg
}
{-| -}
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 :
Config msg
-> (ViewFuncs msg -> Focusable msg)
-> Model
-> Html msg
info config getFocusable model =
view Info config getFocusable model
{-| -}
warning :
Config msg
-> (ViewFuncs msg -> Focusable msg)
-> Model
-> Html msg
warning config getFocusable model =
view Warning config getFocusable model
type Theme
= Info
| Warning
themeToOverlayColor : Theme -> Css.Color
themeToOverlayColor theme =
case theme of
Info ->
Colors.navy
Warning ->
Colors.gray20
themeToTitleColor : Theme -> Css.Color
themeToTitleColor theme =
case theme of
Info ->
Colors.navy
Warning ->
Colors.red
{-| -}
type Focusable msg
= Focusable (Modal.Attribute msg) (List (Modal.Attribute msg))
{-| -}
multipleFocusableElementView :
({ firstFocusableElement : List (Html.Attribute msg)
, lastFocusableElement : List (Html.Attribute msg)
, autofocusElement : Html.Attribute msg
}
-> Html msg
)
-> Focusable msg
multipleFocusableElementView f =
Focusable (Modal.multipleFocusableElementView (\attributes -> f attributes)) []
{-| -}
onlyFocusableElementView : (List (Html.Attribute msg) -> Html msg) -> Focusable msg
onlyFocusableElementView f =
Focusable (Modal.onlyFocusableElementView (\attributes -> f attributes)) [ Modal.autofocusOnLastElement ]
{-| -}
type alias ViewFuncs msg =
{ viewContent : { content : List (Html msg), footer : List (Html msg) } -> Html msg
, closeButton : List (Html.Attribute msg) -> Html msg
}
view :
Theme
-> Config msg
-> (ViewFuncs msg -> Focusable msg)
-> Model
-> Html msg
view theme config getFocusable model =
let
viewFuncs : ViewFuncs msg
viewFuncs =
{ viewContent = viewContent config.visibleTitle
, closeButton = closeButton config.wrapMsg
}
focusables =
case getFocusable viewFuncs of
Focusable fst rst ->
fst :: rst
in
Modal.view
config.wrapMsg
config.title
([ Modal.overlayColor (Nri.Ui.Colors.Extra.withAlpha 0.9 (themeToOverlayColor theme))
, Modal.custom
[ Css.width (Css.px 600)
, Css.margin2 (Css.px 50) Css.auto
, Css.borderRadius (Css.px 20)
, Css.boxShadow5 Css.zero (Css.px 1) (Css.px 10) Css.zero (Css.rgba 0 0 0 0.35)
, Css.backgroundColor Colors.white
-- the modal should grow up to the viewport minus a 50px margin
, Css.maxHeight (Css.calc (Css.pct 100) Css.minus (Css.px 100))
]
, if config.visibleTitle then
Modal.titleStyles
[ Fonts.baseFont
, Css.fontWeight (Css.int 700)
, Css.paddingTop (Css.px 40)
, Css.paddingBottom (Css.px 20)
, Css.margin Css.zero
, Css.fontSize (Css.px 20)
, Css.textAlign Css.center
, Css.color (themeToTitleColor theme)
]
else
Modal.titleStyles
[ -- https://snook.ca/archives/html_and_css/hiding-content-for-accessibility
Css.property "clip" "rect(1px, 1px, 1px, 1px)"
, Css.position Css.absolute
, Css.height (Css.px 1)
, Css.width (Css.px 1)
, Css.overflow Css.hidden
, Css.margin (Css.px -1)
, Css.padding Css.zero
, Css.border Css.zero
]
]
++ focusables
)
model
|> List.singleton
|> div [ css [ Css.position Css.relative, Css.zIndex (Css.int 1) ] ]
{-| -}
viewContent : Bool -> { content : List (Html msg), footer : List (Html msg) } -> Html msg
viewContent visibleTitle { content, footer } =
div []
[ viewInnerContent content visibleTitle (not (List.isEmpty footer))
, viewFooter footer
]
{-| -}
viewInnerContent : List (Html msg) -> Bool -> Bool -> Html msg
viewInnerContent children visibleTitle visibleFooter =
let
titleHeight =
if visibleTitle then
45
else
0
footerHeight =
if visibleFooter then
180
else
0
modalTitleStyles =
if visibleTitle then
[]
else
[ Css.borderTopLeftRadius (Css.px 20)
, Css.borderTopRightRadius (Css.px 20)
, Css.overflowY Css.hidden
]
modalFooterStyles =
if visibleFooter then
[]
else
[ Css.borderBottomLeftRadius (Css.px 20)
, Css.borderBottomRightRadius (Css.px 20)
, Css.overflowY Css.hidden
]
in
div
[ css (modalTitleStyles ++ modalFooterStyles)
]
[ div
[ css
[ Css.overflowY Css.auto
, Css.overflowX Css.hidden
, Css.minHeight (Css.px 150)
, Css.maxHeight
(Css.calc (Css.vh 100)
Css.minus
(Css.px (footerHeight + titleHeight + 145))
)
, Css.width (Css.pct 100)
, Css.boxSizing Css.borderBox
, Css.paddingLeft (Css.px 40)
, Css.paddingRight (Css.px 40)
, if visibleTitle then
Css.paddingTop Css.zero
else
Css.paddingTop (Css.px 40)
, if visibleFooter then
Css.paddingBottom Css.zero
else
Css.paddingBottom (Css.px 40)
, if visibleFooter then
shadow (Transparent.customOpacity 0.15) (Css.px 16)
else
shadow (Transparent.customOpacity 0.4) (Css.px 30)
]
]
children
]
shadow : Transparent.Opacity -> Css.Px -> Css.Style
shadow opacity bottomShadowHeight =
let
to =
Transparent.fromRGBA { red = 0, green = 0, blue = 0, alpha = opacity }
|> Transparent.toRGBAString
in
Css.batch
[ -- Shadows for indicating that the content is scrollable
[ "/* 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), " ++ to ++ ") scroll"
]
|> String.join "\n"
|> Css.property "background"
, Css.backgroundSize2 (Css.pct 100) bottomShadowHeight
, Css.backgroundRepeat Css.noRepeat
]
{-| -}
viewFooter : List (Html msg) -> Html msg
viewFooter children =
if List.isEmpty children then
Html.text ""
else
div
[ css
[ 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.paddingBottom (Css.px 40)
, Css.width (Css.pct 100)
]
]
children
--BUTTONS
{-| -}
closeButton : (Msg -> msg) -> List (Html.Attribute msg) -> Html msg
closeButton wrapMsg focusableElementAttrs =
button
(Widget.label "Close modal"
:: Attributes.map wrapMsg (onClick Modal.close)
:: css
[ -- in the upper-right corner of the modal
Css.position Css.absolute
, Css.top Css.zero
, Css.right Css.zero
-- make the hitspace extend all the way to the corner
, Css.width (Css.px 40)
, Css.height (Css.px 40)
, Css.padding4 (Css.px 20) (Css.px 20) Css.zero Css.zero
-- apply button styles
, Css.borderWidth Css.zero
, Css.backgroundColor Css.transparent
, Css.cursor Css.pointer
, Css.color Colors.azure
, Css.hover [ Css.color Colors.azureDark ]
, Css.Transitions.transition [ Css.Transitions.color 0.1 ]
]
:: focusableElementAttrs
)
[ Nri.Ui.Svg.V1.toHtml Nri.Ui.SpriteSheet.xSvg
]

View File

@ -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
]

View File

@ -7,7 +7,6 @@
"Nri.Ui.AssignmentIcon.V2",
"Nri.Ui.Button.V10",
"Nri.Ui.Button.V8",
"Nri.Ui.Button.V5",
"Nri.Ui.Callout.V1",
"Nri.Ui.Checkbox.V5",
"Nri.Ui.ClickableSvg.V1",
@ -33,16 +32,13 @@
"Nri.Ui.MasteryIcon.V1",
"Nri.Ui.Menu.V1",
"Nri.Ui.Message.V1",
"Nri.Ui.Modal.V2",
"Nri.Ui.Modal.V3",
"Nri.Ui.Modal.V8",
"Nri.Ui.Modal.V9",
"Nri.Ui.Page.V3",
"Nri.Ui.Palette.V1",
"Nri.Ui.Pennant.V2",
"Nri.Ui.PremiumCheckbox.V6",
"Nri.Ui.RadioButton.V1",
"Nri.Ui.SegmentedControl.V6",
"Nri.Ui.SegmentedControl.V9",
"Nri.Ui.Select.V5",
"Nri.Ui.Select.V7",