Merge remote-tracking branch 'origin/master' into simplify-nri-ui-headings

This commit is contained in:
Brian Hicks 2019-07-29 13:22:29 -05:00
commit 0154813dad
15 changed files with 1748 additions and 923 deletions

4
.gitignore vendored
View File

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

View File

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

View File

@ -3,7 +3,7 @@
"name": "NoRedInk/noredink-ui",
"summary": "UI Widgets we use at NRI",
"license": "BSD-3-Clause",
"version": "6.25.0",
"version": "6.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

File diff suppressed because it is too large Load Diff

View File

@ -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
View File

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

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

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

31
script/format-axe-report.sh Executable file
View 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
View File

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

View File

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

View File

@ -0,0 +1,115 @@
module Nri.Ui.PremiumCheckbox.V6 exposing (view)
{-|
@docs view
This module is used when there may or may not be Premium
content to be "checked"!
# Changes from V5
- Allow checkbox to show pennant, or not, based on bool
- Remove PremiumWithWriting, it's only Premium now
-}
import Accessibility.Styled as Html exposing (Html)
import Css exposing (..)
import Html.Styled exposing (fromUnstyled)
import Html.Styled.Attributes as Attributes exposing (css)
import Nri.Ui.Checkbox.V5 as Checkbox
import Svg exposing (..)
import Svg.Attributes exposing (..)
{-| A checkbox that should be used for premium content
- `onChange`: A message for when the user toggles the checkbox
- `onLockedClick`: A message for when the user clicks a checkbox they don't have PremiumLevel for.
If you get this message, you should show an `Nri.Ui.Premium.Model.view`
-}
view :
{ label : String
, id : String
, selected : Checkbox.IsSelected
, disabled : Bool
, isLocked : Bool
, isPremium : Bool
, onChange : Bool -> msg
, onLockedClick : msg
}
-> Html msg
view config =
Html.div
[ css
[ displayFlex
, alignItems center
]
]
[ Checkbox.viewWithLabel
{ identifier = config.id
, label = config.label
, setterMsg =
if config.isLocked then
\_ -> config.onLockedClick
else
config.onChange
, selected = config.selected
, disabled = config.disabled
, theme =
if config.isLocked then
Checkbox.Locked
else
Checkbox.Square
}
, if config.isPremium then
premiumFlag
else
Html.text ""
]
premiumFlag : Html msg
premiumFlag =
svg
[ version "1.1"
, id "Layer_1"
, Svg.Attributes.width "25"
, Svg.Attributes.height "19"
, Svg.Attributes.viewBox "0 0 25 19"
, Svg.Attributes.style "margin-left: 8px;"
]
[ Svg.title [] [ text "Premium" ]
, Svg.style [] [ text " .premium-flag-st0{fill:#FEC709;} .premium-flag-st1{fill:#146AFF;} " ]
, g [ id "Page-1" ]
[ g
[ id "icon_x2F_p-mini-pennant-yellow"
, Svg.Attributes.transform "translate(0.000000, -3.000000)"
]
[ g
[ id "Group"
, Svg.Attributes.transform "translate(0.000000, 3.750000)"
]
[ polygon
[ id "Fill-2"
, class "premium-flag-st0"
, points "12.7,0 0,0 0,13.8 0,15.8 0,17.5 7.3,17.5 24.8,17.5 19.4,8.1 24.8,0 "
]
[]
, Svg.path
[ id "P"
, class "premium-flag-st1"
, d "M7.5,3.8h4.2c1.1,0,1.9,0.3,2.5,0.8s0.9,1.2,0.9,2.1s-0.3,1.6-0.9,2.1c-0.6,0.5-1.4,0.8-2.5,0.8H9.3 v4.1H7.5V3.8z M11.5,8.1c0.6,0,1.1-0.1,1.4-0.4c0.3-0.3,0.5-0.6,0.5-1.1c0-0.5-0.2-0.9-0.5-1.1c-0.3-0.3-0.8-0.4-1.4-0.4H9.3v3 H11.5z"
]
[]
]
]
]
]
|> fromUnstyled

View File

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

View File

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

View File

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