mirror of
https://github.com/NoRedInk/noredink-ui.git
synced 2024-12-24 22:21:45 +03:00
Merge remote-tracking branch 'origin/master' into tessa/use-color-library
This commit is contained in:
commit
34d60a459c
3
.gitignore
vendored
3
.gitignore
vendored
@ -238,4 +238,5 @@ documentation.json
|
||||
# direnv config file
|
||||
.envrc
|
||||
|
||||
/public
|
||||
/public
|
||||
/tests/axe-report.json
|
9
Makefile
9
Makefile
@ -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:
|
||||
|
12
elm.json
12
elm.json
@ -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
709
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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
63
script/axe-puppeteer.js
Normal 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
15
script/axe-report.jq
Normal 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
32
script/format-axe-report.sh
Executable 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
12
script/run-axe.sh
Executable 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
940
src/Nri/Ui/Button/V9.elm
Normal 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
|
||||
]
|
||||
]
|
||||
]
|
277
src/Nri/Ui/ClickableText/V3.elm
Normal file
277
src/Nri/Ui/ClickableText/V3.elm
Normal 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)
|
@ -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
185
src/Nri/Ui/Heading/V2.elm
Normal 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
302
src/Nri/Ui/Modal/V6.elm
Normal 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
|
||||
]
|
115
src/Nri/Ui/PremiumCheckbox/V6.elm
Normal file
115
src/Nri/Ui/PremiumCheckbox/V6.elm
Normal 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
|
368
src/Nri/Ui/SortableTable/V2.elm
Normal file
368
src/Nri/Ui/SortableTable/V2.elm
Normal 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
276
src/Nri/Ui/Table/V5.elm
Normal 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)
|
||||
]
|
@ -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
209
src/Nri/Ui/Text/V4.elm
Normal 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
170
src/Nri/Ui/TextInput/V5.elm
Normal 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
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -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 ]
|
||||
|
@ -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" ]
|
||||
]
|
||||
|
@ -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)
|
||||
]
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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 )
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 ]
|
||||
]
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
115
tests/Spec/Nri/Ui/PremiumCheckbox/V6.elm
Normal file
115
tests/Spec/Nri/Ui/PremiumCheckbox/V6.elm
Normal 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 ]
|
||||
]
|
||||
]
|
6
tests/elm-verify-examples.json
Normal file
6
tests/elm-verify-examples.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"root": "../src",
|
||||
"tests": [
|
||||
"Nri.Ui.Modal.V6"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user