Merge remote-tracking branch 'origin/master' into tessa/use-color-library

This commit is contained in:
Tessa Kelly 2019-08-13 11:34:12 -07:00
commit 34d60a459c
39 changed files with 4165 additions and 562 deletions

3
.gitignore vendored
View File

@ -238,4 +238,5 @@ documentation.json
# direnv config file
.envrc
/public
/public
/tests/axe-report.json

View File

@ -3,6 +3,15 @@ SHELL:=env PATH=${PATH} /bin/sh
.PHONY: test
test: node_modules
npx elm-test
npx elm-verify-examples --run-tests
make axe-report
tests/axe-report.json: public script/run-axe.sh script/axe-puppeteer.js
script/run-axe.sh > $@
.PHONY: axe-report
axe-report: tests/axe-report.json script/format-axe-report.sh script/axe-report.jq
script/format-axe-report.sh $<
.PHONY: checks
checks:

View File

@ -3,7 +3,7 @@
"name": "NoRedInk/noredink-ui",
"summary": "UI Widgets we use at NRI",
"license": "BSD-3-Clause",
"version": "6.25.0",
"version": "6.31.0",
"exposed-modules": [
"Nri.Ui.Alert.V2",
"Nri.Ui.Alert.V3",
@ -15,12 +15,14 @@
"Nri.Ui.BannerAlert.V5",
"Nri.Ui.ClickableText.V1",
"Nri.Ui.ClickableText.V2",
"Nri.Ui.ClickableText.V3",
"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.Checkbox.V3",
"Nri.Ui.Checkbox.V4",
"Nri.Ui.Checkbox.V5",
@ -34,6 +36,7 @@
"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",
"Nri.Ui.Icon.V3",
@ -44,6 +47,7 @@
"Nri.Ui.Modal.V3",
"Nri.Ui.Modal.V4",
"Nri.Ui.Modal.V5",
"Nri.Ui.Modal.V6",
"Nri.Ui.Outline.V2",
"Nri.Ui.Page.V2",
"Nri.Ui.Page.V3",
@ -53,6 +57,7 @@
"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.Select.V5",
@ -62,17 +67,21 @@
"Nri.Ui.SlideModal.V1",
"Nri.Ui.SlideModal.V2",
"Nri.Ui.SortableTable.V1",
"Nri.Ui.SortableTable.V2",
"Nri.Ui.Table.V3",
"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",
"Nri.Ui.TextInput.V5",
"Nri.Ui"
],
"elm-version": "0.19.0 <= v < 0.20.0",
@ -88,6 +97,7 @@
"tesk9/accessible-html": "4.0.0 <= v < 5.0.0",
"tesk9/accessible-html-with-css": "2.1.1 <= v < 3.0.0",
"tesk9/modal": "5.0.1 <= v < 6.0.0",
"tesk9/palette": "2.0.0 <= v < 3.0.0",
"wernerdegroot/listzipper": "3.1.1 <= v < 4.0.0"
},
"test-dependencies": {

709
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -30,6 +30,11 @@
"elm": "^0.19.0-no-deps",
"elm-format": "0.8.1",
"elm-test": "0.19.0-rev6",
"elm-verify-examples": "^4.0.0",
"request": "^2.88.0"
},
"dependencies": {
"axe-core": "^3.3.0",
"puppeteer": "^1.19.0"
}
}

63
script/axe-puppeteer.js Normal file
View File

@ -0,0 +1,63 @@
// this script is from the example axe docs at https://github.com/dequelabs/axe-core/blob/develop/doc/examples/puppeteer/axe-puppeteer.js
// it is licensed MPL 2.0: https://github.com/dequelabs/axe-core/blob/develop/LICENSE
const puppeteer = require('puppeteer');
const axeCore = require('axe-core');
const { parse: parseURL } = require('url');
const assert = require('assert');
// Cheap URL validation
const isValidURL = input => {
const u = parseURL(input);
return u.protocol && u.host;
};
// node axe-puppeteer.js <url>
const url = process.argv[2];
assert(isValidURL(url), 'Invalid URL');
const main = async url => {
let browser;
let results;
try {
// Setup Puppeteer
browser = await puppeteer.launch();
// Get new page
const page = await browser.newPage();
await page.goto(url);
// Inject and run axe-core
const handle = await page.evaluateHandle(`
// Inject axe source code
${axeCore.source}
// Run axe
axe.run()
`);
// Get the results from `axe.run()`.
results = await handle.jsonValue();
// Destroy the handle & return axe results.
await handle.dispose();
} catch (err) {
// Ensure we close the puppeteer connection when possible
if (browser) {
await browser.close();
}
// Re-throw
throw err;
}
await browser.close();
return results;
};
main(url)
.then(results => {
console.log(JSON.stringify(results));
})
.catch(err => {
console.error('Error running axe-core:', err.message);
process.exit(1);
});

15
script/axe-report.jq Normal file
View File

@ -0,0 +1,15 @@
def node: " at \(.target | join(" ")):\n\n \(.failureSummary | gsub("\n"; "\n "))";
def violation: " \(.id): \(.impact) violation with \(.nodes | length) instances.\n\n \(.help) (\(.helpUrl))\n\n\(.nodes | map(node) | join("\n\n"))";
"Tested \(.url) with \(.testEngine.name) \(.testEngine.version) at \(.timestamp)
Agent information:
\(.testEnvironment | to_entries | map("\(.key): \(.value)") | join("\n "))
Summary: \(.passes | length) passes | \(.violations | map(.nodes | length) | add) violations | \(.incomplete | map(.nodes | length) | add) incomplete | \(.inapplicable | length) inapplicable
Violations:
\(.violations | map(violation) | join("\n\n"))
"

32
script/format-axe-report.sh Executable file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
JSON_FILE="${1:-}"
if test -z "$JSON_FILE"; then
echo "Please specify a report JSON file as the first argument."
exit 1
fi
jq -r -f script/axe-report.jq "$JSON_FILE"
# ideally we'd fail on any failures, but we have had a bunch build up over time!
# So right now, we need to fail if the error count is not exactly what we
# expect. This failure reminds us to come back and ratchet down the number of
# failures to the correct value.
NUM_ERRORS="$(jq '.violations | map(.nodes | length) | add' "$JSON_FILE")"
TARGET_ERRORS=161
if test "$NUM_ERRORS" -ne "$TARGET_ERRORS"; then
echo "got $NUM_ERRORS errors, but expected $TARGET_ERRORS."
echo
echo 'If it went down, hooray!'
echo "Check out ${0:-} and change the count to the reported value above."
echo
echo "If it went up, let's fix it instead."
echo "Since there are so many errors right now, a decent debugging strategy is:"
echo
echo " 1. save this output somewhere ('make axe-report > errors.new')"
echo " 2. undo your changes ('git stash' or 'checkout master')"
echo " 3. regenerate the log with 'make axe-report > errors.old'"
echo " 4. see waht's new with 'diff -u errors.old errors.new'"
exit 1
fi

12
script/run-axe.sh Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
# start a web server in the background and tear it down when exiting
./script/serve.sh public &
SERVER_PID=$!
cleanup() {
kill "$SERVER_PID"
}
trap cleanup EXIT INT
node script/axe-puppeteer.js http://localhost:8000

940
src/Nri/Ui/Button/V9.elm Normal file
View File

@ -0,0 +1,940 @@
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 (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 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"
(Attributes.target "_blank" :: config.customAttributes)
ExternalWithTracking ->
linkBase "linkExternalWithTracking"
(List.append
[ Attributes.target "_blank"
, Maybe.map EventExtras.onClickForLinkWithHref config.onClick
|> Maybe.withDefault AttributesExtra.none
]
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
]
]
]

View File

@ -0,0 +1,277 @@
module Nri.Ui.ClickableText.V3 exposing
( button
, link
, Attribute
, small, medium, large
, href, onClick
, icon
, custom
)
{-|
# Changes from V2
- Changes API to be attributes-based rather than config-based
- Makes a hole for custom attributes (like ids and styles)
# 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
# Attributes
@docs Attribute
@docs small, medium, large
@docs href, onClick
@docs icon
@docs custom
-}
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.Html.Attributes.V2 as AttributesExtra
import Nri.Ui.Svg.V1 as NriSvg exposing (Svg)
label : String -> Attribute msg
label label_ =
set (\attributes -> { attributes | label = label_ })
{-| -}
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 })
type Size
= Small
| Medium
| Large
{-| -}
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
}
)
{-| -}
onClick : msg -> Attribute msg
onClick msg =
set (\attributes -> { attributes | onClick = Just msg })
{-| -}
href : String -> Attribute msg
href url =
set (\attributes -> { attributes | url = url })
{-| Creates a `<button>` element
-}
button :
String
-> List (Attribute msg)
-> Html msg
button label_ attributes =
let
config =
(label label_ :: attributes)
|> List.foldl (\(Attribute attribute) b -> attribute b) defaults
in
Nri.Ui.styled Html.button
(dataDescriptor "button")
clickableTextStyles
((Maybe.map Events.onClick config.onClick
|> Maybe.withDefault AttributesExtra.none
)
:: config.customAttributes
)
[ viewContent config ]
{-| Creates a `<a>` element
-}
link :
String
-> List (Attribute msg)
-> Html msg
link label_ attributes =
let
config =
(label label_ :: attributes)
|> List.foldl (\(Attribute attribute) l -> attribute l) defaults
in
Nri.Ui.styled Html.a
(dataDescriptor "link")
clickableTextStyles
(Attributes.href config.url :: config.customAttributes)
[ 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
-- Internals
type alias ClickableTextAttributes msg =
{ label : String
, size : Size
, icon : Maybe Svg
, onClick : Maybe msg
, url : String
, customAttributes : List (Html.Attribute msg)
}
defaults : ClickableTextAttributes msg
defaults =
{ onClick = Nothing
, url = "#"
, size = Medium
, label = ""
, icon = Nothing
, customAttributes = []
}
{-| -}
type Attribute msg
= Attribute (ClickableTextAttributes msg -> ClickableTextAttributes msg)
set :
(ClickableTextAttributes msg -> ClickableTextAttributes msg)
-> Attribute msg
set with =
Attribute (\config -> with config)

View File

@ -5,8 +5,17 @@ module Nri.Ui.Heading.V1 exposing
, view
)
{-| Headings such as you'd find in Nri.Ui.Text.V3, but customization options for
accessibility.
{-| 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

185
src/Nri/Ui/Heading/V2.elm Normal file
View File

@ -0,0 +1,185 @@
module Nri.Ui.Heading.V2 exposing
( h1, h2, h3, h4, h5
, style, Style, css
, customAttr
)
{-| Headings with customization options for accessibility.
@docs h1, h2, h3, h4, h5
@docs style, Style, css
@docs customAttr
-}
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
{-| Make a first-level heading (styled like a top-level heading by default.)
-}
h1 : List (Attribute msg) -> List (Html msg) -> Html msg
h1 attrs content =
view Html.Styled.h1 (style Top :: attrs) content
{-| Make a second-level heading (styled like a tagline by default.)
-}
h2 : List (Attribute msg) -> List (Html msg) -> Html msg
h2 attrs content =
view Html.Styled.h2 (style Tagline :: attrs) content
{-| Make a third-level heading (styled like a subhead by default.)
-}
h3 : List (Attribute msg) -> List (Html msg) -> Html msg
h3 attrs content =
view Html.Styled.h3 (style Subhead :: attrs) content
{-| Make a fourth-level heading (styled like a small heading by default.)
-}
h4 : List (Attribute msg) -> List (Html msg) -> Html msg
h4 attrs content =
view Html.Styled.h4 (style Small :: attrs) content
{-| Make a fifth-level heading (styled like a small heading by default.)
-}
h5 : List (Attribute msg) -> List (Html msg) -> Html msg
h5 attrs content =
view Html.Styled.h5 (style Small :: attrs) content
view : (List (Html.Styled.Attribute msg) -> List (Html msg) -> Html msg) -> List (Attribute msg) -> List (Html msg) -> Html msg
view tag customizations content =
let
( finalStyle, attributes ) =
List.foldr
(\wrapped ( style_, attrs ) ->
case wrapped of
Style_ newStyle ->
( newStyle, attrs )
Css css_ ->
( style_, Html.Styled.Attributes.css css_ :: attrs )
Attribute_ attribute ->
( style_, attribute :: attrs )
)
( Top, [] )
customizations
in
tag (Html.Styled.Attributes.css [ getStyles finalStyle ] :: attributes) content
type Attribute msg
= Style_ Style
| Css (List Css.Style)
| Attribute_ (Html.Styled.Attribute msg)
{-| -}
type Style
= Top
| Tagline
| Subhead
| Small
{-| Select which of the base styles this heading should look like. Each of h1..5
has a default, check their docs to see if you don't need to override this.
-}
style : Style -> Attribute msg
style =
Style_
{-| Set some custom CSS in this heading. For example, maybe you need to tweak
margins. Now you can!
-}
css : List Css.Style -> Attribute msg
css =
Css
{-| Set some custom attribute. You can do _anything_ here, but please don't make
headers interactive! Use buttons or links instead so that keyboard and screen
reader users can use the site too.
-}
customAttr : Html.Styled.Attribute msg -> Attribute msg
customAttr =
Attribute_
-- Style
getStyles : Style -> Css.Style
getStyles style_ =
case style_ 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 : Css.Style
, lineHeight : Float
, size : Float
, weight : Int
}
-> Css.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
]

302
src/Nri/Ui/Modal/V6.elm Normal file
View File

@ -0,0 +1,302 @@
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.V9 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
]

View File

@ -0,0 +1,115 @@
module Nri.Ui.PremiumCheckbox.V6 exposing (view)
{-|
@docs view
This module is used when there may or may not be Premium
content to be "checked"!
# Changes from V5
- Allow checkbox to show pennant, or not, based on bool
- Remove PremiumWithWriting, it's only Premium now
-}
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 (..)
{-| 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
, isPremium : Bool
, 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
}
, if config.isPremium then
premiumFlag
else
Html.text ""
]
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

View File

@ -0,0 +1,368 @@
module Nri.Ui.SortableTable.V2 exposing
( Column, Config, Sorter, State
, init, initDescending
, custom, string, view, viewLoading
, invariantSort, simpleSort, combineSorters
)
{-|
@docs Column, Config, Sorter, State
@docs init, initDescending
@docs custom, string, view, viewLoading
@docs invariantSort, simpleSort, combineSorters
-}
import Color
import Css exposing (..)
import Css.Global exposing (Snippet, adjacentSiblings, children, class, descendants, each, everything, media, selector, withClass)
import Html.Styled as Html exposing (Html)
import Html.Styled.Attributes exposing (css)
import Html.Styled.Events
import Nri.Ui.Colors.Extra
import Nri.Ui.Colors.V1
import Nri.Ui.CssVendorPrefix.V1 as CssVendorPrefix
import Nri.Ui.Table.V5
import Svg.Styled as Svg exposing (Svg)
import Svg.Styled.Attributes as SvgAttributes
type SortDirection
= Ascending
| Descending
{-| -}
type alias Sorter a =
SortDirection -> a -> a -> Order
{-| -}
type Column id entry msg
= Column
{ id : id
, header : Html msg
, view : entry -> Html msg
, sorter : Sorter entry
, width : Int
, cellStyles : entry -> List Style
}
{-| -}
type alias State id =
{ column : id
, sortDirection : SortDirection
}
{-| -}
type alias Config id entry msg =
{ updateMsg : State id -> msg
, columns : List (Column id entry msg)
}
{-| -}
init : id -> State id
init initialSort =
{ column = initialSort
, sortDirection = Ascending
}
{-| -}
initDescending : id -> State id
initDescending initialSort =
{ column = initialSort
, sortDirection = Descending
}
{-| -}
string :
{ id : id
, header : String
, value : entry -> String
, width : Int
, cellStyles : entry -> List Style
}
-> Column id entry msg
string { id, header, value, width, cellStyles } =
Column
{ id = id
, header = Html.text header
, view = value >> Html.text
, sorter = simpleSort value
, width = width
, cellStyles = cellStyles
}
{-| -}
custom :
{ id : id
, header : Html msg
, view : entry -> Html msg
, sorter : Sorter entry
, width : Int
, cellStyles : entry -> List Style
}
-> Column id entry msg
custom config =
Column
{ id = config.id
, header = config.header
, view = config.view
, sorter = config.sorter
, width = config.width
, cellStyles = config.cellStyles
}
{-| Create a sorter function that always orders the entries in the same order.
For example, this is useful when we want to resolve ties and sort the tied
entries by name, no matter of the sort direction set on the table.
-}
invariantSort : (entry -> comparable) -> Sorter entry
invariantSort mapper =
\sortDirection elem1 elem2 ->
compare (mapper elem1) (mapper elem2)
{-| Create a simple sorter function that orders entries by mapping a function
over the collection. It will also reverse it when the sort direction is descending.
-}
simpleSort : (entry -> comparable) -> Sorter entry
simpleSort mapper =
\sortDirection elem1 elem2 ->
let
result =
compare (mapper elem1) (mapper elem2)
in
case sortDirection of
Ascending ->
result
Descending ->
flipOrder result
flipOrder : Order -> Order
flipOrder order =
case order of
LT ->
GT
EQ ->
EQ
GT ->
LT
{-| -}
combineSorters : List (Sorter entry) -> Sorter entry
combineSorters sorters =
\sortDirection elem1 elem2 ->
let
folder =
\sorter acc ->
case acc of
EQ ->
sorter sortDirection elem1 elem2
_ ->
acc
in
List.foldl folder EQ sorters
{-| -}
viewLoading : Config id entry msg -> State id -> Html msg
viewLoading config state =
let
tableColumns =
List.map (buildTableColumn config.updateMsg state) config.columns
in
Nri.Ui.Table.V5.viewLoading
tableColumns
{-| -}
view : Config id entry msg -> State id -> List entry -> Html msg
view config state entries =
let
tableColumns =
List.map (buildTableColumn config.updateMsg state) config.columns
sorter =
findSorter config.columns state.column
in
Nri.Ui.Table.V5.view
tableColumns
(List.sortWith (sorter state.sortDirection) entries)
findSorter : List (Column id entry msg) -> id -> Sorter entry
findSorter columns columnId =
columns
|> listExtraFind (\(Column column) -> column.id == columnId)
|> Maybe.map (\(Column column) -> column.sorter)
|> Maybe.withDefault identitySorter
{-| Taken from <https://github.com/elm-community/list-extra/blob/8.2.0/src/List/Extra.elm#L556>
-}
listExtraFind : (a -> Bool) -> List a -> Maybe a
listExtraFind predicate list =
case list of
[] ->
Nothing
first :: rest ->
if predicate first then
Just first
else
listExtraFind predicate rest
identitySorter : Sorter a
identitySorter =
\sortDirection item1 item2 ->
EQ
buildTableColumn : (State id -> msg) -> State id -> Column id entry msg -> Nri.Ui.Table.V5.Column entry msg
buildTableColumn updateMsg state (Column column) =
Nri.Ui.Table.V5.custom
{ header = viewSortHeader column.header updateMsg state column.id
, view = column.view
, width = Css.px (toFloat column.width)
, cellStyles = column.cellStyles
}
viewSortHeader : Html msg -> (State id -> msg) -> State id -> id -> Html msg
viewSortHeader header updateMsg state id =
let
nextState =
nextTableState state id
in
Html.div
[ css
[ Css.displayFlex
, Css.alignItems Css.center
, Css.justifyContent Css.spaceBetween
, cursor pointer
, CssVendorPrefix.property "user-select" "none"
, if state.column == id then
fontWeight bold
else
fontWeight normal
]
, Html.Styled.Events.onClick (updateMsg nextState)
]
[ Html.div [] [ header ]
, viewSortButton updateMsg state id
]
viewSortButton : (State id -> msg) -> State id -> id -> Html msg
viewSortButton updateMsg state id =
let
arrows upHighlighted downHighlighted =
Html.div
[ css
[ Css.displayFlex
, Css.flexDirection Css.column
, Css.alignItems Css.center
, Css.justifyContent Css.center
]
]
[ sortArrow Up upHighlighted
, sortArrow Down downHighlighted
]
buttonContent =
case ( state.column == id, state.sortDirection ) of
( True, Ascending ) ->
arrows True False
( True, Descending ) ->
arrows False True
( False, _ ) ->
arrows False False
in
Html.div [ css [ padding (px 2) ] ] [ buttonContent ]
nextTableState : State id -> id -> State id
nextTableState state id =
if state.column == id then
{ column = id
, sortDirection = flipSortDirection state.sortDirection
}
else
{ column = id
, sortDirection = Ascending
}
flipSortDirection : SortDirection -> SortDirection
flipSortDirection order =
case order of
Ascending ->
Descending
Descending ->
Ascending
type Direction
= Up
| Down
sortArrow : Direction -> Bool -> Html msg
sortArrow direction active =
Html.div
[ css
[ width (px 8)
, height (px 6)
, position relative
, margin2 (px 1) zero
]
]
[ Svg.svg
[ SvgAttributes.viewBox "0 0 8 6"
, SvgAttributes.css
[ position absolute
, top zero
, left zero
, case direction of
Up ->
Css.batch []
Down ->
Css.batch [ transform <| rotate (deg 180) ]
]
, if active then
SvgAttributes.fill (toCssString Nri.Ui.Colors.V1.azure)
else
SvgAttributes.fill (toCssString Nri.Ui.Colors.V1.gray75)
]
[ Svg.polygon [ SvgAttributes.points "0 6 4 0 8 6 0 6" ] []
]
]
toCssString : Css.Color -> String
toCssString =
Color.toCssString << Nri.Ui.Colors.Extra.toCoreColor

276
src/Nri/Ui/Table/V5.elm Normal file
View File

@ -0,0 +1,276 @@
module Nri.Ui.Table.V5 exposing
( Column, custom, string
, view, viewWithoutHeader
, viewLoading, viewLoadingWithoutHeader
)
{-| Upgrading from V4:
- The columns take an additional `cellStyles` property that allow
you to specify additional styles such as cell background color
or text alignment.
@docs Column, custom, string
@docs view, viewWithoutHeader
@docs viewLoading, viewLoadingWithoutHeader
-}
import Css exposing (..)
import Css.Animations
import Html.Styled as Html exposing (..)
import Html.Styled.Attributes exposing (css)
import Nri.Ui.Colors.V1 exposing (..)
import Nri.Ui.Fonts.V1 exposing (baseFont)
{-| Closed representation of how to render the header and cells of a column
in the table
-}
type Column data msg
= Column (Html msg) (data -> Html msg) Style (data -> List Style)
{-| A column that renders some aspect of a value as text
-}
string :
{ header : String
, value : data -> String
, width : LengthOrAuto compatible
, cellStyles : data -> List Style
}
-> Column data msg
string { header, value, width, cellStyles } =
Column (Html.text header) (value >> Html.text) (Css.width width) cellStyles
{-| A column that renders however you want it to
-}
custom :
{ header : Html msg
, view : data -> Html msg
, width : LengthOrAuto compatible
, cellStyles : data -> List Style
}
-> Column data msg
custom options =
Column options.header options.view (Css.width options.width) options.cellStyles
-- VIEW
{-| Displays a table of data without a header row
-}
viewWithoutHeader : List (Column data msg) -> List data -> Html msg
viewWithoutHeader columns =
tableWithoutHeader [] columns (viewRow columns)
{-| Displays a table of data based on the provided column definitions
-}
view : List (Column data msg) -> List data -> Html msg
view columns =
tableWithHeader [] columns (viewRow columns)
viewRow : List (Column data msg) -> data -> Html msg
viewRow columns data =
tr
[ css rowStyles ]
(List.map (viewColumn data) columns)
viewColumn : data -> Column data msg -> Html msg
viewColumn data (Column _ renderer width cellStyles) =
td
[ css ([ width, verticalAlign middle ] ++ cellStyles data)
]
[ renderer data ]
-- VIEW LOADING
{-| Display a table with the given columns but instead of data, show blocked
out text with an interesting animation. This view lets the user know that
data is on its way and what it will look like when it arrives.
-}
viewLoading : List (Column data msg) -> Html msg
viewLoading columns =
tableWithHeader loadingTableStyles columns (viewLoadingRow columns) (List.range 0 8)
{-| Display the loading table without a header row
-}
viewLoadingWithoutHeader : List (Column data msg) -> Html msg
viewLoadingWithoutHeader columns =
tableWithoutHeader loadingTableStyles columns (viewLoadingRow columns) (List.range 0 8)
viewLoadingRow : List (Column data msg) -> Int -> Html msg
viewLoadingRow columns index =
tr
[ css rowStyles ]
(List.indexedMap (viewLoadingColumn index) columns)
viewLoadingColumn : Int -> Int -> Column data msg -> Html msg
viewLoadingColumn rowIndex colIndex (Column _ _ width _) =
td
[ css (stylesLoadingColumn rowIndex colIndex width ++ [ verticalAlign middle ] ++ loadingCellStyles)
]
[ span [ css loadingContentStyles ] [] ]
stylesLoadingColumn : Int -> Int -> Style -> List Style
stylesLoadingColumn rowIndex colIndex width =
[ width
, property "animation-delay" (String.fromFloat (toFloat (rowIndex + colIndex) * 0.1) ++ "s")
]
-- HELP
tableWithoutHeader : List Style -> List (Column data msg) -> (a -> Html msg) -> List a -> Html msg
tableWithoutHeader styles columns toRow data =
table styles
[ tableBody toRow data
]
tableWithHeader : List Style -> List (Column data msg) -> (a -> Html msg) -> List a -> Html msg
tableWithHeader styles columns toRow data =
table styles
[ tableHeader columns
, tableBody toRow data
]
table : List Style -> List (Html msg) -> Html msg
table styles =
Html.table [ css (styles ++ tableStyles) ]
tableHeader : List (Column data msg) -> Html msg
tableHeader columns =
thead []
[ tr [ css headersStyles ]
(List.map tableRowHeader columns)
]
tableRowHeader : Column data msg -> Html msg
tableRowHeader (Column header _ width _) =
th
[ css (width :: headerStyles)
]
[ header ]
tableBody : (a -> Html msg) -> List a -> Html msg
tableBody toRow items =
tbody [] (List.map toRow items)
-- STYLES
headersStyles : List Style
headersStyles =
[ borderBottom3 (px 3) solid gray75
, height (px 45)
, fontSize (px 15)
]
headerStyles : List Style
headerStyles =
[ padding4 (px 15) (px 12) (px 11) (px 12)
, textAlign left
, fontWeight bold
]
rowStyles : List Style
rowStyles =
[ height (px 45)
, fontSize (px 14)
, color gray20
, pseudoClass "nth-child(odd)"
[ backgroundColor gray96 ]
]
loadingContentStyles : List Style
loadingContentStyles =
[ width (pct 100)
, display inlineBlock
, height (Css.em 1)
, borderRadius (Css.em 1)
, backgroundColor gray75
]
loadingCellStyles : List Style
loadingCellStyles =
[ batch flashAnimation
, padding2 (px 14) (px 10)
]
loadingTableStyles : List Style
loadingTableStyles =
fadeInAnimation
tableStyles : List Style
tableStyles =
[ borderCollapse collapse
, baseFont
, Css.width (Css.pct 100)
]
flash : Css.Animations.Keyframes {}
flash =
Css.Animations.keyframes
[ ( 0, [ Css.Animations.opacity (Css.num 0.6) ] )
, ( 50, [ Css.Animations.opacity (Css.num 0.2) ] )
, ( 100, [ Css.Animations.opacity (Css.num 0.6) ] )
]
fadeIn : Css.Animations.Keyframes {}
fadeIn =
Css.Animations.keyframes
[ ( 0, [ Css.Animations.opacity (Css.num 0) ] )
, ( 100, [ Css.Animations.opacity (Css.num 1) ] )
]
flashAnimation : List Css.Style
flashAnimation =
[ animationName flash
, property "animation-duration" "2s"
, property "animation-iteration-count" "infinite"
, opacity (num 0.6)
]
fadeInAnimation : List Css.Style
fadeInAnimation =
[ animationName fadeIn
, property "animation-duration" "0.4s"
, property "animation-delay" "0.2s"
, property "animation-fill-mode" "forwards"
, animationIterationCount (int 1)
, opacity (num 0)
]

View File

@ -24,7 +24,7 @@ module Nri.Ui.Text.V3 exposing
## Heading styles
Please use `Nri.Ui.Heading.V1` instead of these in new code. If you're here to
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
@ -46,168 +46,79 @@ make a new Text version, please remove them.
-}
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
import Nri.Ui.Heading.V1 as Heading
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.heading content
|> Heading.withVisualLevel Heading.Top
|> Heading.withDocumentLevel Heading.H1
|> Heading.view
Heading.h1 [] content
{-| This is a tagline for a page heading.
-}
tagline : List (Html msg) -> Html msg
tagline content =
Heading.heading content
|> Heading.withVisualLevel Heading.Tagline
|> Heading.withDocumentLevel Heading.H2
|> Heading.view
Heading.h2 [] content
{-| This is a subhead.
-}
subHeading : List (Html msg) -> Html msg
subHeading content =
Heading.heading content
|> Heading.withVisualLevel Heading.Subhead
|> Heading.withDocumentLevel Heading.H3
|> Heading.view
Heading.h3 [] content
{-| This is a small Page Heading.
-}
smallHeading : List (Html msg) -> Html msg
smallHeading content =
Heading.heading content
|> Heading.withVisualLevel Heading.Small
|> Heading.withDocumentLevel Heading.H4
|> Heading.view
Heading.h4 [] content
{-| This is some medium body copy.
-}
mediumBody : List (Html msg) -> Html msg
mediumBody content =
p
[ paragraphStyles
{ font = Fonts.baseFont
, color = gray20
, size = 18
, lineHeight = 28
, weight = 400
, margin = 10
}
]
content
mediumBody =
V4.mediumBody
{-| This is some small body copy.
-}
smallBody : List (Html msg) -> Html msg
smallBody content =
p
[ paragraphStyles
{ font = Fonts.baseFont
, color = gray20
, size = 15
, lineHeight = 23
, weight = 400
, margin = 7
}
]
content
smallBody =
V4.smallBody
{-| This is some small body copy but it's gray.
-}
smallBodyGray : List (Html msg) -> Html msg
smallBodyGray content =
p
[ paragraphStyles
{ font = Fonts.baseFont
, color = gray45
, size = 15
, lineHeight = 23
, weight = 400
, margin = 7
}
]
content
paragraphStyles config =
css
[ config.font
, fontSize (px config.size)
, color config.color
, lineHeight (px config.lineHeight)
, fontWeight (int config.weight)
, padding zero
, textAlign left
, margin4 (px 0) (px 0) (px config.margin) (px 0)
, lastChild
[ margin zero
]
]
smallBodyGray =
V4.smallBodyGray
{-| This is a little note or caption.
-}
caption : List (Html msg) -> Html msg
caption content =
p
[ paragraphStyles
{ font = Fonts.baseFont
, color = gray45
, size = 13
, lineHeight = 18
, weight = 400
, margin = 5
}
]
content
caption =
V4.caption
{-| User-generated text.
-}
ugMediumBody : List (Html msg) -> Html msg
ugMediumBody =
p
[ css
[ Fonts.quizFont
, fontSize (px 18)
, lineHeight (px 30)
, whiteSpace preLine
, color gray20
, margin zero
]
]
V4.ugMediumBody
{-| User-generated text.
-}
ugSmallBody : List (Html msg) -> Html msg
ugSmallBody =
p
[ css
[ Fonts.quizFont
, fontSize (px 16)
, lineHeight (px 25)
, whiteSpace preLine
, color gray20
, margin zero
]
]
V4.ugSmallBody
{-| Eliminate widows (single words on their own line caused by
@ -215,29 +126,5 @@ wrapping) by inserting a non-breaking space if there are at least two
words.
-}
noWidow : String -> String
noWidow inputs =
let
-- this value is a unicode non-breaking space since Elm
-- doesn't support named character entities
nbsp =
"\u{00A0}"
words =
String.split " " inputs
insertPoint =
List.length words - 1
in
words
|> List.indexedMap
(\i word ->
if i == 0 then
word
else if i == insertPoint && insertPoint > 0 then
nbsp ++ word
else
" " ++ word
)
|> String.join ""
noWidow =
V4.noWidow

209
src/Nri/Ui/Text/V4.elm Normal file
View File

@ -0,0 +1,209 @@
module Nri.Ui.Text.V4 exposing
( caption, mediumBody, smallBody, smallBodyGray
, ugMediumBody, ugSmallBody
, noWidow
)
{-| Changes from V3:
- Removes Headings (they now live in Nri.Ui.Heading.V2)
## 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.
## Headings
Headings now live in Nri.Ui.Heading.V2. Here's a mapping to help with upgrades:
| Nri.Ui.Text.V3 | Nri.Ui.Heading.V2 |
|===================|===================|
| Text.heading | Heading.h1 |
| Text.tagline | Heading.h2 |
| Text.subHeading | Heading.h3 |
| Text.smallHeading | Heading.h4 |
If you look at your new code and go "hmm, those shouldn't be at this level of
heading" then you can customize the tag apart from the style using the new
API. See the Nri.Ui.Heading.V2 docs for details.
## Paragraph styles
@docs caption, mediumBody, smallBody, smallBodyGray
## User-authored content blocks:
@docs ugMediumBody, ugSmallBody
## Modifying strings to display nicely:
@docs noWidow
-}
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
{-| This is some medium body copy.
-}
mediumBody : List (Html msg) -> Html msg
mediumBody content =
p
[ paragraphStyles
{ font = Fonts.baseFont
, color = gray20
, size = 18
, lineHeight = 28
, weight = 400
, margin = 10
}
]
content
{-| This is some small body copy.
-}
smallBody : List (Html msg) -> Html msg
smallBody content =
p
[ paragraphStyles
{ font = Fonts.baseFont
, color = gray20
, size = 15
, lineHeight = 23
, weight = 400
, margin = 7
}
]
content
{-| This is some small body copy but it's gray.
-}
smallBodyGray : List (Html msg) -> Html msg
smallBodyGray content =
p
[ paragraphStyles
{ font = Fonts.baseFont
, color = gray45
, size = 15
, lineHeight = 23
, weight = 400
, margin = 7
}
]
content
paragraphStyles config =
css
[ config.font
, fontSize (px config.size)
, color config.color
, lineHeight (px config.lineHeight)
, fontWeight (int config.weight)
, padding zero
, textAlign left
, margin4 (px 0) (px 0) (px config.margin) (px 0)
, lastChild
[ margin zero
]
]
{-| This is a little note or caption.
-}
caption : List (Html msg) -> Html msg
caption content =
p
[ paragraphStyles
{ font = Fonts.baseFont
, color = gray45
, size = 13
, lineHeight = 18
, weight = 400
, margin = 5
}
]
content
{-| User-generated text.
-}
ugMediumBody : List (Html msg) -> Html msg
ugMediumBody =
p
[ css
[ Fonts.quizFont
, fontSize (px 18)
, lineHeight (px 30)
, whiteSpace preLine
, color gray20
, margin zero
]
]
{-| User-generated text.
-}
ugSmallBody : List (Html msg) -> Html msg
ugSmallBody =
p
[ css
[ Fonts.quizFont
, fontSize (px 16)
, lineHeight (px 25)
, whiteSpace preLine
, color gray20
, margin zero
]
]
{-| 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 inputs =
let
-- this value is a unicode non-breaking space since Elm
-- doesn't support named character entities
nbsp =
"\u{00A0}"
words =
String.split " " inputs
insertPoint =
List.length words - 1
in
words
|> List.indexedMap
(\i word ->
if i == 0 then
word
else if i == insertPoint && insertPoint > 0 then
nbsp ++ word
else
" " ++ word
)
|> String.join ""

170
src/Nri/Ui/TextInput/V5.elm Normal file
View File

@ -0,0 +1,170 @@
module Nri.Ui.TextInput.V5 exposing
( Model
, view, writing
, generateId
, number, text, password
)
{-|
# Changes from V4
- adds a password input
@docs Model
@docs view, writing
@docs generateId
## Input types
@docs number, text, password
-}
import Accessibility.Styled.Style as Accessibility
import Css exposing (batch, center, position, px, relative, textAlign)
import Css.Global
import Html.Styled as Html exposing (..)
import Html.Styled.Attributes as Attributes exposing (..)
import Html.Styled.Events as Events exposing (onInput)
import Nri.Ui.Html.Attributes.V2 as Extra
import Nri.Ui.InputStyles.V2 as InputStyles exposing (Theme)
import Nri.Ui.Util exposing (dashify)
{-| -}
type alias Model value msg =
{ label : String
, isInError : Bool
, onInput : value -> msg
, onBlur : Maybe msg
, placeholder : String
, value : value
, autofocus : Bool
, showLabel : Bool
, type_ : InputType value
}
{-| -}
type InputType value
= InputType
{ toString : value -> String
, fromString : String -> value
, fieldType : String
}
{-| An input that allows text entry
-}
text : InputType String
text =
InputType
{ toString = identity
, fromString = identity
, fieldType = "text"
}
{-| An input that allows number entry
-}
number : InputType (Maybe Int)
number =
InputType
{ toString = Maybe.map String.fromInt >> Maybe.withDefault ""
, fromString = String.toInt
, fieldType = "number"
}
{-| An input that allows password entry
-}
password : InputType String
password =
InputType
{ toString = identity
, fromString = identity
, fieldType = "password"
}
{-| -}
view : Model value msg -> Html msg
view model =
view_ InputStyles.Standard model
{-| -}
writing : Model value msg -> Html msg
writing model =
view_ InputStyles.Writing model
view_ : Theme -> Model value msg -> Html msg
view_ theme model =
let
idValue =
generateId model.label
(InputType inputType) =
model.type_
in
div
[ Attributes.css [ position relative ]
]
[ input
[ Attributes.id idValue
, css
[ InputStyles.input theme model.isInError
, if theme == InputStyles.Writing then
Css.Global.withClass "override-sass-styles"
[ textAlign center
, Css.height Css.auto
]
else
Css.Global.withClass "override-sass-styles"
[ Css.height (px 45)
]
]
, placeholder model.placeholder
, value (inputType.toString model.value)
, onInput (inputType.fromString >> model.onInput)
, Maybe.withDefault Extra.none (Maybe.map Events.onBlur model.onBlur)
, autofocus model.autofocus
, type_ inputType.fieldType
, class "override-sass-styles"
, Attributes.attribute "aria-invalid" <|
if model.isInError then
"true"
else
"false"
]
[]
, if model.showLabel then
Html.label
[ for idValue
, css [ InputStyles.label theme model.isInError ]
]
[ Html.text model.label ]
else
Html.label
([ for idValue
, css [ InputStyles.label theme model.isInError ]
]
++ Accessibility.invisible
)
[ Html.text model.label ]
]
{-| Gives you the DOM element id that will be used by a `TextInput.view` with the given label.
This is for use when you need the DOM element id for use in javascript (such as trigger an event to focus a particular text input)
-}
generateId : String -> String
generateId labelText =
"Nri-Ui-TextInput-" ++ dashify labelText

View File

@ -1,30 +1,26 @@
module Examples.Button exposing (Msg, State, example, init, update)
{- \
@docs Msg, State, example, init, update,
{-|
@docs Msg, State, example, init, update
-}
import Css exposing (middle, verticalAlign)
import Debug.Control as Control exposing (Control)
import Headings
import Html.Styled exposing (..)
import Html.Styled.Attributes exposing (css, id)
import ModuleExample as ModuleExample exposing (Category(..), ModuleExample, ModuleMessages)
import Nri.Ui.AssetPath exposing (Asset)
import Nri.Ui.Button.V8 as Button
import Nri.Ui.Button.V9 as Button
import Nri.Ui.Heading.V2 as Heading
import Nri.Ui.Icon.V5 as Icon
import Nri.Ui.Svg.V1 as NriSvg exposing (Svg)
import Nri.Ui.Text.V3 as Text
{-| -}
type Msg
= SetState State
{-| -}
type State
= State (Control Model)
type State parentMsg
= State (Control (Model parentMsg))
{-| -}
@ -35,96 +31,109 @@ type ButtonType
{-| -}
example :
(String -> ModuleMessages Msg parentMsg)
-> State
(String -> ModuleMessages (Msg parentMsg) parentMsg)
-> State parentMsg
-> ModuleExample parentMsg
example unnamedMessages state =
let
messages =
unnamedMessages "ButtonExample"
in
{ name = "Nri.Ui.Button.V8"
{ name = "Nri.Ui.Button.V9"
, category = Buttons
, content =
[ viewButtonExamples messages state ]
, content = [ viewButtonExamples messages state ]
}
{-| -}
init : { r | performance : String, lock : String } -> State
init assets =
Control.record Model
|> Control.field "label" (Control.string "Button")
|> Control.field "icon"
(Control.maybe False <|
Control.choice
( "Performance"
, Icon.performance assets
|> Icon.decorativeIcon
|> NriSvg.fromHtml
|> Control.value
)
[ ( "Lock"
, Icon.lock assets
|> Icon.decorativeIcon
|> NriSvg.fromHtml
|> Control.value
)
]
)
|> Control.field "width"
(Control.choice
( "Nri.Ui.Button.V7.WidthExact 120", Control.value <| Button.WidthExact 120 )
[ ( "Nri.Ui.Button.V7.WidthExact 70", Control.value <| Button.WidthExact 70 )
, ( "Nri.Ui.Button.V7.WidthUnbounded", Control.value <| Button.WidthUnbounded )
, ( "Nri.Ui.Button.V7.WidthFillContainer", Control.value <| Button.WidthFillContainer )
]
)
|> Control.field "button type"
(Control.choice
( "Nri.Ui.Button.V7.button", Control.value Button )
[ ( "Nri.Ui.Button.V7.link", Control.value Link )
]
)
|> Control.field "state (button only)"
(Control.choice
( Debug.toString Button.Enabled, Control.value Button.Enabled )
(List.map (\x -> ( Debug.toString x, Control.value x ))
[ Button.Disabled
, Button.Error
, Button.Unfulfilled
, Button.Loading
, Button.Success
]
)
)
|> State
type Msg parentMsg
= SetState (State parentMsg)
| NoOp
{-| -}
update : Msg -> State -> ( State, Cmd Msg )
update : Msg msg -> State msg -> ( State msg, Cmd (Msg msg) )
update msg state =
case msg of
SetState newState ->
( newState, Cmd.none )
NoOp ->
( state, Cmd.none )
-- INTERNAL
type alias Model =
type alias Model msg =
{ label : String
, icon : Maybe Svg
, width : Button.ButtonWidth
, buttonType : ButtonType
, state : Button.ButtonState
, width : Button.Attribute msg
, state : Button.Attribute msg
}
{-| -}
init : { r | performance : String, lock : String } -> State msg
init assets =
Control.record Model
|> Control.field "label" (Control.string "Label")
|> Control.field "icon" (iconChoice assets)
|> Control.field "button type"
(Control.choice
[ ( "button", Control.value Button )
, ( "link", Control.value Link )
]
)
|> Control.field "width"
(Control.choice
[ ( "exactWidth 120", Control.value (Button.exactWidth 120) )
, ( "exactWidth 70", Control.value (Button.exactWidth 70) )
, ( "unboundedWidth", Control.value Button.unboundedWidth )
, ( "fillContainerWidth", Control.value Button.fillContainerWidth )
]
)
|> Control.field "state (button only)"
(Control.choice
[ ( "enabled", Control.value Button.enabled )
, ( "disabled", Control.value Button.disabled )
, ( "error", Control.value Button.error )
, ( "unfulfilled", Control.value Button.unfulfilled )
, ( "loading", Control.value Button.loading )
, ( "success", Control.value Button.success )
]
)
|> State
iconChoice : { r | performance : String, lock : String } -> Control.Control (Maybe Svg)
iconChoice assets =
Control.choice
[ ( "Nothing"
, Control.value Nothing
)
, ( "Just Performance"
, Icon.performance assets
|> Icon.decorativeIcon
|> NriSvg.fromHtml
|> Just
|> Control.value
)
, ( "Just Lock"
, Icon.lock assets
|> Icon.decorativeIcon
|> NriSvg.fromHtml
|> Just
|> Control.value
)
]
viewButtonExamples :
ModuleMessages Msg parentMsg
-> State
ModuleMessages (Msg parentMsg) parentMsg
-> State parentMsg
-> Html parentMsg
viewButtonExamples messages (State control) =
let
@ -139,80 +148,73 @@ viewButtonExamples messages (State control) =
{ label = "Delete Something"
, onClick = messages.showItWorked "delete"
}
, Button.linkExternalWithTracking
(messages.showItWorked "linkExternalWithTracking clicked")
{ size = Button.Medium
, style = Button.Secondary
, width = Button.WidthUnbounded
, label = "linkExternalWithTracking"
, icon = Nothing
, url = "#"
}
, Button.link "linkExternalWithTracking"
[ Button.unboundedWidth
, Button.secondary
, Button.linkExternalWithTracking
{ url = "#"
, track = messages.showItWorked "linkExternalWithTracking clicked"
}
]
]
|> div []
sizes : List Button.ButtonSize
sizes =
[ Button.Small
, Button.Medium
, Button.Large
]
allStyles : List Button.ButtonStyle
allStyles =
[ Button.Primary
, Button.Secondary
, Button.Danger
, Button.Premium
]
buttons :
ModuleMessages Msg parentMsg
-> Model
ModuleMessages (Msg parentMsg) parentMsg
-> Model parentMsg
-> Html parentMsg
buttons messages model =
let
exampleRow style =
sizes =
[ ( Button.small, "small" )
, ( Button.medium, "medium" )
, ( Button.large, "large" )
]
styles =
[ ( Button.primary, "primary" )
, ( Button.secondary, "secondary" )
, ( Button.danger, "danger" )
, ( Button.premium, "premium" )
]
exampleRow ( style, styleName ) =
List.concat
[ [ td
[ css
[ verticalAlign middle
]
]
[ text <| Debug.toString style ]
[ text styleName ]
]
, sizes
|> List.map (exampleCell style)
, List.map (Tuple.first >> exampleCell style) sizes
]
|> tr []
exampleCell style size =
(case model.buttonType of
buttonOrLink =
case model.buttonType of
Link ->
Button.link
{ size = size
, style = style
, label = model.label
, icon = model.icon
, url = ""
, width = model.width
}
Button ->
Button.button
{ size = size
, style = style
, onClick = messages.showItWorked (Debug.toString ( style, size ))
, width = model.width
}
{ label = model.label
, icon = model.icon
, state = model.state
}
)
exampleCell setStyle setSize =
buttonOrLink model.label
[ setSize
, setStyle
, model.width
, model.state
, Button.custom [ Html.Styled.Attributes.class "styleguide-button" ]
, Button.onClick (messages.showItWorked "Button clicked!")
, case model.icon of
Just icon ->
Button.icon icon
Nothing ->
Button.custom []
]
|> List.singleton
|> td
[ css
@ -223,19 +225,18 @@ buttons messages model =
in
List.concat
[ [ sizes
|> List.map (\size -> th [] [ text <| Debug.toString size ])
|> List.map (\( _, sizeName ) -> th [] [ text sizeName ])
|> (\cells -> tr [] (th [] [] :: cells))
]
, allStyles
|> List.map exampleRow
, List.map exampleRow styles
]
|> table []
toggleButtons : ModuleMessages Msg parentMsg -> Html parentMsg
toggleButtons : ModuleMessages (Msg parentMsg) parentMsg -> Html parentMsg
toggleButtons messages =
div []
[ Headings.h3 [ text "Button toggle" ]
[ Heading.h3 [] [ text "Button toggle" ]
, Button.toggleButton
{ onDeselect = messages.showItWorked "onDeselect"
, onSelect = messages.showItWorked "onSelect"

View File

@ -12,7 +12,7 @@ import Html.Styled.Attributes exposing (css)
import ModuleExample as ModuleExample exposing (Category(..), ModuleExample)
import Nri.Ui.Checkbox.V5 as Checkbox
import Nri.Ui.Data.PremiumLevel as PremiumLevel exposing (PremiumLevel(..))
import Nri.Ui.PremiumCheckbox.V5 as PremiumCheckbox
import Nri.Ui.PremiumCheckbox.V6 as PremiumCheckbox
import Set exposing (Set)
@ -76,13 +76,6 @@ update msg state =
-- INTERNAL
type alias PremiumExampleConfig =
{ disabled : Bool
, teacherPremiumLevel : PremiumLevel
, pennant : PremiumCheckbox.Pennant
}
viewInteractableCheckbox : Id -> State -> Html Msg
viewInteractableCheckbox id state =
Checkbox.viewWithLabel
@ -186,7 +179,7 @@ viewPremiumCheckboxes state =
Checkbox.NotSelected
, disabled = config.disabled
, isLocked = config.isLocked
, pennant = config.pennant
, isPremium = config.isPremium
, onChange = ToggleCheck config.label
, onLockedClick = NoOp
}
@ -196,19 +189,19 @@ viewPremiumCheckboxes state =
{ label = "Identify Adjectives 2 (Premium)"
, disabled = False
, isLocked = False
, pennant = PremiumCheckbox.Premium
, isPremium = True
}
, checkbox
{ label = "Revising Wordy Phrases 1 (Writing)"
{ label = "Identify Adjectives 2 (Free)"
, disabled = False
, isLocked = True
, pennant = PremiumCheckbox.PremiumWithWriting
, isLocked = False
, isPremium = False
}
, checkbox
{ label = "Revising Wordy Phrases 2 (Writing) (Disabled)"
{ label = "Revising Wordy Phrases 2 (Premium, Disabled)"
, disabled = True
, isLocked = True
, pennant = PremiumCheckbox.PremiumWithWriting
, isPremium = True
}
]

View File

@ -8,14 +8,13 @@ module Examples.ClickableText exposing (Msg, State, example, init, update)
import Css exposing (middle, verticalAlign)
import Debug.Control as Control exposing (Control)
import Headings
import Html.Styled exposing (..)
import Html.Styled.Attributes exposing (css, id)
import ModuleExample as ModuleExample exposing (Category(..), ModuleExample, ModuleMessages)
import Nri.Ui.ClickableText.V2 as ClickableText exposing (Size(..))
import Nri.Ui.ClickableText.V3 as ClickableText
import Nri.Ui.Icon.V5 as Icon
import Nri.Ui.Svg.V1 as NriSvg exposing (Svg)
import Nri.Ui.Text.V3 as Text
import Nri.Ui.Text.V4 as Text
{-| -}
@ -38,7 +37,7 @@ example unnamedMessages state =
messages =
unnamedMessages "ClickableTextExample"
in
{ name = "Nri.Ui.ClickableText.V2"
{ name = "Nri.Ui.ClickableText.V3"
, category = Buttons
, content =
[ viewExamples messages state ]
@ -53,13 +52,13 @@ init assets =
|> Control.field "icon"
(Control.maybe True <|
Control.choice
( "Help"
, Icon.helpSvg assets
|> Icon.decorativeIcon
|> NriSvg.fromHtml
|> Control.value
)
[ ( "Performance"
[ ( "Help"
, Icon.helpSvg assets
|> Icon.decorativeIcon
|> NriSvg.fromHtml
|> Control.value
)
, ( "Performance"
, Icon.performance assets
|> Icon.decorativeIcon
|> NriSvg.fromHtml
@ -108,20 +107,29 @@ viewExamples messages (State control) =
, buttons messages model
, Text.smallBody
[ text "Sometimes, we'll want our clickable links: "
, linkView model Small
, ClickableText.link model.label
[ ClickableText.small
, Maybe.map ClickableText.icon model.icon
|> Maybe.withDefault (ClickableText.custom [])
]
, text " and clickable buttons: "
, buttonView messages model Small
, ClickableText.button model.label
[ ClickableText.small
, ClickableText.onClick (messages.showItWorked "in-line button")
, Maybe.map ClickableText.icon model.icon
|> Maybe.withDefault (ClickableText.custom [])
]
, text " to show up in-line."
]
]
|> div []
sizes : List Size
sizes : List ( ClickableText.Attribute msg, String )
sizes =
[ Small
, Medium
, Large
[ ( ClickableText.small, "small" )
, ( ClickableText.medium, "medium" )
, ( ClickableText.large, "large" )
]
@ -131,45 +139,38 @@ buttons :
-> Html parentMsg
buttons messages model =
let
exampleCell view =
view
|> List.singleton
|> td
[ css
[ verticalAlign middle
, Css.width (Css.px 200)
]
]
sizeRow label render =
row label (List.map render sizes)
in
[ sizes
|> List.map (\size -> th [] [ text <| Debug.toString size ])
|> (\sizeHeadings -> tr [] (th [] [ td [] [] ] :: sizeHeadings))
, sizes
|> List.map (linkView model >> exampleCell)
|> (\linkViews -> tr [] (td [] [ text ".link" ] :: linkViews))
, sizes
|> List.map (buttonView messages model >> exampleCell)
|> (\buttonViews -> tr [] (td [] [ text ".button" ] :: buttonViews))
]
|> table []
table []
[ sizeRow "" (\( size, sizeLabel ) -> th [] [ text sizeLabel ])
, sizeRow ".link"
(\( size, sizeLabel ) ->
ClickableText.link model.label
[ size
, Maybe.map ClickableText.icon model.icon
|> Maybe.withDefault (ClickableText.custom [])
]
|> exampleCell
)
, sizeRow ".button"
(\( size, sizeLabel ) ->
ClickableText.button model.label
[ size
, ClickableText.onClick (messages.showItWorked sizeLabel)
, Maybe.map ClickableText.icon model.icon
|> Maybe.withDefault (ClickableText.custom [])
]
|> exampleCell
)
]
linkView : Model -> ClickableText.Size -> Html msg
linkView model size =
ClickableText.link
{ size = size
, label = model.label
, icon = model.icon
, url = "#"
}
[]
row : String -> List (Html msg) -> Html msg
row label tds =
tr [] (th [] [ td [] [ text label ] ] :: tds)
buttonView : ModuleMessages Msg parentMsg -> Model -> ClickableText.Size -> Html parentMsg
buttonView messages model size =
ClickableText.button
{ size = size
, onClick = messages.showItWorked (Debug.toString size)
, label = model.label
, icon = model.icon
}
exampleCell : Html msg -> Html msg
exampleCell view =
td [ css [ verticalAlign middle, Css.width (Css.px 200) ] ] [ view ]

View File

@ -6,11 +6,11 @@ module Examples.Fonts exposing (example)
-}
import Headings
import Html.Styled as Html
import Html.Styled.Attributes exposing (css)
import ModuleExample as ModuleExample exposing (Category(..), ModuleExample)
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Heading.V2 as Heading
{-| -}
@ -19,13 +19,13 @@ example =
{ name = "Nri.Ui.Fonts.V1"
, category = Text
, content =
[ Headings.h3 [ Html.text "baseFont" ]
[ Heading.h3 [] [ Html.text "baseFont" ]
, Html.p [ css [ Fonts.baseFont ] ]
[ Html.text "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz" ]
, Headings.h3 [ Html.text "quizFont" ]
, Heading.h3 [] [ Html.text "quizFont" ]
, Html.p [ css [ Fonts.quizFont ] ]
[ Html.text "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz" ]
, Headings.h3 [ Html.text "ugFont" ]
, Heading.h3 [] [ Html.text "ugFont" ]
, Html.p [ css [ Fonts.ugFont ] ]
[ Html.text "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz" ]
]

View File

@ -13,8 +13,9 @@ import Html.Styled as Html exposing (Html)
import Html.Styled.Attributes exposing (css, style, title)
import ModuleExample exposing (Category(..), ModuleExample)
import Nri.Ui.Colors.V1 exposing (..)
import Nri.Ui.Heading.V2 as Heading
import Nri.Ui.Icon.V5 as Icon
import Nri.Ui.Text.V3 as Text
import Nri.Ui.Text.V4 as Text
{-| -}
@ -128,7 +129,7 @@ viewIconSection :
-> Html msg
viewIconSection headerText icons =
Html.section []
[ Text.subHeading [ Html.text headerText ]
[ Heading.h2 [] [ Html.text headerText ]
, Html.div [ css [ Css.displayFlex, Css.flexWrap Css.wrap ] ]
(List.map viewIcon icons)
]

View File

@ -12,9 +12,10 @@ import Css.Global
import Html as Root
import Html.Styled.Attributes exposing (css)
import ModuleExample exposing (Category(..), ModuleExample)
import Nri.Ui.Button.V9 as Button
import Nri.Ui.Checkbox.V5 as Checkbox
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Modal.V5 as Modal
import Nri.Ui.Modal.V6 as Modal
{-| -}
@ -45,29 +46,44 @@ init =
{-| -}
example : (Msg -> msg) -> State -> ModuleExample msg
example parentMessage state =
{ name = "Nri.Ui.Modal.V5"
{ name = "Nri.Ui.Modal.V6"
, category = Modals
, content =
[ Modal.launchButton InfoModalMsg [] "Launch Info Modal"
, Modal.launchButton WarningModalMsg [] "Launch Warning Modal"
[ Button.button "Launch Info Modal"
[ Button.onClick (InfoModalMsg (Modal.open "launch-info-modal"))
, Button.custom
[ Html.Styled.Attributes.id "launch-info-modal"
, css [ Css.marginRight (Css.px 16) ]
]
, Button.secondary
, Button.medium
]
, Button.button "Launch Warning Modal"
[ Button.onClick (WarningModalMsg (Modal.open "launch-warning-modal"))
, Button.custom [ Html.Styled.Attributes.id "launch-warning-modal" ]
, Button.secondary
, Button.medium
]
, Modal.info
{ title = { title = "Modal.info", visibleTitle = state.visibleTitle }
{ title = "Modal.info"
, visibleTitle = state.visibleTitle
, wrapMsg = InfoModalMsg
, content =
viewContent state
InfoModalMsg
(Modal.primaryButton ForceClose "Continue")
(Modal.secondaryButton ForceClose "Close")
Button.primary
Button.secondary
}
state.infoModal
, Modal.warning
{ title = { title = "Modal.warning", visibleTitle = state.visibleTitle }
{ title = "Modal.warning"
, visibleTitle = state.visibleTitle
, wrapMsg = WarningModalMsg
, content =
viewContent state
WarningModalMsg
(Modal.dangerButton ForceClose "Continue")
(Modal.secondaryButton ForceClose "Close")
Button.danger
Button.secondary
}
state.warningModal
]
@ -78,37 +94,106 @@ example parentMessage state =
viewContent :
State
-> (Modal.Msg -> Msg)
-> (List (Root.Attribute Msg) -> Html Msg)
-> (List (Root.Attribute Msg) -> Html Msg)
-> Button.Attribute Msg
-> Button.Attribute Msg
-> Modal.FocusableElementAttrs Msg
-> Html Msg
viewContent state wrapMsg primaryButton secondaryButton focusableElementAttrs =
div []
[ if state.showX then
Modal.closeButton wrapMsg focusableElementAttrs.firstFocusableElement
else
text ""
, Modal.viewContent [ viewSettings state ]
, if state.showContinue && state.showSecondary then
Modal.viewFooter
[ primaryButton []
, secondaryButton focusableElementAttrs.lastFocusableElement
viewContent state wrapMsg firstButtonStyle secondButtonStyle focusableElementAttrs =
case ( state.showX, state.showContinue, state.showSecondary ) of
( True, True, True ) ->
div []
[ Modal.closeButton wrapMsg focusableElementAttrs.firstFocusableElement
, Modal.viewContent [ viewSettings state ]
, Modal.viewFooter
[ Button.button "Continue"
[ firstButtonStyle
, Button.onClick ForceClose
]
, Button.button "Close"
[ secondButtonStyle
, Button.onClick ForceClose
, Button.custom focusableElementAttrs.lastFocusableElement
]
]
]
else if state.showContinue then
Modal.viewFooter
[ primaryButton focusableElementAttrs.lastFocusableElement
( True, False, True ) ->
div []
[ Modal.closeButton wrapMsg focusableElementAttrs.firstFocusableElement
, Modal.viewContent [ viewSettings state ]
, Modal.viewFooter
[ Button.button "Close"
[ secondButtonStyle
, Button.onClick ForceClose
, Button.custom focusableElementAttrs.lastFocusableElement
]
]
]
else if state.showSecondary then
Modal.viewFooter
[ secondaryButton focusableElementAttrs.lastFocusableElement
( True, False, False ) ->
div []
[ Modal.closeButton wrapMsg focusableElementAttrs.firstFocusableElement
, Modal.viewContent [ viewSettings state ]
]
else
text ""
]
( True, True, False ) ->
div []
[ Modal.closeButton wrapMsg focusableElementAttrs.firstFocusableElement
, Modal.viewContent [ viewSettings state ]
, Modal.viewFooter
[ Button.button "Continue"
[ firstButtonStyle
, Button.onClick ForceClose
, Button.custom focusableElementAttrs.lastFocusableElement
]
]
]
( False, True, True ) ->
div []
[ Modal.viewContent [ viewSettings state ]
, Modal.viewFooter
[ Button.button "Continue"
[ firstButtonStyle
, Button.onClick ForceClose
, Button.custom focusableElementAttrs.firstFocusableElement
]
, Button.button "Close"
[ secondButtonStyle
, Button.onClick ForceClose
, Button.custom focusableElementAttrs.lastFocusableElement
]
]
]
( False, False, True ) ->
div []
[ Modal.viewContent [ viewSettings state ]
, Modal.viewFooter
[ Button.button "Close"
[ secondButtonStyle
, Button.onClick ForceClose
, Button.custom focusableElementAttrs.lastFocusableElement
]
]
]
( False, True, False ) ->
div []
[ Modal.viewContent [ viewSettings state ]
, Modal.viewFooter
[ Button.button "Continue"
[ firstButtonStyle
, Button.onClick ForceClose
, Button.custom focusableElementAttrs.lastFocusableElement
]
]
]
( False, False, False ) ->
div []
[ Modal.viewContent [ viewSettings state ]
]
viewSettings : State -> Html Msg

View File

@ -8,9 +8,9 @@ module Examples.Page exposing (example)
import Css
import Css.Global exposing (Snippet, adjacentSiblings, children, class, descendants, each, everything, media, selector, withClass)
import Headings
import Html.Styled as Html exposing (Html)
import ModuleExample as ModuleExample exposing (Category(..), ModuleExample)
import Nri.Ui.Heading.V2 as Heading
import Nri.Ui.Page.V3 as Page
@ -26,17 +26,17 @@ example noOp =
, Css.flexWrap Css.wrap
]
]
, Headings.h4 [ Html.text "Page: Not Found, recovery text: ReturnTo" ]
, Heading.h4 [] [ Html.text "Page: Not Found, recovery text: ReturnTo" ]
, Page.notFound
{ link = noOp
, recoveryText = Page.ReturnTo "the main page"
}
, Headings.h4 [ Html.text "Page: Broken, recovery text: Reload" ]
, Heading.h4 [] [ Html.text "Page: Broken, recovery text: Reload" ]
, Page.broken
{ link = noOp
, recoveryText = Page.Reload
}
, Headings.h4 [ Html.text "Page: No Permission, recovery text: Custom" ]
, Heading.h4 [] [ Html.text "Page: No Permission, recovery text: Custom" ]
, Page.noPermission
{ link = noOp
, recoveryText = Page.Custom "Hit the road, Jack"

View File

@ -99,15 +99,17 @@ init assets =
Control.record Options
|> Control.field "width"
(Control.choice
( "FitContent", Control.value SegmentedControl.FitContent )
[ ( "FillContainer", Control.value SegmentedControl.FillContainer ) ]
[ ( "FitContent", Control.value SegmentedControl.FitContent )
, ( "FillContainer", Control.value SegmentedControl.FillContainer )
]
)
|> Control.field "icon"
(Control.maybe False (Control.value { alt = "Help", icon = Icon.helpSvg assets }))
|> Control.field "which view function"
(Control.choice
( "view", Control.value False )
[ ( "viewSpa", Control.value True ) ]
[ ( "view", Control.value False )
, ( "viewSpa", Control.value True )
]
)
}

View File

@ -5,11 +5,12 @@ module Examples.Table exposing (Msg, State, example, init, update)
-}
import Css exposing (..)
import Headings
import Html.Styled as Html
import ModuleExample as ModuleExample exposing (Category(..), ModuleExample)
import Nri.Ui.Button.V5 as Button
import Nri.Ui.Table.V4 as Table
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Heading.V2 as Heading
import Nri.Ui.Table.V5 as Table
{-| -}
@ -25,7 +26,7 @@ type alias State =
{-| -}
example : (Msg -> msg) -> State -> ModuleExample msg
example parentMessage state =
{ name = "Nri.Ui.Table.V4"
{ name = "Nri.Ui.Table.V5"
, category = Tables
, content =
let
@ -33,12 +34,28 @@ example parentMessage state =
[ Table.string
{ header = "First Name"
, value = .firstName
, width = calc (pct 50) minus (px 125)
, width = calc (pct 50) minus (px 250)
, cellStyles = always []
}
, Table.string
{ header = "Last Name"
, value = .lastName
, width = calc (pct 50) minus (px 125)
, width = calc (pct 50) minus (px 250)
, cellStyles = always []
}
, Table.string
{ header = "# Submitted"
, value = .submitted >> String.fromInt
, width = px 125
, cellStyles =
\value ->
if value.submitted < 5 then
[ backgroundColor Colors.redLight
, textAlign center
]
else
[ textAlign center ]
}
, Table.custom
{ header =
@ -56,24 +73,27 @@ example parentMessage state =
, state = Button.Enabled
, icon = Nothing
}
, cellStyles = always []
}
]
data =
[ { firstName = "First1", lastName = "Last1" }
, { firstName = "First2", lastName = "Last2" }
, { firstName = "First3", lastName = "Last3" }
, { firstName = "First4", lastName = "Last4" }
, { firstName = "First5", lastName = "Last5" }
[ { firstName = "First1", lastName = "Last1", submitted = 10 }
, { firstName = "First2", lastName = "Last2", submitted = 0 }
, { firstName = "First3", lastName = "Last3", submitted = 3 }
, { firstName = "First4", lastName = "Last4", submitted = 15 }
, { firstName = "First5", lastName = "Last5", submitted = 8 }
]
in
[ Headings.h4 [ Html.text "With header" ]
[ Heading.h4 [] [ Html.text "With header" ]
, Table.view columns data
, Headings.h4 [ Html.text "Without header" ]
, Heading.h4 [] [ Html.text "Without header" ]
, Table.viewWithoutHeader columns data
, Headings.h4 [ Html.text "Loading" ]
, Heading.h4 [] [ Html.text "With additional cell styles" ]
, Table.view columns data
, Heading.h4 [] [ Html.text "Loading" ]
, Table.viewLoading columns
, Headings.h4 [ Html.text "Loading without header" ]
, Heading.h4 [] [ Html.text "Loading without header" ]
, Table.viewLoadingWithoutHeader columns
]
|> List.map (Html.map parentMessage)

View File

@ -8,14 +8,14 @@ module Examples.Text exposing (example)
import Html.Styled as Html
import ModuleExample as ModuleExample exposing (Category(..), ModuleExample)
import Nri.Ui.Heading.V1 as Heading exposing (DocumentLevel(..), VisualLevel(..), heading, withDocumentLevel, withVisualLevel)
import Nri.Ui.Text.V3 as Text
import Nri.Ui.Heading.V2 as Heading
import Nri.Ui.Text.V4 as Text
{-| -}
example : ModuleExample msg
example =
{ name = "Nri.Ui.Text.V3 (with headers from Nri.Ui.Heading.V1)"
{ name = "Nri.Ui.Text.V4 (with headers from Nri.Ui.Heading.V2)"
, category = Text
, content =
let
@ -27,30 +27,18 @@ example =
"""
in
[ Html.text "NOTE: When using these styles, please read the documentation in the Elm module about \"Understanding spacing\""
, heading [ Html.text "This is the main page heading." ]
|> withVisualLevel Top
|> withDocumentLevel H1
|> Heading.view
, heading [ Html.text "This is a tagline" ]
|> withVisualLevel Tagline
|> withDocumentLevel H2
|> Heading.view
, heading [ Html.text "This is a subHeading" ]
|> withVisualLevel Subhead
|> withDocumentLevel H3
|> Heading.view
, heading [ Html.text "This is a smallHeading" ]
|> withVisualLevel Small
|> withDocumentLevel H4
|> Heading.view
, Heading.h1 [] [ Html.text "This is the main page heading." ]
, Heading.h2 [] [ Html.text "This is a tagline" ]
, Heading.h3 [] [ Html.text "This is a subHeading" ]
, Heading.h4 [] [ Html.text "This is a smallHeading" ]
, Html.hr [] []
, Text.heading [ Html.text "Paragraph styles" ]
, Heading.h2 [] [ Html.text "Paragraph styles" ]
, Text.mediumBody [ Html.text <| "This is a mediumBody. " ++ longerBody ]
, Text.smallBody [ Html.text <| "This is a smallBody. " ++ longerBody ]
, Text.smallBodyGray [ Html.text <| "This is a smallBodyGray. " ++ longerBody ]
, Text.caption [ Html.text <| "This is a caption. " ++ longerBody ]
, Html.hr [] []
, Text.heading [ Html.text "Paragraph styles for user-authored content" ]
, Heading.h2 [] [ Html.text "Paragraph styles for user-authored content" ]
, Text.ugMediumBody [ Html.text <| "This is an ugMediumBody. " ++ longerBody ]
, Text.ugSmallBody [ Html.text <| "This is an ugSmallBody. " ++ longerBody ]
]

View File

@ -11,7 +11,7 @@ import Html.Styled as Html
import ModuleExample as ModuleExample exposing (Category(..), ModuleExample)
import Nri.Ui.AssetPath exposing (Asset(..))
import Nri.Ui.Checkbox.V5 as Checkbox
import Nri.Ui.Text.V3 as Text
import Nri.Ui.Heading.V2 as Heading
import Nri.Ui.TextArea.V4 as TextArea
@ -38,7 +38,7 @@ example parentMessage state =
{ name = "Nri.Ui.TextArea.V4"
, category = Inputs
, content =
[ Text.heading [ Html.text "Textarea controls" ]
[ Heading.h1 [] [ Html.text "Textarea controls" ]
, Html.div []
[ Checkbox.viewWithLabel
{ identifier = "show-textarea-label"

View File

@ -1,26 +1,30 @@
module Examples.TextInput exposing (Msg, State, example, init, update)
{- \
@docs Msg, State, example, init, update,
{-|
@docs Msg, State, example, init, update
-}
import Dict exposing (Dict)
import Headings
import Html.Styled as Html
import ModuleExample as ModuleExample exposing (Category(..), ModuleExample)
import Nri.Ui.TextInput.V4 as TextInput
import Nri.Ui.Heading.V2 as Heading
import Nri.Ui.TextInput.V5 as TextInput
{-| -}
type Msg
= SetTextInput Id String
| SetNumberInput (Maybe Int)
| SetPassword String
{-| -}
type alias State =
{ numberInputValue : Maybe Int
, textInputValues : Dict Id String
, passwordInputValue : String
}
@ -56,6 +60,18 @@ example parentMessage state =
, showLabel = True
}
, Html.br [] []
, TextInput.view
{ label = "Password"
, isInError = False
, placeholder = ""
, value = state.passwordInputValue
, onInput = SetPassword
, onBlur = Nothing
, autofocus = False
, type_ = TextInput.password
, showLabel = True
}
, Html.br [] []
, TextInput.view
{ label = "Error"
, isInError = True
@ -67,7 +83,7 @@ example parentMessage state =
, type_ = TextInput.number
, showLabel = True
}
, Headings.h3 [ Html.text "invisible label" ]
, Heading.h3 [] [ Html.text "invisible label" ]
, TextInput.view
{ label = "Criterion"
, isInError = False
@ -91,7 +107,7 @@ example parentMessage state =
, type_ = TextInput.text
, showLabel = False
}
, Headings.h3 [ Html.text "Writing Style" ]
, Heading.h3 [] [ Html.text "Writing Style" ]
, TextInput.writing
{ label = "Writing!"
, isInError = False
@ -149,6 +165,7 @@ init : State
init =
{ numberInputValue = Nothing
, textInputValues = Dict.empty
, passwordInputValue = ""
}
@ -162,6 +179,9 @@ update msg state =
SetNumberInput numberInputValue ->
( { state | numberInputValue = numberInputValue }, Cmd.none )
SetPassword password ->
( { state | passwordInputValue = password }, Cmd.none )
-- INTERNAL

View File

@ -1,29 +0,0 @@
module Headings exposing (h1, h2, h3, h4, h5)
import Html.Styled exposing (Html)
import Nri.Ui.Text.V3 as Text
h1 : List (Html msg) -> Html msg
h1 =
Text.heading
h2 : List (Html msg) -> Html msg
h2 =
Text.heading
h3 : List (Html msg) -> Html msg
h3 =
Text.subHeading
h4 : List (Html msg) -> Html msg
h4 =
Text.subHeading
h5 : List (Html msg) -> Html msg
h5 =
Text.subHeading

View File

@ -124,7 +124,11 @@ categoryForDisplay category =
view : Bool -> ModuleExample msg -> Html msg
view showFocusLink { name, content } =
Html.div
[]
[ -- this class makes the axe accessibility checking output easier to parse
String.replace "." "-" name
|> (++) "module-example__"
|> Attributes.class
]
[ Html.styled Html.div
[ display block
, backgroundColor glacier

View File

@ -31,7 +31,7 @@ import Url exposing (Url)
type alias ModuleStates =
{ buttonExampleState : Examples.Button.State
{ buttonExampleState : Examples.Button.State Msg
, bannerAlertExampleState : Examples.BannerAlert.State
, clickableTextExampleState : Examples.ClickableText.State
, checkboxExampleState : Examples.Checkbox.State
@ -72,7 +72,7 @@ init =
type Msg
= ButtonExampleMsg Examples.Button.Msg
= ButtonExampleMsg (Examples.Button.Msg Msg)
| BannerAlertExampleMsg Examples.BannerAlert.Msg
| ClickableTextExampleMsg Examples.ClickableText.Msg
| CheckboxExampleMsg Examples.Checkbox.Msg

View File

@ -2,7 +2,6 @@ module View exposing (view)
import Browser exposing (Document)
import Css exposing (..)
import Headings
import Html as RootHtml
import Html.Attributes
import Html.Styled as Html exposing (Html, img)
@ -12,6 +11,7 @@ import ModuleExample as ModuleExample exposing (Category(..), ModuleExample, cat
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Css.VendorPrefixed as VendorPrefixed
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Heading.V2 as Heading
import NriModules as NriModules exposing (nriThemedModules)
import Routes as Routes exposing (Route)
import Update exposing (..)
@ -41,7 +41,7 @@ view_ model =
[ Html.styled Html.section
[ sectionStyles ]
[]
[ Headings.h2 [ Html.text ("Viewing " ++ doodad ++ " doodad only") ]
[ Heading.h2 [] [ Html.text ("Viewing " ++ doodad ++ " doodad only") ]
, nriThemedModules model.moduleStates
|> List.filter (\m -> m.name == doodad)
|> List.map (ModuleExample.view False)
@ -54,7 +54,7 @@ view_ model =
[ Html.styled Html.section
[ sectionStyles ]
[]
[ Headings.h2 [ Html.text (categoryForDisplay category) ]
[ Heading.h2 [] [ Html.text (categoryForDisplay category) ]
, nriThemedModules model.moduleStates
|> List.filter (\doodad -> category == doodad.category)
|> List.map (ModuleExample.view True)
@ -67,7 +67,7 @@ view_ model =
[ Html.styled Html.section
[ sectionStyles ]
[]
[ Headings.h2 [ Html.text "All" ]
[ Heading.h2 [] [ Html.text "All" ]
, nriThemedModules model.moduleStates
|> List.map (ModuleExample.view True)
|> Html.div []
@ -129,8 +129,7 @@ navigation route =
, flexShrink zero
]
[]
[ Headings.h4
[ Html.text "Categories" ]
[ Heading.h4 [] [ Html.text "Categories" ]
, (categoryLink (route == Routes.All) "#" "All"
:: List.map
navLink

View File

@ -7,7 +7,7 @@
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"NoRedInk/elm-debug-controls-without-datepicker": "1.0.1",
"avh4/elm-debug-controls": "2.0.0",
"elm/browser": "1.0.1",
"elm/core": "1.0.2",
"elm/html": "1.0.0",
@ -26,9 +26,12 @@
"wernerdegroot/listzipper": "3.2.0"
},
"indirect": {
"NoRedInk/datetimepicker-legacy": "1.0.1",
"Skinney/murmur3": "2.0.8",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2",
"justinmimbs/date": "3.1.2",
"justinmimbs/time-extra": "1.1.0",
"rtfeldman/elm-hex": "1.0.0"
}
},

View File

@ -0,0 +1,115 @@
module Spec.Nri.Ui.PremiumCheckbox.V6 exposing (spec)
import Html.Attributes as Attributes
import Html.Styled
import Nri.Ui.Checkbox.V5 as Checkbox exposing (IsSelected(..))
import Nri.Ui.PremiumCheckbox.V6 as PremiumCheckbox
import Test exposing (..)
import Test.Html.Event as Event
import Test.Html.Query as Query
import Test.Html.Selector as Selector
type Msg
= OnLocked
| OnChange Bool
premiumView config =
PremiumCheckbox.view
{ label = "i am label"
, id = "id"
, selected = config.selected
, disabled = config.disabled
, isLocked = config.isLocked
, isPremium = config.isPremium
, onChange = OnChange
, onLockedClick = OnLocked
}
|> Html.Styled.toUnstyled
|> Query.fromHtml
spec : Test
spec =
describe "Nri.Ui.PremiumCheckbox.V6"
[ describe "premium"
[ test "displays the label" <|
\() ->
premiumView
{ selected = Selected
, disabled = False
, isLocked = False
, isPremium = False
}
|> Query.has [ Selector.text "i am label" ]
, test "appears selected when Selected is passed in" <|
\() ->
premiumView
{ selected = Selected
, disabled = False
, isLocked = False
, isPremium = False
}
|> Query.has [ Selector.attribute (Attributes.checked True) ]
, test "appears unselected when NotSelected is passed in" <|
\() ->
premiumView
{ selected = NotSelected
, disabled = False
, isLocked = False
, isPremium = False
}
|> Query.has [ Selector.attribute (Attributes.checked False) ]
, test "triggers onLockedClick when isLocked = True" <|
\() ->
premiumView
{ selected = Selected
, disabled = False
, isLocked = True
, isPremium = False
}
|> Query.find [ Selector.tag "input" ]
|> Event.simulate (Event.check False)
|> Event.expect OnLocked
, test "triggers onChange when isLocked = False" <|
\() ->
premiumView
{ selected = Selected
, disabled = False
, isLocked = False
, isPremium = False
}
|> Query.find [ Selector.tag "input" ]
|> Event.simulate (Event.check False)
|> Event.expect (OnChange False)
, test "appears with P flag when Premium pennant is passed in" <|
\() ->
premiumView
{ selected = Selected
, disabled = False
, isLocked = False
, isPremium = True
}
|> Query.find [ Selector.tag "title" ]
|> Query.has [ Selector.text "Premium" ]
, test "is not disabled when disabled = False" <|
\() ->
premiumView
{ selected = Selected
, disabled = False
, isLocked = False
, isPremium = False
}
|> Query.has [ Selector.disabled False ]
, test "is disabled when disabled = True" <|
\() ->
premiumView
{ selected = Selected
, disabled = True
, isLocked = False
, isPremium = False
}
|> Query.has [ Selector.disabled True ]
]
]

View File

@ -0,0 +1,6 @@
{
"root": "../src",
"tests": [
"Nri.Ui.Modal.V6"
]
}