mirror of
https://github.com/NoRedInk/noredink-ui.git
synced 2024-11-13 07:48:26 +03:00
Merge remote-tracking branch 'origin/master' into simplify-nri-ui-headings
This commit is contained in:
commit
0154813dad
4
.gitignore
vendored
4
.gitignore
vendored
@ -238,4 +238,6 @@ documentation.json
|
||||
# direnv config file
|
||||
.envrc
|
||||
|
||||
/public
|
||||
/public
|
||||
/tests/axe-report.log
|
||||
/tests/axe-report.json
|
7
Makefile
7
Makefile
@ -3,6 +3,13 @@ SHELL:=env PATH=${PATH} /bin/sh
|
||||
.PHONY: test
|
||||
test: node_modules
|
||||
npx elm-test
|
||||
make tests/axe-report.log
|
||||
|
||||
tests/axe-report.json: public script/run-axe.sh script/axe-puppeteer.js
|
||||
script/run-axe.sh > $@
|
||||
|
||||
tests/axe-report.log: tests/axe-report.json script/format-axe-report.sh script/axe-report.jq
|
||||
script/format-axe-report.sh $< | tee $@
|
||||
|
||||
.PHONY: checks
|
||||
checks:
|
||||
|
4
elm.json
4
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.26.1",
|
||||
"exposed-modules": [
|
||||
"Nri.Ui.Alert.V2",
|
||||
"Nri.Ui.Alert.V3",
|
||||
@ -46,6 +46,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",
|
||||
@ -55,6 +56,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",
|
||||
|
1822
package-lock.json
generated
1822
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -31,5 +31,9 @@
|
||||
"elm-format": "0.8.1",
|
||||
"elm-test": "0.19.0-rev6",
|
||||
"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"))
|
||||
"
|
31
script/format-axe-report.sh
Executable file
31
script/format-axe-report.sh
Executable file
@ -0,0 +1,31 @@
|
||||
#!/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=155
|
||||
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 tests/axe-report.log somewhere ('mv tests/axe-report.log tests/axe-report.log.failing' is one way)"
|
||||
echo " 2. undo your changes ('git stash' or 'checkout master')"
|
||||
echo " 3. regenerate the log with 'make tests/axe-report.log'"
|
||||
echo " 4. compare the output with 'diff -u tests/axe-report.log tests/axe-report.log.failing'"
|
||||
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
|
@ -7,6 +7,16 @@ module Nri.Ui.Heading.V1 exposing
|
||||
|
||||
{-| Headings with customization options for accessibility.
|
||||
|
||||
|
||||
## Understanding spacing
|
||||
|
||||
- All text styles have a specific line-height. This is set so that when text
|
||||
in the given style is long enough to wrap, the spacing between wrapped lines
|
||||
looks good.
|
||||
- No heading styles have padding.
|
||||
- **Heading styles** do not have margin. It is up to the caller to add
|
||||
appropriate margin to the layout.
|
||||
|
||||
@docs Heading, heading
|
||||
|
||||
@docs withVisualLevel, VisualLevel
|
||||
|
295
src/Nri/Ui/Modal/V6.elm
Normal file
295
src/Nri/Ui/Modal/V6.elm
Normal file
@ -0,0 +1,295 @@
|
||||
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
|
||||
|
||||
view : Modal.State -> Html Msg
|
||||
view state =
|
||||
Modal.info
|
||||
{ title = { title = "Modal Header", visibleTitle = True }
|
||||
, wrapMsg = ModalMsg
|
||||
, content =
|
||||
\{ onlyFocusableElement } ->
|
||||
div []
|
||||
[ Modal.viewContent [ text "Content goes here!" ]
|
||||
, Modal.viewFooter
|
||||
[ Button.button "Continue"
|
||||
[ Button.primary
|
||||
, Button.onClick DoSomthing
|
||||
, Button.custom onlyFocusableElement
|
||||
]
|
||||
, text "`onlyFocusableElement` will trap the focus on the 'Continue' button."
|
||||
]
|
||||
]
|
||||
}
|
||||
state
|
||||
|
||||
subscriptions : Modal.State -> Sub Msg
|
||||
subscriptions state =
|
||||
Modal.subscriptions state
|
||||
|
||||
|
||||
## State and updates
|
||||
|
||||
@docs Model, init
|
||||
@docs Msg, update, subscriptions
|
||||
|
||||
@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 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 =
|
||||
toCssString (Nri.Ui.Colors.Extra.withAlpha 0.9 color)
|
||||
|
||||
|
||||
modalStyles : List (Root.Attribute Never)
|
||||
modalStyles =
|
||||
[ style "width" "600px"
|
||||
, style "max-height" "calc(100vh - 100px)"
|
||||
, style "padding" "40px 0 40px 0"
|
||||
, style "margin" "75px auto"
|
||||
, style "background-color" (toCssString Colors.white)
|
||||
, style "border-radius" "20px"
|
||||
, style "box-shadow" "0 1px 10px 0 rgba(0, 0, 0, 0.35)"
|
||||
, style "position" "relative" -- required for closeButtonContainer
|
||||
]
|
||||
|
||||
|
||||
{-| -}
|
||||
viewTitle : Css.Color -> { visibleTitle : Bool, title : String } -> ( String, List (Root.Attribute Never) )
|
||||
viewTitle color { visibleTitle, title } =
|
||||
( title
|
||||
, if visibleTitle then
|
||||
[ style "font-weight" "700"
|
||||
, style "line-height" "27px"
|
||||
, style "margin" "0 49px"
|
||||
, style "font-size" "20px"
|
||||
, style "text-align" "center"
|
||||
, style "color" (toCssString color)
|
||||
]
|
||||
|
||||
else
|
||||
Accessibility.Style.invisible
|
||||
)
|
||||
|
||||
|
||||
toCssString : Css.Color -> String
|
||||
toCssString =
|
||||
Color.toCssString << Nri.Ui.Colors.Extra.toCoreColor
|
||||
|
||||
|
||||
{-| -}
|
||||
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
|
@ -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
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -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
|
||||
|
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 ]
|
||||
]
|
||||
]
|
Loading…
Reference in New Issue
Block a user