Merge remote-tracking branch 'origin/master' into add-button-ternary

This commit is contained in:
Brian J. Cardiff 2022-07-07 15:11:21 -03:00
commit 079f76157e
168 changed files with 14389 additions and 9306 deletions

6
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

View File

@ -8,9 +8,9 @@ jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v13
- uses: cachix/install-nix-action@v17
- uses: cachix/cachix-action@v10
with:

130
README.md
View File

@ -2,50 +2,50 @@
UI widgets we use.
## Versioning policy
## Getting Started
1. Setup your [development environment](#developing-with-nix)
2. Run some [tests](#tests)
3. Check out [some examples](https://noredink-ui.netlify.app/)
We try to avoid breaking changes and the associated major version bumps in this package. The reason for that is to avoid the following scenario:
## Developing with Nix
```
|
x 4.6.0: Adding RadioButton widget
|
x 5.0.0: Breaking change in the TextArea widget
|
x 5.0.1: Styling fix in the Checkbox widget
|
```
You can develop this package without installing anything globally by using Nix.
To get started, install nix from [nixos.org/nix](https://nixos.org/nix/).
Suppose you just released version `5.0.1`, a small styling fix in the checkbox widget, for a story you're working on. If the project you're working in currently pulls in `noredink-ui` at version `4.x`, then getting to your styling fix means pulling in a new major version of `noredink-ui`. This breaks all `TextArea` widgets across the project, so those will need to be fixed before you can do anything else, potentially a big effort.
After that's set up in your shell (just follow the instructions at the end of the installation script) you can run `nix-shell` to get a development environment with everything you need.
To prevent these big Yaks from suddenly showing up in seemingly trivial tasks we prefer to avoid breaking changes in the package. Instead when we need to make a breaking change in a widget, we create a new module for it `Nri.Ui.MyWidget.VX`. Similarly, when we build custom elements in JavaScript we create a file `lib/MyWidget/VX.js` and define a custom element `nri-mywidget-vX`.
If you find that inconvenient, try using [`direnv`](https://direnv.net/).
Once that's set up, `echo use nix > .envrc` and then `direnv allow`.
Anytime you enter the project your shell will automatically pick up the right dependencies.
That said, we may prune unused modules occasionally.
If you find that `direnv` loads too slow, [there are faster loading strategies than the default in their wiki](https://github.com/direnv/direnv/wiki/Nix).
We should change this process if we feel it's not working for us!
### Working with upstream dependencies
## Moving Widgets to `noredink-ui`
We use `niv` to manage Nix dependencies.
It is automatically loaded in the Nix environment.
If you are moving in a widget from the monolith:
- Copy the contents of `Nri.SomeModule` and its tests to `Nri.Ui.SomeModule.V1` in `noredink-ui`
- Publish!
- If you feel confident upgrading pre-existing usages of the widget, switch over to it everywhere!
- If the new version introduces big changes and you'd rather keep the old one around for now, rename `Nri.SomeModule` to `Nri.DEPRECATEDSomeModule` in the monolith and start using `Nri.Ui.SomeModule.V1` where you need it
Here are some things you might need to do:
| Task | Command |
|------|---------|
| Add a non-npm, non-Elm dependency packaged with Nix | Look if it's in nixpkgs, or `niv add github.com/user/repo` |
| Update Nixpkgs | `niv update nixpkgs` |
| See all our dependencies | Look in `shell.nix` |
| See all our sources | `niv show` |
## Phasing out old versions
## Tests
Our goal is to gradually move to the newest version of each widget, and remove the old versions when they are no longer used.
Run tests with
- `shake test`
- `elm-test`
This means:
- We should avoid introducing new references to old versions of a widget
- When touching code that uses a widget, prefer upgrading to the latest version
- If you introduce a new version of a widget, please consider taking the time to upgrade all previous usages
- If for some reason this isn't feasible, create a story in your team's backlog so that you can prioritize it separately without disrupting your current work
- You can delete an old version of a widget when there are no usages left
- Currently, `noredink-ui` is used by the monolith, CCS and tutorials
- Note: this will be a major version bump, so you may want to batch deletions together
You can run the Puppeteer tests for only one component by passing the name of the component to the test script, for example: `./script/puppeteer-tests-no-percy.sh Button`
### CI (Travis)
Travis will run `shake ci` to verify everything looks good.
You can run this locally to catch errors before you push!
## Examples
@ -57,21 +57,10 @@ To see them locally:
script/develop.sh
```
And go to http://localhost:8000/
If you'd like to test your widget in the monolith before publishing, run `script/test-elm-package.py ../path_to_this_repo` from the monolith's directory.
## Tests
Run tests with
```
shake test
```
### CI (Travis)
Travis will run `shake ci` to verify everything looks good.
You can run this locally to catch errors before you push!
## Deploying
Once your PR is merged, you can publish `master` as a new version:
@ -121,29 +110,46 @@ You can also add a tag in https://github.com/NoRedInk/noredink-ui/releases/new i
Once you've published, you should see the latest version at <https://package.elm-lang.org/packages/NoRedInk/noredink-ui/>.
## Developing with Nix
## Versioning policy
You can develop this package without installing anything globally by using Nix.
To get started, install nix from [nixos.org/nix](https://nixos.org/nix/).
We try to avoid breaking changes and the associated major version bumps in this package. The reason for that is to avoid the following scenario:
After that's set up in your shell (just follow the instructions at the end of the installation script) you can run `nix-shell` to get a development environment with everything you need.
```
|
x 4.6.0: Adding RadioButton widget
|
x 5.0.0: Breaking change in the TextArea widget
|
x 5.0.1: Styling fix in the Checkbox widget
|
```
If you find that inconvenient, try using [`direnv`](https://direnv.net/).
Once that's set up, `echo use nix > .envrc` and then `direnv allow`.
Anytime you enter the project your shell will automatically pick up the right dependencies.
Suppose you just released version `5.0.1`, a small styling fix in the checkbox widget, for a story you're working on. If the project you're working in currently pulls in `noredink-ui` at version `4.x`, then getting to your styling fix means pulling in a new major version of `noredink-ui`. This breaks all `TextArea` widgets across the project, so those will need to be fixed before you can do anything else, potentially a big effort.
If you find that `direnv` loads too slow, [there are faster loading strategies than the default in their wiki](https://github.com/direnv/direnv/wiki/Nix).
To prevent these big Yaks from suddenly showing up in seemingly trivial tasks we prefer to avoid breaking changes in the package. Instead when we need to make a breaking change in a widget, we create a new module for it `Nri.Ui.MyWidget.VX`. Similarly, when we build custom elements in JavaScript we create a file `lib/MyWidget/VX.js` and define a custom element `nri-mywidget-vX`.
### Working with upstream dependencies
That said, we may prune unused modules occasionally.
We use `niv` to manage Nix dependencies.
It is automatically loaded in the Nix environment.
We should change this process if we feel it's not working for us!
Here are some things you might need to do:
## Moving Widgets to `noredink-ui`
| Task | Command |
|------|---------|
| Add a non-npm, non-Elm dependency packaged with Nix | Look if it's in nixpkgs, or `niv add github.com/user/repo` |
| Update Nixpkgs | `niv update nixpkgs` |
| See all our dependencies | Look in `shell.nix` |
| See all our sources | `niv show` |
If you are moving in a widget from the monolith:
- Copy the contents of `Nri.SomeModule` and its tests to `Nri.Ui.SomeModule.V1` in `noredink-ui`
- Publish!
- If you feel confident upgrading pre-existing usages of the widget, switch over to it everywhere!
- If the new version introduces big changes and you'd rather keep the old one around for now, rename `Nri.SomeModule` to `Nri.DEPRECATEDSomeModule` in the monolith and start using `Nri.Ui.SomeModule.V1` where you need it
## Phasing out old versions
Our goal is to gradually move to the newest version of each widget, and remove the old versions when they are no longer used.
This means:
- We should avoid introducing new references to old versions of a widget
- When touching code that uses a widget, prefer upgrading to the latest version
- If you introduce a new version of a widget, please consider taking the time to upgrade all previous usages
- If for some reason this isn't feasible, create a story in your team's backlog so that you can prioritize it separately without disrupting your current work
- You can delete an old version of a widget when there are no usages left
- Currently, `noredink-ui` is used by the monolith, CCS and tutorials
- Note: this will be a major version bump, so you may want to batch deletions together

View File

@ -26,7 +26,7 @@ main = do
shakeLintIgnore =
[ "node_modules/**/*",
"elm-stuff/**/*",
"styleguide-app/elm-stuff/**/*"
"styleguide/elm-stuff/**/*"
]
}
$ do
@ -38,7 +38,7 @@ main = do
removeFilesAfter "log" ["//*"]
removeFilesAfter "node_modules" ["//*"]
removeFilesAfter "public" ["//*"]
removeFilesAfter "styleguide-app" ["elm.js", "bundle.js", "elm-stuff"]
removeFilesAfter "styleguide" ["elm.js", "bundle.js", "elm-stuff"]
phony "public" $ need ["log/public.txt"]
@ -48,8 +48,10 @@ main = do
"tests/elm-verify-examples.json",
"log/elm-verify-examples.txt",
"log/elm-test.txt",
"log/axe-report.txt",
"log/percy-tests.txt",
"log/elm-test-styleguide.txt",
"log/elm-review.txt",
"log/elm-review-styleguide.txt",
"log/puppeteer-tests.txt",
"log/forbidden-imports-report.txt",
"log/check-exposed.txt",
"log/format.txt",
@ -83,33 +85,54 @@ main = do
need (["package.json", "elm.json"] ++ elmFiles)
cmd (WithStdout True) (FileStdout out) "elm-test"
"log/elm-test-styleguide.txt" %> \out -> do
elmFiles <- getDirectoryFiles "." ["styleguide/tests/**/*.elm"]
need (["package.json", "styleguide/elm.json"] ++ elmFiles)
cmd (Cwd "styleguide") (WithStdout True) (FileStdout out) "elm-test"
"log/elm-review.txt" %> \out -> do
elmFiles <- getDirectoryFiles "." ["src/**/*.elm", "tests/**/*.elm"]
need (["package.json", "elm.json"] ++ elmFiles)
cmd (WithStdout True) (FileStdout out) "elm-review"
"log/elm-review-styleguide.txt" %> \out -> do
elmFiles <- getDirectoryFiles "." ["styleguide/**/*.elm", "styleguide-app/**/*.elm"]
need (["package.json", "styleguide/elm.json"] ++ elmFiles)
cmd (Cwd "styleguide") (WithStdout True) (FileStdout out) "elm-review"
"log/elm-verify-examples.txt" %> \out -> do
elmFiles <- getDirectoryFiles "." ["src/**/*.elm"]
need (["tests/elm-verify-examples.json"] ++ elmFiles)
cmd (WithStdout True) (FileStdout out) "elm-verify-examples"
"log/format.txt" %> \out -> do
let placesToLook = ["src", "tests", "styleguide-app"]
need ["log/elm-format.txt", "log/prettier.txt"]
writeFileChanged out "formatting checks passed"
"log/elm-format.txt" %> \out -> do
let placesToLook = ["src", "tests", "styleguide", "styleguide-app"]
elmFiles <- getDirectoryFiles "." (map (\place -> place </> "**" </> "*.elm") placesToLook)
need elmFiles
cmd (WithStdout True) (FileStdout out) "elm-format" "--validate" placesToLook
"log/percy-tests.txt" %> \out -> do
"log/prettier.txt" %> \out -> do
(Stdout trackedFilesOut) <- cmd "git" "ls-files"
let trackedFiles = lines trackedFilesOut
let jsFiles = filter (\name -> takeExtension name == ".js") trackedFiles
need ("log/npm-install.txt" : jsFiles)
cmd (WithStdout True) (FileStdout out) "./node_modules/.bin/prettier" "--check" jsFiles
"log/puppeteer-tests.txt" %> \out -> do
percyToken <- getEnv "PERCY_TOKEN"
case percyToken of
Nothing -> do
writeFileChanged out "Skipped running Percy tests, PERCY_TOKEN not set."
writeFileChanged out "PERCY_TOKEN not set, so skipping visual diff testing."
need ["log/npm-install.txt", "log/public.txt"]
cmd (WithStdout True) (FileStdout out) "script/puppeteer-tests-no-percy.sh"
Just _ -> do
need ["log/npm-install.txt", "log/public.txt"]
cmd (WithStdout True) (FileStdout out) "script/percy-tests.sh"
"log/axe-report.json" %> \out -> do
need ["log/npm-install.txt", "script/run-axe.sh", "script/axe-puppeteer.js", "log/public.txt"]
cmd (WithStdout True) (FileStdout out) "script/run-axe.sh"
"log/axe-report.txt" %> \out -> do
need ["log/axe-report.json", "script/format-axe-report.sh", "script/axe-report.jq"]
cmd (WithStdout True) (FileStdout out) "script/format-axe-report.sh" "log/axe-report.json"
cmd (WithStdout True) (FileStdout out) "script/puppeteer-tests-percy.sh"
"log/forbidden-imports-report.txt" %> \out -> do
need ["forbidden-imports.toml"]
@ -129,23 +152,25 @@ main = do
"public/bundle.js" %> \out -> do
libJsFiles <- getDirectoryFiles "." ["lib/**/*.js"]
need (["package.json", "lib/index.js", "styleguide-app/manifest.js", "log/npm-install.txt"] ++ libJsFiles)
cmd_ "./node_modules/.bin/browserify" "--entry" "styleguide-app/manifest.js" "--outfile" out
need (["package.json", "lib/index.js", "styleguide/manifest.js", "log/npm-install.txt"] ++ libJsFiles)
cmd_ "./node_modules/.bin/browserify" "--entry" "styleguide/manifest.js" "--outfile" out
"public/elm.js" %> \out -> do
elmSources <- getDirectoryFiles "." ["styleguide-app/**/*.elm", "src/**/*.elm"]
need elmSources
cmd_ (Cwd "styleguide-app") "elm" "make" "Main.elm" "--output" (".." </> out)
cmd_ (Cwd "styleguide") "elm" "make" "Main.elm" "--output" (".." </> out)
"public/package.json" %> \out -> do
copyFileChanged "elm.json" out
"public/application.json" %> \out -> do
copyFileChanged "styleguide/elm.json" out
"public/**/*" %> \out ->
copyFileChanged (replaceDirectory1 out "styleguide-app") out
copyFileChanged (replaceDirectory1 out "styleguide") out
"log/public.txt" %> \out -> do
styleguideAssets <- getDirectoryFiles ("styleguide-app" </> "assets") ["**/*"]
need
( ["public/index.html", "public/elm.js", "public/bundle.js"]
++ map (("public" </> "assets") </>) styleguideAssets
)
need (["public/index.html", "public/elm.js", "public/bundle.js", "public/package.json", "public/application.json"])
writeFileChanged out "built styleguide app successfully"
-- dev deps we get dynamically instead of from Nix (frowny face)

View File

@ -1,11 +1,5 @@
Nri.Ui.Accordion.V1,upgrade to V3
Nri.Ui.Menu.V1,upgrade to V3
Nri.Ui.Menu.V2,upgrade to V3
Nri.Ui.PremiumCheckbox.V6,upgrade to V8
Nri.Ui.PremiumCheckbox.V7,upgrade to V8
Nri.Ui.RadioButton.V2,upgrade to V4
Nri.Ui.RadioButton.V3,upgrade to V4
Nri.Ui.SideNav.V1,upgrade to V2
Nri.Ui.Table.V4,upgrade to V5
Nri.Ui.SortableTable.V2,upgrade to V3
Nri.Ui.Tabs.V6,upgrade to V7
Nri.Ui.Tooltip.V1,upgrade to V2
Nri.Ui.Tooltip.V1,upgrade to V3

1 Nri.Ui.Accordion.V1 upgrade to V3
2 Nri.Ui.Menu.V1 upgrade to V3
3 Nri.Ui.Menu.V2 Nri.Ui.SortableTable.V2 upgrade to V3
Nri.Ui.PremiumCheckbox.V6 upgrade to V8
Nri.Ui.PremiumCheckbox.V7 upgrade to V8
Nri.Ui.RadioButton.V2 upgrade to V4
Nri.Ui.RadioButton.V3 upgrade to V4
Nri.Ui.SideNav.V1 upgrade to V2
Nri.Ui.Table.V4 upgrade to V5
4 Nri.Ui.Tabs.V6 upgrade to V7
5 Nri.Ui.Tooltip.V1 upgrade to V2 upgrade to V3

View File

@ -3,7 +3,7 @@
"name": "NoRedInk/noredink-ui",
"summary": "UI Widgets we use at NRI",
"license": "BSD-3-Clause",
"version": "15.7.0",
"version": "16.4.0",
"exposed-modules": [
"Nri.Ui",
"Nri.Ui.Accordion.V1",
@ -11,7 +11,9 @@
"Nri.Ui.AssetPath",
"Nri.Ui.AssignmentIcon.V2",
"Nri.Ui.Balloon.V1",
"Nri.Ui.BreadCrumbs.V1",
"Nri.Ui.Button.V10",
"Nri.Ui.Carousel.V1",
"Nri.Ui.Checkbox.V5",
"Nri.Ui.ClickableSvg.V2",
"Nri.Ui.ClickableText.V3",
@ -21,10 +23,10 @@
"Nri.Ui.Confetti.V2",
"Nri.Ui.CssVendorPrefix.V1",
"Nri.Ui.Data.PremiumDisplay",
"Nri.Ui.Data.PremiumLevel",
"Nri.Ui.DisclosureIndicator.V2",
"Nri.Ui.Divider.V2",
"Nri.Ui.Effects.V1",
"Nri.Ui.WhenFocusLeaves.V1",
"Nri.Ui.FocusTrap.V1",
"Nri.Ui.Fonts.V1",
"Nri.Ui.Heading.V2",
@ -36,31 +38,23 @@
"Nri.Ui.MasteryIcon.V1",
"Nri.Ui.MediaQuery.V1",
"Nri.Ui.Menu.V1",
"Nri.Ui.Menu.V2",
"Nri.Ui.Menu.V3",
"Nri.Ui.Message.V3",
"Nri.Ui.Modal.V11",
"Nri.Ui.Page.V3",
"Nri.Ui.Palette.V1",
"Nri.Ui.Pennant.V2",
"Nri.Ui.PremiumCheckbox.V6",
"Nri.Ui.PremiumCheckbox.V7",
"Nri.Ui.PremiumCheckbox.V8",
"Nri.Ui.RadioButton.V2",
"Nri.Ui.RadioButton.V3",
"Nri.Ui.RadioButton.V4",
"Nri.Ui.SegmentedControl.V14",
"Nri.Ui.Select.V8",
"Nri.Ui.Shadows.V1",
"Nri.Ui.SideNav.V1",
"Nri.Ui.SideNav.V2",
"Nri.Ui.Slide.V1",
"Nri.Ui.SlideModal.V2",
"Nri.Ui.SideNav.V3",
"Nri.Ui.SortableTable.V2",
"Nri.Ui.SortableTable.V3",
"Nri.Ui.Sprite.V1",
"Nri.Ui.Svg.V1",
"Nri.Ui.Switch.V1",
"Nri.Ui.Table.V4",
"Nri.Ui.Switch.V2",
"Nri.Ui.Table.V5",
"Nri.Ui.Tabs.V6",
"Nri.Ui.Tabs.V7",
@ -69,7 +63,7 @@
"Nri.Ui.TextArea.V4",
"Nri.Ui.TextInput.V7",
"Nri.Ui.Tooltip.V1",
"Nri.Ui.Tooltip.V2",
"Nri.Ui.Tooltip.V3",
"Nri.Ui.UiIcon.V1"
],
"elm-version": "0.19.0 <= v < 0.20.0",
@ -78,7 +72,6 @@
"Gizra/elm-keyboard-event": "1.0.1 <= v < 2.0.0",
"elm/browser": "1.0.2 <= v < 2.0.0",
"elm/core": "1.0.1 <= v < 2.0.0",
"elm/html": "1.0.0 <= v < 2.0.0",
"elm/http": "2.0.0 <= v < 3.0.0",
"elm/json": "1.1.3 <= v < 2.0.0",
"elm/random": "1.0.0 <= v < 2.0.0",
@ -88,12 +81,13 @@
"elm-community/string-extra": "4.0.1 <= v < 5.0.0",
"pablohirafuji/elm-markdown": "2.0.5 <= v < 3.0.0",
"rtfeldman/elm-css": "17.0.1 <= v < 18.0.0",
"tesk9/accessible-html-with-css": "2.3.1 <= v < 3.0.0",
"tesk9/accessible-html-with-css": "3.0.0 <= v < 4.0.0",
"tesk9/palette": "3.0.1 <= v < 4.0.0"
},
"test-dependencies": {
"avh4/elm-program-test": "3.3.0 <= v < 4.0.0",
"elm/html": "1.0.0 <= v < 2.0.0",
"elm-explorations/test": "1.2.2 <= v < 2.0.0",
"tesk9/accessible-html": "4.1.0 <= v < 5.0.0"
"tesk9/accessible-html": "5.0.0 <= v < 6.0.0"
}
}

View File

@ -1,7 +1,7 @@
# WARNING: this file is managed with `elm-forbid-imports`. Manual edits will
# be overwritten!
roots = ['styleguide-app']
roots = ['styleguide']
[forbidden.Accessibility]
hint = 'Use Accessibility.Styled'
@ -109,6 +109,15 @@ hint = 'upgrade to V8'
hint = 'upgrade to V8'
[forbidden."Nri.Ui.SideNav.V1"]
hint = 'upgrade to V3'
[forbidden."Nri.Ui.SideNav.V2"]
hint = 'upgrade to V3'
[forbidden."Nri.Ui.SortableTable.V2"]
hint = 'upgrade to V3'
[forbidden."Nri.Ui.Switch.V1"]
hint = 'upgrade to V2'
[forbidden."Nri.Ui.Table.V4"]
@ -143,5 +152,8 @@ hint = 'upgrade to V6'
hint = 'upgrade to V7'
[forbidden."Nri.Ui.Tooltip.V1"]
hint = 'upgrade to V2'
usages = ['styleguide-app/../src/Nri/Ui/Menu/V1.elm']
hint = 'upgrade to V3'
usages = ['styleguide/../src/Nri/Ui/Menu/V1.elm']
[forbidden."Nri.Ui.Tooltip.V2"]
hint = 'upgrade to V3'

View File

@ -2,8 +2,7 @@
* @module CustomElement
*/
/**
/**
* Create a DOM Event without worrying about browser compatibility
*
* @param {string} eventName The name of the event to create
@ -13,9 +12,9 @@
* var event = CustomElement.makeEvent('change', { name: this._secretInput.value })
* this.dispatchEvent(event)
*/
exports.makeEvent = makeMakeEvent()
exports.makeEvent = makeMakeEvent();
var makeClass = makeMakeClass()
var makeClass = makeMakeClass();
function noOp() {}
@ -45,19 +44,19 @@ function noOp() {}
* this._button = document.createElement('button')
* },
*
* // Let the custom element runtime know that you want to be notified of
* // changes to the `hello` attribute
* observedAttributes: ['hello'],
* // Let the custom element runtime know that you want to be notified of
* // changes to the `hello` attribute
* observedAttributes: ['hello'],
*
* // Do any updating when an attribute changes on the element. Note the
* // difference between attributes and properties of an element (see:
* // https://javascript.info/dom-attributes-and-properties). This is a
* // proxy for `attributeChangedCallback` (see:
* // https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#Using_the_lifecycle_callbacks).
* // Takes the name of the attribute that changed, the previous string value,
* // and the new string value.
* onAttributeChange: function(name, previous, next) {
* if (name === 'hello') this._hello = next
* // Do any updating when an attribute changes on the element. Note the
* // difference between attributes and properties of an element (see:
* // https://javascript.info/dom-attributes-and-properties). This is a
* // proxy for `attributeChangedCallback` (see:
* // https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#Using_the_lifecycle_callbacks).
* // Takes the name of the attribute that changed, the previous string value,
* // and the new string value.
* onAttributeChange: function(name, previous, next) {
* if (name === 'hello') this._hello = next
* },
*
* // Do any setup work after the element has been inserted into the DOM.
@ -109,38 +108,42 @@ function noOp() {}
*/
exports.create = function create(config) {
if (customElements.get(config.tagName)) {
throw Error('Custom element with tag name ' + config.tagName + ' already exists.')
throw Error(
"Custom element with tag name " + config.tagName + " already exists."
);
}
var observedAttributes = config.observedAttributes || []
var methods = config.methods || {}
var properties = config.properties || {}
var initialize = config.initialize || noOp
var onConnect = config.onConnect || noOp
var onDisconnect = config.onDisconnect || noOp
var onAttributeChange = config.onAttributeChange || noOp
var observedAttributes = config.observedAttributes || [];
var methods = config.methods || {};
var properties = config.properties || {};
var initialize = config.initialize || noOp;
var onConnect = config.onConnect || noOp;
var onDisconnect = config.onDisconnect || noOp;
var onAttributeChange = config.onAttributeChange || noOp;
var Class = makeClass();
var Class = makeClass()
for (var key in methods) {
if (!methods.hasOwnProperty(key)) continue
Class.prototype[key] = methods[key]
if (!methods.hasOwnProperty(key)) continue;
Class.prototype[key] = methods[key];
}
Object.defineProperties(Class.prototype, properties)
Object.defineProperties(Class.prototype, properties);
Class.prototype.connectedCallback = onConnect
Class.prototype.disconnectedCallback = onDisconnect
Class.prototype.attributeChangedCallback = onAttributeChange
Class.prototype.connectedCallback = onConnect;
Class.prototype.disconnectedCallback = onDisconnect;
Class.prototype.attributeChangedCallback = onAttributeChange;
if (Array.isArray(observedAttributes)) {
Object.defineProperty(Class, 'observedAttributes', {
get: function () { return observedAttributes }
})
Object.defineProperty(Class, "observedAttributes", {
get: function () {
return observedAttributes;
},
});
}
Class.displayName = '<' + config.tagName + '> custom element'
customElements.define(config.tagName, Class)
}
Class.displayName = "<" + config.tagName + "> custom element";
customElements.define(config.tagName, Class);
};
/**
* Attempt to make an ES6 class using the Function constructor rather than
@ -151,35 +154,37 @@ exports.create = function create(config) {
*/
function makeMakeClass() {
try {
return new Function([
"return class extends HTMLElement {",
" constructor() {",
" super()",
" for (var key in this) {",
" var value = this[key]",
" if (typeof value !== 'function') continue",
" this[key] = value.bind(this)",
" }",
" }",
"}",
].join("\n"))
return new Function(
[
"return class extends HTMLElement {",
" constructor() {",
" super()",
" for (var key in this) {",
" var value = this[key]",
" if (typeof value !== 'function') continue",
" this[key] = value.bind(this)",
" }",
" }",
"}",
].join("\n")
);
} catch (e) {
return function () {
function Class() {
// This is the best we can do to trick modern browsers into thinking this
// is a real, legitimate class constructor and not a plane old JS function.
var _this = HTMLElement.call(this) || this
var _this = HTMLElement.call(this) || this;
for (var key in _this) {
var value = _this[key]
if (typeof value !== 'function') continue
_this[key] = value.bind(_this)
var value = _this[key];
if (typeof value !== "function") continue;
_this[key] = value.bind(_this);
}
return _this
return _this;
}
Class.prototype = Object.create(HTMLElement.prototype)
Class.prototype.constructor = Class
return Class
}
Class.prototype = Object.create(HTMLElement.prototype);
Class.prototype.constructor = Class;
return Class;
};
}
}
@ -187,20 +192,20 @@ function makeMakeClass() {
* Return a function for making an event based on what the browser supports.
* IE11 doesn't support Event constructor, and uses the old Java-style
* methods instead
*/
*/
function makeMakeEvent() {
try {
// if calling Event with new works, do it that way
var testEvent = new CustomEvent('myEvent', { detail: 1 })
var testEvent = new CustomEvent("myEvent", { detail: 1 });
return function makeEventNewStyle(type, detail) {
return new CustomEvent(type, { detail: detail })
}
return new CustomEvent(type, { detail: detail });
};
} catch (_error) {
// if calling CustomEvent with new throws an error, do it the old way
return function makeEventOldStyle(type, detail) {
var event = document.createEvent('CustomEvent')
event.initCustomEvent(type, false, false, detail)
return event
}
var event = document.createEvent("CustomEvent");
event.initCustomEvent(type, false, false, detail);
return event;
};
}
}

View File

@ -1,57 +1,63 @@
CustomElement = require('../CustomElement')
CustomElement = require("../CustomElement");
CustomElement.create({
tagName: 'nri-textarea-v3',
tagName: "nri-textarea-v3",
initialize: function() {
this._autoresize = false
initialize: function () {
this._autoresize = false;
},
onConnect: function() {
this._textarea = this.querySelector('textarea')
this._updateListener()
onConnect: function () {
this._textarea = this.querySelector("textarea");
this._updateListener();
},
observedAttributes: ['data-autoresize'],
observedAttributes: ["data-autoresize"],
onAttributeChange: function(name, previous, next) {
if (name === 'data-autoresize') {
this._autoresize = next !== null
if (!this._textarea) return
this._updateListener()
onAttributeChange: function (name, previous, next) {
if (name === "data-autoresize") {
this._autoresize = next !== null;
if (!this._textarea) return;
this._updateListener();
}
},
methods: {
_updateListener: function() {
_updateListener: function () {
if (this._autoresize) {
this._textarea.addEventListener('input', this._resize)
this._resize()
this._textarea.addEventListener("input", this._resize);
this._resize();
} else {
this._textarea.removeEventListener('input', this._resize)
this._textarea.removeEventListener("input", this._resize);
}
},
_resize: function() {
var minHeight = null
_resize: function () {
var minHeight = null;
if (this._textarea.style.minHeight) {
minHeight = parseInt(this._textarea.style.minHeight, 10)
minHeight = parseInt(this._textarea.style.minHeight, 10);
} else {
minHeight = parseInt(window.getComputedStyle(this._textarea).minHeight, 10)
minHeight = parseInt(
window.getComputedStyle(this._textarea).minHeight,
10
);
}
if (minHeight === 0) {
minHeight = parseInt(window.getComputedStyle(this._textarea).height, 10)
minHeight = parseInt(
window.getComputedStyle(this._textarea).height,
10
);
}
this._textarea.style.overflowY = 'hidden'
this._textarea.style.minHeight = minHeight + 'px'
this._textarea.style.transition = 'none'
this._textarea.style.overflowY = "hidden";
this._textarea.style.minHeight = minHeight + "px";
this._textarea.style.transition = "none";
if (this._textarea.scrollHeight > minHeight) {
this._textarea.style.height = minHeight + 'px'
this._textarea.style.height = this._textarea.scrollHeight + 'px'
this._textarea.style.height = minHeight + "px";
this._textarea.style.height = this._textarea.scrollHeight + "px";
} else {
this._textarea.style.height = minHeight + 'px'
this._textarea.style.height = minHeight + "px";
}
}
}
})
},
},
});

View File

@ -1,56 +1,67 @@
CustomElement = require('../CustomElement')
CustomElement = require("../CustomElement");
CustomElement.create({
tagName: 'nri-textarea-v4',
tagName: "nri-textarea-v4",
initialize: function() {
this._autoresize = false
initialize: function () {
this._autoresize = false;
},
onConnect: function() {
this._textarea = this.querySelector('textarea')
this._updateListener()
onConnect: function () {
this._textarea = this.querySelector("textarea");
this._updateListener();
},
observedAttributes: ['data-autoresize'],
observedAttributes: ["data-autoresize"],
onAttributeChange: function(name, previous, next) {
if (name === 'data-autoresize') {
this._autoresize = next !== null
if (!this._textarea) return
this._updateListener()
onAttributeChange: function (name, previous, next) {
if (name === "data-autoresize") {
this._autoresize = next !== null;
if (!this._textarea) return;
this._updateListener();
}
},
methods: {
_updateListener: function() {
_updateListener: function () {
if (this._autoresize) {
this._textarea.addEventListener('input', this._resize)
this._resize()
this._textarea.addEventListener("input", this._resize);
this._resize();
} else {
this._textarea.removeEventListener('input', this._resize)
this._textarea.removeEventListener("input", this._resize);
}
},
_resize: function() {
var minHeight = null
_resize: function () {
var minHeight = null;
var computedStyles = window.getComputedStyle(this._textarea);
if (this._textarea.style.minHeight) {
minHeight = parseInt(this._textarea.style.minHeight, 10)
minHeight = parseInt(this._textarea.style.minHeight, 10);
} else {
minHeight = parseInt(window.getComputedStyle(this._textarea).minHeight, 10)
}
if (minHeight === 0) {
minHeight = parseInt(window.getComputedStyle(this._textarea).height, 10)
minHeight = parseInt(computedStyles.minHeight, 10);
}
this._textarea.style.overflowY = 'hidden'
this._textarea.style.minHeight = minHeight + 'px'
this._textarea.style.transition = 'none'
if (this._textarea.scrollHeight > minHeight) {
this._textarea.style.height = this._textarea.scrollHeight + 'px'
} else {
this._textarea.style.height = minHeight + 'px'
if (minHeight === 0) {
minHeight = parseInt(computedStyles.height, 10);
}
}
}
})
this._textarea.style.overflowY = "hidden";
this._textarea.style.minHeight = minHeight + "px";
this._textarea.style.transition = "none";
// the browser does not include border widths in `.scrollHeight`, but we
// sometimes use `box-sizing: border-box` on these elements so we need to
// take it into account when setting the CSS `height`.
var borderOffset = 0;
if (computedStyles.boxSizing === "border-box") {
borderOffset =
parseInt(computedStyles.borderTopWidth, 10) +
parseInt(computedStyles.borderBottomWidth, 10);
}
this._textarea.style.height =
Math.max(minHeight, this._textarea.scrollHeight + borderOffset) + "px";
},
},
});

View File

@ -1,4 +1,4 @@
require('./TextArea/V3')
require('./TextArea/V4')
require("./TextArea/V3");
require("./TextArea/V4");
exports.CustomElement = require('./CustomElement')
exports.CustomElement = require("./CustomElement");

View File

@ -1,3 +1,6 @@
[build.environment]
NODE_ENV = "production"
[build]
command = "script/netlify.sh"
publish = "public"

View File

@ -2,7 +2,7 @@
"elm-forbid-import": {
"branch": "main",
"repo": "https://git.bytes.zone/brian/elm-forbid-import.git",
"rev": "e4514ed0c0c267aa5f192bdc0956b0846ade5e2c",
"rev": "f5d5dc93bb68a58aaa922ba588c94941845e0941",
"type": "git"
},
"niv": {
@ -11,22 +11,22 @@
"homepage": "https://github.com/nmattia/niv",
"owner": "nmattia",
"repo": "niv",
"rev": "e0ca65c81a2d7a4d82a189f1e23a48d59ad42070",
"sha256": "1pq9nh1d8nn3xvbdny8fafzw87mj7gsmp6pxkdl65w2g18rmcmzx",
"rev": "82e5cd1ad3c387863f0545d7591512e76ab0fc41",
"sha256": "090l219mzc0gi33i3psgph6s2pwsc8qy4lyrqjdj4qzkvmaj65a7",
"type": "tarball",
"url": "https://github.com/nmattia/niv/archive/e0ca65c81a2d7a4d82a189f1e23a48d59ad42070.tar.gz",
"url": "https://github.com/nmattia/niv/archive/82e5cd1ad3c387863f0545d7591512e76ab0fc41.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"nixpkgs": {
"branch": "release-21.05",
"branch": "nixpkgs-unstable",
"description": "Nix Packages collection",
"homepage": "",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1dc32d71d5d8fe18ff13f31488fafc1d9145f886",
"sha256": "1pgmg0sqyrk3a4gfm1lqv9wmn5pxlivqmblhls7hig9db08brni6",
"rev": "41cc1d5d9584103be4108c1815c350e07c807036",
"sha256": "1zwbkijhgb8a5wzsm1dya1a4y79bz6di5h49gcmw6klai84xxisv",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/1dc32d71d5d8fe18ff13f31488fafc1d9145f886.tar.gz",
"url": "https://github.com/NixOS/nixpkgs/archive/41cc1d5d9584103be4108c1815c350e07c807036.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
}
}

View File

@ -31,8 +31,28 @@ let
if spec ? branch then "refs/heads/${spec.branch}" else
if spec ? tag then "refs/tags/${spec.tag}" else
abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!";
submodules = if spec ? submodules then spec.submodules else false;
submoduleArg =
let
nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0;
emptyArgWithWarning =
if submodules == true
then
builtins.trace
(
"The niv input \"${name}\" uses submodules "
+ "but your nix's (${builtins.nixVersion}) builtins.fetchGit "
+ "does not support them"
)
{}
else {};
in
if nixSupportsSubmodules
then { inherit submodules; }
else emptyArgWithWarning;
in
builtins.fetchGit { url = spec.repo; inherit (spec) rev; inherit ref; };
builtins.fetchGit
({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg);
fetch_local = spec: spec.path;

7443
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@noredink/ui",
"version": "1.2.2",
"version": "1.2.3",
"description": "UI widgets we use.",
"main": "lib/index.js",
"directories": {
@ -26,16 +26,18 @@
},
"homepage": "https://github.com/NoRedInk/NoRedInk-ui#readme",
"devDependencies": {
"@percy/cli": "^1.0.0-beta.73",
"@percy/puppeteer": "^2.0.0",
"@percy/cli": "^1.4.0",
"@percy/puppeteer": "^2.0.2",
"browserify": "16.2.3",
"puppeteer": "^3.3.0",
"request": "^2.88.0"
"prettier": "^2.7.1",
"puppeteer": "^13.0.1",
"request": "^2.88.2",
"@axe-core/puppeteer": "^4.4.3",
"axe-core": "3.5.6",
"expect": "^27.5.1",
"mocha": "^9.2.2"
},
"dependencies": {
"axe-core": "3.5.6",
"expect": "^27.4.6",
"http-server": "^14.1.0",
"mocha": "^9.2.0"
"http-server": "^14.1.1"
}
}

35
review/elm.json Normal file
View File

@ -0,0 +1,35 @@
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/core": "1.0.5",
"elm/json": "1.1.3",
"elm/project-metadata-utils": "1.0.2",
"jfmengels/elm-review": "2.7.0",
"jfmengels/elm-review-unused": "1.1.20",
"stil4m/elm-syntax": "7.2.9"
},
"indirect": {
"elm/html": "1.0.0",
"elm/parser": "1.1.0",
"elm/random": "1.0.0",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2",
"elm-community/list-extra": "8.5.2",
"elm-explorations/test": "1.2.2",
"miniBill/elm-unicode": "1.0.2",
"rtfeldman/elm-hex": "1.0.0",
"stil4m/structured-writer": "1.0.3"
}
},
"test-dependencies": {
"direct": {
"elm-explorations/test": "1.2.2"
},
"indirect": {}
}
}

View File

@ -0,0 +1,41 @@
module ReviewConfig exposing (config)
{-| Do not rename the ReviewConfig module or the config function, because
`elm-review` will look for these.
To add packages that contain rules, add them to this review project using
`elm install author/packagename`
when inside the directory containing this file.
-}
import NoUnused.CustomTypeConstructorArgs
import NoUnused.CustomTypeConstructors
import NoUnused.Dependencies
import NoUnused.Exports
import NoUnused.Modules
import NoUnused.Parameters
import NoUnused.Patterns
import NoUnused.Variables
import Review.Rule exposing (Rule)
config : List Rule
config =
[ NoUnused.CustomTypeConstructors.rule []
-- sometimes we just want to build a value without extracting it
-- , NoUnused.CustomTypeConstructorArgs.rule
, NoUnused.Dependencies.rule
-- We want to include all functions even if they're unused in this repository.
-- , NoUnused.Exports.rule
, NoUnused.Modules.rule
-- We like to keep parameters around for readability.
-- , NoUnused.Parameters.rule
-- , NoUnused.Patterns.rule
, NoUnused.Variables.rule
]

View File

@ -1,63 +0,0 @@
// 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);
});

View File

@ -1,17 +0,0 @@
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----------\n\n"))
"

View File

@ -12,7 +12,7 @@ shake --compact "$SHAKE_TARGET"
cat <<EOF
== 👋 Hello! ==================================================================
I'm watching files in styleguide-app and src for changes. If you make any
I'm watching files in styleguide, styleguide-app, and src for changes. If you make any
changes, I'll try to be smart about what should change (things end up in the
"public" directory if you want to check my work.) If you remove a file and it's
still showing up, delete the "public" directory and restart me.

View File

@ -1,17 +0,0 @@
#!/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"
NUM_ERRORS="$(jq '.violations | map(.nodes | length) | add' "$JSON_FILE")"
if test "$NUM_ERRORS" -lt 4;
then
echo "$NUM_ERRORS accessibility errors"
exit 1
fi

View File

@ -15,9 +15,10 @@ if test -d public; then rm -rf public; fi
mkdir public
# build the interactive parts
(cd styleguide-app && npx elm make Main.elm --output ../public/elm.js)
npx browserify --entry styleguide-app/manifest.js --outfile public/bundle.js
(cd styleguide && npx elm make Main.elm --output ../public/elm.js)
npx browserify --entry styleguide/manifest.js --outfile public/bundle.js
# copy static files
cp -r styleguide-app/assets public/assets
cp styleguide-app/index.html public/index.html
cp styleguide/index.html public/index.html
cp styleguide/elm.json public/application.json
cp elm.json public/package.json

View File

@ -1,81 +0,0 @@
const expect = require('expect');
const puppeteer = require('puppeteer');
const httpServer = require('http-server');
const percySnapshot = require('@percy/puppeteer');
const platform = require('os').platform();
// We need to change the args passed to puppeteer based on the platform they're using
const puppeteerArgs = /^win/.test(platform) ? [] : ['--single-process'];
const PORT = process.env.PORT_NUMBER || 8000;
describe('Visual tests', function () {
this.timeout(30000);
let page;
let server;
let browser;
before(async () => {
server = httpServer.createServer({ root: `${__dirname}/../public` });
server.listen(PORT);
browser = await puppeteer.launch({
headless: true,
timeout: 10000,
args: puppeteerArgs
});
});
after(() => {
server.close();
});
const defaultProcessing = async (name, location) => {
await page.goto(location)
await page.waitFor(`#${name.replace(".", "-")}`)
await percySnapshot(page, name)
console.log(`Snapshot complete for ${name}`)
}
const specialProcessing = {
'Modal': async (name, location) => {
await page.goto(location)
await page.waitFor(`#${name}`)
await page.click('#launch-modal')
await page.waitFor('[role="dialog"]')
await percySnapshot(page, 'Full Info Modal')
await page.click('[aria-label="Close modal"]')
await page.select('select', 'warning')
await page.click('#launch-modal')
await page.waitFor('[role="dialog"]')
await percySnapshot(page, 'Full Warning Modal')
await page.click('[aria-label="Close modal"]')
}
}
it('All', async function () {
page = await browser.newPage();
await page.goto(`http://localhost:${PORT}`);
await page.$('#maincontent');
await percySnapshot(page, this.test.fullTitle());
page.close();
});
it('Doodads', async function () {
page = await browser.newPage();
await page.goto(`http://localhost:${PORT}`);
await page.$('#maincontent');
let links = await page.evaluate(() => {
let nodes = Array.from(document.querySelectorAll("[data-nri-description='doodad-link']"));
return nodes.map(node => [node.text, node.href])
})
await links.reduce((acc, [name, location]) => {
return acc.then(() => {
let handler = specialProcessing[name] || defaultProcessing
return handler(name, location)
}
)
}, Promise.resolve())
})
});

View File

@ -1,3 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
npx percy exec -- mocha script/percy-tests.js --exit

4
script/prettier-fix-all.sh Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
git ls-files | grep -E '.js$' | xargs node_modules/.bin/prettier --write

View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
env ONLYDOODAD=${1-default} npx mocha script/puppeteer-tests.js --timeout 20000 --exit

View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
env ONLYDOODAD=${1-default} npx percy exec -- mocha script/puppeteer-tests.js --timeout 100000 --exit

187
script/puppeteer-tests.js Normal file
View File

@ -0,0 +1,187 @@
const expect = require("expect");
const puppeteer = require("puppeteer");
const httpServer = require("http-server");
const percySnapshot = require("@percy/puppeteer");
const platform = require("os").platform();
// We need to change the args passed to puppeteer based on the platform they're using
const puppeteerArgs = /^win/.test(platform) ? [] : ["--single-process"];
const PORT = process.env.PORT_NUMBER || 8000;
const { AxePuppeteer } = require("@axe-core/puppeteer");
const assert = require("assert");
describe("UI tests", function () {
let page;
let server;
let browser;
before(async () => {
server = httpServer.createServer({ root: `${__dirname}/../public` });
server.listen(PORT);
browser = await puppeteer.launch({
headless: true,
timeout: 10000,
args: puppeteerArgs,
});
});
after(() => {
server.close();
});
const handleAxeResults = function (name, results) {
const violations = results["violations"];
if (violations.length > 0) {
violations.map(function (violation) {
console.log("\n\n", violation["id"], ":", violation["description"]);
console.log(violation["help"]);
console.log(violation["helpUrl"]);
console.table(violation["nodes"], ["html"]);
});
assert.fail(
`Expected no axe violations in ${name} but got ${violations.length} violations`
);
}
};
const goTo = async (name, location) => {
await page.goto(location, { waitUntil: "load" });
await page.waitForSelector(`#${name.replace(".", "-")}`, { visible: true });
};
const defaultProcessing = async (name, location) => {
await goTo(name, location);
await percySnapshot(page, name);
const results = await new AxePuppeteer(page)
.disableRules(skippedRules[name] || [])
.analyze();
handleAxeResults(name, results);
};
const messageProcessing = async (name, location) => {
await goTo(name, location);
await percySnapshot(page, name);
var axe = await new AxePuppeteer(page)
.disableRules(skippedRules[name] || [])
.analyze();
handleAxeResults(name, axe);
const [theme] = await page.$x("//label[contains(., 'theme')]");
await theme.click();
await page.waitForXPath("//label[contains(., 'theme')]//select", 200);
const [select] = await page.$x("//label[contains(., 'theme')]//select");
const options = await page.$x("//label[contains(., 'theme')]//option");
for (const optionEl of options) {
const option = await page.evaluate((el) => el.innerText, optionEl);
select.select(option);
await percySnapshot(page, `${name} - ${option}`);
axe = await new AxePuppeteer(page)
.withRules(["color-contrast"])
.analyze();
handleAxeResults(`${name} - ${option}`, axe);
}
};
const iconProcessing = async (name, location) => {
await page.goto(location);
await page.waitForSelector(`#${name}`);
await percySnapshot(page, name);
// visible icon names snapshot
await page.click("label");
await page.waitForSelector(".checkbox-V5__Checked");
await percySnapshot(page, `${name} - display icon names`);
const results = await new AxePuppeteer(page)
.disableRules(skippedRules[name] || [])
.analyze();
handleAxeResults(name, results);
};
const skippedRules = {
// Loading's color contrast check seems to change behavior depending on whether Percy snapshots are taken or not
Loading: ["color-contrast"],
RadioButton: ["duplicate-id"],
};
const specialProcessing = {
Message: messageProcessing,
Modal: async (name, location) => {
await goTo(name, location);
await page.click("#launch-modal");
await page.waitForSelector('[role="dialog"]');
await percySnapshot(page, "Full Info Modal");
const results = await new AxePuppeteer(page)
.disableRules(skippedRules[name] || [])
.analyze();
handleAxeResults(name, results);
await page.click('[aria-label="Close modal"]');
await page.select("select", "warning");
await page.click("#launch-modal");
await page.waitForSelector('[role="dialog"]');
await percySnapshot(page, "Full Warning Modal");
await page.click('[aria-label="Close modal"]');
},
AssignmentIcon: iconProcessing,
UiIcon: iconProcessing,
Logo: iconProcessing,
Pennant: iconProcessing,
};
it("All", async function () {
page = await browser.newPage();
await page.goto(`http://localhost:${PORT}`);
await page.$("#maincontent");
await percySnapshot(page, this.test.fullTitle());
const results = await new AxePuppeteer(page)
.disableRules([
"aria-hidden-focus",
"color-contrast",
"duplicate-id-aria",
"duplicate-id",
])
.analyze();
page.close();
handleAxeResults("index view", results);
});
it("Doodads", async function () {
page = await browser.newPage();
await page.goto(`http://localhost:${PORT}`);
await page.$("#maincontent");
let links = await page.evaluate(() => {
let nodes = Array.from(
document.querySelectorAll("[data-nri-description='doodad-link']")
);
return nodes.map((node) => [node.text, node.href]);
});
await links.reduce((acc, [name, location]) => {
return acc.then(() => {
if (
process.env.ONLYDOODAD == "default" ||
process.env.ONLYDOODAD == name
) {
console.log(`Testing ${name}`);
let handler = specialProcessing[name] || defaultProcessing;
return handler(name, location);
}
});
}, Promise.resolve());
page.close();
});
});

View File

@ -1,8 +1,11 @@
let
sources = import ./nix/sources.nix;
system = if builtins.currentSystem == "aarch64-darwin" then "x86_64-darwin" else builtins.currentSystem;
system = if builtins.currentSystem == "aarch64-darwin" then
"x86_64-darwin"
else
builtins.currentSystem;
nixpkgs = import sources.nixpkgs { inherit system; };
niv = nixpkgs.callPackage sources.niv { };
niv = import sources.niv { };
in with nixpkgs;
stdenv.mkDerivation {
name = "noredink-ui";
@ -25,6 +28,8 @@ stdenv.mkDerivation {
elmPackages.elm-format
elmPackages.elm-test
elmPackages.elm-verify-examples
elmPackages.elm-review
elmPackages.elm-json
(pkgs.callPackage sources.elm-forbid-import { })
# preview dependencies

360
src/CheckboxIcons.elm Normal file
View File

@ -0,0 +1,360 @@
module CheckboxIcons exposing
( checked
, checkedPartially
, lockOnInside
, unchecked
)
import Nri.Ui.Colors.Extra exposing (toCssString)
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Svg.V1 exposing (Svg)
import Svg.Styled as Svg
import Svg.Styled.Attributes as SvgAttributes
unchecked : String -> Svg
unchecked idSuffix =
let
filterId =
"filter-2" ++ idSuffix
filterUrl =
"url(#" ++ filterId ++ ")"
in
Svg.svg
[ SvgAttributes.width "27px"
, SvgAttributes.height "27px"
, SvgAttributes.viewBox viewBox
]
[ Svg.defs []
[ Svg.filter
[ SvgAttributes.x "-3.7%"
, SvgAttributes.y "-3.7%"
, SvgAttributes.width "107.4%"
, SvgAttributes.height "107.4%"
, SvgAttributes.filterUnits "objectBoundingBox"
, SvgAttributes.id filterId
]
[ Svg.feOffset
[ SvgAttributes.dx "0"
, SvgAttributes.dy "2"
, SvgAttributes.in_ "SourceAlpha"
, SvgAttributes.result "shadowOffsetInner1"
]
[]
, Svg.feComposite
[ SvgAttributes.in_ "shadowOffsetInner1"
, SvgAttributes.in2 "SourceAlpha"
, SvgAttributes.operator "arithmetic"
, SvgAttributes.k2 "-1"
, SvgAttributes.k3 "1"
, SvgAttributes.result "shadowInnerInner1"
]
[]
, Svg.feColorMatrix
[ SvgAttributes.values "0 0 0 0 0.2 0 0 0 0 0.2 0 0 0 0 0.2 0 0 0 0.1 0"
, SvgAttributes.in_ "shadowInnerInner1"
]
[]
]
]
, Svg.g
[ SvgAttributes.stroke "none"
, SvgAttributes.strokeWidth "1"
, SvgAttributes.fill "none"
, SvgAttributes.fillRule "evenodd"
]
[ Svg.g
[]
[ checkboxBackground
[ SvgAttributes.fill "#EBEBEB"
, SvgAttributes.fillRule "evenodd"
, SvgAttributes.stroke (toCssString Colors.gray75)
]
, checkboxBackground
[ SvgAttributes.fill "black"
, SvgAttributes.fillOpacity "1"
, SvgAttributes.filter filterUrl
]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml
checked : String -> Svg
checked idSuffix =
let
filterId =
"filter-2" ++ idSuffix
filterUrl =
"url(#" ++ filterId ++ ")"
in
Svg.svg
[ SvgAttributes.width "27px"
, SvgAttributes.height "27px"
, SvgAttributes.viewBox viewBox
]
[ Svg.defs []
[ Svg.filter
[ SvgAttributes.x "-3.7%"
, SvgAttributes.y "-3.7%"
, SvgAttributes.width "107.4%"
, SvgAttributes.height "107.4%"
, SvgAttributes.filterUnits "objectBoundingBox"
, SvgAttributes.id filterId
]
[ Svg.feOffset
[ SvgAttributes.dx "0"
, SvgAttributes.dy "2"
, SvgAttributes.in_ "SourceAlpha"
, SvgAttributes.result "shadowOffsetInner1"
]
[]
, Svg.feComposite
[ SvgAttributes.in_ "shadowOffsetInner1"
, SvgAttributes.in2 "SourceAlpha"
, SvgAttributes.operator "arithmetic"
, SvgAttributes.k2 "-1"
, SvgAttributes.k3 "1"
, SvgAttributes.result "shadowInnerInner1"
]
[]
, Svg.feColorMatrix
[ SvgAttributes.values "0 0 0 0 0.2 0 0 0 0 0.2 0 0 0 0 0.2 0 0 0 0.1 0"
, SvgAttributes.in_ "shadowInnerInner1"
]
[]
]
]
, Svg.g
[ SvgAttributes.stroke "none"
, SvgAttributes.strokeWidth "1"
, SvgAttributes.fill "none"
, SvgAttributes.fillRule "evenodd"
]
[ -- Blue background
checkboxBackground
[ SvgAttributes.fill "#D4F0FF"
, SvgAttributes.fillRule "evenodd"
, SvgAttributes.stroke (toCssString Colors.azure)
]
, -- the filter (looks like a box shadow inset on the top)
checkboxBackground
[ SvgAttributes.fill "black"
, SvgAttributes.fillOpacity "1"
, SvgAttributes.filter filterUrl
]
, -- Checkmark
Svg.g
[ SvgAttributes.transform "translate(3.600000, 3.600000)"
, SvgAttributes.fill "#146AFF"
]
[ Svg.path
[ SvgAttributes.d "M7.04980639,17.8647896 C6.57427586,17.8647896 6.11539815,17.6816086 5.77123987,17.3513276 L0.571859358,12.3786105 C-0.167340825,11.672716 -0.193245212,10.5014676 0.513574487,9.7631926 C1.21761872,9.02491757 2.38979222,8.99808803 3.12899241,9.70490773 L6.96746745,13.3750043 L16.7917062,2.73292703 C17.4855737,1.98077465 18.6558969,1.93451682 19.4061989,2.62745917 C20.1574262,3.32132667 20.2046091,4.49164987 19.5116668,5.24195193 L8.4097867,17.2689887 C8.07210452,17.6344256 7.60397524,17.8481368 7.10716611,17.8638644 C7.08866297,17.8647896 7.06923468,17.8647896 7.04980639,17.8647896"
]
[]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml
checkedPartially : String -> Svg
checkedPartially idSuffix =
let
filterId =
"filter-2" ++ idSuffix
filterUrl =
"url(#" ++ filterId ++ ")"
in
Svg.svg
[ SvgAttributes.width "27px"
, SvgAttributes.height "27px"
, SvgAttributes.viewBox viewBox
]
[ Svg.defs []
[ Svg.filter
[ SvgAttributes.x "-3.7%"
, SvgAttributes.y "-3.7%"
, SvgAttributes.width "107.4%"
, SvgAttributes.height "107.4%"
, SvgAttributes.filterUnits "objectBoundingBox"
, SvgAttributes.id filterId
]
[ Svg.feOffset
[ SvgAttributes.dx "0"
, SvgAttributes.dy "2"
, SvgAttributes.in_ "SourceAlpha"
, SvgAttributes.result "shadowOffsetInner1"
]
[]
, Svg.feComposite
[ SvgAttributes.in_ "shadowOffsetInner1"
, SvgAttributes.in2 "SourceAlpha"
, SvgAttributes.operator "arithmetic"
, SvgAttributes.k2 "-1"
, SvgAttributes.k3 "1"
, SvgAttributes.result "shadowInnerInner1"
]
[]
, Svg.feColorMatrix
[ SvgAttributes.values "0 0 0 0 0.2 0 0 0 0 0.2 0 0 0 0 0.2 0 0 0 0.1 0"
, SvgAttributes.in_ "shadowInnerInner1"
]
[]
]
]
, Svg.g
[ SvgAttributes.stroke "none"
, SvgAttributes.strokeWidth "1"
, SvgAttributes.fill "none"
, SvgAttributes.fillRule "evenodd"
]
[ Svg.g
[]
[ Svg.g
[]
[ checkboxBackground
[ SvgAttributes.fill "#EBEBEB"
, SvgAttributes.fillRule "evenodd"
, SvgAttributes.stroke (toCssString Colors.azure)
]
, checkboxBackground
[ SvgAttributes.fill "black"
, SvgAttributes.fillOpacity "1"
, SvgAttributes.filter filterUrl
]
]
, Svg.path
[ SvgAttributes.d "M22.2879231,10.8937777 C22.4823276,11.0881822 22.5430781,11.3676344 22.4701764,11.7321429 C22.1785697,13.2630784 21.6196651,14.4294879 20.793446,15.2314064 C19.9672268,16.033325 18.9830688,16.4342783 17.8409423,16.4342783 C16.9175209,16.4342783 16.073089,16.3006272 15.3076213,16.033321 C14.5421536,15.7660148 13.612671,15.3772116 12.5191457,14.8668998 C11.668626,14.4537903 10.9821454,14.1500378 10.4596833,13.9556333 C9.93722115,13.7612288 9.40869184,13.664028 8.87407945,13.664028 C7.53754849,13.664028 6.68704155,14.3201333 6.32253311,15.6323637 C6.27393198,15.8267682 6.17065614,15.9907946 6.01270248,16.1244477 C5.85474882,16.2581008 5.66642228,16.3249263 5.44771721,16.3249263 C5.20471159,16.3249263 4.9860098,16.2277255 4.7916053,16.033321 C4.59720079,15.8389165 4.5,15.6202147 4.5,15.3772091 C4.5,14.6238916 4.71262674,13.8705855 5.13788659,13.117268 C5.56314644,12.3639506 6.1342011,11.7503706 6.85106771,11.2765096 C7.56793431,10.8026486 8.32731551,10.5657217 9.12923409,10.5657217 C10.076956,10.5657217 10.933538,10.6993728 11.6990058,10.966679 C12.4644735,11.2339852 13.3939561,11.6227884 14.4874814,12.1331002 C15.3380011,12.5462097 16.0244817,12.8499622 16.5469438,13.0443667 C17.0694059,13.2387712 17.5979352,13.335972 18.1325476,13.335972 C18.8129634,13.335972 19.2868173,13.2266211 19.5541234,13.0079161 C19.8214296,12.789211 20.1008819,12.4004078 20.3924887,11.8414949 C20.4653904,11.6470904 20.5747413,11.4162385 20.7205446,11.1489323 C20.9149491,10.7844239 21.2065515,10.6021724 21.5953605,10.6021724 C21.8626667,10.6021724 22.0935186,10.6993732 22.2879231,10.8937777 Z"
, SvgAttributes.fill "#146AFF"
]
[]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml
viewBox : String
viewBox =
"0 -1 27 29"
checkboxBackground attrs =
Svg.rect
([ SvgAttributes.x "0"
, SvgAttributes.y "0"
, SvgAttributes.width "27"
, SvgAttributes.height "27"
, SvgAttributes.strokeWidth "1px"
, SvgAttributes.strokeLinejoin "round"
, SvgAttributes.stroke "none"
, SvgAttributes.rx "4"
]
++ attrs
)
[]
{-| -}
lockOnInside : String -> Svg
lockOnInside idSuffix =
let
filterId =
"filter-2" ++ idSuffix
filterUrl =
"url(#" ++ filterId ++ ")"
in
Svg.svg
[ SvgAttributes.width "27px"
, SvgAttributes.height "27px"
, SvgAttributes.viewBox viewBox
]
[ Svg.defs []
[ Svg.filter
[ SvgAttributes.x "-3.7%"
, SvgAttributes.y "-3.7%"
, SvgAttributes.width "107.4%"
, SvgAttributes.height "107.4%"
, SvgAttributes.filterUnits "objectBoundingBox"
, SvgAttributes.id filterId
]
[ Svg.feOffset
[ SvgAttributes.dx "0"
, SvgAttributes.dy "2"
, SvgAttributes.in_ "SourceAlpha"
, SvgAttributes.result "shadowOffsetInner1"
]
[]
, Svg.feComposite
[ SvgAttributes.in_ "shadowOffsetInner1"
, SvgAttributes.in2 "SourceAlpha"
, SvgAttributes.operator "arithmetic"
, SvgAttributes.k2 "-1"
, SvgAttributes.k3 "1"
, SvgAttributes.result "shadowInnerInner1"
]
[]
, Svg.feColorMatrix
[ SvgAttributes.values "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
, SvgAttributes.in_ "shadowInnerInner1"
]
[]
]
]
, Svg.g
[ SvgAttributes.stroke "none"
, SvgAttributes.strokeWidth "1"
, SvgAttributes.fill "none"
, SvgAttributes.fillRule "evenodd"
]
[ Svg.g
[]
[ Svg.g
[]
[ checkboxBackground
[ SvgAttributes.fill "#EBEBEB"
, SvgAttributes.fillRule "evenodd"
, SvgAttributes.stroke (toCssString Colors.gray75)
]
, checkboxBackground
[ SvgAttributes.fill "black"
, SvgAttributes.fillOpacity "1"
, SvgAttributes.filter filterUrl
]
]
, Svg.g
[ SvgAttributes.transform "translate(4.050000, 4.050000)"
]
[ Svg.g
[ SvgAttributes.transform "translate(3.040000, 0.271429)"
]
[ Svg.path
[ SvgAttributes.d "M10.8889406,8.4420254 L10.8889406,5.41406583 C10.8889406,2.93752663 8.90203465,0.922857143 6.46010875,0.922857143 C4.01774785,0.922857143 2.03105941,2.93752663 2.03105941,5.41406583 L2.03105941,8.4420254 L1.39812057,8.4420254 C0.626196192,8.4420254 0,9.0763794 0,9.85917577 L0,17.0399925 C0,17.8227889 0.626196192,18.4571429 1.39812057,18.4571429 L11.5223144,18.4571429 C12.2942388,18.4571429 12.92,17.8227889 12.92,17.0399925 L12.92,9.85939634 C12.92,9.07659997 12.2942388,8.4420254 11.5223144,8.4420254 L10.8889406,8.4420254 Z M6.8875056,13.8949112 L6.8875056,15.5789491 C6.8875056,15.8187066 6.69588391,16.0128066 6.46010875,16.0128066 C6.22389859,16.0128066 6.0322769,15.8187066 6.0322769,15.5789491 L6.0322769,13.8949112 C5.54876383,13.7173539 5.20271376,13.2490877 5.20271376,12.6972262 C5.20271376,11.9933932 5.76561607,11.4221217 6.46010875,11.4221217 C7.15394892,11.4221217 7.71772125,11.9933932 7.71772125,12.6972262 C7.71772125,13.2497494 7.37101867,13.7180156 6.8875056,13.8949112 L6.8875056,13.8949112 Z M9.21176142,8.4420254 L3.70823858,8.4420254 L3.70823858,5.41406583 C3.70823858,3.87538241 4.94279558,2.62343759 6.46010875,2.62343759 C7.97720442,2.62343759 9.21176142,3.87538241 9.21176142,5.41406583 L9.21176142,8.4420254 L9.21176142,8.4420254 Z"
, SvgAttributes.fill "#E68900"
]
[]
, Svg.rect
[ SvgAttributes.fill "#FFFFFF"
, SvgAttributes.x "0.922857143"
, SvgAttributes.y "10.1514286"
, SvgAttributes.width "10.1514286"
, SvgAttributes.height "5.53714286"
]
[]
, Svg.path
[ SvgAttributes.d "M10.8889406,7.51916826 L10.8889406,4.49120869 C10.8889406,2.01466949 8.90203465,0 6.46010875,0 C4.01774785,0 2.03105941,2.01466949 2.03105941,4.49120869 L2.03105941,7.51916826 L1.39812057,7.51916826 C0.626196192,7.51916826 0,8.15352226 0,8.93631863 L0,16.1171353 C0,16.8999317 0.626196192,17.5342857 1.39812057,17.5342857 L11.5223144,17.5342857 C12.2942388,17.5342857 12.92,16.8999317 12.92,16.1171353 L12.92,8.9365392 C12.92,8.15374283 12.2942388,7.51916826 11.5223144,7.51916826 L10.8889406,7.51916826 Z M6.8875056,12.9720541 L6.8875056,14.6560919 C6.8875056,14.8958495 6.69588391,15.0899495 6.46010875,15.0899495 C6.22389859,15.0899495 6.0322769,14.8958495 6.0322769,14.6560919 L6.0322769,12.9720541 C5.54876383,12.7944967 5.20271376,12.3262305 5.20271376,11.774369 C5.20271376,11.0705361 5.76561607,10.4992645 6.46010875,10.4992645 C7.15394892,10.4992645 7.71772125,11.0705361 7.71772125,11.774369 C7.71772125,12.3268922 7.37101867,12.7951584 6.8875056,12.9720541 L6.8875056,12.9720541 Z M9.21176142,7.51916826 L3.70823858,7.51916826 L3.70823858,4.49120869 C3.70823858,2.95252527 4.94279558,1.70058044 6.46010875,1.70058044 C7.97720442,1.70058044 9.21176142,2.95252527 9.21176142,4.49120869 L9.21176142,7.51916826 L9.21176142,7.51916826 Z"
, SvgAttributes.fill "#FEC900"
]
[]
]
]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml

View File

@ -1,25 +1,41 @@
module ClickableAttributes exposing
( ClickableAttributes
, href
, init
, linkExternal
, linkExternalWithTracking
, linkSpa
, linkWithMethod
, linkWithTracking
( ClickableAttributes, init
, onClick
, toButtonAttributes
, href, linkWithMethod, linkWithTracking
, linkSpa
, linkExternal, linkExternalWithTracking
, toLinkAttributes
)
{-| -}
{-|
@docs ClickableAttributes, init
# For buttons
@docs onClick
@docs toButtonAttributes
# For links
@docs href, linkWithMethod, linkWithTracking
@docs linkSpa
@docs linkExternal, linkExternalWithTracking
@docs toLinkAttributes
-}
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Role as Role
import EventExtras
import Html.Styled exposing (Attribute)
import Html.Styled.Attributes as Attributes
import Html.Styled.Events as Events
import Json.Decode
import Nri.Ui.Html.Attributes.V2 as AttributeExtras exposing (targetBlank)
import Nri.Ui.Html.Attributes.V2 exposing (targetBlank)
{-| -}
@ -112,8 +128,25 @@ toButtonAttributes clickableAttributes =
{-| -}
toLinkAttributes : (route -> String) -> ClickableAttributes route msg -> ( String, List (Attribute msg) )
toLinkAttributes routeToString clickableAttributes =
toLinkAttributes : { routeToString : route -> String, isDisabled : Bool } -> ClickableAttributes route msg -> ( String, List (Attribute msg) )
toLinkAttributes { routeToString, isDisabled } clickableAttributes =
let
( linkTypeName, attributes ) =
toEnabledLinkAttributes routeToString clickableAttributes
in
( linkTypeName
, if isDisabled then
[ Role.link
, Aria.disabled True
]
else
attributes
)
toEnabledLinkAttributes : (route -> String) -> ClickableAttributes route msg -> ( String, List (Attribute msg) )
toEnabledLinkAttributes routeToString clickableAttributes =
let
stringUrl =
case ( clickableAttributes.urlString, clickableAttributes.url ) of

View File

@ -15,7 +15,7 @@ module InputErrorAndGuidanceInternal exposing
-}
import Accessibility.Styled.Aria as Aria
import Css exposing (Style)
import Css
import Html.Styled as Html exposing (Html)
import Nri.Ui.Html.Attributes.V2
import Nri.Ui.Message.V3 as Message

View File

@ -11,7 +11,6 @@ import Accessibility.Styled.Style as Accessibility
import Css
import Html.Styled.Attributes as Attributes
import InputErrorAndGuidanceInternal exposing (ErrorState)
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.InputStyles.V3 as InputStyles exposing (Theme)

View File

@ -13,7 +13,6 @@ module Nri.Ui.Accordion.V1 exposing
-}
import Accessibility.Styled exposing (Attribute, Html, button, div, text)
import Accessibility.Styled.Role as Role
import Css exposing (..)
import Css.Global
import Html.Styled.Attributes as Attributes

View File

@ -59,18 +59,15 @@ module Nri.Ui.Accordion.V3 exposing
-}
import Accessibility.Styled exposing (Attribute, Html, button, div, section, text)
import Accessibility.Styled exposing (Html, button, div, section)
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Key as Key
import Accessibility.Styled.Role as Role
import Accessibility.Styled.Widget as Widget
import Css exposing (..)
import Css.Global
import Html.Styled.Attributes as Attributes
import Html.Styled.Events as Events exposing (onClick)
import Html.Styled.Keyed
import Json.Decode as Decode
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as AttributesExtra
@ -340,9 +337,9 @@ viewEntry focus arrows ({ headerId, headerLevel, caret, headerContent, entryClas
, ( accordionEntryHeaderExpandedClass, isExpanded )
, ( accordionEntryHeaderCollapsedClass, not isExpanded )
]
, Widget.disabled (config.toggle == Nothing)
, Widget.expanded isExpanded
, Aria.controls panelId
, Aria.disabled (config.toggle == Nothing)
, Aria.expanded isExpanded
, Aria.controls [ panelId ]
, config.toggle
|> Maybe.map (\toggle -> onClick (toggle (not isExpanded)))
|> Maybe.withDefault AttributesExtra.none

View File

@ -1,17 +0,0 @@
module Nri.Ui.AssetPath.Css exposing (url)
{-| Helper for constructing commonly-used CSS functions
that reference assets.
@docs url
-}
import Nri.Ui.AssetPath as AssetPath exposing (Asset)
{-| Given an `Asset`, wrap its URL in a call to `url()`.
-}
url : Asset -> String
url asset =
"url(" ++ AssetPath.url asset ++ ")"

File diff suppressed because one or more lines are too long

View File

@ -10,6 +10,14 @@ module Nri.Ui.Balloon.V1 exposing
{-| You propably want to use `Nri.Tooltip` not this.
This is used to display a ballon-like container.
# Changelog
## Patch changes
- use `Shadows`
@docs balloon
@ -31,6 +39,7 @@ import Css exposing (..)
import Html.Styled as Html exposing (Html, div, styled)
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Shadows.V1 as Shadows
@ -275,7 +284,7 @@ viewBalloon theme_ width_ padding contents =
, Just (textAlign left)
, Just (position relative)
, Just (Css.borderRadius (px 8))
, Just (property "box-shadow" "0 1px 1px hsl(0deg 0% 0% / 0.075), 0 2px 2px hsl(0deg 0% 0% / 0.075), 0 4px 4px hsl(0deg 0% 0% / 0.075), 0 8px 8px hsl(0deg 0% 0% / 0.075), 0 16px 16px hsl(0deg 0% 0% / 0.075)")
, Just Shadows.high
, Just padding
, Just (balloonTheme theme_)
, width_
@ -317,8 +326,8 @@ balloonTheme theme =
Green ->
batch
[ backgroundColor Colors.greenDark
, border3 (px 1) solid Colors.greenDark
[ backgroundColor Colors.greenDarkest
, border3 (px 1) solid Colors.greenDarkest
, Fonts.baseFont
, fontSize (px 15)
, color Colors.white
@ -430,8 +439,8 @@ arrowTheme theme =
Green ->
batch
[ backgroundColor Colors.greenDark
, border3 (px 1) solid Colors.greenDark
[ backgroundColor Colors.greenDarkest
, border3 (px 1) solid Colors.greenDarkest
, Fonts.baseFont
, fontSize (px 15)
, color Colors.white

View File

@ -0,0 +1,372 @@
module Nri.Ui.BreadCrumbs.V1 exposing
( view, IconStyle(..)
, BreadCrumbs, init
, BreadCrumb, after
, headerId
, toPageTitle, toPageTitleWithSecondaryBreadCrumbs
)
{-| Learn more about 'breadcrumbs' to help a user orient themselves within a site here: <https://www.w3.org/WAI/WCAG21/Techniques/general/G65>.
Wide Viewport (with Circled IconStyle):
Home
🏠 Home > 🟠 Category 1
🏠 > 🟠 Category 1 > 🟣 Sub-Category 2
Narrow Viewport (with Circled IconStyle):
Home
🏠 > 🟠 Category 1
🏠 > 🟠 > 🟣 Sub-Category 2
@docs view, IconStyle
@docs BreadCrumbs, init
@docs BreadCrumb, after
@docs headerId
@docs toPageTitle, toPageTitleWithSecondaryBreadCrumbs
-}
import Accessibility.Styled exposing (..)
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Style as Style
import Css exposing (..)
import Css.Global
import Css.Media as Media
import Html.Styled
import Html.Styled.Attributes as Attributes exposing (css)
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.MediaQuery.V1 as MediaQuery
import Nri.Ui.Svg.V1 as Svg
import Nri.Ui.UiIcon.V1 as UiIcon
{-| -}
type alias BreadCrumb route =
{ icon : Maybe Svg.Svg
, iconStyle : IconStyle
, id : String
, text : String
, route : route
}
{-| -}
type BreadCrumbs route
= BreadCrumbs (List (BreadCrumb route))
{-| -}
init : BreadCrumb route -> BreadCrumbs route
init breadCrumb =
BreadCrumbs [ breadCrumb ]
{-| -}
after : BreadCrumbs route -> BreadCrumb route -> BreadCrumbs route
after (BreadCrumbs previous) new =
BreadCrumbs (new :: previous)
{-| -}
headerId : BreadCrumbs route -> String
headerId (BreadCrumbs list) =
case list of
{ id } :: _ ->
id
_ ->
-- It should be impossible to construct a BreadCrumbs without
-- any elements.
--
""
{-| -}
type IconStyle
= Circled
| Default
{-| Generate an HTML page title using the breadcrumbs,
in the form "Sub-Category | Category | NoRedInk" for breadCrumbs like:
Category > Sub - Category
-}
toPageTitle : BreadCrumbs a -> String
toPageTitle (BreadCrumbs breadcrumbs) =
String.join " | " (List.map .text breadcrumbs ++ [ "NoRedInk" ])
{-| -}
toPageTitleWithSecondaryBreadCrumbs : BreadCrumbs a -> String
toPageTitleWithSecondaryBreadCrumbs (BreadCrumbs breadcrumbs) =
(List.take 1 breadcrumbs |> List.map .text)
++ [ "NoRedInk" ]
|> String.join " | "
{-| Usually, the label value will be the string "breadcrumbs".
It's configurable so that if more than one set of BreadCrumbs ever appear on the page, the aria-label for the nav can still be unique.
-}
view :
{ aTagAttributes : route -> List (Attribute msg)
, isCurrentRoute : route -> Bool
, label : String
}
-> BreadCrumbs route
-> Html msg
view config (BreadCrumbs breadCrumbs) =
styled nav
[ alignItems center
, displayFlex
, Media.withMedia [ MediaQuery.mobile ] [ marginBottom (px 10) ]
]
[ Aria.label config.label ]
(viewBreadCrumbs config (List.reverse breadCrumbs))
viewBreadCrumbs :
{ config
| aTagAttributes : route -> List (Attribute msg)
, isCurrentRoute : route -> Bool
}
-> List (BreadCrumb route)
-> List (Html msg)
viewBreadCrumbs config breadCrumbs =
let
breadCrumbCount : Int
breadCrumbCount =
List.length breadCrumbs
in
List.indexedMap
(\i ->
viewBreadCrumb config
{ isFirst = i == 0
, isLast = (i + 1) == breadCrumbCount
, isIconOnly =
-- the first breadcrumb should collapse when there
-- are 3 breadcrumbs or more
--
-- Hypothetically, if there were 4 breadcrumbs, then the
-- first 2 breadcrumbs should collapse
(breadCrumbCount - i) > 2
}
)
breadCrumbs
|> List.intersperse (Svg.toHtml arrowRight)
viewBreadCrumb :
{ config
| aTagAttributes : route -> List (Attribute msg)
, isCurrentRoute : route -> Bool
}
-> { isFirst : Bool, isLast : Bool, isIconOnly : Bool }
-> BreadCrumb route
-> Html msg
viewBreadCrumb config iconConfig crumb =
let
isLink =
not (config.isCurrentRoute crumb.route)
linkAttrs =
if isLink then
css
[ hover
[ Css.Global.descendants
[ Css.Global.class circleIconClass
[ backgroundColor Colors.glacier
, borderColor Colors.azureDark
, color Colors.azure
]
]
]
]
:: config.aTagAttributes crumb.route
else
[]
withIconIfPresent viewIcon =
case crumb.icon of
Just icon ->
[ viewIcon iconConfig.isFirst crumb.iconStyle icon
, viewHeadingWithIcon iconConfig crumb.text
]
Nothing ->
[ text crumb.text ]
in
case ( iconConfig.isLast, isLink ) of
( True, False ) ->
pageHeader crumb.id
(withIconIfPresent viewIconForHeading)
( True, True ) ->
pageHeader crumb.id
[ Html.Styled.styled Html.Styled.a
[]
(css commonCss :: linkAttrs)
(withIconIfPresent viewIconForLink)
]
( False, _ ) ->
Html.Styled.styled Html.Styled.a
[ fontWeight normal ]
(css commonCss :: Attributes.id crumb.id :: linkAttrs)
(withIconIfPresent viewIconForLink)
pageHeader : String -> List (Html msg) -> Html msg
pageHeader id =
styled h1
[ fontWeight bold ]
[ Aria.currentPage
, Attributes.id id
, Attributes.tabindex -1
, css commonCss
]
viewIconForHeading : Bool -> IconStyle -> Svg.Svg -> Html msg
viewIconForHeading isFirst iconStyle svg =
case iconStyle of
Circled ->
text ""
Default ->
withoutIconCircle isFirst svg
viewIconForLink : Bool -> IconStyle -> Svg.Svg -> Html msg
viewIconForLink isFirst iconStyle svg =
case iconStyle of
Circled ->
withIconCircle svg
Default ->
withoutIconCircle isFirst svg
viewHeadingWithIcon : { config | isLast : Bool, isIconOnly : Bool } -> String -> Html msg
viewHeadingWithIcon { isIconOnly, isLast } title =
div
(if isIconOnly then
Style.invisible
else if isLast then
[ css [ marginLeft horizontalSpacing ] ]
else
[ css
[ marginLeft horizontalSpacing
, Media.withMedia [ MediaQuery.mobile ]
[ Style.invisibleStyle
]
]
]
)
[ text title
]
commonCss : List Style
commonCss =
[ alignItems center
, displayFlex
, margin zero
, fontSize (px 30)
, Media.withMedia [ MediaQuery.mobile ] [ fontSize (px 25) ]
, Fonts.baseFont
, textDecoration none
, color Colors.navy
]
circleIconClass : String
circleIconClass =
"Nri-BreadCrumb-base-circled-icon"
withIconCircle : Svg.Svg -> Html msg
withIconCircle icon =
styled div
[ borderRadius (pct 50)
, border3 (px 1) solid Colors.azure
, color Colors.azure
, borderBottomWidth (px 2)
, backgroundColor Colors.white
, height largeIconSize
, width largeIconSize
, fontSize (px 16)
, property "transition" "background-color 0.2s, color 0.2s"
, displayFlex
, alignItems center
, justifyContent center
]
[ Attributes.class circleIconClass ]
[ icon
|> Svg.withWidth circledInnerIconSize
|> Svg.withHeight circledInnerIconSize
|> Svg.toHtml
]
withoutIconCircle : Bool -> Svg.Svg -> Html msg
withoutIconCircle isFirst icon =
let
size =
if isFirst then
largeIconSize
else
iconSize
in
icon
|> Svg.withWidth size
|> Svg.withHeight size
|> Svg.withCss [ Css.flexShrink Css.zero ]
|> Svg.toHtml
horizontalSpacing : Css.Px
horizontalSpacing =
Css.px 10
circledInnerIconSize : Css.Px
circledInnerIconSize =
Css.px 25
largeIconSize : Css.Px
largeIconSize =
Css.px 40
iconSize : Css.Px
iconSize =
Css.px 31
arrowRight : Svg.Svg
arrowRight =
UiIcon.arrowRight
|> Svg.withColor Colors.gray75
|> Svg.withHeight (px 15)
|> Svg.withWidth (px 15)
|> Svg.withCss
[ marginRight horizontalSpacing
, marginLeft horizontalSpacing
, flexShrink zero
]

View File

@ -28,6 +28,7 @@ adding a span around the text could potentially lead to regressions.
- adds `modal` helper, an alias for `large` size
- adds `notMobileCss`, `mobileCss`, `quizEngineMobileCss`
- adds `hideIconForMobile` and `hideIconFor`
- support 'disabled' links according to [Scott O'Hara's disabled links](https://www.scottohara.me/blog/2021/05/28/disabled-links.html) article
- adds `tertiary` style
@ -84,9 +85,8 @@ adding a span around the text could potentially lead to regressions.
-}
import Accessibility.Styled as Html exposing (Html)
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Role as Role
import Accessibility.Styled.Style exposing (invisibleStyle)
import Accessibility.Styled.Widget as Widget
import ClickableAttributes exposing (ClickableAttributes)
import Css exposing (Style)
import Css.Global
@ -474,6 +474,28 @@ type ButtonState
| Success
isDisabled : ButtonState -> Bool
isDisabled state =
case state of
Enabled ->
False
Disabled ->
True
Error ->
True
Unfulfilled ->
False
Loading ->
True
Success ->
True
{-| -}
enabled : Attribute msg
enabled =
@ -487,7 +509,19 @@ unfulfilled =
set (\attributes -> { attributes | state = Unfulfilled })
{-| Shows inactive styling. If a button, this attribute will disable it.
{-| Shows inactive styling.
If a button, this attribute will disable it as you'd expect.
If a link, this attribute will follow the pattern laid out in [Scott O'Hara's disabled links](https://www.scottohara.me/blog/2021/05/28/disabled-links.html) article,
and essentially make the anchor a disabled placeholder.
_Caveat!_
The styleguide example will NOT work correctly because of <https://github.com/elm/browser/issues/34>, which describes a problem where "a tags without href generate a navigation event".
In most cases, if you're not using Browser.application, disabled links should work just fine.
-}
disabled : Attribute msg
disabled =
@ -568,32 +602,12 @@ 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_ config.customStyles ]
(ClickableAttributes.toButtonAttributes config.clickableAttributes
++ Attributes.disabled isDisabled
++ Attributes.disabled (isDisabled config.state)
:: Attributes.type_ "button"
:: config.customAttributes
)
@ -607,7 +621,11 @@ renderLink ((ButtonOrLink config) as link_) =
getColorPalette link_
( linkFunctionName, attributes ) =
ClickableAttributes.toLinkAttributes identity config.clickableAttributes
ClickableAttributes.toLinkAttributes
{ routeToString = identity
, isDisabled = isDisabled config.state
}
config.clickableAttributes
in
Nri.Ui.styled Styled.a
(styledName linkFunctionName)
@ -642,7 +660,7 @@ delete config =
[ 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
Aria.label config.label
]
[ Svg.svg [ Svg.Attributes.viewBox "0 0 25 25" ]
[ Svg.title [] [ Styled.toUnstyled (Styled.text "Delete") ]
@ -698,7 +716,7 @@ toggleButton config =
else
config.onSelect
)
, Widget.pressed <| Just config.pressed
, Aria.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
@ -786,6 +804,7 @@ buttonStyle =
, Css.margin Css.zero
, Css.hover [ Css.textDecoration Css.none ]
, Css.disabled [ Css.cursor Css.notAllowed ]
, Css.Global.withAttribute "aria-disabled=true" [ Css.cursor Css.notAllowed ]
, Css.display Css.inlineFlex
, Css.alignItems Css.center
, Css.justifyContent Css.center

View File

@ -0,0 +1,64 @@
module Nri.Ui.Carousel.V1 exposing
( view
, Item
, buildItem
)
{-|
@docs view
@docs Item
@docs buildItem
-}
import Css exposing (..)
import Html.Styled as Html exposing (Html)
import TabsInternal.V2 as TabsInternal
{-| -}
type Item id msg
= Item (TabsInternal.Tab id msg)
{-| Builds an selectable item in the Caroursel
`controlHtml` represents the element that will appear in the list of options.
`slideHtml` represents the element that will be shown in your carousel when this item is selected.
-}
buildItem : { id : id, idString : String, slideHtml : Html msg, controlHtml : Html Never } -> Item id msg
buildItem config =
Item
(TabsInternal.fromList { id = config.id, idString = config.idString }
[ \tab -> { tab | panelView = config.slideHtml }
, \tab -> { tab | tabView = [ Html.map never config.controlHtml ] }
]
)
{-| -}
view :
{ focusAndSelect : { select : id, focus : Maybe String } -> msg
, selected : id
, controlStyles : Bool -> List Style
, controlListStyles : List Style
, items : List (Item id msg)
}
-> { controls : Html msg, slides : Html msg }
view config =
let
{ tabList, tabPanels } =
TabsInternal.views
{ focusAndSelect = config.focusAndSelect
, selected = config.selected
, tabs = List.map (\(Item t) -> t) config.items
, tabStyles = always config.controlStyles
, tabListStyles = config.controlListStyles
}
in
{ controls = tabList
, slides = tabPanels
}

View File

@ -2,6 +2,7 @@ module Nri.Ui.Checkbox.V5 exposing
( Model, Theme(..), IsSelected(..)
, view, viewWithLabel
, selectedFromBool
, viewIcon, checkboxLockOnInside
)
{-|
@ -25,25 +26,23 @@ module Nri.Ui.Checkbox.V5 exposing
@docs selectedFromBool
@docs viewIcon, checkboxLockOnInside
-}
import Accessibility.Styled as Html
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Style
import Accessibility.Styled.Widget as Widget
import CheckboxIcons
import Css exposing (..)
import Css.Global
import Html.Events
import Html.Styled
import Html.Styled.Attributes as Attributes exposing (css)
import Html.Styled.Events as Events
import Json.Decode
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.V3 as HtmlExtra exposing (defaultOptions)
import Nri.Ui.Svg.V1 exposing (Svg)
import Svg.Styled as Svg
import Svg.Styled.Attributes as SvgAttributes
{-| -}
@ -133,13 +132,13 @@ buildCheckbox model labelView =
icon =
case model.selected of
Selected ->
checkboxChecked model.identifier
CheckboxIcons.checked model.identifier
NotSelected ->
checkboxUnchecked model.identifier
CheckboxIcons.unchecked model.identifier
PartiallySelected ->
checkboxCheckedPartially model.identifier
CheckboxIcons.checkedPartially model.identifier
in
if model.disabled then
viewDisabledLabel model labelView icon
@ -193,7 +192,7 @@ viewCheckbox model =
(selectedToMaybe model.selected)
[ Attributes.id model.identifier
, if model.disabled then
Widget.disabled True
Aria.disabled True
else
Events.onCheck (\_ -> onCheck model)
@ -291,6 +290,7 @@ textStyle =
]
{-| -}
viewIcon : List Style -> Svg -> Html.Html msg
viewIcon styles icon =
Html.div
@ -319,338 +319,7 @@ viewIcon styles icon =
]
checkboxUnchecked : String -> Svg
checkboxUnchecked idSuffix =
let
filterId =
"filter-2" ++ idSuffix
filterUrl =
"url(#" ++ filterId ++ ")"
in
Svg.svg
[ SvgAttributes.width "27px"
, SvgAttributes.height "27px"
, SvgAttributes.viewBox "0 0 27 27"
]
[ Svg.defs []
[ Svg.filter
[ SvgAttributes.x "-3.7%"
, SvgAttributes.y "-3.7%"
, SvgAttributes.width "107.4%"
, SvgAttributes.height "107.4%"
, SvgAttributes.filterUnits "objectBoundingBox"
, SvgAttributes.id filterId
]
[ Svg.feOffset
[ SvgAttributes.dx "0"
, SvgAttributes.dy "2"
, SvgAttributes.in_ "SourceAlpha"
, SvgAttributes.result "shadowOffsetInner1"
]
[]
, Svg.feComposite
[ SvgAttributes.in_ "shadowOffsetInner1"
, SvgAttributes.in2 "SourceAlpha"
, SvgAttributes.operator "arithmetic"
, SvgAttributes.k2 "-1"
, SvgAttributes.k3 "1"
, SvgAttributes.result "shadowInnerInner1"
]
[]
, Svg.feColorMatrix
[ SvgAttributes.values "0 0 0 0 0.2 0 0 0 0 0.2 0 0 0 0 0.2 0 0 0 0.1 0"
, SvgAttributes.in_ "shadowInnerInner1"
]
[]
]
]
, Svg.g
[ SvgAttributes.stroke "none"
, SvgAttributes.strokeWidth "1"
, SvgAttributes.fill "none"
, SvgAttributes.fillRule "evenodd"
]
[ Svg.g
[]
[ checkboxBackground
[ SvgAttributes.fill "#EBEBEB"
, SvgAttributes.fillRule "evenodd"
]
, checkboxBackground
[ SvgAttributes.fill "black"
, SvgAttributes.fillOpacity "1"
, SvgAttributes.filter filterUrl
]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml
checkboxChecked : String -> Svg
checkboxChecked idSuffix =
let
filterId =
"filter-2" ++ idSuffix
filterUrl =
"url(#" ++ filterId ++ ")"
in
Svg.svg
[ SvgAttributes.width "27px"
, SvgAttributes.height "27px"
, SvgAttributes.viewBox "0 0 27 27"
]
[ Svg.defs []
[ Svg.filter
[ SvgAttributes.x "-3.7%"
, SvgAttributes.y "-3.7%"
, SvgAttributes.width "107.4%"
, SvgAttributes.height "107.4%"
, SvgAttributes.filterUnits "objectBoundingBox"
, SvgAttributes.id filterId
]
[ Svg.feOffset
[ SvgAttributes.dx "0"
, SvgAttributes.dy "2"
, SvgAttributes.in_ "SourceAlpha"
, SvgAttributes.result "shadowOffsetInner1"
]
[]
, Svg.feComposite
[ SvgAttributes.in_ "shadowOffsetInner1"
, SvgAttributes.in2 "SourceAlpha"
, SvgAttributes.operator "arithmetic"
, SvgAttributes.k2 "-1"
, SvgAttributes.k3 "1"
, SvgAttributes.result "shadowInnerInner1"
]
[]
, Svg.feColorMatrix
[ SvgAttributes.values "0 0 0 0 0.2 0 0 0 0 0.2 0 0 0 0 0.2 0 0 0 0.1 0"
, SvgAttributes.in_ "shadowInnerInner1"
]
[]
]
]
, Svg.g
[ SvgAttributes.stroke "none"
, SvgAttributes.strokeWidth "1"
, SvgAttributes.fill "none"
, SvgAttributes.fillRule "evenodd"
]
[ Svg.g
[]
[ Svg.g []
[ checkboxBackground
[ SvgAttributes.fill "#D4F0FF"
, SvgAttributes.fillRule "evenodd"
]
, checkboxBackground
[ SvgAttributes.fill "black"
, SvgAttributes.fillOpacity "1"
, SvgAttributes.filter filterUrl
]
]
, Svg.g
[ SvgAttributes.transform "translate(3.600000, 3.600000)"
, SvgAttributes.fill "#146AFF"
]
[ Svg.path
[ SvgAttributes.d "M7.04980639,17.8647896 C6.57427586,17.8647896 6.11539815,17.6816086 5.77123987,17.3513276 L0.571859358,12.3786105 C-0.167340825,11.672716 -0.193245212,10.5014676 0.513574487,9.7631926 C1.21761872,9.02491757 2.38979222,8.99808803 3.12899241,9.70490773 L6.96746745,13.3750043 L16.7917062,2.73292703 C17.4855737,1.98077465 18.6558969,1.93451682 19.4061989,2.62745917 C20.1574262,3.32132667 20.2046091,4.49164987 19.5116668,5.24195193 L8.4097867,17.2689887 C8.07210452,17.6344256 7.60397524,17.8481368 7.10716611,17.8638644 C7.08866297,17.8647896 7.06923468,17.8647896 7.04980639,17.8647896"
]
[]
]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml
checkboxCheckedPartially : String -> Svg
checkboxCheckedPartially idSuffix =
let
filterId =
"filter-2" ++ idSuffix
filterUrl =
"url(#" ++ filterId ++ ")"
in
Svg.svg
[ SvgAttributes.width "27px"
, SvgAttributes.height "27px"
, SvgAttributes.viewBox "0 0 27 27"
]
[ Svg.defs []
[ Svg.filter
[ SvgAttributes.x "-3.7%"
, SvgAttributes.y "-3.7%"
, SvgAttributes.width "107.4%"
, SvgAttributes.height "107.4%"
, SvgAttributes.filterUnits "objectBoundingBox"
, SvgAttributes.id filterId
]
[ Svg.feOffset
[ SvgAttributes.dx "0"
, SvgAttributes.dy "2"
, SvgAttributes.in_ "SourceAlpha"
, SvgAttributes.result "shadowOffsetInner1"
]
[]
, Svg.feComposite
[ SvgAttributes.in_ "shadowOffsetInner1"
, SvgAttributes.in2 "SourceAlpha"
, SvgAttributes.operator "arithmetic"
, SvgAttributes.k2 "-1"
, SvgAttributes.k3 "1"
, SvgAttributes.result "shadowInnerInner1"
]
[]
, Svg.feColorMatrix
[ SvgAttributes.values "0 0 0 0 0.2 0 0 0 0 0.2 0 0 0 0 0.2 0 0 0 0.1 0"
, SvgAttributes.in_ "shadowInnerInner1"
]
[]
]
]
, Svg.g
[ SvgAttributes.stroke "none"
, SvgAttributes.strokeWidth "1"
, SvgAttributes.fill "none"
, SvgAttributes.fillRule "evenodd"
]
[ Svg.g
[]
[ Svg.g
[]
[ checkboxBackground
[ SvgAttributes.fill "#EBEBEB"
, SvgAttributes.fillRule "evenodd"
]
, checkboxBackground
[ SvgAttributes.fill "black"
, SvgAttributes.fillOpacity "1"
, SvgAttributes.filter filterUrl
]
]
, Svg.path
[ SvgAttributes.d "M22.2879231,10.8937777 C22.4823276,11.0881822 22.5430781,11.3676344 22.4701764,11.7321429 C22.1785697,13.2630784 21.6196651,14.4294879 20.793446,15.2314064 C19.9672268,16.033325 18.9830688,16.4342783 17.8409423,16.4342783 C16.9175209,16.4342783 16.073089,16.3006272 15.3076213,16.033321 C14.5421536,15.7660148 13.612671,15.3772116 12.5191457,14.8668998 C11.668626,14.4537903 10.9821454,14.1500378 10.4596833,13.9556333 C9.93722115,13.7612288 9.40869184,13.664028 8.87407945,13.664028 C7.53754849,13.664028 6.68704155,14.3201333 6.32253311,15.6323637 C6.27393198,15.8267682 6.17065614,15.9907946 6.01270248,16.1244477 C5.85474882,16.2581008 5.66642228,16.3249263 5.44771721,16.3249263 C5.20471159,16.3249263 4.9860098,16.2277255 4.7916053,16.033321 C4.59720079,15.8389165 4.5,15.6202147 4.5,15.3772091 C4.5,14.6238916 4.71262674,13.8705855 5.13788659,13.117268 C5.56314644,12.3639506 6.1342011,11.7503706 6.85106771,11.2765096 C7.56793431,10.8026486 8.32731551,10.5657217 9.12923409,10.5657217 C10.076956,10.5657217 10.933538,10.6993728 11.6990058,10.966679 C12.4644735,11.2339852 13.3939561,11.6227884 14.4874814,12.1331002 C15.3380011,12.5462097 16.0244817,12.8499622 16.5469438,13.0443667 C17.0694059,13.2387712 17.5979352,13.335972 18.1325476,13.335972 C18.8129634,13.335972 19.2868173,13.2266211 19.5541234,13.0079161 C19.8214296,12.789211 20.1008819,12.4004078 20.3924887,11.8414949 C20.4653904,11.6470904 20.5747413,11.4162385 20.7205446,11.1489323 C20.9149491,10.7844239 21.2065515,10.6021724 21.5953605,10.6021724 C21.8626667,10.6021724 22.0935186,10.6993732 22.2879231,10.8937777 Z"
, SvgAttributes.fill "#146AFF"
]
[]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml
checkboxBackground attrs =
Svg.rect
([ SvgAttributes.x "0"
, SvgAttributes.y "0"
, SvgAttributes.width "27"
, SvgAttributes.height "27"
, SvgAttributes.rx "4"
]
++ attrs
)
[]
{-| -}
checkboxLockOnInside : String -> Svg
checkboxLockOnInside idSuffix =
let
filterId =
"filter-2" ++ idSuffix
filterUrl =
"url(#" ++ filterId ++ ")"
in
Svg.svg
[ SvgAttributes.width "27px"
, SvgAttributes.height "27px"
, SvgAttributes.viewBox "0 0 27 27"
]
[ Svg.defs []
[ Svg.filter
[ SvgAttributes.x "-3.7%"
, SvgAttributes.y "-3.7%"
, SvgAttributes.width "107.4%"
, SvgAttributes.height "107.4%"
, SvgAttributes.filterUnits "objectBoundingBox"
, SvgAttributes.id filterId
]
[ Svg.feOffset
[ SvgAttributes.dx "0"
, SvgAttributes.dy "2"
, SvgAttributes.in_ "SourceAlpha"
, SvgAttributes.result "shadowOffsetInner1"
]
[]
, Svg.feComposite
[ SvgAttributes.in_ "shadowOffsetInner1"
, SvgAttributes.in2 "SourceAlpha"
, SvgAttributes.operator "arithmetic"
, SvgAttributes.k2 "-1"
, SvgAttributes.k3 "1"
, SvgAttributes.result "shadowInnerInner1"
]
[]
, Svg.feColorMatrix
[ SvgAttributes.values "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
, SvgAttributes.in_ "shadowInnerInner1"
]
[]
]
]
, Svg.g
[ SvgAttributes.stroke "none"
, SvgAttributes.strokeWidth "1"
, SvgAttributes.fill "none"
, SvgAttributes.fillRule "evenodd"
]
[ Svg.g
[]
[ Svg.g
[]
[ checkboxBackground
[ SvgAttributes.fill "#EBEBEB"
, SvgAttributes.fillRule "evenodd"
]
, checkboxBackground
[ SvgAttributes.fill "black"
, SvgAttributes.fillOpacity "1"
, SvgAttributes.filter filterUrl
]
]
, Svg.g
[ SvgAttributes.transform "translate(4.050000, 4.050000)"
]
[ Svg.g
[ SvgAttributes.transform "translate(3.040000, 0.271429)"
]
[ Svg.path
[ SvgAttributes.d "M10.8889406,8.4420254 L10.8889406,5.41406583 C10.8889406,2.93752663 8.90203465,0.922857143 6.46010875,0.922857143 C4.01774785,0.922857143 2.03105941,2.93752663 2.03105941,5.41406583 L2.03105941,8.4420254 L1.39812057,8.4420254 C0.626196192,8.4420254 0,9.0763794 0,9.85917577 L0,17.0399925 C0,17.8227889 0.626196192,18.4571429 1.39812057,18.4571429 L11.5223144,18.4571429 C12.2942388,18.4571429 12.92,17.8227889 12.92,17.0399925 L12.92,9.85939634 C12.92,9.07659997 12.2942388,8.4420254 11.5223144,8.4420254 L10.8889406,8.4420254 Z M6.8875056,13.8949112 L6.8875056,15.5789491 C6.8875056,15.8187066 6.69588391,16.0128066 6.46010875,16.0128066 C6.22389859,16.0128066 6.0322769,15.8187066 6.0322769,15.5789491 L6.0322769,13.8949112 C5.54876383,13.7173539 5.20271376,13.2490877 5.20271376,12.6972262 C5.20271376,11.9933932 5.76561607,11.4221217 6.46010875,11.4221217 C7.15394892,11.4221217 7.71772125,11.9933932 7.71772125,12.6972262 C7.71772125,13.2497494 7.37101867,13.7180156 6.8875056,13.8949112 L6.8875056,13.8949112 Z M9.21176142,8.4420254 L3.70823858,8.4420254 L3.70823858,5.41406583 C3.70823858,3.87538241 4.94279558,2.62343759 6.46010875,2.62343759 C7.97720442,2.62343759 9.21176142,3.87538241 9.21176142,5.41406583 L9.21176142,8.4420254 L9.21176142,8.4420254 Z"
, SvgAttributes.fill "#E68900"
]
[]
, Svg.rect
[ SvgAttributes.fill "#FFFFFF"
, SvgAttributes.x "0.922857143"
, SvgAttributes.y "10.1514286"
, SvgAttributes.width "10.1514286"
, SvgAttributes.height "5.53714286"
]
[]
, Svg.path
[ SvgAttributes.d "M10.8889406,7.51916826 L10.8889406,4.49120869 C10.8889406,2.01466949 8.90203465,0 6.46010875,0 C4.01774785,0 2.03105941,2.01466949 2.03105941,4.49120869 L2.03105941,7.51916826 L1.39812057,7.51916826 C0.626196192,7.51916826 0,8.15352226 0,8.93631863 L0,16.1171353 C0,16.8999317 0.626196192,17.5342857 1.39812057,17.5342857 L11.5223144,17.5342857 C12.2942388,17.5342857 12.92,16.8999317 12.92,16.1171353 L12.92,8.9365392 C12.92,8.15374283 12.2942388,7.51916826 11.5223144,7.51916826 L10.8889406,7.51916826 Z M6.8875056,12.9720541 L6.8875056,14.6560919 C6.8875056,14.8958495 6.69588391,15.0899495 6.46010875,15.0899495 C6.22389859,15.0899495 6.0322769,14.8958495 6.0322769,14.6560919 L6.0322769,12.9720541 C5.54876383,12.7944967 5.20271376,12.3262305 5.20271376,11.774369 C5.20271376,11.0705361 5.76561607,10.4992645 6.46010875,10.4992645 C7.15394892,10.4992645 7.71772125,11.0705361 7.71772125,11.774369 C7.71772125,12.3268922 7.37101867,12.7951584 6.8875056,12.9720541 L6.8875056,12.9720541 Z M9.21176142,7.51916826 L3.70823858,7.51916826 L3.70823858,4.49120869 C3.70823858,2.95252527 4.94279558,1.70058044 6.46010875,1.70058044 C7.97720442,1.70058044 9.21176142,2.95252527 9.21176142,4.49120869 L9.21176142,7.51916826 L9.21176142,7.51916826 Z"
, SvgAttributes.fill "#FEC900"
]
[]
]
]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml
checkboxLockOnInside =
CheckboxIcons.lockOnInside

View File

@ -63,7 +63,7 @@ In practice, we don't use these sizes. Remove them!
-}
import Accessibility.Styled.Widget as Widget
import Accessibility.Styled.Aria as Aria
import ClickableAttributes exposing (ClickableAttributes)
import Css exposing (Color, Style)
import Css.Media
@ -482,7 +482,7 @@ renderButton ((ButtonOrLink config) as button_) =
, Attributes.type_ "button"
, Attributes.css (buttonOrLinkStyles config theme ++ config.customStyles)
, Attributes.disabled config.disabled
, Widget.label config.label
, Aria.label config.label
]
++ ClickableAttributes.toButtonAttributes config.clickableAttributes
++ config.customAttributes
@ -491,20 +491,15 @@ renderButton ((ButtonOrLink config) as button_) =
]
type Link
= Default
| WithTracking
| SinglePageApp
| WithMethod String
| External
| ExternalWithTracking
renderLink : ButtonOrLink msg -> Html msg
renderLink ((ButtonOrLink config) as link_) =
let
( linkFunctionName, extraAttrs ) =
ClickableAttributes.toLinkAttributes identity config.clickableAttributes
ClickableAttributes.toLinkAttributes
{ routeToString = identity
, isDisabled = config.disabled
}
config.clickableAttributes
theme =
if config.disabled then
@ -516,8 +511,8 @@ renderLink ((ButtonOrLink config) as link_) =
Html.a
([ Attributes.class ("Nri-Ui-Clickable-Svg-" ++ linkFunctionName)
, Attributes.css (buttonOrLinkStyles config theme ++ config.customStyles)
, Widget.disabled config.disabled
, Widget.label config.label
, Aria.disabled config.disabled
, Aria.label config.label
]
++ (if not config.disabled then
extraAttrs

View File

@ -365,7 +365,11 @@ link label_ attributes =
|> List.foldl (\(Attribute attribute) l -> attribute l) defaults
( name, clickableAttributes ) =
ClickableAttributes.toLinkAttributes identity config.clickableAttributes
ClickableAttributes.toLinkAttributes
{ routeToString = identity
, isDisabled = False
}
config.clickableAttributes
in
Nri.Ui.styled Html.a
(dataDescriptor name)

View File

@ -1,6 +1,7 @@
module Nri.Ui.Colors.Extra exposing
( toCssColor, fromCssColor
, withAlpha
, toCssString
)
{-| Helpers for working with colors.
@ -10,6 +11,7 @@ module Nri.Ui.Colors.Extra exposing
@docs toCssColor, fromCssColor
@docs withAlpha
@docs toCssString
-}
@ -44,3 +46,9 @@ withAlpha 0.5 grassland -- "{ value = "rgba(86, 191, 116, 0.5)", color = Compati
withAlpha : Float -> Css.Color -> Css.Color
withAlpha alpha { red, green, blue } =
Css.rgba red green blue alpha
{-| -}
toCssString : Css.Color -> String
toCssString =
SolidColor.toRGBString << fromCssColor

View File

@ -57,7 +57,7 @@ consider [elm-color-extra](http://package.elm-lang.org/packages/eskimoblood/elm-
-}
import Css exposing (hex, rgba)
import Css exposing (hex)
import Nri.Ui.Colors.Extra exposing (withAlpha)
@ -156,7 +156,7 @@ gray20 =
-}
gray45 : Css.Color
gray45 =
hex "#727272"
hex "#707070"
{-| See <https://noredink-ui.netlify.com/#category/Colors>

View File

@ -44,7 +44,8 @@ type Confetti
}
{-| -}
{-| `center` An argument to Particle.withLocation that determines the horizontal center of viewport where you would like confetti to rain.
-}
init : Float -> Model
init center =
System (ParticleSystem.init (Random.initialSeed 0)) center

View File

@ -1,6 +1,8 @@
module Nri.Ui.Container.V2 exposing
( view, Attribute
, paddingPx, custom, css, testId, id
, custom, testId, id
, css, notMobileCss, mobileCss, quizEngineMobileCss
, paddingPx
, plaintext, markdown, html
, gray, default, disabled, invalid, pillow, buttony
)
@ -11,6 +13,12 @@ module Nri.Ui.Container.V2 exposing
# Changelog
## Patch changes
- use `Shadows`
- add notMobileCss, mobileCss, quizEngineMobileCss
## Changes from V1
- removes fullHeight
@ -31,7 +39,10 @@ module Nri.Ui.Container.V2 exposing
## View
@docs view, Attribute
@docs paddingPx, custom, css, testId, id
@docs custom, testId, id
@docs css, notMobileCss, mobileCss, quizEngineMobileCss
@docs paddingPx
## Content
@ -53,8 +64,8 @@ import Markdown
import Nri.Ui
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Html.Attributes.V2 as ExtraAttributes
import Nri.Ui.MediaQuery.V1 exposing (mobile)
import Nri.Ui.Text.V6 as Text
import Nri.Ui.MediaQuery.V1 as MediaQuery
import Nri.Ui.Shadows.V1 as Shadows
{-| -}
@ -115,6 +126,45 @@ css css_ =
Attribute <| \config -> { config | css = config.css ++ css_ }
{-| Set styles that will only apply if the viewport is wider than NRI's mobile breakpoint.
Equivalent to:
Container.css
[ Css.Media.withMedia [ Nri.Ui.MediaQuery.V1.notMobile ] styles ]
-}
notMobileCss : List Style -> Attribute msg
notMobileCss styles =
css [ Css.Media.withMedia [ MediaQuery.notMobile ] styles ]
{-| Set styles that will only apply if the viewport is narrower than NRI's mobile breakpoint.
Equivalent to:
Container.css
[ Css.Media.withMedia [ Nri.Ui.MediaQuery.V1.mobile ] styles ]
-}
mobileCss : List Style -> Attribute msg
mobileCss styles =
css [ Css.Media.withMedia [ MediaQuery.mobile ] styles ]
{-| Set styles that will only apply if the viewport is narrower than NRI's quiz-engine-specific mobile breakpoint.
Equivalent to:
Container.css
[ Css.Media.withMedia [ Nri.Ui.MediaQuery.V1.quizEngineMobile ] styles ]
-}
quizEngineMobileCss : List Style -> Attribute msg
quizEngineMobileCss styles =
css [ Css.Media.withMedia [ MediaQuery.quizEngineMobile ] styles ]
{-| -}
view : List (Attribute msg) -> Html msg
view attributes =
@ -153,7 +203,7 @@ defaultStyles : List Css.Style
defaultStyles =
[ borderRadius (px 8)
, border3 (px 1) solid Colors.gray92
, property "box-shadow" "0 0.8px 0.7px hsl(0deg 0% 0% / 0.1), 0 1px 1px -1.2px hsl(0deg 0% 0% / 0.1), 0 5px 2.5px -2.5px hsl(0deg 0% 0% / 0.1);"
, Shadows.low
, backgroundColor Colors.white
]
@ -237,9 +287,9 @@ pillowStyles : List Style
pillowStyles =
[ borderRadius (px 20)
, border3 (px 1) solid Colors.gray92
, property "box-shadow" "0 0.5px 0.7px hsl(0deg 0% 0% / 0.075), 0 1.6px 2px -0.8px hsl(0deg 0% 0% / 0.075), 0 4.1px 5.2px -1.7px hsl(0deg 0% 0% / 0.075), 5px 10px 12.6px -2.5px hsl(0deg 0% 0% / 0.075);"
, Shadows.medium
, backgroundColor Colors.white
, withMedia [ mobile ]
, withMedia [ MediaQuery.mobile ]
[ borderRadius (px 8)
, padding (px 20)
]
@ -264,7 +314,7 @@ buttonyStyles =
, border3 (px 1) solid Colors.gray85
, borderBottom3 (px 4) solid Colors.gray85
, backgroundColor Colors.white
, withMedia [ mobile ]
, withMedia [ MediaQuery.mobile ]
[ borderRadius (px 8)
]
]

View File

@ -1,269 +0,0 @@
module Nri.Ui.CssFlexBoxWithVendorPrefix exposing
( displayFlex, displayInlineFlex, flexDirection, justifyContent, alignItems, alignSelf, flexBasis
, flexGrow, flexShrink, row, rowReverse, column, columnReverse, flexStart, flexEnd, baseline, stretch, center, spaceBetween, spaceAround, flexWrap, nowrap, wrap, wrapReverse
)
{-|
@docs displayFlex, displayInlineFlex, flexDirection, justifyContent, alignItems, alignSelf, flexBasis
@docs flexGrow, flexShrink, row, rowReverse, column, columnReverse, flexStart, flexEnd, baseline, stretch, center, spaceBetween, spaceAround, flexWrap, nowrap, wrap, wrapReverse
-}
import Css exposing (Style, batch, property)
{-| -}
displayFlex : Style
displayFlex =
batch
[ property "display" "-webkit-box" -- OLD - iOS 6-, Safari 3.1-6
, property "display" "-moz-box" -- OLD - Firefox 19- (buggy but mostly works)
, property "display" "-ms-flexbox" -- TWEENER - IE 10
, property "display" "-webkit-flex" -- NEW - Chrome
, property "display" "flex" -- NEW, Spec - Opera 12.1, Firefox 20+
]
{-| -}
displayInlineFlex : Style
displayInlineFlex =
batch
[ property "display" "-webkit-inline-box" -- OLD - iOS 6-, Safari 3.1-6
, property "display" "-moz-inline-box" -- OLD - Firefox 19- (buggy but mostly works)
, property "display" "-ms-inline-flexbox" -- TWEENER - IE 10
, property "display" "-webkit-inline-flex" -- NEW - Chrome
, property "display" "inline-flex" -- NEW, Spec - Opera 12.1, Firefox 20+
]
{-| -}
flexDirection : Direction -> Style
flexDirection direction =
addPrefix "flex-direction" <|
case direction of
Row ->
"row"
RowReverse ->
"row-reverse"
Column ->
"column"
ColumnReverse ->
"column-reverse"
type Direction
= Row
| RowReverse
| Column
| ColumnReverse
{-| Direction row.
-}
row : Direction
row =
Row
{-| Direction rowReverse.
-}
rowReverse : Direction
rowReverse =
RowReverse
{-| Direction column.
-}
column : Direction
column =
Column
{-| Direction columnReverse.
-}
columnReverse : Direction
columnReverse =
ColumnReverse
{-| -}
justifyContent : Alignment JustifyContent a -> Style
justifyContent =
addPrefix "justify-content" << alignmentToString
{-| -}
alignItems : Alignment a AlignItems -> Style
alignItems =
addPrefix "align-items" << alignmentToString
{-| -}
alignSelf : Alignment a AlignItems -> Style
alignSelf =
addPrefix "align-self" << alignmentToString
{-| -}
flexBasis : Css.Length compatible units -> Style
flexBasis =
addPrefix "flex-basis" << .value
{-| -}
flexGrow : Float -> Style
flexGrow value =
addPrefix "flex-grow" (toString value)
{-| -}
flexShrink : Float -> Style
flexShrink value =
addPrefix "flex-shrink" (toString value)
{-| -}
flexWrap : Wrap -> Style
flexWrap value =
addPrefix "flex-wrap" <|
case value of
Nowrap ->
"nowrap"
Wrap ->
"wrap"
WrapReverse ->
"wrap-reverse"
type Wrap
= Nowrap
| Wrap
| WrapReverse
{-| flex-wrap nowrap
-}
nowrap : Wrap
nowrap =
Nowrap
{-| flex-wrap wrap
-}
wrap : Wrap
wrap =
Wrap
{-| flex-wrap wrapReverse
-}
wrapReverse : Wrap
wrapReverse =
WrapReverse
type Alignment justify align
= FlexStart justify align
| FlexEnd justify align
| Center justify align
| SpaceBetween justify
| SpaceAround justify
| Baseline align
| Stretch align
alignmentToString : Alignment a b -> String
alignmentToString value =
case value of
FlexStart _ _ ->
"flex-start"
FlexEnd _ _ ->
"flex-end"
Center _ _ ->
"center"
SpaceBetween _ ->
"space-between"
SpaceAround _ ->
"space-around"
Baseline _ ->
"baseline"
Stretch _ ->
"stretch"
type JustifyContent
= JustifyContent
type AlignItems
= AlignItems
{-| align-items/justify-content flexStart
-}
flexStart : Alignment JustifyContent AlignItems
flexStart =
FlexStart JustifyContent AlignItems
{-| align-items/justify-content flexEnd
-}
flexEnd : Alignment JustifyContent AlignItems
flexEnd =
FlexEnd JustifyContent AlignItems
{-| align-items/justify-content center
-}
center : Alignment JustifyContent AlignItems
center =
Center JustifyContent AlignItems
{-| justify-content spaceBetween
-}
spaceBetween : Alignment JustifyContent Never
spaceBetween =
SpaceBetween JustifyContent
{-| justify-content spaceAround
-}
spaceAround : Alignment JustifyContent Never
spaceAround =
SpaceAround JustifyContent
{-| align-items baseline
-}
baseline : Alignment Never AlignItems
baseline =
Baseline AlignItems
{-| align-items stretch
-}
stretch : Alignment Never AlignItems
stretch =
Stretch AlignItems
addPrefix : String -> String -> Style
addPrefix propertyName value =
batch
[ property ("-webkit-" ++ propertyName) value
, property propertyName value
, property ("-ms-" ++ propertyName) value
]

View File

@ -1,53 +0,0 @@
module Nri.Ui.Data.PremiumLevel exposing (PremiumLevel(..), allowedFor, highest, lowest)
{-|
@docs PremiumLevel, allowedFor, highest, lowest
-}
{-| -}
type PremiumLevel
= Free
| Premium
| PremiumWithWriting
{-| Is content of the required premium level accessbile by the actor?
-}
allowedFor : PremiumLevel -> PremiumLevel -> Bool
allowedFor requirement actor =
order requirement <= order actor
{-| The highest premium level in a list
-}
highest : List PremiumLevel -> Maybe PremiumLevel
highest privileges =
privileges
|> List.sortBy order
|> List.reverse
|> List.head
{-| The lowest premium level in a list
-}
lowest : List PremiumLevel -> Maybe PremiumLevel
lowest privileges =
privileges
|> List.sortBy order
|> List.head
order : PremiumLevel -> Int
order privileges =
case privileges of
PremiumWithWriting ->
2
Premium ->
1
Free ->
0

View File

@ -1,35 +0,0 @@
module Nri.Ui.DatePickerConstants exposing
( datePickerTag
, dialogTag
, footerTag
)
{-|
@docs datePickerTag
@docs dialogTag
@docs footerTag
-}
{-| The class of the entire date picker
-}
datePickerTag : String
datePickerTag =
"date-time-picker"
{-| The class of just the dialog that shows up when you open the datepicker
-}
dialogTag : String
dialogTag =
"date-time-picker-dialog"
{-| The class of the footer in the dialog.
This is where the pretty-printed date is displayed.
-}
footerTag : String
footerTag =
"date-time-picker-footer"

View File

@ -20,7 +20,7 @@ A caret that indicates that a section can expand and collapse. When `isOpen` is
-}
import Css exposing (..)
import Html.Styled as Html exposing (..)
import Html.Styled exposing (..)
import Html.Styled.Attributes exposing (css)
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.SpriteSheet exposing (arrowLeft)

View File

@ -6,14 +6,8 @@ module Nri.Ui.FocusTrap.V1 exposing (FocusTrap, toAttribute)
-}
import Accessibility.Styled as Html exposing (..)
import Accessibility.Styled.Key as Key
import Browser.Dom as Dom
import Browser.Events
import Html.Styled.Attributes as Attributes exposing (class, id)
import Html.Styled.Events as Events exposing (onClick)
import Json.Decode as Decode exposing (Decoder)
import Task
import Accessibility.Styled as Html
import Nri.Ui.WhenFocusLeaves.V1 as WhenFocusLeaves
{-| Defines how focus will wrap in reponse to tab keypresses in a part of the UI.
@ -33,53 +27,13 @@ type alias FocusTrap msg =
-}
toAttribute : FocusTrap msg -> Html.Attribute msg
toAttribute { firstId, lastId, focus } =
onTab <|
\elementId shiftKey ->
-- if the user tabs back while on the first id,
-- we want to wrap around to the last id.
if elementId == firstId && shiftKey then
Decode.succeed
{ message = focus lastId
, preventDefault = True
, stopPropagation = False
}
else if elementId == lastId && not shiftKey then
-- if the user tabs forward while on the last id,
-- we want to wrap around to the first id.
Decode.succeed
{ message = focus firstId
, preventDefault = True
, stopPropagation = False
}
else
Decode.fail "No need to intercept the key press"
onTab :
(String
-> Bool
-> Decoder { message : msg, preventDefault : Bool, stopPropagation : Bool }
)
-> Html.Attribute msg
onTab do =
Events.custom "keydown"
(Decode.andThen
(\( id, keyCode, shiftKey ) ->
if keyCode == 9 then
do id shiftKey
else
Decode.fail "No need to intercept the key press"
)
decodeKeydown
)
decodeKeydown : Decoder ( String, Int, Bool )
decodeKeydown =
Decode.map3 (\id keyCode shiftKey -> ( id, keyCode, shiftKey ))
(Decode.at [ "target", "id" ] Decode.string)
(Decode.field "keyCode" Decode.int)
(Decode.field "shiftKey" Decode.bool)
WhenFocusLeaves.toAttribute
{ firstId = firstId
, lastId = lastId
, -- if the user tabs back while on the first id,
-- we want to wrap around to the last id.
tabBackAction = focus lastId
, -- if the user tabs forward while on the last id,
-- we want to wrap around to the first id.
tabForwardAction = focus firstId
}

View File

@ -25,7 +25,7 @@ Headings with customization options for accessibility.
import Css exposing (..)
import Html.Styled exposing (..)
import Html.Styled.Attributes as Attributes exposing (css)
import Html.Styled.Attributes as Attributes
import Nri.Ui.Colors.V1 exposing (..)
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as ExtraAttributes

View File

@ -10,9 +10,7 @@ module Nri.Ui.Html.V3 exposing
-}
import Char
import Html.Styled as Html exposing (Attribute, Html, span, text)
import Html.Styled.Attributes exposing (..)
import Html.Styled as Html exposing (Attribute, Html)
import Html.Styled.Events as Events exposing (..)
import Json.Decode

View File

@ -10,14 +10,14 @@ module Nri.Ui.Loading.V1 exposing
-}
import Css exposing (..)
import Css
import Css.Animations
import Html.Styled as Html exposing (Html)
import Html.Styled.Attributes as Attributes
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Svg.V1
import Nri.Ui.UiIcon.V1 as UiIcon
import Svg.Styled as Svg exposing (Svg)
import Svg.Styled as Svg
import Svg.Styled.Attributes as SvgAttributes

View File

@ -3,7 +3,9 @@ module Nri.Ui.Logo.V1 exposing
, clever, cleverC, cleverLibrary
, googleClassroom, googleG
, canvas
, canvasCircle
, schoology
, schoologyCircle
, facebook, twitter
)
@ -13,7 +15,9 @@ module Nri.Ui.Logo.V1 exposing
@docs clever, cleverC, cleverLibrary
@docs googleClassroom, googleG
@docs canvas
@docs canvasCircle
@docs schoology
@docs schoologyCircle
@docs facebook, twitter
-}
@ -260,6 +264,23 @@ canvas =
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
canvasCircle : Nri.Ui.Svg.V1.Svg
canvasCircle =
Svg.svg
[ Attributes.viewBox "0 0 200 200"
, Attributes.width "100%"
, Attributes.height "100%"
]
[ Svg.path
[ Attributes.fill "#D64027"
, Attributes.d "M29.2 100c0-14.9-11.2-26.9-25.5-28.4C1.5 80.6 0 89.6 0 100s1.5 19.4 3.7 28.4C18 126.9 29.2 114.2 29.2 100L29.2 100zM46.4 90.3c5 0 9 4 9 9s-4 9-9 9 -9-4-9-9S41.5 90.3 46.4 90.3zM170.8 100c0 14.9 11.2 26.9 25.5 28.4 2.2-9 3.7-18.7 3.7-28.4s-1.5-19.4-3.7-28.4C182 73.1 170.8 85.1 170.8 100L170.8 100zM151.3 90.3c5 0 9 4 9 9s-4 9-9 9c-5 0-9-4-9-9S146.3 90.3 151.3 90.3zM99.6 170.9c-15 0-27 11.2-28.5 25.4 9 2.2 18.7 3.7 28.5 3.7s19.5-1.5 28.5-3.7C126.6 182.1 114.6 170.9 99.6 170.9L99.6 170.9zM98.9 142.5c5 0 9 4 9 9 0 4.9-4 9-9 9 -5 0-9-4-9-9C89.9 146.5 93.9 142.5 98.9 142.5zM99.6 29.1c15 0 27-11.2 28.5-25.4 -9-2.2-18.7-3.7-28.5-3.7S80.1 1.5 71.2 3.7C72.7 17.9 84.6 29.1 99.6 29.1L99.6 29.1zM98.9 38.1c5 0 9 4 9 9s-4 9-9 9c-5 0-9-4-9-9S93.9 38.1 98.9 38.1zM149.8 150c-10.5 10.4-11.2 26.9-2.2 38.1 16.5-9.7 30.7-23.9 40.4-40.3C176.8 138.8 160.3 139.6 149.8 150L149.8 150zM136.3 127.6c5 0 9 4 9 9 0 4.9-4 9-9 9 -5 0-9-4-9-9C127.3 131.6 131.4 127.6 136.3 127.6zM49.4 50c10.5-10.4 11.2-26.9 2.2-38.1C35.2 21.6 21 35.8 11.2 52.2 22.5 61.2 39 60.4 49.4 50L49.4 50zM61.4 53c5 0 9 4 9 9s-4 9-9 9 -9-4-9-9S56.5 53 61.4 53zM149.8 50c10.5 10.4 27 11.2 38.2 2.2 -9.7-16.4-24-30.6-40.4-40.3C138.6 23.1 139.3 39.6 149.8 50L149.8 50zM136.3 53c5 0 9 4 9 9s-4 9-9 9c-5 0-9-4-9-9S131.4 53 136.3 53zM49.4 150c-10.5-10.4-27-11.2-38.2-2.2 9.7 16.4 24 30.6 40.4 40.3C60.7 176.1 59.9 160.4 49.4 150L49.4 150zM61.4 127.6c5 0 9 4 9 9 0 4.9-4 9-9 9s-9-4-9-9C52.4 131.6 56.5 127.6 61.4 127.6z"
]
[]
]
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
schoology : Nri.Ui.Svg.V1.Svg
schoology =
@ -285,3 +306,27 @@ schoology =
]
]
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
schoologyCircle : Nri.Ui.Svg.V1.Svg
schoologyCircle =
Svg.svg
[ Attributes.viewBox "0 0 163 163"
, Attributes.width "100%"
, Attributes.height "100%"
]
[ Svg.g []
[ Svg.path
[ Attributes.d "M81.5 163C36.6 163 0 126.4 0 81.5S36.6 0 81.5 0 163 36.6 163 81.5c0 45-36.5 81.5-81.5 81.5zm0-149.4c-37.5 0-68 30.5-68 68s30.5 68 68 68 68-30.5 68-68c0-37.6-30.5-68-68-68z"
, Attributes.fill "#50ade1"
]
[]
, Svg.path
[ Attributes.d "M78.2 86.3c-13.4-3.2-29.9-6.9-29.9-24.2 0-15.2 12.9-24.8 32.2-24.8 15.9 0 32.9 7.6 35.7 27.2l-15.2 2c-.2-5.1-.5-9.2-6-13.5-5.5-4.2-11.8-4.9-16.1-4.9-11 0-16.4 6.5-16.4 12.2 0 8 9 10.4 20 13.1l8 1.9c9.9 2.3 27.8 6.6 27.8 24.1 0 13.6-12 26.3-35 26.3-9.4 0-19.1-1.9-25.8-6.7-2.7-2-10.8-8.7-12.4-22.6L61 93.9c-.2 3.7-.2 10.8 6.2 16.3 4.9 4.2 11.2 4.8 16.8 4.8 12.4 0 19.6-4.8 19.6-13.8 0-9.5-7.2-11.3-17.3-13.3l-8.1-1.6z"
, Attributes.fill "#333"
]
[]
]
]
|> Nri.Ui.Svg.V1.fromHtml

View File

@ -3,6 +3,8 @@ module Nri.Ui.MediaQuery.V1 exposing
, mobileBreakpoint
, quizEngineMobile
, quizEngineBreakpoint
, narrowMobile
, narrowMobileBreakPoint
)
{-| Standard media queries for responsive pages.
@ -24,6 +26,9 @@ module Nri.Ui.MediaQuery.V1 exposing
@docs quizEngineMobile
@docs quizEngineBreakpoint
@docs narrowMobile
@docs narrowMobileBreakPoint
-}
import Css exposing (px)
@ -73,3 +78,22 @@ quizEngineMobile =
quizEngineBreakpoint : Css.Px
quizEngineBreakpoint =
px 750
{-| Styles using the `narrowMobileBreakPoint` value as the maxWidth
-}
narrowMobile : MediaQuery
narrowMobile =
only screen
[ --`minWidth (px 1)` is for a bug in IE which causes the media query to initially trigger regardless of window size
--See: <http://stackoverflow.com/questions/25673707/ie11-triggers-css-transition-on-page-load-when-non-applied-media-query-exists/25850649#25850649>
minWidth (px 1)
, maxWidth narrowMobileBreakPoint
]
{-| 500px
-}
narrowMobileBreakPoint : Css.Px
narrowMobileBreakPoint =
px 500

View File

@ -27,7 +27,6 @@ module Nri.Ui.Menu.V1 exposing
import Accessibility.Styled.Aria as Aria exposing (controls)
import Accessibility.Styled.Role as Role
import Accessibility.Styled.Widget as Widget
import Css exposing (..)
import Css.Global exposing (descendants)
import Html.Styled as Html exposing (..)
@ -169,10 +168,10 @@ view config =
]
, onClick <| config.onToggle (not config.isOpen)
, Attributes.disabled config.isDisabled
, Widget.disabled config.isDisabled
, Widget.hasMenuPopUp
, Widget.expanded config.isOpen
, controls menuId
, Aria.disabled config.isDisabled
, Aria.hasMenuPopUp
, Aria.expanded config.isOpen
, controls [ menuId ]
, Attributes.id buttonId
, config.buttonWidth
-- TODO: don't set this value as an inline style unnecessarily
@ -276,7 +275,7 @@ viewDropdown config =
, Role.menu
, Aria.labelledBy config.buttonId
, Attributes.id config.menuId
, Widget.hidden (not config.isOpen)
, Aria.hidden (not config.isOpen)
, config.menuWidth
-- TODO: don't set this style inline unnecessarily
|> Maybe.map (\w -> Attributes.style "width" (String.fromInt w ++ "px"))
@ -345,9 +344,9 @@ iconButton config =
]
:: buttonLinkResets
)
, Widget.disabled config.isDisabled
, Aria.disabled config.isDisabled
, Attributes.disabled config.isDisabled
, Widget.label config.label
, Aria.label config.label
]
++ perhapsOnclick
)
@ -470,9 +469,9 @@ iconLink config =
[]
)
)
, Widget.disabled config.isDisabled
, Aria.disabled config.isDisabled
, Attributes.id (String.Extra.dasherize config.label)
, Widget.label config.label
, Aria.label config.label
]
++ perhapsHref
)

View File

@ -1,673 +0,0 @@
module Nri.Ui.Menu.V2 exposing
( view, Alignment(..), TitleWrapping(..)
, viewCustom
, Entry, group, entry
)
{-| Changes from V1:
- Improves keyboard support
- replaces `onToggle` with `focusAndToggle`
- remove `iconButton` and `iconLink` (use ClickableSvg instead)
- replace iconButtonWithMenu helper with a custom helper
- explicitly pass in a buttonId and a menuId (instead of the container id)
- when wrapping the menu title, use the title in the description rather than an HTML id string
- separate out the data for the viewCustom menu and the data used to make a nice looking shared default button
- remove None
- change the Single API to allow passing-in of the id and passing-back of the menu item attributes
A togglable menu view and related buttons.
<https://zpl.io/a75OrE2>
## Menu buttons
@docs view, Alignment, TitleWrapping
@docs viewCustom
## Menu content
@docs Entry, group, entry
-}
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Key as Key
import Accessibility.Styled.Role as Role
import Accessibility.Styled.Widget as Widget
import Css exposing (..)
import Css.Global exposing (descendants)
import Html.Styled as Html exposing (..)
import Html.Styled.Attributes as Attributes exposing (class, classList, css)
import Html.Styled.Events as Events
import Json.Decode
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Fonts.V1
import Nri.Ui.Html.Attributes.V2 as AttributesExtra
import Nri.Ui.Html.V3 exposing (viewJust)
import Nri.Ui.Svg.V1 as Svg exposing (Svg)
import Nri.Ui.UiIcon.V1 as UiIcon
import String.Extra
-- ENTRIES
{-| Represents zero or more entries within the menu content
-}
type Entry msg
= Single String (List (Attribute msg) -> Html msg)
| Batch String (List (Entry msg))
{-| Represents a group of entries with a named legend.
-}
group : String -> List (Entry msg) -> Entry msg
group legendName entries =
Batch legendName entries
{-| Represents a single **focusable** entry.
Pass in the id you'd like for your menu item, which will be used to manage the focus.
Menu.entry "my-button-id"
(\attributes -> Button.button "One great button" [ Button.custom attributes ])
-}
entry : String -> (List (Attribute msg) -> Html msg) -> Entry msg
entry id =
Single id
{-| Determines how we deal with long titles. Should we make the menu expand in
height to show the full title or truncate it instead?
-}
type TitleWrapping
= WrapAndExpandTitle
| TruncateTitle
{-| Whether the menu content sticks to the left or right side of the button
-}
type Alignment
= Left
| Right
{-| Menu/pulldown configuration:
- `entries`: the entries of the menu
- `focusAndToggle`: the message produced to control the open/closed state and DOM focus
- `alignment`: where the menu popover should appear relative to the button
- `isDisabled`: whether the menu can be openned
- `menuWidth` : optionally fix the width of the popover
- `buttonId`: a unique string identifier for the button that opens/closes the menu
- `menuId`: a unique string identifier for the menu
Button configuration:
- `icon`: display a particular icon to the left of the title
- `title`: the text to display in the menu button
- `wrapping`: WrapAndExpandTitle | TruncateTitle
- `hasBorder`: whether the menu button has a border
- `buttonWidth`: optionally fix the width of the button to a number of pixels
-}
view :
{ entries : List (Entry msg)
, isOpen : Bool
, focusAndToggle : { isOpen : Bool, focus : Maybe String } -> msg
, alignment : Alignment
, isDisabled : Bool
, menuWidth : Maybe Int
, buttonId : String
, menuId : String
}
->
{ icon : Maybe Svg.Svg
, title : String
, wrapping : TitleWrapping
, hasBorder : Bool
, buttonWidth : Maybe Int
}
-> Html msg
view menuConfig buttonConfig =
viewCustom menuConfig <|
\buttonAttributes ->
Html.button
([ classList [ ( "ToggleButton", True ), ( "WithBorder", buttonConfig.hasBorder ) ]
, css
[ Nri.Ui.Fonts.V1.baseFont
, fontSize (px 15)
, backgroundColor Colors.white
, border zero
, padding (px 4)
, textAlign left
, height (pct 100)
, fontWeight (int 600)
, cursor pointer
, if menuConfig.isDisabled then
Css.batch
[ opacity (num 0.4)
, cursor notAllowed
]
else
Css.batch []
, Css.batch <|
if buttonConfig.hasBorder then
[ border3 (px 1) solid Colors.gray75
, borderBottom3 (px 3) solid Colors.gray75
, borderRadius (px 8)
, padding2 (px 10) (px 15)
]
else
[]
, Maybe.map (\w -> Css.width (Css.px (toFloat w))) buttonConfig.buttonWidth
|> Maybe.withDefault (Css.batch [])
]
]
++ buttonAttributes
)
[ div styleButtonInner
[ viewTitle { icon = buttonConfig.icon, wrapping = buttonConfig.wrapping, title = buttonConfig.title }
, viewArrow { isOpen = menuConfig.isOpen }
]
]
viewArrow : { isOpen : Bool } -> Html msg
viewArrow { isOpen } =
span
[ classList [ ( "Arrow", True ), ( "Open", isOpen ) ]
, css
[ width (px 12)
, height (px 7)
, marginLeft (px 5)
, color Colors.azure
, Css.flexShrink (Css.num 0)
, descendants
[ Css.Global.svg [ display block ]
]
, property "transform-origin" "center"
, property "transition" "transform 0.1s"
, if isOpen then
transform (rotate (deg 180))
else
Css.batch []
]
]
[ Svg.toHtml UiIcon.arrowDown ]
viewTitle :
{ icon : Maybe Svg.Svg
, wrapping : TitleWrapping
, title : String
}
-> Html msg
viewTitle config =
div styleTitle
[ viewJust (\icon -> span styleIconContainer [ Svg.toHtml icon ])
config.icon
, span
(case config.wrapping of
WrapAndExpandTitle ->
[ Attributes.attribute "data-nri-description" config.title ]
TruncateTitle ->
[ class "Truncated"
, css
[ whiteSpace noWrap
, overflow hidden
, textOverflow ellipsis
]
]
)
[ Html.text config.title ]
]
{-|
- `entries`: the entries of the menu
- `focusAndToggle`: the message produced to control the open/closed state and DOM focus
- `alignment`: where the menu popover should appear relative to the button
- `isDisabled`: whether the menu can be openned
- `menuWidth` : optionally fix the width of the popover
- `buttonId`: a unique string identifier for the button that opens/closes the menu
- `menuId`: a unique string identifier for the menu
-}
viewCustom :
{ entries : List (Entry msg)
, isOpen : Bool
, focusAndToggle : { isOpen : Bool, focus : Maybe String } -> msg
, alignment : Alignment
, isDisabled : Bool
, menuWidth : Maybe Int
, buttonId : String
, menuId : String
}
-> (List (Attribute msg) -> Html msg)
-> Html msg
viewCustom config content =
let
( maybeFirstFocusableElementId, maybeLastFocusableElementId ) =
( List.head (getFirstIds config.entries), List.head (getLastIds config.entries) )
contentVisible =
config.isOpen && not config.isDisabled
in
div
(Attributes.id (config.buttonId ++ "__container")
:: Key.onKeyDown
[ Key.escape
(config.focusAndToggle
{ isOpen = False
, focus = Just config.buttonId
}
)
, Key.tab
(config.focusAndToggle
{ isOpen = False
, focus = Nothing
}
)
, Key.tabBack
(config.focusAndToggle
{ isOpen = False
, focus = Nothing
}
)
]
:: styleContainer
)
[ if config.isOpen then
div
(Events.onClick
(config.focusAndToggle
{ isOpen = False
, focus = Nothing
}
)
:: class "Nri-Menu-Overlay"
:: styleOverlay
)
[]
else
Html.text ""
, div styleInnerContainer
[ content
[ Widget.disabled config.isDisabled
, Widget.hasMenuPopUp
, Widget.expanded config.isOpen
, -- Whether the menu is open or closed, move to the
-- first menu item if the "down" arrow is pressed
case ( maybeFirstFocusableElementId, maybeLastFocusableElementId ) of
( Just firstFocusableElementId, Just lastFocusableElementId ) ->
Key.onKeyDown
[ Key.down
(config.focusAndToggle
{ isOpen = True
, focus = Just firstFocusableElementId
}
)
, Key.up
(config.focusAndToggle
{ isOpen = True
, focus = Just lastFocusableElementId
}
)
]
_ ->
AttributesExtra.none
, Aria.controls config.menuId
, Attributes.id config.buttonId
, if config.isDisabled then
AttributesExtra.none
else
Events.custom "click"
(Json.Decode.succeed
{ preventDefault = True
, stopPropagation = True
, message =
config.focusAndToggle
{ isOpen = not config.isOpen
, focus = Nothing
}
}
)
]
, div
[ classList [ ( "Content", True ), ( "ContentVisible", contentVisible ) ]
, styleContent contentVisible config.alignment
, Role.menu
, Aria.labelledBy config.buttonId
, Attributes.id config.menuId
, Widget.hidden (not config.isOpen)
, css
[ Maybe.map (\w -> Css.width (Css.px (toFloat w))) config.menuWidth
|> Maybe.withDefault (Css.batch [])
]
]
(viewEntries
{ focusAndToggle = config.focusAndToggle
, previousId = Maybe.withDefault "" maybeLastFocusableElementId
, nextId = Maybe.withDefault "" maybeFirstFocusableElementId
}
config.entries
)
]
]
getFirstIds : List (Entry msg) -> List String
getFirstIds entries =
let
getIdString elem =
case elem of
Single idString _ ->
Just idString
Batch _ es ->
Maybe.andThen getIdString (List.head es)
in
List.filterMap getIdString entries
getLastIds : List (Entry msg) -> List String
getLastIds entries =
let
getIdString elem =
case elem of
Single idString _ ->
Just idString
Batch _ es ->
Maybe.andThen getIdString (List.head (List.reverse es))
in
List.filterMap getIdString (List.reverse entries)
viewEntries :
{ focusAndToggle : { isOpen : Bool, focus : Maybe String } -> msg
, previousId : String
, nextId : String
}
-> List (Entry msg)
-> List (Html msg)
viewEntries { previousId, nextId, focusAndToggle } entries =
let
firstIds =
getFirstIds entries
lastIds =
getLastIds entries
in
List.map3
(\e upId downId ->
viewEntry focusAndToggle
{ upId = upId
, downId = downId
, entry_ = e
}
)
entries
(previousId :: List.reverse lastIds)
(List.drop 1 firstIds ++ [ nextId ])
viewEntry :
({ isOpen : Bool, focus : Maybe String } -> msg)
-> { upId : String, downId : String, entry_ : Entry msg }
-> Html msg
viewEntry focusAndToggle { upId, downId, entry_ } =
case entry_ of
Single id view_ ->
div
[ class "MenuEntryContainer"
, css
[ padding2 (px 5) zero
, position relative
, firstChild
[ paddingTop zero ]
, lastChild
[ paddingBottom zero ]
]
]
[ view_
[ Role.menuItem
, Attributes.id id
, Key.tabbable False
, Key.onKeyDown
[ Key.up
(focusAndToggle
{ isOpen = True
, focus = Just upId
}
)
, Key.down
(focusAndToggle
{ isOpen = True
, focus = Just downId
}
)
]
]
]
Batch title childList ->
case childList of
[] ->
Html.text ""
_ ->
fieldset styleGroupContainer <|
legend styleGroupTitle
[ span styleGroupTitleText [ Html.text title ] ]
:: viewEntries
{ focusAndToggle = focusAndToggle
, previousId = upId
, nextId = downId
}
childList
-- STYLES
buttonLinkResets : List Style
buttonLinkResets =
[ boxSizing borderBox
, border zero
, padding zero
, margin zero
, backgroundColor transparent
, cursor pointer
, display inlineBlock
, verticalAlign middle
]
{-| -}
styleInnerContainer : List (Attribute msg)
styleInnerContainer =
[ class "InnerContainer"
, css [ position relative ]
]
styleOverlay : List (Attribute msg)
styleOverlay =
[ class "Overlay"
, css
[ position fixed
, width (pct 100)
, height (pct 100)
, left zero
, top zero
, zIndex (int 1)
]
]
styleTitle : List (Attribute msg)
styleTitle =
[ class "Title"
, css
[ width (pct 100)
, overflow hidden
, Css.displayFlex
, Css.alignItems Css.center
, color Colors.gray20
]
]
styleGroupTitle : List (Attribute msg)
styleGroupTitle =
[ class "GroupTitle"
, css
[ Nri.Ui.Fonts.V1.baseFont
, fontSize (px 12)
, color Colors.gray45
, margin zero
, padding2 (px 5) zero
, lineHeight initial
, borderBottom zero
, position relative
, before
[ property "content" "\"\""
, width (pct 100)
, backgroundColor Colors.gray75
, height (px 1)
, marginTop (px -1)
, display block
, top (pct 50)
, position absolute
]
]
]
styleGroupTitleText : List (Attribute msg)
styleGroupTitleText =
[ class "GroupTitleText"
, css
[ backgroundColor Colors.white
, marginLeft (px 22)
, padding2 zero (px 5)
, zIndex (int 2)
, position relative
]
]
styleGroupContainer : List (Attribute msg)
styleGroupContainer =
[ class "GroupContainer"
, css
[ margin zero
, padding zero
, paddingBottom (px 15)
, lastChild
[ paddingBottom zero ]
]
]
styleButtonInner : List (Attribute msg)
styleButtonInner =
[ class "ButtonInner"
, css
[ Css.displayFlex
, Css.justifyContent Css.spaceBetween
, Css.alignItems Css.center
]
]
styleIconContainer : List (Attribute msg)
styleIconContainer =
[ class "IconContainer"
, css
[ width (px 21)
, height (px 21)
, marginRight (px 5)
, display inlineBlock
, Css.flexShrink (Css.num 0)
, color Colors.azure
]
]
styleContent : Bool -> Alignment -> Attribute msg
styleContent contentVisible alignment =
css
[ padding (px 25)
, border3 (px 1) solid Colors.gray85
, minWidth (px 202)
, position absolute
, borderRadius (px 8)
, marginTop (px 10)
, zIndex (int 2)
, backgroundColor Colors.white
, listStyle Css.none
, Css.property "box-shadow" "0 0 10px 0 rgba(0,0,0,0.1)"
, zIndex (int 2)
, before
[ property "content" "\"\""
, position absolute
, top (px -12)
, border3 (px 6) solid transparent
, borderBottomColor Colors.gray85
]
, after
[ property "content" "\"\""
, position absolute
, top (px -10)
, zIndex (int 2)
, border3 (px 5) solid transparent
, borderBottomColor Colors.white
]
, case alignment of
Left ->
Css.batch
[ left zero
, before [ left (px 19) ]
, after [ left (px 20) ]
]
Right ->
Css.batch
[ right zero
, before [ right (px 19) ]
, after [ right (px 20) ]
]
, if contentVisible then
display block
else
display Css.none
]
styleContainer : List (Attribute msg)
styleContainer =
[ class "Container"
, css
[ position relative
, display inlineBlock
]
]

View File

@ -1,14 +1,18 @@
module Nri.Ui.Menu.V3 exposing
( view, button, custom, Config
, Attribute, Button, ButtonAttribute
, alignment, isDisabled, menuWidth, buttonId, menuId, menuZIndex
, alignment, isDisabled, menuWidth, buttonId, menuId, menuZIndex, opensOnHover
, Alignment(..)
, icon, wrapping, hasBorder, buttonWidth
, TitleWrapping(..)
, Entry, group, entry
)
{-| Changes from V2:
{-| Patch changes:
- Use `Shadows`
Changes from V2:
- Adpoted attribute pattern
- Added option to customize the z-index
@ -26,7 +30,7 @@ A togglable menu view and related buttons.
## Menu attributes
@docs alignment, isDisabled, menuWidth, buttonId, menuId, menuZIndex
@docs alignment, isDisabled, menuWidth, buttonId, menuId, menuZIndex, opensOnHover
@docs Alignment
@ -45,7 +49,6 @@ A togglable menu view and related buttons.
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Key as Key
import Accessibility.Styled.Role as Role
import Accessibility.Styled.Widget as Widget
import Css exposing (..)
import Css.Global exposing (descendants)
import EventExtras exposing (onKeyDownPreventDefault)
@ -57,6 +60,7 @@ import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Fonts.V1
import Nri.Ui.Html.Attributes.V2 as AttributesExtra
import Nri.Ui.Html.V3 exposing (viewJust)
import Nri.Ui.Shadows.V1 as Shadows
import Nri.Ui.Svg.V1 as Svg
import Nri.Ui.UiIcon.V1 as UiIcon
@ -100,6 +104,7 @@ type alias MenuConfig msg =
, buttonId : String
, menuId : String
, zIndex : Int
, opensOnHover : Bool
}
@ -189,6 +194,13 @@ menuZIndex value =
Attribute <| \config -> { config | zIndex = value }
{-| Whether the menu will be opened/closed by mouseEnter and mouseLeave interaction. Defaults to `False`.
-}
opensOnHover : Bool -> Attribute msg
opensOnHover value =
Attribute <| \config -> { config | opensOnHover = value }
{-| Menu/pulldown configuration:
- `attributes`: List of (attributes)[#menu-attributes] to apply to the menu.
@ -213,6 +225,7 @@ view attributes config =
, buttonId = ""
, menuId = ""
, zIndex = 1
, opensOnHover = False
}
menuConfig =
@ -438,12 +451,32 @@ viewCustom config =
else
Html.text ""
, div styleInnerContainer
, div
[ class "InnerContainer"
, css
[ position relative
, if config.isOpen then
zIndex (int <| config.zIndex + 1)
else
Css.batch []
]
, if not config.isDisabled && config.opensOnHover && config.isOpen then
Events.onMouseLeave
(config.focusAndToggle
{ isOpen = False
, focus = Nothing
}
)
else
AttributesExtra.none
]
[ let
buttonAttributes =
[ Widget.disabled config.isDisabled
, Widget.hasMenuPopUp
, Widget.expanded config.isOpen
[ Aria.disabled config.isDisabled
, Aria.hasMenuPopUp
, Aria.expanded config.isOpen
, -- Whether the menu is open or closed, move to the
-- first menu item if the "down" arrow is pressed
case ( maybeFirstFocusableElementId, maybeLastFocusableElementId ) of
@ -465,7 +498,7 @@ viewCustom config =
_ ->
AttributesExtra.none
, Aria.controls config.menuId
, Aria.controls [ config.menuId ]
, Attributes.id config.buttonId
, if config.isDisabled then
AttributesExtra.none
@ -482,6 +515,16 @@ viewCustom config =
}
}
)
, if not config.isDisabled && config.opensOnHover then
Events.onMouseEnter
(config.focusAndToggle
{ isOpen = True
, focus = Nothing
}
)
else
AttributesExtra.none
]
in
case config.button of
@ -490,25 +533,34 @@ viewCustom config =
CustomButton customButton ->
customButton buttonAttributes
, div
[ classList [ ( "Content", True ), ( "ContentVisible", contentVisible ) ]
, styleContent contentVisible config
, Role.menu
, Aria.labelledBy config.buttonId
, Attributes.id config.menuId
, Widget.hidden (not config.isOpen)
, css
[ Maybe.map (\w -> Css.width (Css.px (toFloat w))) config.menuWidth
|> Maybe.withDefault (Css.batch [])
, div [ styleOuterContent contentVisible config ]
[ div
[ AttributesExtra.nriDescription "menu-hover-bridge"
, css
[ Css.height (px 10)
]
]
[]
, div
[ classList [ ( "Content", True ), ( "ContentVisible", contentVisible ) ]
, styleContent contentVisible config
, Role.menu
, Aria.labelledBy config.buttonId
, Attributes.id config.menuId
, Aria.hidden (not config.isOpen)
, css
[ Maybe.map (\w -> Css.width (Css.px (toFloat w))) config.menuWidth
|> Maybe.withDefault (Css.batch [])
]
]
(viewEntries config
{ focusAndToggle = config.focusAndToggle
, previousId = Maybe.withDefault "" maybeLastFocusableElementId
, nextId = Maybe.withDefault "" maybeFirstFocusableElementId
}
config.entries
)
]
(viewEntries config
{ focusAndToggle = config.focusAndToggle
, previousId = Maybe.withDefault "" maybeLastFocusableElementId
, nextId = Maybe.withDefault "" maybeFirstFocusableElementId
}
config.entries
)
]
]
@ -633,14 +685,6 @@ viewEntry config focusAndToggle { upId, downId, entry_ } =
-- STYLES
{-| -}
styleInnerContainer : List (Html.Attribute msg)
styleInnerContainer =
[ class "InnerContainer"
, css [ position relative ]
]
styleOverlay : MenuConfig msg -> List (Html.Attribute msg)
styleOverlay config =
[ class "Overlay"
@ -745,30 +789,41 @@ styleIconContainer =
]
styleOuterContent : Bool -> MenuConfig msg -> Html.Attribute msg
styleOuterContent contentVisible config =
css
[ position absolute
, zIndex (int <| config.zIndex + 1)
, case config.alignment of
Left ->
left zero
Right ->
right zero
]
styleContent : Bool -> MenuConfig msg -> Html.Attribute msg
styleContent contentVisible config =
css
[ padding (px 25)
, border3 (px 1) solid Colors.gray85
, minWidth (px 202)
, position absolute
, borderRadius (px 8)
, marginTop (px 10)
, zIndex (int <| config.zIndex + 1)
, backgroundColor Colors.white
, listStyle Css.none
, Css.property "box-shadow" "0 1px 1px hsl(0deg 0% 0% / 0.075), 0 2px 2px hsl(0deg 0% 0% / 0.075), 0 4px 4px hsl(0deg 0% 0% / 0.075), 0 8px 8px hsl(0deg 0% 0% / 0.075), 0 16px 16px hsl(0deg 0% 0% / 0.075)"
, Shadows.high
, before
[ property "content" "\"\""
, position absolute
, top (px -12)
, top (px -2)
, border3 (px 6) solid transparent
, borderBottomColor Colors.gray85
]
, after
[ property "content" "\"\""
, position absolute
, top (px -10)
, top (px 1)
, zIndex (int 2)
, border3 (px 5) solid transparent
, borderBottomColor Colors.white
@ -776,15 +831,13 @@ styleContent contentVisible config =
, case config.alignment of
Left ->
Css.batch
[ left zero
, before [ left (px 19) ]
[ before [ left (px 19) ]
, after [ left (px 20) ]
]
Right ->
Css.batch
[ right zero
, before [ right (px 19) ]
[ before [ right (px 19) ]
, after [ right (px 20) ]
]
, if contentVisible then
@ -798,6 +851,7 @@ styleContent contentVisible config =
styleContainer : List (Html.Attribute msg)
styleContainer =
[ class "Container"
, AttributesExtra.nriDescription "Nri-Ui-Menu-V3"
, css
[ position relative
, display inlineBlock

View File

@ -15,6 +15,7 @@ module Nri.Ui.Message.V3 exposing
- adds `notMobileCss`, `mobileCss`, `quizEngineMobileCss`
- adds `hideIconForMobile` and `hideIconFor`
- use `Shadows`
Changes from V2:
@ -63,12 +64,10 @@ Changes from V2:
import Accessibility.Styled as Html exposing (..)
import Accessibility.Styled.Role as Role
import Accessibility.Styled.Style exposing (invisibleStyle)
import Accessibility.Styled.Widget as Widget
import Css exposing (..)
import Css.Global
import Css.Media exposing (MediaQuery)
import Html.Styled.Attributes as Attributes
import Html.Styled.Events exposing (onClick)
import Http
import Markdown
import Nri.Ui
@ -77,6 +76,7 @@ import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as ExtraAttributes
import Nri.Ui.MediaQuery.V1 as MediaQuery
import Nri.Ui.Shadows.V1 as Shadows
import Nri.Ui.Svg.V1 as NriSvg exposing (Svg)
import Nri.Ui.UiIcon.V1 as UiIcon
@ -178,7 +178,7 @@ view attributes_ =
borderRadius (px 8)
, padding (px 20)
, backgroundColor_
, property "box-shadow" "0 0.8px 0.7px hsl(0deg 0% 0% / 0.1), 0 1px 1px -1.2px hsl(0deg 0% 0% / 0.1), 0 5px 2.5px -2.5px hsl(0deg 0% 0% / 0.1);"
, Shadows.low
, Css.Media.withMedia
[ Css.Media.all [ Css.Media.maxWidth (px 1000) ] ]
[ padding (px 15)
@ -333,7 +333,7 @@ viewCodeDetails errorMessageForEngineers =
, code
[ Attributes.css
[ display block
, whiteSpace normal
, whiteSpace preWrap
, overflowWrap breakWord
, color Colors.gray45
, backgroundColor Colors.gray96
@ -712,7 +712,7 @@ getColor size theme =
Colors.redDark
_ ->
Colors.navy
Colors.redDark
Tip ->
Colors.navy
@ -731,7 +731,7 @@ getBackgroundColor size theme =
Css.backgroundColor Colors.sunshine
( Banner, Tip ) ->
Css.backgroundColor Colors.frost
Css.backgroundColor Colors.sunshine
( _, Error ) ->
Css.backgroundColor Colors.purpleLight
@ -779,7 +779,7 @@ getIcon customIcon size theme =
Colors.red
_ ->
Colors.ochre
Colors.red
in
UiIcon.exclamation
|> NriSvg.withColor color
@ -793,31 +793,55 @@ getIcon customIcon size theme =
( Nothing, Tip ) ->
case size of
Tiny ->
UiIcon.bulb
|> NriSvg.withColor Colors.yellow
|> NriSvg.withWidth iconSize
|> NriSvg.withHeight iconSize
|> NriSvg.withCss [ marginRight, Css.flexShrink Css.zero ]
|> NriSvg.withLabel "Tip"
|> NriSvg.withNriDescription messageIconDescription
|> NriSvg.toHtml
div
[ Attributes.css
[ borderRadius (pct 50)
, height (px 20)
, width (px 20)
, Css.marginRight (Css.px 5)
, backgroundColor Colors.navy
, displayFlex
, Css.flexShrink Css.zero
, alignItems center
, justifyContent center
]
, ExtraAttributes.nriDescription messageIconDescription
]
[ UiIcon.baldBulb
|> NriSvg.withColor Colors.mustard
|> NriSvg.withWidth (Css.px 13)
|> NriSvg.withHeight (Css.px 13)
|> NriSvg.toHtml
]
Large ->
UiIcon.bulb
|> NriSvg.withColor Colors.navy
|> NriSvg.withWidth iconSize
|> NriSvg.withHeight iconSize
|> NriSvg.withCss [ marginRight, Css.flexShrink Css.zero ]
|> NriSvg.withLabel "Tip"
|> NriSvg.withNriDescription messageIconDescription
|> NriSvg.toHtml
div
[ Attributes.css
[ borderRadius (pct 50)
, height (px 35)
, width (px 35)
, Css.marginRight (Css.px 10)
, backgroundColor Colors.navy
, displayFlex
, Css.flexShrink Css.zero
, alignItems center
, justifyContent center
]
, ExtraAttributes.nriDescription messageIconDescription
]
[ UiIcon.sparkleBulb
|> NriSvg.withColor Colors.mustard
|> NriSvg.withWidth (Css.px 22)
|> NriSvg.withHeight (Css.px 22)
|> NriSvg.toHtml
]
Banner ->
div
[ Attributes.css
[ borderRadius (pct 50)
, height (px 50)
, width (px 50)
, height (px 35)
, width (px 35)
, Css.marginRight (Css.px 10)
, backgroundColor Colors.navy
, displayFlex
@ -832,22 +856,16 @@ getIcon customIcon size theme =
]
, ExtraAttributes.nriDescription messageIconDescription
]
[ UiIcon.bulb
[ UiIcon.sparkleBulb
|> NriSvg.withColor Colors.mustard
|> NriSvg.withWidth (Css.px 32)
|> NriSvg.withHeight (Css.px 32)
|> NriSvg.withCss
[ Css.Media.withMedia
[ Css.Media.all [ Css.Media.maxWidth (px 1000) ] ]
[ height (px 20)
]
]
|> NriSvg.withWidth (Css.px 22)
|> NriSvg.withHeight (Css.px 22)
|> NriSvg.toHtml
]
( Nothing, Success ) ->
UiIcon.checkmarkInCircle
|> NriSvg.withColor Colors.green
|> NriSvg.withColor Colors.greenDark
|> NriSvg.withWidth iconSize
|> NriSvg.withHeight iconSize
|> NriSvg.withCss [ marginRight, Css.flexShrink Css.zero ]

View File

@ -14,8 +14,9 @@ module Nri.Ui.Modal.V11 exposing
# Patch changes:
- adds `testId` helper
- adds data-nri-descriptions to the header, content, and footer
- adds `testId` helper
- adds data-nri-descriptions to the header, content, and footer
- use `Shadows`
# Changes from V10:
@ -161,8 +162,6 @@ import Accessibility.Styled as Html exposing (..)
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Key as Key
import Accessibility.Styled.Role as Role
import Accessibility.Styled.Widget as Widget
import Browser
import Browser.Dom as Dom
import Browser.Events
import Css exposing (..)
@ -170,18 +169,17 @@ import Css.Media
import Css.Transitions
import Html.Styled as Root
import Html.Styled.Attributes as Attrs exposing (id)
import Html.Styled.Events as Events exposing (onClick)
import Json.Decode as Decode exposing (Decoder)
import Html.Styled.Events exposing (onClick)
import Nri.Ui.Colors.Extra
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.FocusTrap.V1 as FocusTrap exposing (FocusTrap)
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as ExtraAttributes
import Nri.Ui.MediaQuery.V1 exposing (mobile)
import Nri.Ui.Shadows.V1 as Shadows
import Nri.Ui.SpriteSheet
import Nri.Ui.Svg.V1
import Task
import TransparentColor as Transparent
{-| -}
@ -237,7 +235,6 @@ isOpen model =
type Msg
= CloseButtonClicked
| EscOrOverlayClicked
| Focus String
| Focused (Result Dom.Error ())
@ -267,9 +264,6 @@ update { dismissOnEscAndOverlayClick } msg model =
else
( model, Cmd.none )
Focus id ->
( model, Task.attempt Focused (Dom.focus id) )
Focused _ ->
-- TODO: consider adding error handling when we didn't successfully
-- fous an element
@ -426,7 +420,7 @@ modalStyles =
-- Border
, borderRadius (px 20)
, property "box-shadow" "0 1px 1px hsl(0deg 0% 0% / 0.075), 0 2px 2px hsl(0deg 0% 0% / 0.075), 0 4px 4px hsl(0deg 0% 0% / 0.075), 0 8px 8px hsl(0deg 0% 0% / 0.075), 0 16px 16px hsl(0deg 0% 0% / 0.075)"
, Shadows.high
, Css.Media.withMedia [ mobile ]
[ borderRadius zero
]
@ -570,7 +564,7 @@ viewModal :
viewModal config =
section
([ Role.dialog
, Widget.modal True
, Aria.modal True
, Aria.labeledBy modalTitleId
]
++ config.customAttributes
@ -728,7 +722,7 @@ closeButtonId =
viewCloseButton : msg -> Html msg
viewCloseButton closeModal =
button
(Widget.label "Close modal"
(Aria.label "Close modal"
:: onClick closeModal
:: Attrs.css
[ -- in the upper-right corner of the modal

View File

@ -17,6 +17,7 @@ import Html.Styled as Html exposing (Html)
import Html.Styled.Attributes as Attributes
import Http
import Nri.Ui.Button.V10 as Button
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Heading.V2 as Heading
import Nri.Ui.Html.V3 exposing (viewIf)
@ -263,7 +264,7 @@ viewDetails detailsForEngineers =
]
[]
[ Html.styled Html.summary
[ color (hex "8F8F8F") ]
[ color Colors.gray45 ]
[]
[ Html.text "Details for NoRedInk engineers" ]
, Html.styled Html.code

View File

@ -10,7 +10,7 @@ module Nri.Ui.Palette.V1 exposing
-}
import Css exposing (..)
import Css
import Nri.Ui.Colors.V1 as Colors

View File

@ -1,84 +0,0 @@
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"!
# Patch changes
- Use Nri.Ui.Pennant.V2.premiumFlag instead of Nri.Ui.Pennant.V1.premiumFlag
# 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
import Html.Styled.Attributes as Attributes exposing (css)
import Nri.Ui.Checkbox.V5 as Checkbox
import Nri.Ui.Pennant.V2 exposing (premiumFlag)
import Nri.Ui.Svg.V1 as Svg
{-| 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
[ Css.displayFlex
, Css.alignItems Css.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
|> Svg.withLabel "Premium"
|> Svg.withWidth (Css.px 25)
|> Svg.withHeight (Css.px 30)
|> Svg.withCss [ Css.marginLeft (Css.px 8) ]
|> Svg.toHtml
else
Html.text ""
]

View File

@ -1,223 +0,0 @@
module Nri.Ui.PremiumCheckbox.V7 exposing
( view
, selected, partiallySelected
, premium, showPennant
, Attribute
, disabled, enabled
)
{-|
# Changes from V6
- Move the Premium pennant to the left of the checkbox
- list based API instead of record based
@docs view
@docs selected, partiallySelected
### Content
@docs premium, showPennant
### Attributes
@docs Attribute
@docs disabled, enabled
-}
import Accessibility.Styled as Html exposing (Html)
import Css
import Html.Styled.Attributes as Attributes exposing (css)
import Nri.Ui.Checkbox.V5 as Checkbox
import Nri.Ui.Data.PremiumLevel as PremiumLevel exposing (PremiumLevel)
import Nri.Ui.Pennant.V2 exposing (premiumFlag)
import Nri.Ui.Svg.V1 as Svg
import Nri.Ui.Util exposing (removePunctuation)
import String exposing (toLower)
import String.Extra exposing (dasherize)
{-| This disables the input
-}
disabled : Attribute msg
disabled =
Attribute <| \config -> { config | isDisabled = True }
{-| This enables the input, this is the default behavior
-}
enabled : Attribute msg
enabled =
Attribute <| \config -> { config | isDisabled = False }
{-| Lock Premium content if the user does not have Premium.
-}
premium :
{ teacherPremiumLevel : PremiumLevel
, contentPremiumLevel : PremiumLevel
}
-> Attribute msg
premium { teacherPremiumLevel, contentPremiumLevel } =
Attribute <|
\config ->
{ config
| teacherPremiumLevel = Just teacherPremiumLevel
, contentPremiumLevel = Just contentPremiumLevel
}
{-| Show Premium pennant on Premium content.
When a locked premium checkbox is clicked, the msg that's passed in will fire.
-}
showPennant : msg -> Attribute msg
showPennant premiumMsg =
Attribute <| \config -> { config | premiumMsg = Just premiumMsg }
setSelectionStatus : Checkbox.IsSelected -> Attribute msg
setSelectionStatus status =
Attribute (\config -> { config | selected = status })
{-| -}
selected : Bool -> Attribute msg
selected isSelected =
setSelectionStatus <|
if isSelected then
Checkbox.Selected
else
Checkbox.NotSelected
{-| -}
partiallySelected : Attribute msg
partiallySelected =
setSelectionStatus Checkbox.PartiallySelected
{-| Customizations for the RadioButton.
-}
type Attribute msg
= Attribute (Config msg -> Config msg)
{-| This is private. The public API only exposes `Attribute`.
-}
type alias Config msg =
{ id : Maybe String
, teacherPremiumLevel : Maybe PremiumLevel
, contentPremiumLevel : Maybe PremiumLevel
, isDisabled : Bool
, containerCss : List Css.Style
, selected : Checkbox.IsSelected
, premiumMsg : Maybe msg
}
emptyConfig : Config msg
emptyConfig =
{ id = Nothing
, teacherPremiumLevel = Nothing
, contentPremiumLevel = Nothing
, isDisabled = False
, containerCss =
[ Css.displayFlex
, Css.alignItems Css.center
]
, selected = Checkbox.NotSelected
, premiumMsg = Nothing
}
applyConfig : List (Attribute msg) -> Config msg -> Config msg
applyConfig attributes beginningConfig =
List.foldl (\(Attribute update) config -> update config)
beginningConfig
attributes
{-| -}
view :
{ label : String
, onChange : Bool -> msg
}
-> List (Attribute msg)
-> Html msg
view { label, onChange } attributes =
let
config =
applyConfig attributes emptyConfig
idValue =
case config.id of
Just specificId ->
specificId
Nothing ->
"checkbox-" ++ dasherize (removePunctuation (toLower label))
isLocked =
Maybe.map2 PremiumLevel.allowedFor config.contentPremiumLevel config.teacherPremiumLevel
|> Maybe.withDefault True
|> not
in
Html.div [ css config.containerCss ]
[ case config.contentPremiumLevel of
Just PremiumLevel.Premium ->
viewPremiumFlag
Just PremiumLevel.PremiumWithWriting ->
viewPremiumFlag
_ ->
-- left-align the checkbox with checkboxes that _do_ have the premium pennant
Html.div [ css [ Css.width (Css.px (iconWidth + iconRightMargin)) ] ] []
, Checkbox.viewWithLabel
{ identifier = idValue
, label = label
, setterMsg =
case ( isLocked, config.premiumMsg ) of
( True, Just onLockedClick ) ->
\_ -> onLockedClick
_ ->
onChange
, selected = config.selected
, disabled = isLocked || config.isDisabled
, theme =
if isLocked then
Checkbox.Locked
else
Checkbox.Square
}
]
viewPremiumFlag : Html msg
viewPremiumFlag =
premiumFlag
|> Svg.withLabel "Premium"
|> Svg.withWidth (Css.px iconWidth)
|> Svg.withHeight (Css.px 30)
|> Svg.withCss [ Css.marginRight (Css.px iconRightMargin) ]
|> Svg.toHtml
iconWidth : Float
iconWidth =
25
iconRightMargin : Float
iconRightMargin =
8

View File

@ -1,7 +1,7 @@
module Nri.Ui.PremiumCheckbox.V8 exposing
( view
, selected, partiallySelected
, premium, showPennant
, premium, onLockedClick
, Attribute
, disabled, enabled
, id
@ -10,6 +10,8 @@ module Nri.Ui.PremiumCheckbox.V8 exposing
{-| Changes from V7:
- Use PremiumDisplay instead of PremiumLevel
- Rename showPennant to onLockedClick
- Fix clicking on locked checkbox to send a onLockedClick
@docs view
@ -18,7 +20,7 @@ module Nri.Ui.PremiumCheckbox.V8 exposing
### Content
@docs premium, showPennant
@docs premium, onLockedClick
### Attributes
@ -30,10 +32,14 @@ module Nri.Ui.PremiumCheckbox.V8 exposing
-}
import Accessibility.Styled as Html exposing (Html)
import Css
import Html.Styled.Attributes exposing (css)
import Css exposing (..)
import Html.Styled.Attributes as Attributes exposing (class, css)
import Html.Styled.Events as Events
import Nri.Ui.Checkbox.V5 as Checkbox
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Data.PremiumDisplay as PremiumDisplay exposing (PremiumDisplay)
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as Extra
import Nri.Ui.Pennant.V2 exposing (premiumFlag)
import Nri.Ui.Svg.V1 as Svg
import Nri.Ui.Util exposing (removePunctuation)
@ -77,9 +83,9 @@ premium premiumDisplay =
When a locked premium checkbox is clicked, the msg that's passed in will fire.
-}
showPennant : msg -> Attribute msg
showPennant premiumMsg =
Attribute <| \config -> { config | premiumMsg = Just premiumMsg }
onLockedClick : msg -> Attribute msg
onLockedClick onLockedMsg =
Attribute <| \config -> { config | onLockedMsg = Just onLockedMsg }
setSelectionStatus : Checkbox.IsSelected -> Attribute msg
@ -118,7 +124,7 @@ type alias Config msg =
, isDisabled : Bool
, containerCss : List Css.Style
, selected : Checkbox.IsSelected
, premiumMsg : Maybe msg
, onLockedMsg : Maybe msg
}
@ -132,7 +138,7 @@ emptyConfig =
, Css.alignItems Css.center
]
, selected = Checkbox.NotSelected
, premiumMsg = Nothing
, onLockedMsg = Nothing
}
@ -163,39 +169,98 @@ view { label, onChange } attributes =
Nothing ->
"checkbox-" ++ dasherize (removePunctuation (toLower label))
isPremium =
config.premiumDisplay /= PremiumDisplay.Free
isLocked =
config.premiumDisplay == PremiumDisplay.PremiumLocked
in
Html.div [ css config.containerCss ]
[ case config.premiumDisplay of
PremiumDisplay.PremiumLocked ->
viewPremiumFlag
PremiumDisplay.PremiumUnlocked ->
viewPremiumFlag
PremiumDisplay.Free ->
-- left-align the checkbox with checkboxes that _do_ have the premium pennant
Html.div [ css [ Css.width (Css.px (iconWidth + iconRightMargin)) ] ] []
, Checkbox.viewWithLabel
{ identifier = idValue
if isLocked then
viewLockedButton
{ idValue = idValue
, label = label
, setterMsg =
case ( isLocked, config.premiumMsg ) of
( True, Just onLockedClick ) ->
\_ -> onLockedClick
_ ->
onChange
, selected = config.selected
, disabled = isLocked || config.isDisabled
, theme =
if isLocked then
Checkbox.Locked
else
Checkbox.Square
, containerCss = config.containerCss
, onLockedMsg = config.onLockedMsg
}
else
Html.div [ css config.containerCss ]
[ if isPremium then
viewPremiumFlag
else
-- left-align the checkbox with checkboxes that _do_ have the premium pennant
Html.div [ css [ Css.width (Css.px (iconWidth + iconRightMargin)), Css.flexShrink Css.zero ] ] []
, Checkbox.viewWithLabel
{ identifier = idValue
, label = label
, setterMsg = onChange
, selected = config.selected
, disabled = config.isDisabled
, theme =
if isLocked then
Checkbox.Locked
else
Checkbox.Square
}
]
viewLockedButton : { a | idValue : String, label : String, containerCss : List Style, onLockedMsg : Maybe msg } -> Html msg
viewLockedButton { idValue, label, containerCss, onLockedMsg } =
Html.button
[ css
[ height inherit
, width (Css.pct 100)
, position relative
, backgroundColor Css.transparent
, border Css.zero
, padding zero
, cursor pointer
, Css.batch containerCss
]
, Attributes.id (idValue ++ "-container")
, case onLockedMsg of
Just msg ->
Events.onClick msg
Nothing ->
Extra.none
]
[ viewPremiumFlag
, Html.span
[ css
[ outline Css.none
, Fonts.baseFont
, color Colors.navy
, margin zero
, marginLeft (px -4)
, padding zero
, fontSize (px 15)
, Css.property "font-weight" "600"
, display inlineBlock
, Css.property "transition" "all 0.4s ease"
, cursor pointer
]
]
[ Html.span
[ class "premium-checkbox-locked-V8__Label"
, css
[ display inlineBlock
, padding4 (px 13) zero (px 13) (px 40)
, position relative
, Fonts.baseFont
, fontSize (px 15)
, fontWeight (int 600)
, color Colors.navy
, outline none
]
]
[ Checkbox.viewIcon [] (Checkbox.checkboxLockOnInside idValue)
, Html.span [] [ Html.text label ]
]
]
]
@ -205,7 +270,7 @@ viewPremiumFlag =
|> Svg.withLabel "Premium"
|> Svg.withWidth (Css.px iconWidth)
|> Svg.withHeight (Css.px 30)
|> Svg.withCss [ Css.marginRight (Css.px iconRightMargin) ]
|> Svg.withCss [ Css.marginRight (Css.px iconRightMargin), Css.flexShrink Css.zero ]
|> Svg.toHtml

View File

@ -1,426 +0,0 @@
module Nri.Ui.RadioButton.V2 exposing (view, premium)
{-| Changes from V1:
- adds an outline when a radio button is focused
- remove NoOp/event swallowing (it broke default radio button behavior)
@docs view, premium
-}
import Accessibility.Styled exposing (..)
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Style as Style
import Accessibility.Styled.Widget as Widget
import Css exposing (..)
import Css.Global
import Html.Styled as Html
import Html.Styled.Attributes exposing (..)
import Html.Styled.Events exposing (onClick, stopPropagationOn)
import Json.Decode
import Nri.Ui.ClickableSvg.V2 as ClickableSvg
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Data.PremiumLevel as PremiumLevel exposing (PremiumLevel)
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as Attributes
import Nri.Ui.Html.V3 exposing (viewIf)
import Nri.Ui.Pennant.V2 as Pennant
import Nri.Ui.Svg.V1 exposing (Svg, fromHtml)
import String exposing (toLower)
import String.Extra exposing (dasherize)
import Svg.Styled as Svg
import Svg.Styled.Attributes as SvgAttributes
{-| View a single radio button.
If used in a group, all radio buttons in the group should have the same name attribute.
-}
view :
{ label : String
, value : a
, name : String
, selectedValue : Maybe a
, onSelect : a -> msg
, valueToString : a -> String
}
-> Html msg
view config =
internalView
{ label = config.label
, value = config.value
, name = config.name
, selectedValue = config.selectedValue
, isLocked = False
, isDisabled = False
, onSelect = config.onSelect
, premiumMsg = Nothing
, valueToString = config.valueToString
, showPennant = False
}
{-| A radio button that should be used for premium content.
This radio button is locked when the premium level of the content
is greater than the premium level of the teacher.
- `onChange`: A message for when the user selected the radio button
- `onLockedClick`: A message for when the user clicks a radio button they don't have PremiumLevel for.
If you get this message, you should show an `Nri.Premium.Model.view`
-}
premium :
{ label : String
, value : a
, name : String
, selectedValue : Maybe a
, teacherPremiumLevel : PremiumLevel
, contentPremiumLevel : PremiumLevel
, onSelect : a -> msg
, premiumMsg : msg
, valueToString : a -> String
, showPennant : Bool
, isDisabled : Bool
}
-> Html msg
premium config =
let
isLocked =
not <|
PremiumLevel.allowedFor
config.contentPremiumLevel
config.teacherPremiumLevel
in
internalView
{ label = config.label
, value = config.value
, name = config.name
, selectedValue = config.selectedValue
, isLocked = isLocked
, isDisabled = config.isDisabled
, onSelect = config.onSelect
, valueToString = config.valueToString
, premiumMsg = Just config.premiumMsg
, showPennant =
case config.contentPremiumLevel of
PremiumLevel.Premium ->
config.showPennant
PremiumLevel.PremiumWithWriting ->
config.showPennant
PremiumLevel.Free ->
False
}
type alias InternalConfig a msg =
{ label : String
, value : a
, name : String
, selectedValue : Maybe a
, isLocked : Bool
, isDisabled : Bool
, onSelect : a -> msg
, premiumMsg : Maybe msg
, valueToString : a -> String
, showPennant : Bool
}
internalView : InternalConfig a msg -> Html msg
internalView config =
let
isChecked =
config.selectedValue == Just config.value
id_ =
config.name ++ "-" ++ dasherize (toLower (config.valueToString config.value))
in
Html.span
[ id (id_ ++ "-container")
, classList [ ( "Nri-RadioButton-PremiumClass", config.showPennant ) ]
, css
[ position relative
, marginLeft (px -4)
, display inlineBlock
, Css.height (px 34)
, pseudoClass "focus-within"
[ Css.Global.descendants
[ Css.Global.class "Nri-RadioButton-RadioButtonIcon"
[ borderColor (rgb 0 95 204)
]
]
]
]
]
[ radio config.name
(config.valueToString config.value)
isChecked
[ id id_
, Widget.disabled (config.isLocked || config.isDisabled)
, if not config.isDisabled then
onClick (config.onSelect config.value)
else
Attributes.none
, class "Nri-RadioButton-HiddenRadioInput"
, css
[ position absolute
, top (px 4)
, left (px 4)
, opacity zero
]
]
, Html.label
[ for id_
, classList
[ ( "Nri-RadioButton-RadioButton", True )
, ( "Nri-RadioButton-RadioButtonChecked", isChecked )
]
, css
[ padding4 (px 6) zero (px 4) (px 40)
, if config.isDisabled then
Css.batch
[ color Colors.gray45
, cursor notAllowed
]
else
cursor pointer
, fontSize (px 15)
, Fonts.baseFont
, Css.property "font-weight" "600"
, position relative
, outline none
, margin zero
, display inlineBlock
, color Colors.navy
]
]
[ radioInputIcon
{ isLocked = config.isLocked
, isDisabled = config.isDisabled
, isChecked = isChecked
}
, span
(if config.showPennant then
[ css
[ displayFlex
, alignItems center
, Css.height (px 20)
]
]
else
[ css [ verticalAlign middle ] ]
)
[ Html.text config.label
, viewIf
(\() ->
ClickableSvg.button "Premium"
Pennant.premiumFlag
[ Maybe.map ClickableSvg.onClick config.premiumMsg
|> Maybe.withDefault (ClickableSvg.custom [])
, ClickableSvg.exactWidth 26
, ClickableSvg.exactHeight 24
, ClickableSvg.css [ marginLeft (px 8) ]
]
)
config.showPennant
]
]
]
onEnterAndSpacePreventDefault : msg -> Attribute msg
onEnterAndSpacePreventDefault msg =
Nri.Ui.Html.V3.onKeyUp
{ stopPropagation = False, preventDefault = True }
(\code ->
if code == 13 || code == 32 then
Just msg
else
Nothing
)
radioInputIcon :
{ isChecked : Bool
, isLocked : Bool
, isDisabled : Bool
}
-> Html msg
radioInputIcon config =
let
image =
case ( config.isDisabled, config.isLocked, config.isChecked ) of
( _, True, _ ) ->
lockedSvg
( True, _, _ ) ->
unselectedSvg
( _, False, True ) ->
selectedSvg
( _, False, False ) ->
unselectedSvg
in
div
[ classList
[ ( "Nri-RadioButton-RadioButtonIcon", True )
, ( "Nri-RadioButton-RadioButtonDisabled", config.isDisabled )
]
, css
[ Css.batch <|
if config.isDisabled then
[ opacity (num 0.4) ]
else
[]
, position absolute
, left zero
, top zero
, Css.property "transition" ".3s all"
, border3 (px 2) solid transparent
, borderRadius (px 50)
, padding (px 2)
, displayFlex
, justifyContent center
, alignItems center
]
]
[ image
|> Nri.Ui.Svg.V1.withHeight (Css.px 26)
|> Nri.Ui.Svg.V1.withWidth (Css.px 26)
|> Nri.Ui.Svg.V1.toHtml
]
unselectedSvg : Svg
unselectedSvg =
Svg.svg [ SvgAttributes.viewBox "0 0 27 27" ]
[ Svg.defs []
[ Svg.rect [ SvgAttributes.id "unselected-path-1", SvgAttributes.x "0", SvgAttributes.y "0", SvgAttributes.width "27", SvgAttributes.height "27", SvgAttributes.rx "13.5" ] []
, Svg.filter [ SvgAttributes.id "unselected-filter-2", SvgAttributes.x "-3.7%", SvgAttributes.y "-3.7%", SvgAttributes.width "107.4%", SvgAttributes.height "107.4%", SvgAttributes.filterUnits "objectBoundingBox" ] [ Svg.feOffset [ SvgAttributes.dx "0", SvgAttributes.dy "2", SvgAttributes.in_ "SourceAlpha", SvgAttributes.result "shadowOffsetInner1" ] [], Svg.feComposite [ SvgAttributes.in_ "shadowOffsetInner1", SvgAttributes.in2 "SourceAlpha", SvgAttributes.operator "arithmetic", SvgAttributes.k2 "-1", SvgAttributes.k3 "1", SvgAttributes.result "shadowInnerInner1" ] [], Svg.feColorMatrix [ SvgAttributes.values "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0", SvgAttributes.in_ "shadowInnerInner1" ] [] ]
]
, Svg.g
[ SvgAttributes.stroke "none"
, SvgAttributes.strokeWidth "1"
, SvgAttributes.fill "none"
, SvgAttributes.fillRule "evenodd"
]
[ Svg.g []
[ Svg.g []
[ Svg.use
[ SvgAttributes.fill "#EBEBEB"
, SvgAttributes.fillRule "evenodd"
, SvgAttributes.xlinkHref "#unselected-path-1"
]
[]
, Svg.use
[ SvgAttributes.fill "black"
, SvgAttributes.fillOpacity "1"
, SvgAttributes.filter "url(#unselected-filter-2)"
, SvgAttributes.xlinkHref "#unselected-path-1"
]
[]
]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml
selectedSvg : Svg
selectedSvg =
Svg.svg [ SvgAttributes.viewBox "0 0 27 27" ]
[ Svg.defs []
[ Svg.rect [ SvgAttributes.id "selected-path-1", SvgAttributes.x "0", SvgAttributes.y "0", SvgAttributes.width "27", SvgAttributes.height "27", SvgAttributes.rx "13.5" ] []
, Svg.filter
[ SvgAttributes.id "selected-filter-2", SvgAttributes.x "-3.7%", SvgAttributes.y "-3.7%", SvgAttributes.width "107.4%", SvgAttributes.height "107.4%", SvgAttributes.filterUnits "objectBoundingBox" ]
[ Svg.feOffset [ SvgAttributes.dx "0", SvgAttributes.dy "2", SvgAttributes.in_ "SourceAlpha", SvgAttributes.result "shadowOffsetInner1" ] [], Svg.feComposite [ SvgAttributes.in_ "shadowOffsetInner1", SvgAttributes.in2 "SourceAlpha", SvgAttributes.operator "arithmetic", SvgAttributes.k2 "-1", SvgAttributes.k3 "1", SvgAttributes.result "shadowInnerInner1" ] [], Svg.feColorMatrix [ SvgAttributes.values "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0", SvgAttributes.in_ "shadowInnerInner1" ] [] ]
]
, Svg.g
[ SvgAttributes.stroke "none"
, SvgAttributes.strokeWidth "1"
, SvgAttributes.fill "none"
, SvgAttributes.fillRule "evenodd"
]
[ Svg.g []
[ Svg.g []
[ Svg.use
[ SvgAttributes.fill "#D4F0FF"
, SvgAttributes.fillRule "evenodd"
, SvgAttributes.xlinkHref "#selected-path-1"
]
[]
, Svg.use
[ SvgAttributes.fill "black"
, SvgAttributes.fillOpacity "1"
, SvgAttributes.filter "url(#selected-filter-2)"
, SvgAttributes.xlinkHref "#selected-path-1"
]
[]
]
, Svg.circle
[ SvgAttributes.fill "#146AFF"
, SvgAttributes.cx "13.5"
, SvgAttributes.cy "13.5"
, SvgAttributes.r "6.3"
]
[]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml
lockedSvg : Svg
lockedSvg =
Svg.svg [ SvgAttributes.viewBox "0 0 30 30" ]
[ Svg.defs []
[ Svg.rect [ SvgAttributes.id "locked-path-1", SvgAttributes.x "0", SvgAttributes.y "0", SvgAttributes.width "30", SvgAttributes.height "30", SvgAttributes.rx "15" ] []
, Svg.filter [ SvgAttributes.id "locked-filter-2", SvgAttributes.x "-3.3%", SvgAttributes.y "-3.3%", SvgAttributes.width "106.7%", SvgAttributes.height "106.7%", SvgAttributes.filterUnits "objectBoundingBox" ] [ Svg.feOffset [ SvgAttributes.dx "0", SvgAttributes.dy "2", SvgAttributes.in_ "SourceAlpha", SvgAttributes.result "shadowOffsetInner1" ] [], Svg.feComposite [ SvgAttributes.in_ "shadowOffsetInner1", SvgAttributes.in2 "SourceAlpha", SvgAttributes.operator "arithmetic", SvgAttributes.k2 "-1", SvgAttributes.k3 "1", SvgAttributes.result "shadowInnerInner1" ] [], Svg.feColorMatrix [ SvgAttributes.values "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0", SvgAttributes.in_ "shadowInnerInner1" ] [] ]
]
, Svg.g
[ SvgAttributes.stroke "none"
, SvgAttributes.strokeWidth "1"
, SvgAttributes.fill "none"
, SvgAttributes.fillRule "evenodd"
]
[ Svg.g []
[ Svg.use
[ SvgAttributes.fill "#EBEBEB"
, SvgAttributes.fillRule "evenodd"
, SvgAttributes.xlinkHref "#locked-path-1"
]
[]
, Svg.use
[ SvgAttributes.fill "black"
, SvgAttributes.fillOpacity "1"
, SvgAttributes.filter "url(#locked-filter-2)"
, SvgAttributes.xlinkHref "#locked-path-1"
]
[]
]
, Svg.g
[ SvgAttributes.transform "translate(8.000000, 5.000000)"
]
[ Svg.path
[ SvgAttributes.d "M11.7991616,9.36211885 L11.7991616,5.99470414 C11.7991616,3.24052783 9.64616757,1 7.00011784,1 C4.35359674,1 2.20083837,3.24052783 2.20083837,5.99470414 L2.20083837,9.36211885 L1.51499133,9.36211885 C0.678540765,9.36211885 -6.21724894e-14,10.0675883 -6.21724894e-14,10.9381415 L-6.21724894e-14,18.9239773 C-6.21724894e-14,19.7945305 0.678540765,20.5 1.51499133,20.5 L12.48548,20.5 C13.3219306,20.5 14,19.7945305 14,18.9239773 L14,10.9383868 C14,10.0678336 13.3219306,9.36211885 12.48548,9.36211885 L11.7991616,9.36211885 Z M7.46324136,15.4263108 L7.46324136,17.2991408 C7.46324136,17.5657769 7.25560176,17.7816368 7.00011784,17.7816368 C6.74416256,17.7816368 6.53652295,17.5657769 6.53652295,17.2991408 L6.53652295,15.4263108 C6.01259238,15.228848 5.63761553,14.7080859 5.63761553,14.0943569 C5.63761553,13.3116195 6.24757159,12.6763045 7.00011784,12.6763045 C7.75195704,12.6763045 8.36285584,13.3116195 8.36285584,14.0943569 C8.36285584,14.7088218 7.98717193,15.2295839 7.46324136,15.4263108 L7.46324136,15.4263108 Z M9.98178482,9.36211885 L4.01821518,9.36211885 L4.01821518,5.99470414 C4.01821518,4.2835237 5.35597044,2.89122723 7.00011784,2.89122723 C8.64402956,2.89122723 9.98178482,4.2835237 9.98178482,5.99470414 L9.98178482,9.36211885 L9.98178482,9.36211885 Z"
, SvgAttributes.fill "#E68800"
]
[]
, Svg.path
[ SvgAttributes.d "M11.7991616,8.14770554 L11.7991616,4.8666348 C11.7991616,2.18307839 9.64616757,-7.10542736e-15 7.00011784,-7.10542736e-15 C4.35359674,-7.10542736e-15 2.20083837,2.18307839 2.20083837,4.8666348 L2.20083837,8.14770554 L1.51499133,8.14770554 C0.678540765,8.14770554 -6.21724894e-14,8.83508604 -6.21724894e-14,9.6833174 L-6.21724894e-14,17.4643881 C-6.21724894e-14,18.3126195 0.678540765,19 1.51499133,19 L12.48548,19 C13.3219306,19 14,18.3126195 14,17.4643881 L14,9.68355641 C14,8.83532505 13.3219306,8.14770554 12.48548,8.14770554 L11.7991616,8.14770554 Z M7.46324136,14.0564054 L7.46324136,15.8812141 C7.46324136,16.1410134 7.25560176,16.3513384 7.00011784,16.3513384 C6.74416256,16.3513384 6.53652295,16.1410134 6.53652295,15.8812141 L6.53652295,14.0564054 C6.01259238,13.8640057 5.63761553,13.3565966 5.63761553,12.7586042 C5.63761553,11.9959369 6.24757159,11.376912 7.00011784,11.376912 C7.75195704,11.376912 8.36285584,11.9959369 8.36285584,12.7586042 C8.36285584,13.3573136 7.98717193,13.8647228 7.46324136,14.0564054 L7.46324136,14.0564054 Z M9.98178482,8.14770554 L4.01821518,8.14770554 L4.01821518,4.8666348 C4.01821518,3.19933078 5.35597044,1.84273423 7.00011784,1.84273423 C8.64402956,1.84273423 9.98178482,3.19933078 9.98178482,4.8666348 L9.98178482,8.14770554 L9.98178482,8.14770554 Z"
, SvgAttributes.fill "#FEC709"
]
[]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml

View File

@ -1,671 +0,0 @@
module Nri.Ui.RadioButton.V3 exposing
( view
, premium, showPennant
, disclosure
, onSelect
, Attribute
, hiddenLabel, visibleLabel
, containerCss, labelCss, custom, nriDescription, id, testId
, disabled, enabled, errorIf, errorMessage, guidance
)
{-| Changes from V2:
- list based API instead of record based
- add disclosure to show rich content when the radio is selected
- allow customization of the id
- add gray and azure borders to make the radios easier to distinguish visually
@docs view
### Content
@docs premium, showPennant
@docs disclosure
### Event handlers
@docs onSelect
### Attributes
@docs Attribute
@docs hiddenLabel, visibleLabel
@docs containerCss, labelCss, custom, nriDescription, id, testId
@docs disabled, enabled, errorIf, errorMessage, guidance
-}
import Accessibility.Styled exposing (..)
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Style as Style
import Accessibility.Styled.Widget as Widget
import Css as Css exposing (..)
import Css.Global
import Html.Styled as Html
import Html.Styled.Attributes as Attributes exposing (class, classList, css, for)
import Html.Styled.Events exposing (onClick, stopPropagationOn)
import InputErrorAndGuidanceInternal exposing (ErrorState, Guidance)
import Json.Decode
import Nri.Ui.ClickableSvg.V2 as ClickableSvg
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Data.PremiumLevel as PremiumLevel exposing (PremiumLevel)
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as Extra
import Nri.Ui.Html.V3 exposing (viewJust)
import Nri.Ui.Pennant.V2 as Pennant
import Nri.Ui.Svg.V1 exposing (Svg, fromHtml)
import String exposing (toLower)
import String.Extra exposing (dasherize)
import Svg.Styled as Svg
import Svg.Styled.Attributes as SvgAttributes
{-| This disables the input
-}
disabled : Attribute value msg
disabled =
Attribute <| \config -> { config | isDisabled = True }
{-| This enables the input, this is the default behavior
-}
enabled : Attribute value msg
enabled =
Attribute <| \config -> { config | isDisabled = False }
{-| Sets whether or not the field will be highlighted as having a validation error.
-}
errorIf : Bool -> Attribute value msg
errorIf =
Attribute << InputErrorAndGuidanceInternal.setErrorIf
{-| If `Just`, the field will be highlighted as having a validation error,
and the given error message will be shown.
-}
errorMessage : Maybe String -> Attribute value msg
errorMessage =
Attribute << InputErrorAndGuidanceInternal.setErrorMessage
{-| A guidance message shows below the input, unless an error message is showing instead.
-}
guidance : String -> Attribute value msg
guidance =
Attribute << InputErrorAndGuidanceInternal.setGuidance
{-| Fire a message parameterized by the value type when selecting a radio option
-}
onSelect : (value -> msg) -> Attribute value msg
onSelect onSelect_ =
Attribute <| \config -> { config | onSelect = Just onSelect_ }
{-| Lock Premium content if the user does not have Premium.
-}
premium :
{ teacherPremiumLevel : PremiumLevel
, contentPremiumLevel : PremiumLevel
}
-> Attribute value msg
premium { teacherPremiumLevel, contentPremiumLevel } =
Attribute <|
\config ->
{ config
| teacherPremiumLevel = Just teacherPremiumLevel
, contentPremiumLevel = Just contentPremiumLevel
}
{-| Show Premium pennant on Premium content.
When the pennant is clicked, the msg that's passed in will fire.
For RadioButton.V4, consider removing `showPennant` from the API.
-}
showPennant : msg -> Attribute value msg
showPennant premiumMsg =
Attribute <| \config -> { config | premiumMsg = Just premiumMsg }
{-| Content that shows when this RadioButton is selected
-}
disclosure : List (Html msg) -> Attribute value msg
disclosure childNodes =
Attribute <| \config -> { config | disclosedContent = childNodes }
{-| Adds CSS to the element containing the input.
-}
containerCss : List Css.Style -> Attribute value msg
containerCss styles =
Attribute <| \config -> { config | containerCss = config.containerCss ++ styles }
{-| Adds CSS to the element containing the label text.
Note that these styles don't apply to the literal HTML label element, since it contains the icon SVG as well.
-}
labelCss : List Css.Style -> Attribute value msg
labelCss styles =
Attribute <| \config -> { config | labelCss = config.labelCss ++ styles }
{-| Hides the visible label. (There will still be an invisible label for screen readers.)
-}
hiddenLabel : Attribute value msg
hiddenLabel =
Attribute <| \config -> { config | hideLabel = True }
{-| Shows the visible label. This is the default behavior
-}
visibleLabel : Attribute value msg
visibleLabel =
Attribute <| \config -> { config | hideLabel = False }
{-| Set a custom ID for this text input and label. If you don't set this,
we'll automatically generate one from the label you pass in, but this can
cause problems if you have more than one radio input with the same label on
the page. You might also use this helper if you're manually managing focus.
-}
id : String -> Attribute value msg
id id_ =
Attribute <| \config -> { config | id = Just id_ }
{-| Use this helper to add custom attributes.
Do NOT use this helper to add css styles, as they may not be applied the way
you want/expect if underlying styles change.
Instead, please use the `css` helper.
-}
custom : List (Html.Attribute Never) -> Attribute value msg
custom attributes =
Attribute <| \config -> { config | custom = config.custom ++ attributes }
{-| -}
nriDescription : String -> Attribute value msg
nriDescription description =
custom [ Extra.nriDescription description ]
{-| -}
testId : String -> Attribute value msg
testId id_ =
custom [ Extra.testId id_ ]
{-| Customizations for the RadioButton.
-}
type Attribute value msg
= Attribute (Config value msg -> Config value msg)
{-| This is private. The public API only exposes `Attribute`.
-}
type alias Config value msg =
{ name : Maybe String
, id : Maybe String
, teacherPremiumLevel : Maybe PremiumLevel
, contentPremiumLevel : Maybe PremiumLevel
, isDisabled : Bool
, guidance : Guidance
, error : ErrorState
, hideLabel : Bool
, containerCss : List Css.Style
, labelCss : List Css.Style
, custom : List (Html.Attribute Never)
, onSelect : Maybe (value -> msg)
, premiumMsg : Maybe msg
, disclosedContent : List (Html msg)
}
emptyConfig : Config value msg
emptyConfig =
{ name = Nothing
, id = Nothing
, teacherPremiumLevel = Nothing
, contentPremiumLevel = Nothing
, isDisabled = False
, guidance = InputErrorAndGuidanceInternal.noGuidance
, error = InputErrorAndGuidanceInternal.noError
, hideLabel = False
, containerCss = []
, labelCss = []
, custom = []
, onSelect = Nothing
, premiumMsg = Nothing
, disclosedContent = []
}
applyConfig : List (Attribute value msg) -> Config value msg -> Config value msg
applyConfig attributes beginningConfig =
List.foldl (\(Attribute update) config -> update config)
beginningConfig
attributes
maybeAttr : (a -> Html.Attribute msg) -> Maybe a -> Html.Attribute msg
maybeAttr attr maybeValue =
maybeValue
|> Maybe.map attr
|> Maybe.withDefault Extra.none
{-| View a single radio button.
-}
view :
{ label : String
, name : String
, value : value
, valueToString : value -> String
, selectedValue : Maybe value
}
-> List (Attribute value msg)
-> Html msg
view { label, name, value, valueToString, selectedValue } attributes =
let
config =
applyConfig attributes emptyConfig
stringValue =
valueToString value
idValue =
case config.id of
Just specificId ->
specificId
Nothing ->
name ++ "-" ++ dasherize (toLower stringValue)
isChecked =
selectedValue == Just value
isLocked =
Maybe.map2 PremiumLevel.allowedFor config.contentPremiumLevel config.teacherPremiumLevel
|> Maybe.withDefault True
|> not
( disclosureIds, disclosedElements ) =
config.disclosedContent
|> List.indexedMap
(\index element ->
let
id_ =
(idValue ++ "-disclosure-content-") ++ String.fromInt index
in
( id_, span [ Attributes.id id_ ] [ element ] )
)
|> List.unzip
isInError =
InputErrorAndGuidanceInternal.getIsInError config.error
errorMessage_ =
InputErrorAndGuidanceInternal.getErrorMessage config.error
in
Html.span
[ Attributes.id (idValue ++ "-container")
, css
[ position relative
, marginLeft (px -4)
, Css.paddingLeft (Css.px 40)
, Css.paddingTop (px 6)
, Css.paddingBottom (px 4)
, display inlineBlock
, pseudoClass "focus-within"
[ Css.Global.descendants
[ Css.Global.class "Nri-RadioButton-RadioButtonIcon"
[ borderColor (rgb 0 95 204)
]
]
]
, Css.batch config.containerCss
]
]
([ radio name
stringValue
isChecked
([ Attributes.id idValue
, Widget.disabled (isLocked || config.isDisabled)
, InputErrorAndGuidanceInternal.describedBy idValue config
, case ( config.onSelect, config.isDisabled ) of
( Just onSelect_, False ) ->
onClick (onSelect_ value)
_ ->
Extra.none
, class "Nri-RadioButton-HiddenRadioInput"
, Aria.describedBy disclosureIds
, css
[ position absolute
, top (pct 50)
, left (px 4)
, opacity zero
, pseudoClass "focus"
[ Css.Global.adjacentSiblings
[ Css.Global.everything
[ Css.Global.descendants
[ Css.Global.class "Nri-RadioButton-RadioButtonIcon"
[ borderColor (rgb 0 95 204)
]
]
]
]
]
]
]
++ List.map (Attributes.map never) config.custom
)
, Html.label
[ for idValue
, classList
[ ( "Nri-RadioButton-RadioButton", True )
, ( "Nri-RadioButton-RadioButtonChecked", isChecked )
]
, css
[ outline Css.none
, Fonts.baseFont
, Css.batch
(if config.isDisabled then
[ color Colors.gray45
, cursor notAllowed
]
else if isInError then
[ color Colors.purple
, cursor pointer
]
else
[ color Colors.navy
, cursor pointer
]
)
, margin zero
, padding zero
, fontSize (px 15)
, Css.property "font-weight" "600"
, display inlineBlock
, Css.property "transition" "all 0.4s ease"
]
]
[ radioInputIcon
{ isLocked = isLocked
, isDisabled = config.isDisabled
, isChecked = isChecked
}
, span
[ css
[ display inlineFlex
, alignItems center
, Css.height (px 20)
]
]
[ Html.span
[ css <|
if config.hideLabel then
[ Css.width (px 1)
, overflow Css.hidden
, margin (px -1)
, padding (px 0)
, border (px 0)
, display inlineBlock
, textIndent (px 1)
]
else
config.labelCss
]
[ Html.text label ]
, case ( config.contentPremiumLevel, config.premiumMsg ) of
( Nothing, _ ) ->
text ""
( Just PremiumLevel.Free, _ ) ->
text ""
( Just _, Just premiumMsg ) ->
premiumPennant premiumMsg
_ ->
text ""
]
]
, InputErrorAndGuidanceInternal.view idValue config
]
++ (if isChecked then
disclosedElements
else
[]
)
)
premiumPennant : msg -> Html msg
premiumPennant onClick =
ClickableSvg.button "Premium"
Pennant.premiumFlag
[ ClickableSvg.onClick onClick
, ClickableSvg.exactWidth 26
, ClickableSvg.exactHeight 24
, ClickableSvg.css
[ marginLeft (px 8)
, verticalAlign middle
]
]
radioInputIcon :
{ isChecked : Bool
, isLocked : Bool
, isDisabled : Bool
}
-> Html msg
radioInputIcon config =
let
image =
case ( config.isDisabled, config.isLocked, config.isChecked ) of
( _, True, _ ) ->
lockedSvg
( True, _, _ ) ->
unselectedSvg
( _, False, True ) ->
selectedSvg
( _, False, False ) ->
unselectedSvg
iconHeight =
26
borderWidth =
2
iconPadding =
2
in
div
[ classList
[ ( "Nri-RadioButton-RadioButtonIcon", True )
, ( "Nri-RadioButton-RadioButtonDisabled", config.isDisabled )
]
, css
[ Css.batch <|
if config.isDisabled then
[ opacity (num 0.4) ]
else
[]
, position absolute
, left zero
, top (calc (pct 50) Css.minus (Css.px ((iconHeight + borderWidth + iconPadding) / 2)))
, Css.property "transition" ".3s all"
, border3 (px borderWidth) solid transparent
, borderRadius (px 50)
, padding (px iconPadding)
, displayFlex
, justifyContent center
, alignItems center
]
]
[ image
|> Nri.Ui.Svg.V1.withHeight (Css.px iconHeight)
|> Nri.Ui.Svg.V1.withWidth (Css.px 26)
|> Nri.Ui.Svg.V1.toHtml
]
unselectedSvg : Svg
unselectedSvg =
Svg.svg [ SvgAttributes.viewBox "0 0 27 27" ]
[ Svg.defs []
[ Svg.rect [ SvgAttributes.id "unselected-path-1", SvgAttributes.x "0", SvgAttributes.y "0", SvgAttributes.width "27", SvgAttributes.height "27", SvgAttributes.rx "13.5" ] []
, Svg.filter [ SvgAttributes.id "unselected-filter-2", SvgAttributes.x "-3.7%", SvgAttributes.y "-3.7%", SvgAttributes.width "107.4%", SvgAttributes.height "107.4%", SvgAttributes.filterUnits "objectBoundingBox" ] [ Svg.feOffset [ SvgAttributes.dx "0", SvgAttributes.dy "2", SvgAttributes.in_ "SourceAlpha", SvgAttributes.result "shadowOffsetInner1" ] [], Svg.feComposite [ SvgAttributes.in_ "shadowOffsetInner1", SvgAttributes.in2 "SourceAlpha", SvgAttributes.operator "arithmetic", SvgAttributes.k2 "-1", SvgAttributes.k3 "1", SvgAttributes.result "shadowInnerInner1" ] [], Svg.feColorMatrix [ SvgAttributes.values "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0", SvgAttributes.in_ "shadowInnerInner1" ] [] ]
]
, Svg.g
[ SvgAttributes.stroke "none"
, SvgAttributes.strokeWidth "1"
, SvgAttributes.fill "none"
, SvgAttributes.fillRule "evenodd"
]
[ Svg.g []
[ Svg.g []
[ Svg.use
[ SvgAttributes.fill "#EBEBEB"
, SvgAttributes.fillRule "evenodd"
, SvgAttributes.xlinkHref "#unselected-path-1"
]
[]
, Svg.use
[ SvgAttributes.fill "black"
, SvgAttributes.fillOpacity "1"
, SvgAttributes.filter "url(#unselected-filter-2)"
, SvgAttributes.xlinkHref "#unselected-path-1"
]
[]
]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml
|> withImageBorder Colors.gray85
selectedSvg : Svg
selectedSvg =
Svg.svg [ SvgAttributes.viewBox "0 0 27 27" ]
[ Svg.defs []
[ Svg.rect [ SvgAttributes.id "selected-path-1", SvgAttributes.x "0", SvgAttributes.y "0", SvgAttributes.width "27", SvgAttributes.height "27", SvgAttributes.rx "13.5" ] []
, Svg.filter
[ SvgAttributes.id "selected-filter-2", SvgAttributes.x "-3.7%", SvgAttributes.y "-3.7%", SvgAttributes.width "107.4%", SvgAttributes.height "107.4%", SvgAttributes.filterUnits "objectBoundingBox" ]
[ Svg.feOffset [ SvgAttributes.dx "0", SvgAttributes.dy "2", SvgAttributes.in_ "SourceAlpha", SvgAttributes.result "shadowOffsetInner1" ] [], Svg.feComposite [ SvgAttributes.in_ "shadowOffsetInner1", SvgAttributes.in2 "SourceAlpha", SvgAttributes.operator "arithmetic", SvgAttributes.k2 "-1", SvgAttributes.k3 "1", SvgAttributes.result "shadowInnerInner1" ] [], Svg.feColorMatrix [ SvgAttributes.values "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0", SvgAttributes.in_ "shadowInnerInner1" ] [] ]
]
, Svg.g
[ SvgAttributes.stroke "none"
, SvgAttributes.strokeWidth "1"
, SvgAttributes.fill "none"
, SvgAttributes.fillRule "evenodd"
]
[ Svg.g []
[ Svg.g []
[ Svg.use
[ SvgAttributes.fill "#D4F0FF"
, SvgAttributes.fillRule "evenodd"
, SvgAttributes.xlinkHref "#selected-path-1"
]
[]
, Svg.use
[ SvgAttributes.fill "black"
, SvgAttributes.fillOpacity "1"
, SvgAttributes.filter "url(#selected-filter-2)"
, SvgAttributes.xlinkHref "#selected-path-1"
]
[]
]
, Svg.circle
[ SvgAttributes.fill "#146AFF"
, SvgAttributes.cx "13.5"
, SvgAttributes.cy "13.5"
, SvgAttributes.r "6.3"
]
[]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml
|> withImageBorder Colors.azure
lockedSvg : Svg
lockedSvg =
Svg.svg [ SvgAttributes.viewBox "0 0 30 30" ]
[ Svg.defs []
[ Svg.rect [ SvgAttributes.id "locked-path-1", SvgAttributes.x "0", SvgAttributes.y "0", SvgAttributes.width "30", SvgAttributes.height "30", SvgAttributes.rx "15" ] []
, Svg.filter [ SvgAttributes.id "locked-filter-2", SvgAttributes.x "-3.3%", SvgAttributes.y "-3.3%", SvgAttributes.width "106.7%", SvgAttributes.height "106.7%", SvgAttributes.filterUnits "objectBoundingBox" ] [ Svg.feOffset [ SvgAttributes.dx "0", SvgAttributes.dy "2", SvgAttributes.in_ "SourceAlpha", SvgAttributes.result "shadowOffsetInner1" ] [], Svg.feComposite [ SvgAttributes.in_ "shadowOffsetInner1", SvgAttributes.in2 "SourceAlpha", SvgAttributes.operator "arithmetic", SvgAttributes.k2 "-1", SvgAttributes.k3 "1", SvgAttributes.result "shadowInnerInner1" ] [], Svg.feColorMatrix [ SvgAttributes.values "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0", SvgAttributes.in_ "shadowInnerInner1" ] [] ]
]
, Svg.g
[ SvgAttributes.stroke "none"
, SvgAttributes.strokeWidth "1"
, SvgAttributes.fill "none"
, SvgAttributes.fillRule "evenodd"
]
[ Svg.g []
[ Svg.use
[ SvgAttributes.fill "#EBEBEB"
, SvgAttributes.fillRule "evenodd"
, SvgAttributes.xlinkHref "#locked-path-1"
]
[]
, Svg.use
[ SvgAttributes.fill "black"
, SvgAttributes.fillOpacity "1"
, SvgAttributes.filter "url(#locked-filter-2)"
, SvgAttributes.xlinkHref "#locked-path-1"
]
[]
]
, Svg.g
[ SvgAttributes.transform "translate(8.000000, 5.000000)"
]
[ Svg.path
[ SvgAttributes.d "M11.7991616,9.36211885 L11.7991616,5.99470414 C11.7991616,3.24052783 9.64616757,1 7.00011784,1 C4.35359674,1 2.20083837,3.24052783 2.20083837,5.99470414 L2.20083837,9.36211885 L1.51499133,9.36211885 C0.678540765,9.36211885 -6.21724894e-14,10.0675883 -6.21724894e-14,10.9381415 L-6.21724894e-14,18.9239773 C-6.21724894e-14,19.7945305 0.678540765,20.5 1.51499133,20.5 L12.48548,20.5 C13.3219306,20.5 14,19.7945305 14,18.9239773 L14,10.9383868 C14,10.0678336 13.3219306,9.36211885 12.48548,9.36211885 L11.7991616,9.36211885 Z M7.46324136,15.4263108 L7.46324136,17.2991408 C7.46324136,17.5657769 7.25560176,17.7816368 7.00011784,17.7816368 C6.74416256,17.7816368 6.53652295,17.5657769 6.53652295,17.2991408 L6.53652295,15.4263108 C6.01259238,15.228848 5.63761553,14.7080859 5.63761553,14.0943569 C5.63761553,13.3116195 6.24757159,12.6763045 7.00011784,12.6763045 C7.75195704,12.6763045 8.36285584,13.3116195 8.36285584,14.0943569 C8.36285584,14.7088218 7.98717193,15.2295839 7.46324136,15.4263108 L7.46324136,15.4263108 Z M9.98178482,9.36211885 L4.01821518,9.36211885 L4.01821518,5.99470414 C4.01821518,4.2835237 5.35597044,2.89122723 7.00011784,2.89122723 C8.64402956,2.89122723 9.98178482,4.2835237 9.98178482,5.99470414 L9.98178482,9.36211885 L9.98178482,9.36211885 Z"
, SvgAttributes.fill "#E68800"
]
[]
, Svg.path
[ SvgAttributes.d "M11.7991616,8.14770554 L11.7991616,4.8666348 C11.7991616,2.18307839 9.64616757,-7.10542736e-15 7.00011784,-7.10542736e-15 C4.35359674,-7.10542736e-15 2.20083837,2.18307839 2.20083837,4.8666348 L2.20083837,8.14770554 L1.51499133,8.14770554 C0.678540765,8.14770554 -6.21724894e-14,8.83508604 -6.21724894e-14,9.6833174 L-6.21724894e-14,17.4643881 C-6.21724894e-14,18.3126195 0.678540765,19 1.51499133,19 L12.48548,19 C13.3219306,19 14,18.3126195 14,17.4643881 L14,9.68355641 C14,8.83532505 13.3219306,8.14770554 12.48548,8.14770554 L11.7991616,8.14770554 Z M7.46324136,14.0564054 L7.46324136,15.8812141 C7.46324136,16.1410134 7.25560176,16.3513384 7.00011784,16.3513384 C6.74416256,16.3513384 6.53652295,16.1410134 6.53652295,15.8812141 L6.53652295,14.0564054 C6.01259238,13.8640057 5.63761553,13.3565966 5.63761553,12.7586042 C5.63761553,11.9959369 6.24757159,11.376912 7.00011784,11.376912 C7.75195704,11.376912 8.36285584,11.9959369 8.36285584,12.7586042 C8.36285584,13.3573136 7.98717193,13.8647228 7.46324136,14.0564054 L7.46324136,14.0564054 Z M9.98178482,8.14770554 L4.01821518,8.14770554 L4.01821518,4.8666348 C4.01821518,3.19933078 5.35597044,1.84273423 7.00011784,1.84273423 C8.64402956,1.84273423 9.98178482,3.19933078 9.98178482,4.8666348 L9.98178482,8.14770554 L9.98178482,8.14770554 Z"
, SvgAttributes.fill "#FEC709"
]
[]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml
|> withImageBorder Colors.gray85
withImageBorder : Color -> Svg -> Svg
withImageBorder color =
Nri.Ui.Svg.V1.withCss
[ Css.border3 (px 1) solid color
, Css.borderRadius (Css.pct 50)
]

View File

@ -1,6 +1,6 @@
module Nri.Ui.RadioButton.V4 exposing
( view
, premium, onLockedPennantClick
, premium, onLockedClick
, disclosure
, onSelect
, Attribute
@ -12,14 +12,15 @@ module Nri.Ui.RadioButton.V4 exposing
{-| Changes from V3:
- use PremiumDisplay instead of PremiumLevel
- rename showPennant to onLockedPennantClick since its display depends on premium now
- rename showPennant to onLockedClick since its display depends on premium now
- make onLockedClick be triggers when clicking anywhere and not just pennant to match PremiumChecbox
@docs view
### Content
@docs premium, onLockedPennantClick
@docs premium, onLockedClick
@docs disclosure
@ -39,14 +40,12 @@ module Nri.Ui.RadioButton.V4 exposing
import Accessibility.Styled exposing (..)
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Widget as Widget
import Css as Css exposing (..)
import Css exposing (..)
import Css.Global
import Html.Styled as Html
import Html.Styled.Attributes as Attributes exposing (class, classList, css, for)
import Html.Styled.Events exposing (onClick)
import InputErrorAndGuidanceInternal exposing (ErrorState, Guidance)
import Nri.Ui.ClickableSvg.V2 as ClickableSvg
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Data.PremiumDisplay as PremiumDisplay exposing (PremiumDisplay)
import Nri.Ui.Fonts.V1 as Fonts
@ -116,9 +115,9 @@ premium premiumDisplay =
When the pennant is clicked, the msg that's passed in will fire.
-}
onLockedPennantClick : msg -> Attribute value msg
onLockedPennantClick premiumMsg =
Attribute <| \config -> { config | premiumMsg = Just premiumMsg }
onLockedClick : msg -> Attribute value msg
onLockedClick onLockedMsg =
Attribute <| \config -> { config | onLockedMsg = Just onLockedMsg }
{-| Content that shows when this RadioButton is selected
@ -213,7 +212,7 @@ type alias Config value msg =
, labelCss : List Css.Style
, custom : List (Html.Attribute Never)
, onSelect : Maybe (value -> msg)
, premiumMsg : Maybe msg
, onLockedMsg : Maybe msg
, disclosedContent : List (Html msg)
}
@ -231,7 +230,7 @@ emptyConfig =
, labelCss = []
, custom = []
, onSelect = Nothing
, premiumMsg = Nothing
, onLockedMsg = Nothing
, disclosedContent = []
}
@ -273,6 +272,9 @@ view { label, name, value, valueToString, selectedValue } attributes =
isChecked =
selectedValue == Just value
isPremium =
config.premiumDisplay /= PremiumDisplay.Free
isLocked =
config.premiumDisplay == PremiumDisplay.PremiumLocked
@ -291,7 +293,145 @@ view { label, name, value, valueToString, selectedValue } attributes =
isInError =
InputErrorAndGuidanceInternal.getIsInError config.error
in
Html.span
if isLocked then
viewLockedButton { idValue = idValue, label = label } config
else
Html.span
[ Attributes.id (idValue ++ "-container")
, css
[ position relative
, marginLeft (px -4)
, Css.paddingLeft (Css.px 40)
, Css.paddingTop (px 6)
, Css.paddingBottom (px 4)
, display inlineBlock
, pseudoClass "focus-within"
[ Css.Global.descendants
[ Css.Global.class "Nri-RadioButton-RadioButtonIcon"
[ borderColor (rgb 0 95 204)
]
]
]
, Css.batch config.containerCss
]
]
([ radio name
stringValue
isChecked
([ Attributes.id idValue
, Aria.disabled config.isDisabled
, InputErrorAndGuidanceInternal.describedBy idValue config
, case config.onSelect of
Just onSelect_ ->
onClick (onSelect_ value)
Nothing ->
Extra.none
, class "Nri-RadioButton-HiddenRadioInput"
, Aria.describedBy disclosureIds
, css
[ position absolute
, top (pct 50)
, left (px 4)
, opacity zero
, pseudoClass "focus"
[ Css.Global.adjacentSiblings
[ Css.Global.everything
[ Css.Global.descendants
[ Css.Global.class "Nri-RadioButton-RadioButtonIcon"
[ borderColor (rgb 0 95 204)
]
]
]
]
]
]
]
++ List.map (Attributes.map never) config.custom
)
, Html.label
[ for idValue
, classList
[ ( "Nri-RadioButton-RadioButton", True )
, ( "Nri-RadioButton-RadioButtonChecked", isChecked )
]
, css
[ outline Css.none
, Fonts.baseFont
, Css.batch
(if config.isDisabled then
[ color Colors.gray45
, cursor notAllowed
]
else if isInError then
[ color Colors.purple
, cursor pointer
]
else
[ color Colors.navy
, cursor pointer
]
)
, margin zero
, padding zero
, fontSize (px 15)
, Css.property "font-weight" "600"
, display inlineBlock
, Css.property "transition" "all 0.4s ease"
]
]
[ radioInputIcon
{ isLocked = isLocked
, isDisabled = config.isDisabled
, isChecked = isChecked
}
, span
[ css
[ display inlineFlex
, alignItems center
, Css.height (px 20)
]
]
[ Html.span
[ css <|
if config.hideLabel then
[ Css.width (px 1)
, overflow Css.hidden
, margin (px -1)
, padding (px 0)
, border (px 0)
, display inlineBlock
, textIndent (px 1)
]
else
config.labelCss
]
[ Html.text label ]
, if isPremium then
premiumPennant
else
text ""
]
]
, InputErrorAndGuidanceInternal.view idValue config
]
++ (if isChecked then
disclosedElements
else
[]
)
)
viewLockedButton : { idValue : String, label : String } -> Config value msg -> Html msg
viewLockedButton { idValue, label } config =
button
[ Attributes.id (idValue ++ "-container")
, css
[ position relative
@ -300,87 +440,36 @@ view { label, name, value, valueToString, selectedValue } attributes =
, Css.paddingTop (px 6)
, Css.paddingBottom (px 4)
, display inlineBlock
, pseudoClass "focus-within"
[ Css.Global.descendants
[ Css.Global.class "Nri-RadioButton-RadioButtonIcon"
[ borderColor (rgb 0 95 204)
]
]
]
, backgroundColor Css.transparent
, border Css.zero
, cursor pointer
, Css.batch config.containerCss
]
]
([ radio name
stringValue
isChecked
([ Attributes.id idValue
, Widget.disabled (isLocked || config.isDisabled)
, InputErrorAndGuidanceInternal.describedBy idValue config
, case ( config.onSelect, config.isDisabled ) of
( Just onSelect_, False ) ->
onClick (onSelect_ value)
, case config.onLockedMsg of
Just msg ->
onClick msg
_ ->
Extra.none
, class "Nri-RadioButton-HiddenRadioInput"
, Aria.describedBy disclosureIds
, css
[ position absolute
, top (pct 50)
, left (px 4)
, opacity zero
, pseudoClass "focus"
[ Css.Global.adjacentSiblings
[ Css.Global.everything
[ Css.Global.descendants
[ Css.Global.class "Nri-RadioButton-RadioButtonIcon"
[ borderColor (rgb 0 95 204)
]
]
]
]
]
]
]
++ List.map (Attributes.map never) config.custom
)
, Html.label
[ for idValue
, classList
[ ( "Nri-RadioButton-RadioButton", True )
, ( "Nri-RadioButton-RadioButtonChecked", isChecked )
]
Nothing ->
Extra.none
]
[ Html.div
[ class "Nri-RadioButton-LockedPremiumButton"
, css
[ outline Css.none
, Fonts.baseFont
, Css.batch
(if config.isDisabled then
[ color Colors.gray45
, cursor notAllowed
]
else if isInError then
[ color Colors.purple
, cursor pointer
]
else
[ color Colors.navy
, cursor pointer
]
)
, color Colors.navy
, margin zero
, padding zero
, fontSize (px 15)
, Css.property "font-weight" "600"
, display inlineBlock
, displayFlex
, Css.property "transition" "all 0.4s ease"
]
]
[ radioInputIcon
{ isLocked = isLocked
, isDisabled = config.isDisabled
, isChecked = isChecked
{ isLocked = True
, isDisabled = False
, isChecked = False
}
, span
[ css
@ -405,52 +494,23 @@ view { label, name, value, valueToString, selectedValue } attributes =
config.labelCss
]
[ Html.text label ]
, case ( config.premiumDisplay, config.premiumMsg ) of
( PremiumDisplay.Free, _ ) ->
text ""
( PremiumDisplay.PremiumUnlocked, _ ) ->
premiumPennant Nothing
( PremiumDisplay.PremiumLocked, premiumMsg ) ->
premiumPennant premiumMsg
, premiumPennant
]
]
, InputErrorAndGuidanceInternal.view idValue config
]
++ (if isChecked then
disclosedElements
else
[]
)
)
, InputErrorAndGuidanceInternal.view idValue config
]
premiumPennant : Maybe msg -> Html msg
premiumPennant onClick =
case onClick of
Just msg ->
ClickableSvg.button "Premium"
Pennant.premiumFlag
[ ClickableSvg.onClick msg
, ClickableSvg.exactWidth 26
, ClickableSvg.exactHeight 24
, ClickableSvg.css
[ marginLeft (px 8)
, verticalAlign middle
]
]
Nothing ->
Pennant.premiumFlag
|> Nri.Ui.Svg.V1.withWidth (Css.px 26)
|> Nri.Ui.Svg.V1.withHeight (Css.px 24)
|> Nri.Ui.Svg.V1.withCss
[ marginLeft (px 8)
, verticalAlign middle
]
|> Nri.Ui.Svg.V1.toHtml
premiumPennant : Html msg
premiumPennant =
Pennant.premiumFlag
|> Nri.Ui.Svg.V1.withWidth (Css.px 26)
|> Nri.Ui.Svg.V1.withHeight (Css.px 24)
|> Nri.Ui.Svg.V1.withCss
[ marginLeft (px 8)
, verticalAlign middle
]
|> Nri.Ui.Svg.V1.toHtml
radioInputIcon :
@ -548,7 +608,7 @@ unselectedSvg =
]
]
|> Nri.Ui.Svg.V1.fromHtml
|> withImageBorder Colors.gray85
|> withImageBorder Colors.gray75
selectedSvg : Svg
@ -641,7 +701,7 @@ lockedSvg =
]
]
|> Nri.Ui.Svg.V1.fromHtml
|> withImageBorder Colors.gray85
|> withImageBorder Colors.gray75
withImageBorder : Color -> Svg -> Svg

View File

@ -4,7 +4,11 @@ module Nri.Ui.SegmentedControl.V14 exposing
, Positioning(..), Width(..)
)
{-| Changes from V13:
{-| Patch changes:
- use Tooltip.V3 instead of Tooltip.V2
Changes from V13:
- Adds tooltip support to `viewRadioGroup`
@ -18,20 +22,15 @@ import Accessibility.Styled exposing (..)
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Role as Role
import Accessibility.Styled.Style as Style
import Accessibility.Styled.Widget as Widget
import Css exposing (..)
import EventExtras
import Html.Styled
import Html.Styled.Attributes as Attributes exposing (css, href)
import Html.Styled.Attributes as Attributes exposing (css)
import Html.Styled.Events as Events
import Json.Encode as Encode
import Nri.Ui
import Nri.Ui.Colors.Extra exposing (withAlpha)
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as AttributesExtra
import Nri.Ui.Svg.V1 as Svg exposing (Svg)
import Nri.Ui.Tooltip.V2 as Tooltip
import Nri.Ui.Tooltip.V3 as Tooltip
import Nri.Ui.Util exposing (dashify)
import TabsInternal.V2 as TabsInternal

View File

@ -48,14 +48,13 @@ import Html.Styled.Attributes as Attributes exposing (css)
import Html.Styled.Events as Events
import InputErrorAndGuidanceInternal exposing (ErrorState, Guidance)
import InputLabelInternal
import Json.Decode exposing (Decoder)
import Json.Decode
import Nri.Ui
import Nri.Ui.Colors.Extra as ColorsExtra
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.CssVendorPrefix.V1 as VendorPrefixed
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as Extra
import Nri.Ui.Html.V3 exposing (viewJust)
import Nri.Ui.InputStyles.V3 as InputStyles
import Nri.Ui.Util
import SolidColor

View File

@ -1,453 +0,0 @@
module Nri.Ui.SideNav.V1 exposing
( view, Config
, entry, entryWithChildren, html, Entry
, icon, custom, css, nriDescription, testId, id
, onClick
, href, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
, primary, secondary
, premiumLevel
)
{-|
@docs view, Config
@docs entry, entryWithChildren, html, Entry
@docs icon, custom, css, nriDescription, testId, id
## Behavior
@docs onClick
@docs href, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
## Change the color scheme
@docs primary, secondary
## Change the state
@docs premiumLevel
-}
import Accessibility.Styled exposing (..)
import Accessibility.Styled.Style as Style
import ClickableAttributes exposing (ClickableAttributes)
import Css exposing (..)
import Css.Media as Media
import Html.Styled
import Html.Styled.Attributes as Attributes exposing (css)
import Html.Styled.Events as Events
import Nri.Ui
import Nri.Ui.ClickableText.V3 as ClickableText
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Data.PremiumLevel as PremiumLevel exposing (PremiumLevel)
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as ExtraAttributes
import Nri.Ui.Html.V3 exposing (viewJust)
import Nri.Ui.Svg.V1 as Svg exposing (Svg)
import Nri.Ui.UiIcon.V1 as UiIcon
import String exposing (toLower)
import String.Extra exposing (dasherize)
{-| Use `entry` to create a sidebar entry.
-}
type Entry route msg
= Entry (List (Entry route msg)) (EntryConfig route msg)
| Html (List (Html msg))
{-| -}
entry : String -> List (Attribute route msg) -> Entry route msg
entry title attributes =
attributes
|> List.foldl (\(Attribute attribute) b -> attribute b) (build title)
|> Entry []
{-| -}
entryWithChildren : String -> List (Attribute route msg) -> List (Entry route msg) -> Entry route msg
entryWithChildren title attributes children =
attributes
|> List.foldl (\(Attribute attribute) b -> attribute b) (build title)
|> Entry children
{-| -}
html : List (Html msg) -> Entry route msg
html =
Html
{-| -}
type alias Config route msg =
{ userPremiumLevel : PremiumLevel
, isCurrentRoute : route -> Bool
, routeToString : route -> String
, onSkipNav : msg
, css : List Style
}
{-| -}
view : Config route msg -> List (Entry route msg) -> Html msg
view config entries =
styled nav
[ flexBasis (px 250)
, flexShrink (num 0)
, borderRadius (px 8)
, backgroundColor Colors.gray96
, padding (px 20)
, marginRight (px 20)
, batch config.css
]
[]
(viewSkipLink config.onSkipNav
:: List.map (viewSidebarEntry config []) entries
)
viewSkipLink : msg -> Html msg
viewSkipLink onSkip =
ClickableText.button "Skip to main content"
[ ClickableText.icon UiIcon.arrowPointingRight
, ClickableText.small
, ClickableText.css
[ Css.pseudoClass "not(:focus)"
[ Style.invisibleStyle
]
]
, ClickableText.onClick onSkip
]
viewSidebarEntry : Config route msg -> List Css.Style -> Entry route msg -> Html msg
viewSidebarEntry config extraStyles entry_ =
case entry_ of
Entry children entryConfig ->
if PremiumLevel.allowedFor entryConfig.premiumLevel config.userPremiumLevel then
if anyLinkDescendants (isCurrentRoute config) children then
div [ Attributes.css extraStyles ]
(styled span
(sharedEntryStyles
++ [ backgroundColor Colors.gray92
, color Colors.navy
, fontWeight bold
, cursor default
, marginBottom (px 10)
]
)
[]
[ text entryConfig.title ]
:: List.map (viewSidebarEntry config [ marginLeft (px 20) ]) children
)
else
viewSidebarLeaf config extraStyles entryConfig
else
viewLockedEntry extraStyles entryConfig
Html html_ ->
div [ Attributes.css extraStyles ] html_
isCurrentRoute : Config route msg -> EntryConfig route msg -> Bool
isCurrentRoute config { route } =
Maybe.map config.isCurrentRoute route
|> Maybe.withDefault False
anyLinkDescendants : (EntryConfig route msg -> Bool) -> List (Entry route msg) -> Bool
anyLinkDescendants f children =
List.any
(\entry_ ->
case entry_ of
Entry children_ entryConfig ->
f entryConfig || anyLinkDescendants f children_
Html _ ->
False
)
children
viewSidebarLeaf :
Config route msg
-> List Style
-> EntryConfig route msg
-> Html msg
viewSidebarLeaf config extraStyles entryConfig =
let
( linkFunctionName, attributes ) =
ClickableAttributes.toLinkAttributes config.routeToString
entryConfig.clickableAttributes
in
Nri.Ui.styled Html.Styled.a
("Nri-Ui-SideNav-" ++ linkFunctionName)
(sharedEntryStyles
++ extraStyles
++ (if isCurrentRoute config entryConfig then
[ backgroundColor Colors.glacier
, color Colors.navy
, fontWeight bold
, visited [ color Colors.navy ]
]
else
[]
)
++ entryConfig.customStyles
)
(attributes ++ entryConfig.customAttributes)
[ viewJust
(\icon_ ->
icon_
|> Svg.withWidth (px 20)
|> Svg.withHeight (px 20)
|> Svg.withCss [ marginRight (px 5) ]
|> Svg.toHtml
)
entryConfig.icon
, text entryConfig.title
]
viewLockedEntry : List Style -> EntryConfig route msg -> Html msg
viewLockedEntry extraStyles entryConfig =
styled Html.Styled.button
[ batch sharedEntryStyles
, important (color Colors.gray45)
, borderWidth zero
, batch extraStyles
]
(case entryConfig.onLockedContent of
Just event ->
Events.onClick event :: entryConfig.customAttributes
Nothing ->
entryConfig.customAttributes
)
[ UiIcon.premiumLock
|> Svg.withWidth (px 17)
|> Svg.withHeight (px 25)
|> Svg.withCss [ marginRight (px 10), minWidth (px 17) ]
|> Svg.toHtml
, text entryConfig.title
]
sharedEntryStyles : List Style
sharedEntryStyles =
[ paddingLeft (px 20)
, paddingRight (px 20)
, height (px 45)
, displayFlex
, borderRadius (px 8)
, alignItems center
, Fonts.baseFont
, color Colors.navy
, backgroundColor transparent
, textDecoration none
, fontSize (px 15)
, fontWeight (int 600)
, textAlign left
, cursor pointer
]
-- Entry Customization helpers
{-| -}
type alias EntryConfig route msg =
{ icon : Maybe Svg
, title : String
, route : Maybe route
, clickableAttributes : ClickableAttributes route msg
, customAttributes : List (Html.Styled.Attribute msg)
, customStyles : List Style
, premiumLevel : PremiumLevel
, onLockedContent : Maybe msg
}
build : String -> EntryConfig route msg
build title =
{ icon = Nothing
, title = title
, route = Nothing
, clickableAttributes = ClickableAttributes.init
, customAttributes = []
, customStyles = []
, premiumLevel = PremiumLevel.Free
, onLockedContent = Nothing
}
type Attribute route msg
= Attribute (EntryConfig route msg -> EntryConfig route msg)
{-| -}
icon : Svg -> Attribute route msg
icon icon_ =
Attribute (\attributes -> { attributes | icon = Just icon_ })
{-| -}
premiumLevel : PremiumLevel -> msg -> Attribute route msg
premiumLevel level ifLocked =
Attribute
(\attributes ->
{ attributes
| premiumLevel = level
, onLockedContent = Just ifLocked
}
)
{-| Use this helper to add custom attributes.
Do NOT use this helper to add css styles, as they may not be applied the way
you want/expect if underlying Button styles change.
Instead, please use the `css` helper.
-}
custom : List (Html.Styled.Attribute msg) -> Attribute route msg
custom attributes =
Attribute
(\config ->
{ config
| customAttributes = List.append config.customAttributes attributes
}
)
{-| -}
nriDescription : String -> Attribute route msg
nriDescription description =
custom [ ExtraAttributes.nriDescription description ]
{-| -}
testId : String -> Attribute route msg
testId id_ =
custom [ ExtraAttributes.testId id_ ]
{-| -}
id : String -> Attribute route msg
id id_ =
custom [ Attributes.id id_ ]
{-| -}
css : List Style -> Attribute route msg
css styles =
Attribute
(\config ->
{ config
| customStyles = List.append config.customStyles styles
}
)
{-| -}
primary : Attribute route msg
primary =
Attribute (\attributes -> { attributes | customStyles = [] })
{-| -}
secondary : Attribute route msg
secondary =
Attribute
(\attributes ->
{ attributes
| customStyles =
[ backgroundColor Colors.white
, boxShadow3 zero (px 2) Colors.gray75
, border3 (px 1) solid Colors.gray75
]
}
)
-- LINKING, CLICKING, and TRACKING BEHAVIOR
setClickableAttributes :
Maybe route
-> (ClickableAttributes route msg -> ClickableAttributes route msg)
-> Attribute route msg
setClickableAttributes route apply =
Attribute
(\attributes ->
{ attributes
| route =
case route of
Just r ->
Just r
Nothing ->
attributes.route
, clickableAttributes = apply attributes.clickableAttributes
}
)
{-| -}
onClick : msg -> Attribute route msg
onClick msg =
setClickableAttributes Nothing (ClickableAttributes.onClick msg)
{-| -}
href : route -> Attribute route msg
href route =
setClickableAttributes (Just route) (ClickableAttributes.href route)
{-| Use this link for routing within a single page app.
This will make a normal <a> tag, but change the Events.onClick behavior to avoid reloading the page.
See <https://github.com/elm-lang/html/issues/110> for details on this implementation.
-}
linkSpa : route -> Attribute route msg
linkSpa route =
setClickableAttributes (Just route)
(ClickableAttributes.linkSpa route)
{-| -}
linkWithMethod : { method : String, url : route } -> Attribute route msg
linkWithMethod config =
setClickableAttributes (Just config.url)
(ClickableAttributes.linkWithMethod config)
{-| -}
linkWithTracking : { track : msg, url : route } -> Attribute route msg
linkWithTracking config =
setClickableAttributes (Just config.url)
(ClickableAttributes.linkWithTracking config)
{-| -}
linkExternal : String -> Attribute route msg
linkExternal url =
setClickableAttributes Nothing (ClickableAttributes.linkExternal url)
{-| -}
linkExternalWithTracking : { track : msg, url : String } -> Attribute route msg
linkExternalWithTracking config =
setClickableAttributes Nothing (ClickableAttributes.linkExternalWithTracking config)

View File

@ -1,6 +1,8 @@
module Nri.Ui.SideNav.V2 exposing
( view, Config
, entry, entryWithChildren, html, Entry
module Nri.Ui.SideNav.V3 exposing
( view, Config, NavAttribute
, navLabel
, navCss, navNotMobileCss, navMobileCss, navQuizEngineMobileCss
, entry, entryWithChildren, html, Entry, Attribute
, icon, custom, css, nriDescription, testId, id
, onClick
, href, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
@ -8,12 +10,21 @@ module Nri.Ui.SideNav.V2 exposing
, premiumDisplay
)
{-| Changes from V1:
{-|
- Use PremiumDisplay instead of PremiumLevel
@docs view, Config
@docs entry, entryWithChildren, html, Entry
# Changes from V2
- change to `NavAttribute` list-based API
@docs view, Config, NavAttribute
@docs navLabel
@docs navCss, navNotMobileCss, navMobileCss, navQuizEngineMobileCss
## Entries
@docs entry, entryWithChildren, html, Entry, Attribute
@docs icon, custom, css, nriDescription, testId, id
@ -35,9 +46,11 @@ module Nri.Ui.SideNav.V2 exposing
-}
import Accessibility.Styled exposing (..)
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Style as Style
import ClickableAttributes exposing (ClickableAttributes)
import Css exposing (..)
import Css.Media
import Html.Styled
import Html.Styled.Attributes as Attributes
import Html.Styled.Events as Events
@ -48,6 +61,7 @@ import Nri.Ui.Data.PremiumDisplay as PremiumDisplay exposing (PremiumDisplay)
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as ExtraAttributes
import Nri.Ui.Html.V3 exposing (viewJust)
import Nri.Ui.MediaQuery.V1 as MediaQuery
import Nri.Ui.Svg.V1 as Svg exposing (Svg)
import Nri.Ui.UiIcon.V1 as UiIcon
@ -86,23 +100,88 @@ type alias Config route msg =
{ isCurrentRoute : route -> Bool
, routeToString : route -> String
, onSkipNav : msg
, css : List Style
}
{-| -}
view : Config route msg -> List (Entry route msg) -> Html msg
view config entries =
styled nav
type NavAttribute
= NavAttribute (NavAttributeConfig -> NavAttributeConfig)
type alias NavAttributeConfig =
{ navLabel : Maybe String
, css : List Style
}
defaultNavAttributeConfig : NavAttributeConfig
defaultNavAttributeConfig =
{ navLabel = Nothing
, css =
[ flexBasis (px 250)
, flexShrink (num 0)
, borderRadius (px 8)
, backgroundColor Colors.gray96
, padding (px 20)
, marginRight (px 20)
, batch config.css
]
[]
}
{-| Give screenreader users context on what this particular sidenav is for.
-}
navLabel : String -> NavAttribute
navLabel str =
NavAttribute (\config -> { config | navLabel = Just str })
{-| These styles are included automatically in the nav container:
[ flexBasis (px 250)
, flexShrink (num 0)
, borderRadius (px 8)
, backgroundColor Colors.gray96
, padding (px 20)
, marginRight (px 20)
]
-}
navCss : List Style -> NavAttribute
navCss styles =
NavAttribute (\config -> { config | css = List.append config.css styles })
{-| -}
navNotMobileCss : List Style -> NavAttribute
navNotMobileCss styles =
navCss [ Css.Media.withMedia [ MediaQuery.notMobile ] styles ]
{-| -}
navMobileCss : List Style -> NavAttribute
navMobileCss styles =
navCss [ Css.Media.withMedia [ MediaQuery.mobile ] styles ]
{-| -}
navQuizEngineMobileCss : List Style -> NavAttribute
navQuizEngineMobileCss styles =
navCss [ Css.Media.withMedia [ MediaQuery.quizEngineMobile ] styles ]
{-| -}
view : Config route msg -> List NavAttribute -> List (Entry route msg) -> Html msg
view config navAttributes entries =
let
appliedNavAttributes =
List.foldl (\(NavAttribute f) b -> f b) defaultNavAttributeConfig navAttributes
in
styled nav
appliedNavAttributes.css
([ Maybe.map Aria.label appliedNavAttributes.navLabel
]
|> List.filterMap identity
)
(viewSkipLink config.onSkipNav
:: List.map (viewSidebarEntry config []) entries
)
@ -180,7 +259,10 @@ viewSidebarLeaf :
viewSidebarLeaf config extraStyles entryConfig =
let
( linkFunctionName, attributes ) =
ClickableAttributes.toLinkAttributes config.routeToString
ClickableAttributes.toLinkAttributes
{ routeToString = config.routeToString
, isDisabled = False
}
entryConfig.clickableAttributes
in
Nri.Ui.styled Html.Styled.a
@ -239,9 +321,9 @@ viewLockedEntry extraStyles entryConfig =
sharedEntryStyles : List Style
sharedEntryStyles =
[ paddingLeft (px 20)
, paddingRight (px 20)
, height (px 45)
[ padding2 (px 13) (px 20)
, Css.property "word-break" "normal"
, Css.property "overflow-wrap" "anywhere"
, displayFlex
, borderRadius (px 8)
, alignItems center
@ -286,6 +368,7 @@ build title =
}
{-| -}
type Attribute route msg
= Attribute (EntryConfig route msg -> EntryConfig route msg)

View File

@ -1,107 +0,0 @@
module Nri.Ui.Slide.V1 exposing
( AnimationDirection(..)
, withSlidingContents
, animateIn, animateOut
)
{-| Note: You'll almost certainly want to used keyed nodes if you're
using this module.
@docs AnimationDirection
@docs withSlidingContents
@docs animateIn, animateOut
-}
import Css
import Css.Animations
{-| Slide from right to left or from left to right.
-}
type AnimationDirection
= FromRTL
| FromLTR
translateXBy : Float
translateXBy =
700
slideDuration : Css.Style
slideDuration =
Css.animationDuration (Css.ms 700)
slideTimingFunction : Css.Style
slideTimingFunction =
Css.property "animation-timing-function" "ease-in-out"
{-| Add this class to the container whose descendents are sliding.
-}
withSlidingContents : Css.Style
withSlidingContents =
Css.batch
[ Css.position Css.relative
, Css.overflowX Css.hidden
]
{-| Add this style to the element you want to animate in.
-}
animateIn : AnimationDirection -> Css.Style
animateIn direction =
let
( start, end ) =
case direction of
FromRTL ->
( Css.px translateXBy, Css.zero )
FromLTR ->
( Css.px -translateXBy, Css.zero )
in
Css.batch
[ slideDuration
, slideTimingFunction
, Css.property "animation-delay" "-50ms"
, Css.animationName
(Css.Animations.keyframes
[ ( 0, [ Css.Animations.transform [ Css.translateX start ] ] )
, ( 100, [ Css.Animations.transform [ Css.translateX end ] ] )
]
)
]
{-| Add this style to the element you want to animate out.
Note: this will absolutely position the element.
You must add `withSlidingContents` to one of its ancestors.
-}
animateOut : AnimationDirection -> Css.Style
animateOut direction =
let
( start, end ) =
case direction of
FromRTL ->
( Css.zero, Css.px -translateXBy )
FromLTR ->
( Css.zero, Css.px translateXBy )
in
Css.batch
[ Css.position Css.absolute
, Css.transform (Css.translate2 end Css.zero)
, Css.property "animation-delay" "-50ms"
, Css.batch
[ slideDuration
, slideTimingFunction
, Css.animationName
(Css.Animations.keyframes
[ ( 0, [ Css.Animations.transform [ Css.translateX start ] ] )
, ( 100, [ Css.Animations.transform [ Css.translateX end ] ] )
]
)
]
]

View File

@ -1,437 +0,0 @@
module Nri.Ui.SlideModal.V2 exposing
( Config, Panel
, State, closed, open
, view
)
{-|
@docs Config, Panel
@docs State, closed, open
@docs view
-}
import Accessibility.Styled as Html exposing (..)
import Accessibility.Styled.Aria exposing (labelledBy)
import Accessibility.Styled.Role as Role
import Accessibility.Styled.Style
import Accessibility.Styled.Widget as Widget
import Css
import Css.Animations
import Css.Global
import Html.Styled
import Html.Styled.Attributes exposing (css)
import Html.Styled.Events exposing (onClick)
import Html.Styled.Keyed as Keyed
import Nri.Ui
import Nri.Ui.AssetPath exposing (Asset(..))
import Nri.Ui.Button.V10 as Button
import Nri.Ui.Colors.Extra
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Heading.V2 as Heading
import Nri.Ui.Slide.V1 as Slide exposing (AnimationDirection(..))
import SolidColor
{-| -}
type alias Config msg =
{ panels : List Panel
, height : Css.Px
, parentMsg : State -> msg
}
{-| -}
type State
= State
{ currentPanelIndex : Maybe Int
, previousPanel : Maybe ( AnimationDirection, Panel )
}
{-| Create the open state for the modal (the first panel will show).
-}
open : State
open =
State
{ currentPanelIndex = Just 0
, previousPanel = Nothing
}
{-| Close the modal.
-}
closed : State
closed =
State
{ currentPanelIndex = Nothing
, previousPanel = Nothing
}
{-| View the modal (includes the modal backdrop).
-}
view : Config msg -> State -> Html msg
view config ((State { currentPanelIndex }) as state) =
case Maybe.andThen (summarize config.panels) currentPanelIndex of
Just summary ->
viewBackdrop
(viewModal config state summary)
Nothing ->
Html.text ""
type alias Summary =
{ current : Panel
, upcoming : List ( State, String )
, previous : List ( State, String )
}
summarize : List Panel -> Int -> Maybe Summary
summarize panels current =
let
indexedPanels =
List.indexedMap (\i { title } -> ( i, title )) panels
toOtherPanel direction currentPanel ( i, title ) =
( State
{ currentPanelIndex = Just i
, previousPanel =
Just
( direction
, { currentPanel | content = currentPanel.content }
)
}
, title
)
in
case List.drop current panels of
currentPanel :: rest ->
Just
{ current = currentPanel
, upcoming =
indexedPanels
|> List.drop (current + 1)
|> List.map (toOtherPanel FromRTL currentPanel)
, previous =
indexedPanels
|> List.take current
|> List.map (toOtherPanel FromLTR currentPanel)
}
[] ->
Nothing
viewModal : Config msg -> State -> Summary -> Html msg
viewModal config (State { previousPanel }) summary =
Keyed.node "div"
[ css
[ Css.boxSizing Css.borderBox
, Css.margin2 (Css.px 75) Css.auto
, Css.backgroundColor Colors.white
, Css.borderRadius (Css.px 20)
, Css.property "box-shadow" "0 1px 10px 0 rgba(0, 0, 0, 0.35)"
, Slide.withSlidingContents
]
, Role.dialog
, Widget.modal True
, labelledBy (panelId summary.current)
]
(case previousPanel of
Just ( direction, panelView ) ->
( panelId panelView
, panelContainer config.height
[ Slide.animateOut direction ]
[ viewIcon panelView.icon
, Heading.h3 []
[ span [ Html.Styled.Attributes.id (panelId panelView) ] [ Html.text panelView.title ]
]
, viewContent panelView.content
]
)
:: viewModalContent config summary [ Slide.animateIn direction ]
Nothing ->
viewModalContent config summary []
)
viewModalContent : Config msg -> Summary -> List Css.Style -> List ( String, Html msg )
viewModalContent config summary styles =
[ ( panelId summary.current
, panelContainer config.height
styles
[ viewIcon summary.current.icon
, Heading.h3 []
[ span
[ Html.Styled.Attributes.id (panelId summary.current)
, css [ Css.margin2 Css.zero (Css.px 21) ]
]
[ Html.text summary.current.title ]
]
, viewContent summary.current.content
]
)
, ( panelId summary.current ++ "-footer"
, viewActiveFooter summary
|> Html.map config.parentMsg
)
]
viewBackdrop : Html msg -> Html msg
viewBackdrop modal =
Nri.Ui.styled div
"modal-backdrop-container"
(Css.backgroundColor (Nri.Ui.Colors.Extra.withAlpha 0.9 Colors.navy)
:: [ Css.height (Css.vh 100)
, Css.left Css.zero
, Css.overflowX Css.hidden
, -- allow the user to scroll when the content doesn't fit the
-- viewport, but don't display a scrollbar if we don't need it
Css.overflowY Css.visible
, Css.position Css.fixed
, Css.top Css.zero
, Css.width (Css.pct 100)
, Css.zIndex (Css.int 200)
, Css.displayFlex
, Css.alignItems Css.center
, Css.justifyContent Css.center
]
)
[]
[ -- This global <style> node sets overflow to hidden on the body element,
-- thereby preventing the page from scrolling behind the backdrop when the modal is
-- open (and this node is present on the page).
Css.Global.global [ Css.Global.body [ Css.overflow Css.hidden ] ]
, modal
]
{-| Configuration for a single modal view in the sequence of modal views.
-}
type alias Panel =
{ icon : Html Never
, title : String
, content : Html Never
, buttonLabel : String
}
panelContainer : Css.Px -> List Css.Style -> List (Html msg) -> Html msg
panelContainer height animationStyles panel =
div
[ css
[ -- Layout
Css.minHeight (Css.px 400)
, Css.minHeight (Css.px 360)
, Css.maxHeight <| Css.calc (Css.vh 100) Css.minus (Css.px 100)
, Css.height height
, Css.width (Css.px 600)
-- Interior positioning
, Css.displayFlex
, Css.alignItems Css.center
, Css.flexDirection Css.column
, Css.flexWrap Css.noWrap
-- Styles
, Fonts.baseFont
, Css.batch animationStyles
]
]
panel
panelId : Panel -> String
panelId { title } =
"modal-header__" ++ String.replace " " "-" title
viewContent : Html Never -> Html msg
viewContent content =
Nri.Ui.styled div
"modal-content"
[ Css.overflowY Css.auto
, Css.margin2 Css.zero (Css.px 21)
, Css.paddingTop (Css.px 30)
, Css.paddingRight (Css.px 40)
, Css.paddingLeft (Css.px 40)
-- , Css.paddingBottom Css.zero
-- padding bottom used to be 30px. We removed it because it caused
-- scrolling where we didn't want any
, Css.width (Css.pct 100)
, Css.height (Css.pct 100)
, Css.marginBottom Css.auto
, Css.boxSizing Css.borderBox
-- Shadows for indicating that the content is scrollable
, Css.property "background"
"""
/* TOP shadow */
top linear-gradient(to top, rgb(255, 255, 255), rgb(255, 255, 255)) local,
top linear-gradient(to top, rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.15)) scroll,
/* BOTTOM shadow */
bottom linear-gradient(to bottom, rgb(255, 255, 255), rgb(255, 255, 255)) local,
bottom linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.15)) scroll
"""
, Css.backgroundSize2 (Css.pct 100) (Css.px 10)
, Css.backgroundRepeat Css.noRepeat
]
[]
[ Html.map never content ]
viewIcon : Html Never -> Html msg
viewIcon svg =
div
[ css
[ Css.width (Css.px 100)
, Css.height (Css.px 100)
, Css.marginTop (Css.px 35)
, Css.flexShrink Css.zero
, Css.displayFlex
, Css.alignItems Css.center
, Css.justifyContent Css.center
, Css.Global.children
[ Css.Global.svg
[ Css.maxHeight (Css.px 100)
, Css.width (Css.px 100)
]
]
]
]
[ svg ]
|> Html.map never
viewActiveFooter : Summary -> Html State
viewActiveFooter { previous, current, upcoming } =
let
nextPanel =
List.head upcoming
|> Maybe.map Tuple.first
|> Maybe.withDefault closed
dots =
List.map (uncurry Inactive) previous
++ Active
:: List.map (uncurry InactiveDisabled) upcoming
in
viewFlexibleFooter
{ buttonLabel = current.buttonLabel
, buttonMsg = nextPanel
, buttonState = Button.enabled
}
dots
viewFlexibleFooter :
{ buttonLabel : String
, buttonMsg : msg
, buttonState : Button.Attribute msg
}
-> List (Dot msg)
-> Html msg
viewFlexibleFooter { buttonLabel, buttonMsg, buttonState } dotList =
Nri.Ui.styled div
"modal-footer"
[ Css.flexShrink Css.zero
, Css.displayFlex
, Css.flexDirection Css.column
, Css.alignItems Css.center
, Css.margin4 (Css.px 20) Css.zero (Css.px 25) Css.zero
, Css.minHeight (Css.px 125) -- so the footer doesn't compress on Safari
]
[]
[ Button.button buttonLabel
[ Button.onClick buttonMsg
, Button.large
, Button.exactWidth 230
, buttonState
]
, dotList
|> List.map dot
|> div [ css [ Css.marginTop (Css.px 16) ] ]
]
uncurry : (a -> b -> c) -> ( a, b ) -> c
uncurry f ( a, b ) =
f a b
type Dot msg
= Active
| Inactive msg String
| InactiveDisabled msg String
dot : Dot msg -> Html.Html msg
dot type_ =
let
styles ( startColor, endColor ) cursor =
css
[ Css.height (Css.px 10)
, Css.width (Css.px 10)
, Css.borderRadius (Css.px 5)
, Css.margin2 Css.zero (Css.px 2)
, Css.display Css.inlineBlock
, Css.verticalAlign Css.middle
, Css.cursor cursor
-- Color
, Css.animationDuration (Css.ms 600)
, Css.property "animation-timing-function" "linear"
, Css.animationName
(Css.Animations.keyframes
[ ( 0, [ animateBackgroundColor startColor ] )
, ( 100, [ animateBackgroundColor endColor ] )
]
)
, Css.backgroundColor endColor
-- resets
, Css.borderWidth Css.zero
, Css.padding Css.zero
, Css.hover [ Css.outline Css.none ]
]
animateBackgroundColor color =
Nri.Ui.Colors.Extra.fromCssColor color
|> SolidColor.toRGBString
|> Css.Animations.property "background-color"
in
case type_ of
Active ->
Html.div
[ styles ( Colors.gray75, Colors.azure ) Css.auto
]
[]
Inactive goTo title ->
Html.button
[ styles ( Colors.gray75, Colors.gray75 ) Css.pointer
, onClick goTo
]
[ span Accessibility.Styled.Style.invisible
[ text ("Go to " ++ title) ]
]
InactiveDisabled goTo title ->
Html.button
[ styles ( Colors.gray75, Colors.gray75 ) Css.auto
, Html.Styled.Attributes.disabled True
]
[ span Accessibility.Styled.Style.invisible
[ text ("Go to " ++ title) ]
]

View File

@ -5,7 +5,10 @@ module Nri.Ui.SortableTable.V2 exposing
, invariantSort, simpleSort, combineSorters
)
{-|
{-| TODO for next major version:
- make sure the "sort" feature is fully accessible
- use Nri.Ui.UiIcon.V1 sortArrow and Nri.Ui.UiIcon.V1 sortArrowDown icons for the sort indicators
@docs Column, Config, Sorter, State
@docs init, initDescending
@ -15,16 +18,14 @@ module Nri.Ui.SortableTable.V2 exposing
-}
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.Extra exposing (toCssString)
import Nri.Ui.Colors.V1
import Nri.Ui.CssVendorPrefix.V1 as CssVendorPrefix
import Nri.Ui.Table.V5
import SolidColor
import Svg.Styled as Svg exposing (Svg)
import Svg.Styled as Svg
import Svg.Styled.Attributes as SvgAttributes
@ -361,8 +362,3 @@ sortArrow direction active =
[ Svg.polygon [ SvgAttributes.points "0 6 4 0 8 6 0 6" ] []
]
]
toCssString : Css.Color -> String
toCssString =
SolidColor.toRGBString << Nri.Ui.Colors.Extra.fromCssColor

View File

@ -0,0 +1,386 @@
module Nri.Ui.SortableTable.V3 exposing
( Column, Config, Sorter, State
, init, initDescending
, custom, string, view, viewLoading
, invariantSort, simpleSort, combineSorters
)
{-| TODO for next major version:
- add the possibility to pass Aria.sortAscending and Aria.sortDescending attributes to the <th> tag
Changes from V2:
- made column non-sortable (e.g. buttons in a column should not be sorted)
- use a button instead of a clickable div in headers
- use Aria.roleDescription instead of Aria.label in sortable columns headers
- use Nri.Ui.UiIcon.V1 sortArrow and Nri.Ui.UiIcon.V1 sortArrowDown icons for the sort indicators
@docs Column, Config, Sorter, State
@docs init, initDescending
@docs custom, string, view, viewLoading
@docs invariantSort, simpleSort, combineSorters
-}
import Accessibility.Styled.Aria as Aria
import Css exposing (..)
import Html.Styled as Html exposing (Html)
import Html.Styled.Attributes exposing (css)
import Html.Styled.Events
import Nri.Ui.Colors.V1
import Nri.Ui.CssVendorPrefix.V1 as CssVendorPrefix
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Svg.V1
import Nri.Ui.Table.V5
import Nri.Ui.UiIcon.V1
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 : Maybe (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 = Just (simpleSort value)
, width = width
, cellStyles = cellStyles
}
{-| -}
custom :
{ id : id
, header : Html msg
, view : entry -> Html msg
, sorter : Maybe (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.andThen (\(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.sorter /= Nothing) column.header updateMsg state column.id
, view = column.view
, width = Css.px (toFloat column.width)
, cellStyles = column.cellStyles
}
viewSortHeader : Bool -> Html msg -> (State id -> msg) -> State id -> id -> Html msg
viewSortHeader isSortable header updateMsg state id =
let
nextState =
nextTableState state id
in
if isSortable then
Html.button
[ css
[ Css.displayFlex
, Css.alignItems Css.center
, Css.justifyContent Css.spaceBetween
, CssVendorPrefix.property "user-select" "none"
, if state.column == id then
fontWeight bold
else
fontWeight normal
, cursor pointer
-- make this look less "buttony"
, Css.border Css.zero
, Css.backgroundColor Css.transparent
, Css.width (Css.pct 100)
, Css.height (Css.pct 100)
, Css.margin Css.zero
, Css.padding Css.zero
, Fonts.baseFont
, Css.fontSize (Css.em 1)
]
, Html.Styled.Events.onClick (updateMsg nextState)
-- screen readers should know what clicking this button will do
, Aria.roleDescription "sort button"
]
[ Html.div [] [ header ]
, viewSortButton updateMsg state id
]
else
Html.div
[ css [ fontWeight normal ]
]
[ header ]
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 =
let
arrow =
case direction of
Up ->
Nri.Ui.UiIcon.V1.sortArrow
Down ->
Nri.Ui.UiIcon.V1.sortArrowDown
color =
if active then
Nri.Ui.Colors.V1.azure
else
Nri.Ui.Colors.V1.gray75
in
arrow
|> Nri.Ui.Svg.V1.withHeight (px 6)
|> Nri.Ui.Svg.V1.withWidth (px 8)
|> Nri.Ui.Svg.V1.withColor color
|> Nri.Ui.Svg.V1.withCss
[ displayFlex
, margin2 (px 1) zero
]
|> Nri.Ui.Svg.V1.toHtml

View File

@ -14,8 +14,8 @@ module Nri.Ui.Svg.V1 exposing
-}
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Role as Role
import Accessibility.Styled.Widget as Widget
import Css exposing (Color)
import Html.Styled as Html exposing (Html)
import Html.Styled.Attributes as Attributes
@ -87,7 +87,7 @@ withHeight height (Svg record) =
-}
withCss : List Css.Style -> Svg -> Svg
withCss css (Svg record) =
Svg { record | css = css }
Svg { record | css = record.css ++ css }
{-| -}
@ -122,8 +122,8 @@ toHtml (Svg record) =
else
Just (Attributes.css (Css.display Css.inlineBlock :: css))
, Maybe.map Widget.label record.label
|> Maybe.withDefault (Widget.hidden True)
, Maybe.map Aria.label record.label
|> Maybe.withDefault (Aria.hidden True)
|> Just
, Just Role.img
]

View File

@ -1,21 +1,48 @@
module Nri.Ui.Switch.V1 exposing (view, Attribute, onSwitch, disabled, id, label, custom)
module Nri.Ui.Switch.V2 exposing
( view
, Attribute
, selected
, containerCss, labelCss, custom, nriDescription, testId
, onSwitch, disabled
)
{-|
@docs view, Attribute, onSwitch, disabled, id, label, custom
# Changes from V1:
- Fixes invalid ARIA use, [conformance requirements](https://www.w3.org/TR/html-aria/#docconformance)
- labels should only support strings (this is the only way they're actually used in practice)
- extends API to be more consistent with other form/control components
- Use Colors values instead of hardcoded hex strings
- Move the status (selected or not selected) to the list api
- REQUIRE label and id always
- Move custom attributes to the container
- change disabled to take a bool (which I think is the slighty more common pattern)
@docs view
### Attributes
@docs Attribute
@docs selected
@docs containerCss, labelCss, custom, nriDescription, testId
@docs onSwitch, disabled
-}
import Accessibility.Styled as Html exposing (Html)
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Widget as Widget
import Css
import Css exposing (Color, Style)
import Css.Global as Global
import Css.Media
import Html.Styled as WildWildHtml
import Html.Styled.Attributes as Attributes
import Html.Styled.Events as Events
import Nri.Ui.Colors.Extra exposing (toCssString)
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as Extra
import Nri.Ui.Svg.V1 exposing (Svg)
import Svg.Styled as Svg
import Svg.Styled.Attributes as SvgAttributes
@ -23,188 +50,190 @@ import Svg.Styled.Attributes as SvgAttributes
{-| -}
type Attribute msg
= OnSwitch (Bool -> msg)
| Id String
| Label (Html msg)
| Disabled
| Custom (List (Html.Attribute Never))
= Attribute (Config msg -> Config msg)
{-| What is the status of the Switch, selected or not?
-}
selected : Bool -> Attribute msg
selected isSelected =
Attribute <| \config -> { config | isSelected = isSelected }
{-| Specify what happens when the switch is toggled.
-}
onSwitch : (Bool -> msg) -> Attribute msg
onSwitch =
OnSwitch
onSwitch onSwitch_ =
Attribute <| \config -> { config | onSwitch = Just onSwitch_ }
{-| Explicitly specify that you want this switch to be disabled. If you don't
specify `onSwitch`, this is the default, but it's provided so you don't have
to resort to `filterMap` or similar to build a clean list of attributes.
-}
disabled : Attribute msg
disabled =
Disabled
{-| Set the HTML ID of the switch toggle. If you have only one on the page,
you don't need to set this, but you should definitely set it if you have
more than one.
-}
id : String -> Attribute msg
id =
Id
{-| Add labeling text to the switch. This text should be descriptive and
able to be displayed inline. It should _not_ be interactive (if it were
ergonomic to make this argument `Html Never`, we would!)
-}
label : Html msg -> Attribute msg
label =
Label
disabled : Bool -> Attribute msg
disabled isDisabled =
Attribute <| \config -> { config | isDisabled = isDisabled }
{-| Pass custom attributes through to be attached to the underlying input.
Do NOT use this helper to add css styles, as they may not be applied the way
you want/expect if underlying styles change.
Instead, please use `containerCss` or `labelCss`.
-}
custom : List (Html.Attribute Never) -> Attribute msg
custom =
Custom
custom custom_ =
Attribute <| \config -> { config | custom = config.custom ++ custom_ }
{-| -}
nriDescription : String -> Attribute msg
nriDescription description =
custom [ Extra.nriDescription description ]
{-| -}
testId : String -> Attribute msg
testId id_ =
custom [ Extra.testId id_ ]
{-| Adds CSS to the Switch container.
-}
containerCss : List Css.Style -> Attribute msg
containerCss styles =
Attribute <| \config -> { config | containerCss = config.containerCss ++ styles }
{-| Adds CSS to the element containing the label text.
Note that these styles don't apply to the literal HTML label element, since it contains the icon SVG as well.
-}
labelCss : List Css.Style -> Attribute msg
labelCss styles =
Attribute <| \config -> { config | labelCss = config.labelCss ++ styles }
type alias Config msg =
{ onSwitch : Maybe (Bool -> msg)
, id : String
, label : Maybe (Html msg)
, attributes : List (Html.Attribute Never)
, containerCss : List Style
, labelCss : List Style
, isDisabled : Bool
, isSelected : Bool
, custom : List (Html.Attribute Never)
}
defaultConfig : Config msg
defaultConfig =
{ onSwitch = Nothing
, id = "nri-ui-switch-with-default-id"
, label = Nothing
, attributes = []
, containerCss = []
, labelCss = []
, isDisabled = False
, isSelected = False
, custom = []
}
customize : Attribute msg -> Config msg -> Config msg
customize attr config =
case attr of
OnSwitch onSwitch_ ->
{ config | onSwitch = Just onSwitch_ }
Disabled ->
{ config | onSwitch = Nothing }
Id id_ ->
{ config | id = id_ }
Label label_ ->
{ config | label = Just label_ }
Custom custom_ ->
{ config | attributes = custom_ }
{-| Render a switch. The boolean here indicates whether the switch is on
or not.
-}
view : List (Attribute msg) -> Bool -> Html msg
view attrs isOn =
view : { label : String, id : String } -> List (Attribute msg) -> Html msg
view { label, id } attrs =
let
config =
List.foldl customize defaultConfig attrs
List.foldl (\(Attribute update) -> update) defaultConfig attrs
notOperable =
config.onSwitch == Nothing || config.isDisabled
in
WildWildHtml.label
[ Attributes.id (config.id ++ "-container")
, Attributes.css
Html.label
([ Attributes.id (id ++ "-container")
, Attributes.css
[ Css.display Css.inlineFlex
, Css.alignItems Css.center
, Css.position Css.relative
, Css.pseudoClass "focus-within"
[ Global.descendants
[ Global.class "switch-slider"
[ -- azure, but can't use the Color type here
Css.property "stroke" "#146AFF"
[ stroke Colors.azure
, Css.property "stroke-width" "3px"
]
]
]
, Css.cursor
(if config.onSwitch /= Nothing then
Css.pointer
(if notOperable then
Css.notAllowed
else
Css.notAllowed
Css.pointer
)
, Css.batch config.containerCss
]
, Aria.controls config.id
, Widget.checked (Just isOn)
]
, Attributes.for id
]
++ List.map (Attributes.map never) config.custom
)
[ viewCheckbox
{ id = config.id
{ id = id
, onCheck = config.onSwitch
, checked = isOn
, attributes = config.attributes
, isDisabled = config.isDisabled
, selected = config.isSelected
}
, Nri.Ui.Svg.V1.toHtml
(viewSwitch
{ id = config.id
, isOn = isOn
, enabled = config.onSwitch /= Nothing
{ id = id
, isSelected = config.isSelected
, isDisabled = notOperable
}
)
, case config.label of
Just label_ ->
Html.span
[ Attributes.css
[ Css.fontWeight (Css.int 600)
, Css.color Colors.navy
, Css.paddingLeft (Css.px 5)
]
, Attributes.for config.id
]
[ label_ ]
Nothing ->
Html.text ""
, Html.span
[ Attributes.css
[ Css.fontWeight (Css.int 600)
, Css.color Colors.navy
, Css.paddingLeft (Css.px 5)
, Fonts.baseFont
, Css.batch config.labelCss
]
]
[ Html.text label ]
]
viewCheckbox :
{ id : String
, onCheck : Maybe (Bool -> msg)
, checked : Bool
, attributes : List (Html.Attribute Never)
, selected : Bool
, isDisabled : Bool
}
-> Html msg
viewCheckbox config =
Html.checkbox config.id
(Just config.checked)
([ Attributes.id config.id
, Attributes.css
(Just config.selected)
[ Attributes.id config.id
, Attributes.css
[ Css.position Css.absolute
, Css.top (Css.px 10)
, Css.left (Css.px 10)
, Css.zIndex (Css.int 0)
, Css.opacity (Css.num 0)
]
, case config.onCheck of
Just onCheck ->
, case ( config.onCheck, config.isDisabled ) of
( Just onCheck, False ) ->
Events.onCheck onCheck
Nothing ->
Widget.disabled True
]
++ List.map (Attributes.map never) config.attributes
)
_ ->
Aria.disabled True
]
viewSwitch :
{ id : String
, isOn : Bool
, enabled : Bool
, isSelected : Bool
, isDisabled : Bool
}
-> Svg
viewSwitch config =
@ -221,11 +250,11 @@ viewSwitch config =
, SvgAttributes.viewBox "0 0 43 32"
, SvgAttributes.css
[ Css.zIndex (Css.int 1)
, if config.enabled then
Css.opacity (Css.num 1)
, if config.isDisabled then
Css.opacity (Css.num 0.4)
else
Css.opacity (Css.num 0.4)
Css.opacity (Css.num 1)
]
]
[ Svg.defs []
@ -277,7 +306,7 @@ viewSwitch config =
[ Svg.use
[ SvgAttributes.xlinkHref ("#" ++ shadowBoxId)
, SvgAttributes.css
[ if config.isOn then
[ if config.isSelected then
Css.fill Colors.glacier
else
@ -295,7 +324,7 @@ viewSwitch config =
]
, Svg.g
[ SvgAttributes.css
[ if config.isOn then
[ if config.isSelected then
Css.transform (Css.translateX (Css.px 11))
else
@ -309,13 +338,11 @@ viewSwitch config =
, SvgAttributes.r "14.5"
, SvgAttributes.fill "#FFF"
, SvgAttributes.css
[ if config.isOn then
-- azure, but can't use the Color type here
Css.property "stroke" "#146AFF"
[ if config.isSelected then
stroke Colors.azure
else
-- gray75, but can't use the Color type here
Css.property "stroke" "#BFBFBF"
stroke Colors.gray75
, transition "stroke 0.1s"
]
, SvgAttributes.class "switch-slider"
@ -327,12 +354,11 @@ viewSwitch config =
, SvgAttributes.strokeWidth "3"
, SvgAttributes.d "M8 15.865L12.323 20 21.554 10"
, SvgAttributes.css
[ if config.isOn then
-- azure, but can't use the Color type here
Css.property "stroke" "#146AFF"
[ if config.isSelected then
stroke Colors.azure
else
Css.property "stroke" "rgba(255,255,255,0)"
stroke Colors.white
, transition "stroke 0.2s"
]
]
@ -343,6 +369,11 @@ viewSwitch config =
|> Nri.Ui.Svg.V1.fromHtml
stroke : Color -> Style
stroke color =
Css.property "stroke" (toCssString color)
transition : String -> Css.Style
transition transitionRules =
Css.Media.withMediaQuery

View File

@ -1,289 +0,0 @@
module Nri.Ui.Table.V4 exposing
( Column, custom, string
, view, viewWithoutHeader
, viewLoading, viewLoadingWithoutHeader
)
{-| Upgrading from V1:
- All the `width` fields in column configurations now take an elm-css length
value rather than an Integer. Change `width = 100` to `width = px 100` to get
the same widths as before.
- Tables now by default take the full width of the container they are placed in.
If this is not what you want, wrap the table in an element with a fixed width.
- The table module now makes use of `Html.Styled` and no longer exposes a
separate `styles` value.
Check out the [elm-css](http://package.elm-lang.org/packages/rtfeldman/elm-css/14.0.0/Html-Styled)
documentation on Html.Styled to see how to work with it.
- The default cell padding has been removed and content is not vertically
centered in its cell. If you need to overwrite this, wrap your cells in
elements providing custom styling to the cell.
@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
{-| A column that renders some aspect of a value as text
-}
string :
{ header : String
, value : data -> String
, width : LengthOrAuto compatible
}
-> Column data msg
string { header, value, width } =
Column (Html.text header) (value >> Html.text) (Css.width width)
{-| A column that renders however you want it to
-}
custom :
{ header : Html msg
, view : data -> Html msg
, width : LengthOrAuto compatible
}
-> Column data msg
custom options =
Column options.header options.view (Css.width options.width)
-- 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) =
td
[ css (width :: cellStyles)
]
[ 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 ++ cellStyles ++ 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 ]
]
cellStyles : List Style
cellStyles =
[ verticalAlign middle
]
loadingContentStyles : List Style
loadingContentStyles =
[ width (pct 100)
, display inlineBlock
, height (Css.em 1)
, borderRadius (Css.em 1)
, backgroundColor gray75
]
loadingCellStyles : List Style
loadingCellStyles =
[ batch flashAnimation
, padding2 (px 14) (px 10)
]
loadingTableStyles : List Style
loadingTableStyles =
fadeInAnimation
tableStyles : List Style
tableStyles =
[ borderCollapse collapse
, baseFont
, Css.width (Css.pct 100)
]
flash : Css.Animations.Keyframes {}
flash =
Css.Animations.keyframes
[ ( 0, [ Css.Animations.opacity (Css.num 0.6) ] )
, ( 50, [ Css.Animations.opacity (Css.num 0.2) ] )
, ( 100, [ Css.Animations.opacity (Css.num 0.6) ] )
]
fadeIn : Css.Animations.Keyframes {}
fadeIn =
Css.Animations.keyframes
[ ( 0, [ Css.Animations.opacity (Css.num 0) ] )
, ( 100, [ Css.Animations.opacity (Css.num 1) ] )
]
flashAnimation : List Css.Style
flashAnimation =
[ animationName flash
, property "animation-duration" "2s"
, property "animation-iteration-count" "infinite"
, opacity (num 0.6)
]
fadeInAnimation : List Css.Style
fadeInAnimation =
[ animationName fadeIn
, property "animation-duration" "0.4s"
, property "animation-delay" "0.2s"
, property "animation-fill-mode" "forwards"
, animationIterationCount (int 1)
, opacity (num 0)
]

View File

@ -18,10 +18,11 @@ module Nri.Ui.Table.V5 exposing
-}
import Accessibility.Styled.Style as Style
import Css exposing (..)
import Css.Animations
import Html.Styled as Html exposing (..)
import Html.Styled.Attributes exposing (css)
import Html.Styled.Attributes as Attributes exposing (css)
import Nri.Ui.Colors.V1 exposing (..)
import Nri.Ui.Fonts.V1 exposing (baseFont)
@ -141,7 +142,8 @@ stylesLoadingColumn rowIndex colIndex width =
tableWithoutHeader : List Style -> List (Column data msg) -> (a -> Html msg) -> List a -> Html msg
tableWithoutHeader styles columns toRow data =
table styles
[ tableBody toRow data
[ thead [] [ tr Style.invisible (List.map tableRowHeader columns) ]
, tableBody toRow data
]
@ -169,7 +171,8 @@ tableHeader columns =
tableRowHeader : Column data msg -> Html msg
tableRowHeader (Column header _ width _) =
th
[ css (width :: headerStyles)
[ Attributes.scope "col"
, css (width :: headerStyles)
]
[ header ]

View File

@ -16,7 +16,7 @@ module Nri.Ui.Tabs.V6 exposing
-}
import Css exposing (..)
import Html.Styled as Html exposing (Attribute, Html)
import Html.Styled as Html exposing (Html)
import Html.Styled.Attributes as Attributes
import Nri.Ui
import Nri.Ui.Colors.Extra exposing (withAlpha)

View File

@ -7,7 +7,11 @@ module Nri.Ui.Tabs.V7 exposing
, spaHref
)
{-| Changes from V6:
{-| Patch changes:
- use Tooltip.V3 instead of Tooltip.V2
Changes from V6:
- Changes Tab construction to follow attributes-based approach
- Adds tooltip support
@ -22,7 +26,6 @@ module Nri.Ui.Tabs.V7 exposing
-}
import Accessibility.Styled.Aria as Aria
import Css exposing (..)
import Html.Styled as Html exposing (Html)
import Html.Styled.Attributes as Attributes
@ -30,7 +33,7 @@ import Nri.Ui
import Nri.Ui.Colors.Extra exposing (withAlpha)
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Tooltip.V2 as Tooltip
import Nri.Ui.Tooltip.V3 as Tooltip
import TabsInternal.V2 as TabsInternal

View File

@ -54,7 +54,7 @@ You're in the wrong place! Headings live in Nri.Ui.Heading.V2.
import Accessibility.Styled as Html exposing (..)
import Css exposing (..)
import Css.Global exposing (a, descendants)
import Css.Global
import Html.Styled.Attributes as Attributes
import Markdown
import Nri.Ui.Colors.V1 exposing (..)

View File

@ -24,7 +24,7 @@ custom element, or else autosizing will break! This means doing the following:
-}
import Accessibility.Styled.Style
import Css exposing (plus, px)
import Css exposing (px)
import Html.Styled as Html exposing (Html)
import Html.Styled.Attributes as Attributes
import Html.Styled.Events as Events
@ -32,8 +32,6 @@ import Nri.Ui.Html.Attributes.V2 as Extra
import Nri.Ui.InputStyles.V3 as InputStyles
exposing
( Theme(..)
, input
, label
)
import Nri.Ui.Util exposing (dashify, removePunctuation)

View File

@ -51,8 +51,6 @@ module Nri.Ui.TextInput.V7 exposing
-}
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Style as Accessibility
import Css exposing (center, num, position, px, relative, textAlign)
import Css.Global
import Html.Styled as Html exposing (..)
@ -60,12 +58,11 @@ import Html.Styled.Attributes as Attributes exposing (..)
import Html.Styled.Events as Events
import InputErrorAndGuidanceInternal exposing (ErrorState, Guidance)
import InputLabelInternal
import Keyboard.Event exposing (KeyboardEvent)
import Keyboard.Event
import Nri.Ui.ClickableSvg.V2 as ClickableSvg
import Nri.Ui.ClickableText.V3 as ClickableText
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Html.Attributes.V2 as Extra
import Nri.Ui.Html.V3 exposing (viewJust)
import Nri.Ui.InputStyles.V3 as InputStyles exposing (defaultMarginTop)
import Nri.Ui.Svg.V1 as Svg
import Nri.Ui.UiIcon.V1 as UiIcon
@ -585,9 +582,6 @@ view label attributes =
isInError =
InputErrorAndGuidanceInternal.getIsInError config.error
errorMessage_ =
InputErrorAndGuidanceInternal.getErrorMessage config.error
( opacity, disabled_ ) =
case ( config.disabled, config.loading ) of
( False, False ) ->

View File

@ -55,10 +55,9 @@ Example usage:
-}
import Accessibility.Styled as Html exposing (Attribute, Html, text)
import Accessibility.Styled as Html exposing (Attribute, Html)
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Role as Role
import Accessibility.Styled.Widget as Widget
import Css exposing (Color, Style)
import Css.Global as Global
import EventExtras
@ -258,7 +257,7 @@ toggleTip { isOpen, onTrigger, extraButtonAttrs, label } tooltip_ =
)
[]
[ Html.button
([ Widget.label label
([ Aria.label label
, css buttonStyleOverrides
]
++ eventsForTrigger OnHover onTrigger

View File

@ -1,82 +1,57 @@
module Nri.Ui.Tooltip.V2 exposing
( view, toggleTip
module Nri.Ui.Tooltip.V3 exposing
( view, viewToggleTip
, Attribute
, plaintext, html
, withoutTail
, onTop, onBottom, onLeft, onRight
, onTopForMobile, onBottomForMobile, onLeftForMobile, onRightForMobile
, alignStart, alignMiddle, alignEnd
, alignStartForMobile, alignMiddleForMobile, alignEndForMobile
, exactWidth, fitToContent
, smallPadding, normalPadding, customPadding
, onClick, onHover
, onToggle
, open
, css, containerCss
, custom, customTriggerAttributes
, css, notMobileCss, mobileCss, quizEngineMobileCss, containerCss
, custom
, nriDescription, testId
, primaryLabel, auxillaryDescription
, primaryLabel, auxiliaryDescription, disclosure
)
{-| Known issues:
{-| Changes from V2:
- tooltips with focusable content (e.g., a link) will not handle focus correctly for
keyboard-only users when using the onHover attribute
- Support `disclosure` pattern for rich-content tooltips
- render tooltip content in the DOM when closed (now, they're hidden with display:none)
- tooltips MUST be closable via keyboard without moving focus. [Understanding Success Criterion 1.4.13: Content on Hover or Focus](https://www.w3.org/WAI/WCAG21/Understanding/content-on-hover-or-focus.html)
- remove onClick helper
- prefer the accessible name to using aria-labelledby and aria-label together
- :skull: remove customTooltipAttributes
- change `css` to extend the current list of styles, NOT override them entirely.
- fix spelling of "auxillary" to "auxiliary"
- toggleTip -> viewToggleTip
- Adds notMobileCss, mobileCss, quizEngineMobileCss
- onHover -> onToggle
Post-release patches:
These tooltips aim to follow the accessibility recommendations from:
- fix overlay for onClick toolTip having a border
- mark customTriggerAttributes as deprecated
- add containerCss
- adds `nriDescription` and `testId`
- fix <https://github.com/NoRedInk/noredink-ui/issues/766>
- <https://inclusive-components.design/tooltips-toggletips>
- <https://sarahmhigley.com/writing/tooltips-in-wcag-21/>
Changes from V1:
- {Position, withPosition} -> {onTop, onBottom, onLeft, onRight}
- withTooltipStyleOverrides -> css
- {Width, withWidth} -> {exactWidth, fitToContent}
- {Padding, withPadding} -> {smallPadding, normalPadding}
- adds customPadding
- adds custom for custom attributes
- adds plaintext, html helpers for setting the content
- pass a list of attributes rather than requiring a pipeline to set up the tooltip
- move Trigger into the attributes
- change primaryLabel and auxillaryDescription to attributes, adding view
- move the onTrigger event to the attributes
- extraButtonAttrs becomes attribute `customTriggerAttributes`
- isOpen field becomes the `open` attribute
- fold toggleTip and view into each other, so there's less to maintain
- adds withoutTail
These tooltips follow the accessibility recommendations from: <https://inclusive-components.design/tooltips-toggletips>
Example usage:
Tooltip.view
{ trigger =
\attrs ->
ClickableText.button "Click me to open the tooltip"
[ ClickableText.custom attrs ]
, id = "my-tooltip"
}
[ Tooltip.plaintext "Gradebook"
, Tooltip.primaryLabel
, Tooltip.onClick MyOnTriggerMsg
, Tooltip.open True
]
@docs view, toggleTip
@docs view, viewToggleTip
@docs Attribute
@docs plaintext, html
@docs withoutTail
@docs onTop, onBottom, onLeft, onRight
@docs onTopForMobile, onBottomForMobile, onLeftForMobile, onRightForMobile
@docs alignStart, alignMiddle, alignEnd
@docs alignStartForMobile, alignMiddleForMobile, alignEndForMobile
@docs exactWidth, fitToContent
@docs smallPadding, normalPadding, customPadding
@docs onClick, onHover
@docs onToggle
@docs open
@docs css, containerCss
@docs custom, customTriggerAttributes
@docs css, notMobileCss, mobileCss, quizEngineMobileCss, containerCss
@docs custom
@docs nriDescription, testId
@docs primaryLabel, auxillaryDescription
@docs primaryLabel, auxiliaryDescription, disclosure
-}
@ -86,17 +61,19 @@ import Accessibility.Styled.Key as Key
import Accessibility.Styled.Role as Role
import Css exposing (Color, Px, Style)
import Css.Global as Global
import EventExtras
import Css.Media
import Html.Styled as Root
import Html.Styled.Attributes as Attributes
import Html.Styled.Events as Events
import Json.Encode as Encode
import Nri.Ui
import Nri.Ui.ClickableSvg.V2 as ClickableSvg
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as ExtraAttributes
import Nri.Ui.MediaQuery.V1 as MediaQuery
import Nri.Ui.Shadows.V1 as Shadows
import Nri.Ui.UiIcon.V1 as UiIcon
import Nri.Ui.WhenFocusLeaves.V1 as WhenFocusLeaves
import String.Extra
@ -108,6 +85,8 @@ type Attribute msg
type alias Tooltip msg =
{ direction : Direction
, alignment : Alignment
, mobileDirection : Direction
, mobileAlignment : Alignment
, tail : Tail
, content : List (Html msg)
, attributes : List (Html.Attribute Never)
@ -129,6 +108,8 @@ buildAttributes =
defaultTooltip =
{ direction = OnTop
, alignment = Middle
, mobileDirection = OnTop
, mobileAlignment = Middle
, tail = WithTail
, content = []
, attributes = []
@ -227,6 +208,51 @@ alignEnd position =
withAligment (End position)
withMobileAligment : Alignment -> Attribute msg
withMobileAligment alignment =
Attribute (\config -> { config | mobileAlignment = alignment })
{-| Put the tail at the "start" of the tooltip when the viewport has a mobile width.
For onTop & onBottom tooltips, this means "left".
For onLeft & onRight tooltip, this means "top".
__________
|_ ______|
\/
-}
alignStartForMobile : Px -> Attribute msg
alignStartForMobile position =
withMobileAligment (Start position)
{-| Put the tail at the "middle" of the tooltip when the viewport has a mobile width. This is the default behavior.
__________
|___ ____|
\/
-}
alignMiddleForMobile : Attribute msg
alignMiddleForMobile =
withMobileAligment Middle
{-| Put the tail at the "end" of the tooltip when the viewport has a mobile width.
For onTop & onBottom tooltips, this means "right".
For onLeft & onRight tooltip, this means "bottom".
__________
|______ _|
\/
-}
alignEndForMobile : Px -> Attribute msg
alignEndForMobile position =
withMobileAligment (End position)
{-| Where should this tooltip be positioned relative to the trigger?
-}
type Direction
@ -292,12 +318,106 @@ onLeft =
withPosition OnLeft
{-| Set some custom styles on the tooltip. These will be treated as overrides,
so be careful!
withPositionForMobile : Direction -> Attribute msg
withPositionForMobile direction =
Attribute (\config -> { config | mobileDirection = direction })
{-| Set the position of the tooltip when the mobile breakpoint applies.
__________
| |
|___ ____|
\/
-}
onTopForMobile : Attribute msg
onTopForMobile =
withPositionForMobile OnTop
{-| Set the position of the tooltip when the mobile breakpoint applies.
__________
| |
< |
|_________|
-}
onRightForMobile : Attribute msg
onRightForMobile =
withPositionForMobile OnRight
{-| Set the position of the tooltip when the mobile breakpoint applies.
___/\_____
| |
|_________|
-}
onBottomForMobile : Attribute msg
onBottomForMobile =
withPositionForMobile OnBottom
{-| Set the position of the tooltip when the mobile breakpoint applies.
__________
| |
| >
|_________|
-}
onLeftForMobile : Attribute msg
onLeftForMobile =
withPositionForMobile OnLeft
{-| Set some custom styles on the tooltip.
-}
css : List Style -> Attribute msg
css tooltipStyleOverrides =
Attribute (\config -> { config | tooltipStyleOverrides = tooltipStyleOverrides })
Attribute (\config -> { config | tooltipStyleOverrides = config.tooltipStyleOverrides ++ tooltipStyleOverrides })
{-| Set styles that will only apply if the viewport is wider than NRI's mobile breakpoint.
Equivalent to:
Tooltip.css
[ Css.Media.withMedia [ Nri.Ui.MediaQuery.V1.notMobile ] styles ]
-}
notMobileCss : List Style -> Attribute msg
notMobileCss styles =
css [ Css.Media.withMedia [ MediaQuery.notMobile ] styles ]
{-| Set styles that will only apply if the viewport is narrower than NRI's mobile breakpoint.
Equivalent to:
Tooltip.css
[ Css.Media.withMedia [ Nri.Ui.MediaQuery.V1.mobile ] styles ]
-}
mobileCss : List Style -> Attribute msg
mobileCss styles =
css [ Css.Media.withMedia [ MediaQuery.mobile ] styles ]
{-| Set styles that will only apply if the viewport is narrower than NRI's quiz-engine-specific mobile breakpoint.
Equivalent to:
Tooltip.css
[ Css.Media.withMedia [ Nri.Ui.MediaQuery.V1.quizEngineMobile ] styles ]
-}
quizEngineMobileCss : List Style -> Attribute msg
quizEngineMobileCss styles =
css [ Css.Media.withMedia [ MediaQuery.quizEngineMobile ] styles ]
{-| Use this helper to add custom attributes.
@ -324,13 +444,6 @@ testId id_ =
custom [ ExtraAttributes.testId id_ ]
{-| DEPRECATED -- a future release will remove this helper.
-}
customTriggerAttributes : List (Html.Attribute msg) -> Attribute msg
customTriggerAttributes attributes =
Attribute (\config -> { config | triggerAttributes = config.triggerAttributes ++ attributes })
{-| -}
containerCss : List Style -> Attribute msg
containerCss styles =
@ -412,31 +525,32 @@ customPadding value =
type Trigger msg
= OnHover (Bool -> msg)
| OnClick (Bool -> msg)
{-| The tooltip opens when hovering over the trigger element, and closes when the hover stops.
{-| The Tooltip event cycle depends on whether you're following the Disclosure pattern, but disguising the Disclosure as a tooltip visually or you're actually adding a hint or label for sighted users.
If you're adding a tooltip to an element that _does_ something on its own, e.g., a "Print" ClickableSvg, then it doesn't make sense for the tooltip to change state on click/enter/space.
However, if you're adding a tooltip to an element that is not interactive at all if you don't count the tooltip, then we can use the click/enter/space events to manage the tooltip state too. This style of "tooltip" is the only kind that will be accessible for touch users on mobile -- it's important to get the access pattern right!
If the tooltip behavior you're seeing doesn't _feel_ quite right, consider whether you need to change tooltip "types" to `disclosure` or to `auxiliaryDescription`.
-}
onHover : (Bool -> msg) -> Attribute msg
onHover msg =
onToggle : (Bool -> msg) -> Attribute msg
onToggle msg =
Attribute (\config -> { config | trigger = Just (OnHover msg) })
{-| The tooltip opens when clicking the root element, and closes when anything but the tooltip is clicked again.
-}
onClick : (Bool -> msg) -> Attribute msg
onClick msg =
Attribute (\config -> { config | trigger = Just (OnClick msg) })
type Purpose
= PrimaryLabel
| AuxillaryDescription
| Disclosure { triggerId : String, lastId : Maybe String }
{-| Used when the content of the tooltip is the "primary label" for its content, for example,
when the trigger content is an icon. The tooltip content will supercede the content of the trigger
HTML for screen readers.
{-| Used when the content of the tooltip is identical to the accessible name.
For example, when using the Tooltip component with the ClickableSvg component, the Tooltip is providing
extra information to sighted users that screenreader users already have.
This is the default.
@ -446,14 +560,37 @@ primaryLabel =
Attribute (\config -> { config | purpose = PrimaryLabel })
{-| Used when the content of the tooltip provides an "auxillary description" for its content.
{-| Used when the content of the tooltip provides an "auxiliary description" for its content.
An auxiliary description is used when the tooltip content provides supplementary information about its trigger content.
-}
auxillaryDescription : Attribute msg
auxillaryDescription =
auxiliaryDescription : Attribute msg
auxiliaryDescription =
Attribute (\config -> { config | purpose = AuxillaryDescription })
{-| -}
{-| Sometimes a "tooltip" only _looks_ like a tooltip, but is really more about hiding and showing extra information when the user asks for it.
If clicking the "tooltip trigger" only ever shows you more info (and especially if this info is rich or interactable), use this attribute.
For more information, please read [Sarah Higley's "Tooltips in the time of WCAG 2.1" post](https://sarahmhigley.com/writing/tooltips-in-wcag-21).
You will need to pass in the last focusable element in the disclosed content in order for:
- any focusable elements in the disclosed content to be keyboard accessible
- the disclosure to close appropriately when the user tabs past all of the disclosed content
You may pass a lastId of Nothing if there is NO focusable content within the disclosure.
-}
disclosure : { triggerId : String, lastId : Maybe String } -> Attribute msg
disclosure exitFocusManager =
Attribute (\config -> { config | purpose = Disclosure exitFocusManager })
{-| Pass a bool indicating whether the tooltip should be open or closed.
-}
open : Bool -> Attribute msg
open isOpen =
Attribute (\config -> { config | isOpen = isOpen })
@ -476,12 +613,18 @@ view config attributes =
{-| Supplementary information triggered by a "?" icon.
This is a helper for setting up a commonly-used `disclosure` tooltip. Please see the documentation for `disclosure` to learn more.
-}
toggleTip : { label : String } -> List (Attribute msg) -> Html msg
toggleTip { label } attributes_ =
viewToggleTip : { label : String, lastId : Maybe String } -> List (Attribute msg) -> Html msg
viewToggleTip { label, lastId } attributes_ =
let
id =
String.Extra.dasherize label
triggerId =
"tooltip-trigger__" ++ id
in
view
{ trigger =
@ -491,6 +634,7 @@ toggleTip { label } attributes_ =
[ ClickableSvg.exactWidth 20
, ClickableSvg.exactHeight 20
, ClickableSvg.custom events
, ClickableSvg.id triggerId
, ClickableSvg.css
[ -- Take up enough room within the document flow
Css.margin (Css.px 5)
@ -502,6 +646,7 @@ toggleTip { label } attributes_ =
[ Attributes.class "Nri-Ui-Tooltip-V2-ToggleTip"
, Attributes.id id
]
:: disclosure { triggerId = triggerId, lastId = lastId }
:: attributes_
)
@ -520,25 +665,32 @@ viewTooltip_ { trigger, id } tooltip =
let
( containerEvents, buttonEvents ) =
case tooltip.trigger of
Just (OnClick msg) ->
( []
, [ EventExtras.onClickStopPropagation
(msg (not tooltip.isOpen))
]
)
Just (OnHover msg) ->
( [ Events.onMouseEnter (msg True)
, Events.onMouseLeave (msg False)
]
, [ Events.onFocus (msg True)
case tooltip.purpose of
Disclosure { triggerId, lastId } ->
( [ Events.onMouseEnter (msg True)
, Events.onMouseLeave (msg False)
, WhenFocusLeaves.toAttribute
{ firstId = triggerId
, lastId = Maybe.withDefault triggerId lastId
, tabBackAction = msg False
, tabForwardAction = msg False
}
]
, [ Events.onClick (msg (not tooltip.isOpen))
, Key.onKeyDown [ Key.escape (msg False) ]
]
)
-- TODO: this blur event means that we cannot focus links
-- that are within the tooltip without a mouse
, Events.onBlur (msg False)
, Events.onClick (msg True)
]
)
_ ->
( [ Events.onMouseEnter (msg True)
, Events.onMouseLeave (msg False)
]
, [ Events.onFocus (msg True)
, Events.onBlur (msg False)
, Key.onKeyDown [ Key.escape (msg False) ]
]
)
Nothing ->
( [], [] )
@ -560,30 +712,24 @@ viewTooltip_ { trigger, id } tooltip =
]
]
[ trigger
((if tooltip.isOpen then
case tooltip.purpose of
PrimaryLabel ->
Aria.labeledBy id
((case tooltip.purpose of
PrimaryLabel ->
[-- The content should already have an accessible name.
]
AuxillaryDescription ->
Aria.describedBy [ id ]
AuxillaryDescription ->
[ Aria.describedBy [ id ] ]
else
-- when our tooltips are closed, they're not rendered in the
-- DOM. This means that the ID references above would be
-- invalid and jumping to a reference would not work, so we
-- skip labels and descriptions if the tooltip is closed.
Attributes.property "data-closed-tooltip" Encode.null
Disclosure _ ->
[ Aria.expanded tooltip.isOpen
, Aria.controls [ id ]
]
)
:: buttonEvents
++ buttonEvents
++ tooltip.triggerAttributes
)
, hoverBridge tooltip
]
, viewOverlay tooltip
-- Popout is rendered after the overlay, to allow client code to give it
-- priority when clicking by setting its position
, viewTooltip id tooltip
]
@ -641,21 +787,31 @@ hoverBridge { isOpen, direction } =
viewTooltip : String -> Tooltip msg -> Html msg
viewTooltip tooltipId config =
if config.isOpen then
viewOpenTooltip tooltipId config
else
text ""
viewOpenTooltip : String -> Tooltip msg -> Html msg
viewOpenTooltip tooltipId config =
Html.div
[ Attributes.css
[ Css.position Css.absolute
, positionTooltip config.direction config.alignment
, Css.Media.withMedia [ MediaQuery.notMobile ]
(positionTooltip config.direction config.alignment)
, Css.Media.withMedia [ MediaQuery.mobile ]
(positionTooltip config.mobileDirection config.mobileAlignment)
, Css.boxSizing Css.borderBox
, if config.isOpen then
Css.batch []
else
Css.display Css.none
]
, -- Used for tests, since the visibility is controlled via CSS, which elm-program-test cannot account for
Attributes.attribute "data-tooltip-visible" <|
if config.isOpen then
"true"
else
"false"
, -- If the tooltip is the "primary label" for the content, then we can trust that the content
-- in the tooltip is redundant. For example, if we have a ClickableSvg "Print" button, the button will
-- *already have* an accessible name. It is not helpful to have the "Print" read out twice.
Aria.hidden (config.purpose == PrimaryLabel)
]
[ Html.div
([ Attributes.css
@ -670,10 +826,36 @@ viewOpenTooltip tooltipId config =
, paddingToStyle config.padding
, Css.position Css.absolute
, Css.zIndex (Css.int 100)
, Css.backgroundColor Colors.navy
, Css.border3 (Css.px 1) Css.solid Colors.navy
, Css.Media.withMedia [ MediaQuery.notMobile ]
[ positioning config.direction config.alignment
, case config.tail of
WithTail ->
tailForDirection config.direction
WithoutTail ->
Css.batch []
]
, Css.Media.withMedia [ MediaQuery.mobile ]
[ positioning config.mobileDirection config.mobileAlignment
, case config.tail of
WithTail ->
tailForDirection config.mobileDirection
WithoutTail ->
Css.batch []
]
, Fonts.baseFont
, Css.fontSize (Css.px 16)
, Css.fontWeight (Css.int 600)
, Css.color Colors.white
, Shadows.high
, Global.descendants [ Global.a [ Css.textDecoration Css.underline ] ]
, Global.descendants [ Global.a [ Css.color Colors.white ] ]
]
++ config.tooltipStyleOverrides
)
, pointerBox config.tail config.direction config.alignment
-- We need to keep this animation in tests to make it pass: check out
-- the NoAnimations middleware. So if you change the name here, please
@ -703,9 +885,9 @@ offCenterOffset =
20
{-| This returns an absolute positioning style attribute for the popout container for a given tail position.
{-| This returns absolute positioning styles for the popout container for a given tail position.
-}
positionTooltip : Direction -> Alignment -> Style
positionTooltip : Direction -> Alignment -> List Style
positionTooltip direction alignment =
let
ltrPosition =
@ -730,85 +912,26 @@ positionTooltip direction alignment =
End customOffset ->
Css.bottom customOffset
in
Css.batch <|
case direction of
OnTop ->
[ ltrPosition
, Css.top (Css.calc (Css.px (negate tailSize)) Css.minus (Css.px 2))
]
OnBottom ->
[ ltrPosition
, Css.bottom (Css.calc (Css.px (negate tailSize)) Css.minus (Css.px 2))
]
OnLeft ->
[ topToBottomPosition
, Css.left (Css.calc (Css.px (negate tailSize)) Css.minus (Css.px 2))
]
OnRight ->
[ topToBottomPosition
, Css.right (Css.calc (Css.px (negate tailSize)) Css.minus (Css.px 2))
]
pointerBox : Tail -> Direction -> Alignment -> Html.Attribute msg
pointerBox tail direction alignment =
Attributes.css
[ Css.backgroundColor Colors.navy
, Css.border3 (Css.px 1) Css.solid Colors.navy
, positioning direction alignment
, case tail of
WithTail ->
tailForDirection direction
WithoutTail ->
Css.batch []
, Fonts.baseFont
, Css.fontSize (Css.px 16)
, Css.fontWeight (Css.int 600)
, Css.color Colors.white
, Css.property "box-shadow" "0 1px 1px hsl(0deg 0% 0% / 0.075), 0 2px 2px hsl(0deg 0% 0% / 0.075), 0 4px 4px hsl(0deg 0% 0% / 0.075), 0 8px 8px hsl(0deg 0% 0% / 0.075), 0 16px 16px hsl(0deg 0% 0% / 0.075)"
, Global.descendants [ Global.a [ Css.textDecoration Css.underline ] ]
, Global.descendants [ Global.a [ Css.color Colors.white ] ]
]
viewOverlay : Tooltip msg -> Html msg
viewOverlay { isOpen, trigger } =
case ( isOpen, trigger ) of
( True, Just (OnClick msg) ) ->
-- if we display the click-to-close overlay on hover, you will have to
-- close the overlay by moving the mouse out of the window or clicking.
viewCloseTooltipOverlay (msg False)
_ ->
text ""
viewCloseTooltipOverlay : msg -> Html msg
viewCloseTooltipOverlay msg =
Html.button
[ Attributes.css
[ Css.width (Css.pct 100)
, -- ancestor uses transform property, which interacts with
-- position: fixed, forcing this hack.
-- https://www.w3.org/TR/css-transforms-1/#propdef-transform
Css.height (Css.calc (Css.px 1000) Css.plus (Css.calc (Css.pct 100) Css.plus (Css.px 1000)))
, Css.left Css.zero
, Css.top (Css.px -1000)
, Css.cursor Css.pointer
, Css.position Css.fixed
, Css.zIndex (Css.int 90) -- TODO: From Nri.ZIndex in monolith, bring ZIndex here?
, Css.backgroundColor Css.transparent
, Css.border Css.zero
, Css.outline Css.none
case direction of
OnTop ->
[ ltrPosition
, Css.top (Css.calc (Css.px (negate tailSize)) Css.minus (Css.px 2))
]
OnBottom ->
[ ltrPosition
, Css.bottom (Css.calc (Css.px (negate tailSize)) Css.minus (Css.px 2))
]
OnLeft ->
[ topToBottomPosition
, Css.left (Css.calc (Css.px (negate tailSize)) Css.minus (Css.px 2))
]
OnRight ->
[ topToBottomPosition
, Css.right (Css.calc (Css.px (negate tailSize)) Css.minus (Css.px 2))
]
, EventExtras.onClickStopPropagation msg
, Key.tabbable False
]
[]

View File

@ -1,19 +1,21 @@
module Nri.Ui.UiIcon.V1 exposing
( seeMore, openClose, download, sort, gear, flipper, sortArrow
( seeMore, openClose, download, sort, gear, flipper
, archive, unarchive
, playInCircle, pauseInCircle, stopInCircle, skip
, playInCircle, pauseInCircle, stopInCircle
, play, skip
, share, preview, copyToClipboard, gift
, activity
, footsteps, compass, speedometer, bulb, help, checklist
, footsteps, compass, speedometer, help, checklist, checklistComplete
, sparkleBulb, baldBulb, bulb
, hat, keychain
, sprout, sapling, tree
, person, couple, class, leaderboard, performance
, calendar, clock
, emptyCalendar, calendar, clock
, missingDocument, document, documents, newspaper, openBook, openBooks
, edit, pen, highlighter
, speechBalloon, mail
, arrowTop, arrowRight, arrowDown, arrowLeft, arrowPointingRight, arrowPointingRightThick
, checkmark, checkmarkInCircle, x
, arrowTop, arrowRight, arrowDown, arrowLeft, arrowPointingRight, arrowPointingRightThick, sortArrow, sortArrowDown
, checkmark, checkmarkInCircle, checkmarkInCircleInverse, emptyCircle, x, xInCircle
, attention, exclamation
, flag, star, starFilled, starOutline
, equals, plus, null
@ -24,25 +26,28 @@ module Nri.Ui.UiIcon.V1 exposing
, search, searchInCicle
, openQuotationMark, closeQuotationMark
, microscope, scale
, openInNewTab, sync
)
{-| How to add new icons: <https://paper.dropbox.com/doc/How-to-create-a-new-SVG-icon-for-use-in-Elm--Ay9uhSLfGUAix0ERIiJ0Dm8dAg-8WNqtARdr4EgjmYEHPeYD>
@docs seeMore, openClose, download, sort, gear, flipper, sortArrow
@docs seeMore, openClose, download, sort, gear, flipper
@docs archive, unarchive
@docs playInCircle, pauseInCircle, stopInCircle, skip
@docs playInCircle, pauseInCircle, stopInCircle
@docs play, skip
@docs share, preview, copyToClipboard, gift
@docs activity
@docs footsteps, compass, speedometer, bulb, help, checklist
@docs footsteps, compass, speedometer, help, checklist, checklistComplete
@docs sparkleBulb, baldBulb, bulb
@docs hat, keychain
@docs sprout, sapling, tree
@docs person, couple, class, leaderboard, performance
@docs calendar, clock
@docs emptyCalendar, calendar, clock
@docs missingDocument, document, documents, newspaper, openBook, openBooks
@docs edit, pen, highlighter
@docs speechBalloon, mail
@docs arrowTop, arrowRight, arrowDown, arrowLeft, arrowPointingRight, arrowPointingRightThick
@docs checkmark, checkmarkInCircle, x
@docs arrowTop, arrowRight, arrowDown, arrowLeft, arrowPointingRight, arrowPointingRightThick, sortArrow, sortArrowDown
@docs checkmark, checkmarkInCircle, checkmarkInCircleInverse, emptyCircle, x, xInCircle
@docs attention, exclamation
@docs flag, star, starFilled, starOutline
@docs equals, plus, null
@ -53,6 +58,7 @@ module Nri.Ui.UiIcon.V1 exposing
@docs search, searchInCicle
@docs openQuotationMark, closeQuotationMark
@docs microscope, scale
@docs openInNewTab, sync
import Html.Styled exposing (..)
import Nri.Ui.Colors.V1 as Colors
@ -211,7 +217,7 @@ performance =
[ Attributes.width "100%"
, Attributes.height "100%"
, Attributes.fill "currentcolor"
, Attributes.viewBox "0 0 30 30"
, Attributes.viewBox "0 0 25 25"
]
[ Svg.path
[ Attributes.d "M2.575,22.5 L2.55333333,2.47096774 L2.55333333,5.68434189e-14 L1.53166667,5.68434189e-14 C0.275833333,5.68434189e-14 0,0.345967742 0,1.48225806 L0.0216666667,23.4887097 C0.0216666667,24.7185484 0.3275,24.9709677 1.55333333,24.9709677 L23.4891667,24.9709677 C24.7191667,24.9709677 25.0216667,24.7483871 25.0216667,23.4887097 L25.0216667,22.5 L22.4675,22.5 L2.575,22.5 Z" ]
@ -309,6 +315,34 @@ sort =
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
emptyCalendar : Nri.Ui.Svg.V1.Svg
emptyCalendar =
Svg.svg
[ Attributes.width "100%"
, Attributes.height "100%"
, Attributes.viewBox "0 0 23 25"
]
[ Svg.g
[ Attributes.stroke "none"
, Attributes.strokeWidth "1"
, Attributes.fill "none"
, Attributes.fillRule "evenodd"
]
[ Svg.g
[ Attributes.transform "translate(-1.000000, 0.000000)"
, Attributes.fill "currentcolor"
]
[ Svg.path
[ Attributes.d "M24,6.06748352 L24,22.38807 C23.8127484,23.6415842 22.9490956,24.6604992 21.8248757,25 L3.21296367,25 C1.93924203,24.615352 1,23.3585754 1,21.876587 L1,6.57896739 C1,4.79325 2.36367767,3.33452174 4.0330319,3.33452174 L7.14841914,3.33452174 L7.14841914,0.86348913 C7.14841914,0.387695652 7.51085533,0 7.95564466,0 C8.40043508,0 8.76287128,0.387695652 8.76287128,0.86348913 L8.76287128,3.33452174 L16.2749681,3.33452174 L16.2749681,0.86348913 C16.2749681,0.387695652 16.6374043,0 17.0821914,0 C17.5269786,0 17.8894147,0.387695652 17.8894147,0.86348913 L17.8894147,3.33452174 L21.0048086,3.33452174 C22.5115487,3.33452174 23.7692689,4.52290311 24,6.06748352 Z M4.0330319,5.05952174 L7.14841914,5.05952174 L7.14841914,6.25 C7.14841914,6.72619565 7.51085533,7.11348913 7.95564466,7.11348913 C8.40043508,7.11348913 8.76287128,6.72619565 8.76287128,6.25 L8.76287128,5.05952174 L16.2475214,5.05952174 L16.2475214,6.25 C16.2475214,6.72619565 16.6095852,7.11348913 17.0547447,7.11348913 C17.4995319,7.11348913 17.8615957,6.72619565 17.8615957,6.25 L17.8615957,5.05952174 L20.9773509,5.05952174 C21.7567552,5.05952174 22.3959362,5.74483696 22.3959362,6.57738043 L22.3959362,9.46348913 L2.61445214,9.46348913 L2.61445214,6.57738043 C2.61445214,5.74483696 3.25325852,5.05952174 4.0330319,5.05952174 Z M21.0048086,23.3944446 L4.0330319,23.3944446 C3.25325852,23.3944446 2.61445214,22.7091272 2.61445214,21.876587 L2.61445214,11.2214239 L22.4233828,11.2214239 L22.4233828,21.876587 C22.4233828,22.7091272 21.7845852,23.3944446 21.0048086,23.3944446 Z"
]
[]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
calendar : Nri.Ui.Svg.V1.Svg
calendar =
@ -639,16 +673,30 @@ arrowPointingRight =
|> Nri.Ui.Svg.V1.fromHtml
sortArrow_ : List (Svg.Attribute msg) -> Svg.Svg msg
sortArrow_ transforms =
Svg.svg
([ Attributes.width "100%"
, Attributes.height "100%"
, Attributes.fill "currentcolor"
, Attributes.viewBox "0 0 8 6"
]
++ transforms
)
[ Svg.polygon [ Attributes.points "0 6 4 0 8 6 0 6" ] [] ]
{-| -}
sortArrow : Nri.Ui.Svg.V1.Svg
sortArrow =
Svg.svg
[ Attributes.width "100%"
, Attributes.height "100%"
, Attributes.fill "currentcolor"
, Attributes.viewBox "0 0 8 6"
]
[ Svg.polygon [ Attributes.points "0 6 4 0 8 6 0 6" ] [] ]
sortArrow_ []
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
sortArrowDown : Nri.Ui.Svg.V1.Svg
sortArrowDown =
sortArrow_ [ Attributes.transform "rotate(180)" ]
|> Nri.Ui.Svg.V1.fromHtml
@ -692,6 +740,46 @@ checkmarkInCircle =
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
checkmarkInCircleInverse : Nri.Ui.Svg.V1.Svg
checkmarkInCircleInverse =
Svg.svg
[ Attributes.viewBox "0 0 75 75"
, Attributes.width "100%"
, Attributes.height "100%"
]
[ Svg.g []
[ Svg.path
[ Attributes.fill "currentcolor"
, Attributes.fillRule "evenodd"
, Attributes.d "M54.11,25.09l-22.4,22.4-9.93-9.92a1.78,1.78,0,0,0-2.52,2.52L30.44,51.28a1.78,1.78,0,0,0,2.52,0L56.63,27.61a1.78,1.78,0,0,0-2.52-2.52ZM37.5,71.43C19.11,71.43,3.57,55.89,3.57,37.5S19.11,3.57,37.5,3.57,71.43,19.11,71.43,37.5,55.89,71.43,37.5,71.43ZM37.5,0C17.17,0,0,17.17,0,37.5S17.17,75,37.5,75,75,57.83,75,37.5,57.83,0,37.5,0Z"
]
[]
]
]
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
emptyCircle : Nri.Ui.Svg.V1.Svg
emptyCircle =
Svg.svg
[ Attributes.viewBox "0 0 75 75"
, Attributes.width "100%"
, Attributes.height "100%"
]
[ Svg.g []
[ Svg.path
[ Attributes.fill "currentcolor"
, Attributes.fillRule "evenodd"
, Attributes.d "M37.5,71.43C19.11,71.43,3.57,55.89,3.57,37.5S19.11,3.57,37.5,3.57,71.43,19.11,71.43,37.5,55.89,71.43,37.5,71.43ZM37.5,0C17.17,0,0,17.17,0,37.5S17.17,75,37.5,75,75,57.83,75,37.5,57.83,0,37.5,0Z"
]
[]
]
]
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
x : Nri.Ui.Svg.V1.Svg
x =
@ -705,6 +793,32 @@ x =
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
xInCircle : Nri.Ui.Svg.V1.Svg
xInCircle =
Svg.svg
[ Attributes.width "100%"
, Attributes.height "100%"
, Attributes.viewBox "0 0 50 50"
]
[ Svg.circle
[ Attributes.fill "currentcolor"
, Attributes.cx "25"
, Attributes.cy "25"
, Attributes.r "25"
]
[]
, Svg.g
[ Attributes.id "white-x"
, Attributes.transform "translate(15.000000, 15.000000)"
, Attributes.fill "#FFFFFF"
]
[ Svg.path [ Attributes.d "M0.853242321,4.81228669 C-0.284414107,3.67463026 -0.284414107,1.99089875 0.853242321,0.853242321 C1.99089875,-0.284414107 3.67463026,-0.284414107 4.81228669,0.853242321 L9.90898749,5.94994312 L15.0056883,0.853242321 C16.1433447,-0.284414107 18.0318544,-0.284414107 19.1467577,0.853242321 C20.2844141,1.99089875 20.2844141,3.87940842 19.1467577,4.99431172 L14.0500569,10.0910125 L19.1467577,15.1877133 C20.2844141,16.3253697 20.2844141,18.0091013 19.1467577,19.1467577 C18.0091013,20.2844141 16.3253697,20.2844141 15.1877133,19.1467577 L10.0910125,14.0500569 L4.99431172,19.1467577 C3.85665529,20.2844141 1.96814562,20.2844141 0.853242321,19.1467577 C-0.284414107,18.0091013 -0.284414107,16.1205916 0.853242321,15.0056883 L5.94994312,10.0910125 L0.853242321,4.81228669 Z" ] []
]
]
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
exclamation : Nri.Ui.Svg.V1.Svg
exclamation =
@ -989,6 +1103,29 @@ playInCircle =
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
play : Nri.Ui.Svg.V1.Svg
play =
Svg.svg
[ Attributes.width "100%"
, Attributes.height "100%"
, Attributes.viewBox "0 0 23 25"
]
[ Svg.g
[ Attributes.stroke "none"
, Attributes.strokeWidth "1"
, Attributes.fillRule "evenodd"
, Attributes.fill "currentcolor"
]
[ Svg.path
[ Attributes.d "M2.89855072,24.7426357 C2.30242319,25.0857881 1.56232657,25.0857881 0.966183575,24.7426357 C0.370056039,24.4004395 0,23.7604025 0,23.0750694 L0,1.9249306 C0,1.2357264 0.370056039,0.599545034 0.966183575,0.257364281 C1.56232657,-0.0857880938 2.30242319,-0.0857880938 2.89855072,0.257364281 L21.2560386,10.8343538 C21.8521816,11.1775062 22.2222222,11.8127313 22.2222222,12.5028917 C22.2222222,13.1882249 21.8560464,13.8244062 21.2560386,14.1666024 L2.89855072,24.7426357 Z"
]
[]
]
]
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
stopInCircle : Nri.Ui.Svg.V1.Svg
stopInCircle =
@ -1114,8 +1251,8 @@ plus =
{-| -}
bulb : Nri.Ui.Svg.V1.Svg
bulb =
sparkleBulb : Nri.Ui.Svg.V1.Svg
sparkleBulb =
Svg.svg
[ Attributes.width "100%"
, Attributes.height "100%"
@ -1131,6 +1268,52 @@ bulb =
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
baldBulb : Nri.Ui.Svg.V1.Svg
baldBulb =
Svg.svg
[ Attributes.width "100%"
, Attributes.height "100%"
, Attributes.viewBox "0 0 17 25"
]
[ Svg.g
[ Attributes.stroke "none"
, Attributes.strokeWidth "1"
, Attributes.fill "none"
, Attributes.fillRule "evenodd"
]
[ Svg.g []
[ Svg.path
[ Attributes.d "M11.5310851,18.9188911 L4.99280647,18.9188911 C4.5382651,18.9188911 4.17021008,19.2869348 4.17021008,19.7414875 C4.17021008,20.1945649 4.53825384,20.5626199 4.99280647,20.5626199 L11.5310851,20.5626199 C11.9856265,20.5626199 12.3536815,20.1945761 12.3536815,19.7414875 C12.3536815,19.2869461 11.9856378,18.9188911 11.5310851,18.9188911 Z"
, Attributes.fill "currentcolor"
]
[]
, Svg.path
[ Attributes.d "M6.57418552,23.6143414 L6.57418552,24.0659548 C6.57418552,24.5806312 6.99062635,24.998536 7.50530277,25 L8.83525577,25 C9.34846823,24.9985338 9.7634451,24.5820952 9.76490906,24.0688828 L9.76490906,23.6333729 C10.8353279,23.5747206 11.6740279,22.6934726 11.6799213,21.6215898 L4.84397035,21.6215898 C4.84983371,22.6216259 5.58447227,23.4691464 6.57422306,23.6143038 L6.57418552,23.6143414 Z"
, Attributes.fill "currentcolor"
]
[]
, Svg.path
[ Attributes.d "M8.25826165,0 C3.69370381,0 7.73218198e-06,3.79920237 7.73218198e-06,8.47975104 L7.73218198e-06,8.48121875 C-0.00145216848,9.5706691 0.203832815,10.6498717 0.604143787,11.6631172 C0.653997317,11.7877529 0.706786279,11.9138563 0.762506918,12.038492 L0.761039202,12.038492 C1.11881645,12.8361635 1.59831274,13.572236 2.18040648,14.2232899 L4.27867667,17.4476808 L12.243755,17.4476808 L14.3420252,14.2232899 C14.9241565,13.5722398 15.4036228,12.8361635 15.7613925,12.038492 C15.8171131,11.9153203 15.8684344,11.7906846 15.9197556,11.6631172 L15.9182917,11.6631172 C16.3200553,10.6499093 16.5238839,9.57070288 16.5238839,8.48121875 C16.5238839,3.79654096 12.8214494,0 8.25813027,0 L8.25826165,0 Z M7.88288683,3.12326492 C7.01336859,3.17605013 6.16877524,3.43265636 5.41948955,3.87401457 C3.84614352,4.80805974 3.02793902,6.50163833 2.98687301,8.85523847 L2.98687301,8.85670619 C2.97954194,9.30392775 2.61296591,9.66316146 2.16574059,9.66316146 C1.94726118,9.65876206 1.73905203,9.56931775 1.5880012,9.41095837 C1.43697289,9.25406296 1.35339193,9.04438234 1.35779133,8.82589918 C1.42231075,5.07215094 3.19359944,3.26697342 4.66870988,2.41825095 C5.65553275,1.85518871 6.76258818,1.53698347 7.89753396,1.49152558 C8.35061137,1.49152558 8.71866638,1.85956934 8.71866638,2.31265801 C8.71866638,2.76719938 8.35062263,3.1352544 7.89753396,3.1352544 L7.88288683,3.12326492 Z"
, Attributes.fill "currentcolor"
]
[]
]
]
]
|> Nri.Ui.Svg.V1.fromHtml
{-| `bulb` will be removed in a future version of noredink-ui.
Use the more-specific `baldBulb` instead please.
-}
bulb : Nri.Ui.Svg.V1.Svg
bulb =
baldBulb
{-| -}
help : Nri.Ui.Svg.V1.Svg
help =
@ -1441,12 +1624,14 @@ link =
[ Attributes.width "100%"
, Attributes.height "100%"
, Attributes.fill "currentcolor"
, Attributes.viewBox "0 0 20 20"
, Attributes.viewBox "0 0 100 100"
]
[ Svg.path [ Attributes.d "M9.36117647,7.80088235 C9.46457647,7.58718824 9.60244412,7.39188235 9.77017647,7.22414706 L9.96778824,7.02653529 L9.73800882,7.25631471 C9.58405588,7.41716176 9.45767941,7.60099118 9.36117647,7.80090294 L9.36117647,7.80088235 Z" ] []
, Svg.path [ Attributes.d "M11.8544118,14.4002941 L7.79205882,18.4626471 C6.66844118,19.5495 5.05538235,19.9631176 3.54794118,19.5506471 C2.03944118,19.1382059 0.861823529,17.9605882 0.449411765,16.4521176 C0.0369705882,14.9447647 0.450560588,13.3318235 1.53741176,12.208 L5.59976471,8.14564706 C5.81001176,7.93425 6.04094118,7.74467647 6.28911765,7.58038235 C6.09265588,8.43170588 6.11793235,9.3175 6.36264706,10.1562059 C6.39941176,10.2756912 6.43847353,10.3859853 6.47983529,10.5100588 L3.16189412,13.8327059 C2.35307059,14.6656471 2.36342353,15.9949412 3.18372324,16.8165294 C4.00519382,17.6368529 5.33445853,17.6471765 6.16754676,16.8383585 L10.2298997,12.7760056 C10.7468997,12.2590056 10.9617526,11.5122115 10.7974585,10.7998879 C10.7549497,10.6137674 10.6894615,10.434535 10.5998468,10.2667997 C10.5492968,10.1737379 10.4918497,10.0852732 10.4275115,10.0002556 C10.2632203,9.80379382 10.1092762,9.59813794 9.96795265,9.38443206 C9.83582912,9.11329088 9.79102324,8.80884382 9.83927618,8.51125559 C9.88523206,8.220585 10.0219497,7.95175559 10.2298938,7.743785 L10.6894526,7.30490265 C11.5695115,7.73804971 12.2806879,8.45034382 12.7115115,9.33154971 C12.7367874,9.38899382 12.7666585,9.44873794 12.791935,9.50618206 C12.8172106,9.56362618 12.8195085,9.57281735 12.8332938,9.607285 C12.8470809,9.64175265 12.8838468,9.73136735 12.9068232,9.80029971 C12.9298015,9.869235 12.9596732,9.95654971 12.9826497,10.0300791 C13.2514909,10.9572262 13.2112791,11.9475791 12.8677615,12.8494615 C12.8126144,12.9976703 12.7482762,13.1435791 12.6747468,13.2837556 C12.6598112,13.3170732 12.6414291,13.3492438 12.6218968,13.3802644 C12.5943232,13.4342615 12.5633026,13.4871115 12.529985,13.5388115 C12.5322829,13.5445559 12.5322829,13.5514494 12.529985,13.5571938 C12.4886262,13.6307232 12.4426703,13.6996585 12.3944144,13.7708879 C12.2347174,13.9972203 12.0543556,14.2086232 11.8544438,14.4004762 L11.8544118,14.4002941 Z" ] []
, Svg.path [ Attributes.d "M18.4626471,1.53735294 C17.632,0.709 16.5072353,0.243705882 15.3341176,0.243705882 C14.161,0.243705882 13.0363235,0.709 12.2055882,1.53735294 L8.14529412,5.59970588 C7.56279412,6.18564706 7.15494118,6.92208824 6.96652941,7.72747059 C6.95274265,7.79180882 6.93895588,7.85614706 6.92746765,7.92048529 C6.91597853,7.98482353 6.90678735,8.03767353 6.89989412,8.09511765 C6.89759618,8.11005324 6.89759618,8.12613824 6.89989412,8.14107353 C6.89989412,8.18932647 6.88610706,8.23987941 6.88380941,8.28813235 C6.82636529,8.84763235 6.87921382,9.41404412 7.03776235,9.95404412 C7.05384676,10.0137853 7.07223,10.0712324 7.09290941,10.1286765 C7.11358941,10.1861206 7.12278,10.2182912 7.13886529,10.2619471 L7.15724765,10.3125 C7.55707118,11.3534118 8.33715941,12.2047353 9.34015941,12.6930294 L9.79971824,12.2334706 C10.0674124,11.9669265 10.2144829,11.6027353 10.2087182,11.2247353 C10.2041224,10.8479118 10.0455741,10.4882941 9.76983588,10.2297941 C9.37001235,9.83226471 9.14598294,9.29114706 9.14598294,8.72702941 C9.14598294,8.16291176 9.37001824,7.62179412 9.76983588,7.22426471 L13.8321888,3.16191176 C14.66513,2.35308824 15.9944241,2.36344118 16.8160124,3.18374088 C17.6363359,4.00521147 17.6466594,5.33447618 16.8378415,6.16756441 L13.5175474,9.48785853 C13.5175474,9.51543206 13.5359297,9.54530265 13.5474179,9.57287618 C13.5772885,9.66019382 13.6071591,9.75210559 13.6324356,9.84631441 C13.8748532,10.6838732 13.9001297,11.5696674 13.705965,12.4198438 C13.9541268,12.2555526 14.1850532,12.0659909 14.3953179,11.8545791 L18.4623768,7.794285 C19.2907297,6.96363794 19.7560238,5.83887324 19.7560238,4.66575559 C19.7560238,3.49263794 19.2907297,2.36796147 18.4623768,1.53722618 L18.4626471,1.53735294 Z" ] []
, Svg.path [ Attributes.d "M9.20029412,9.19588235 C9.15318824,8.98448529 9.13940294,8.76620588 9.15893529,8.55020588 C9.14055294,8.7662 9.15548853,8.98335294 9.20029412,9.19588235 Z" ] []
[ Svg.path
[ Attributes.d "M92.1882,7.8105 C81.7742,-2.6035 64.8872,-2.6035 54.4772,7.8105 L39.5982,22.6855 C40.7779,22.55659 41.9654,22.50191 43.1607,22.50191 C46.9263,22.50191 50.5865,23.09957 54.0557,24.25191 L62.4854,15.82221 C65.3799,12.92381 69.2315,11.33001 73.3294,11.33001 C77.4232,11.33001 81.2747,12.92381 84.1734,15.82221 C87.0679,18.71671 88.6617,22.56051 88.6617,26.66221 C88.6617,30.75601 87.0679,34.60751 84.1734,37.50221 L67.6774,53.99821 C64.779,56.89661 60.9274,58.49041 56.8334,58.49041 C52.7318,58.49041 48.8881,56.89661 45.9894,53.99821 C44.5792,52.59591 43.4816,50.95911 42.7238,49.17791 C40.8449,49.28338 39.0871,50.06463 37.7433,51.40451 L33.3488,55.80291 C34.5519,58.02951 36.0949,60.13101 37.9738,62.01771 C48.3878,72.43171 65.2748,72.43171 75.6888,62.01771 L92.1888,45.51371 C102.5988,35.10371 102.5988,18.22071 92.1888,7.81071 L92.1882,7.8105 Z" ]
[]
, Svg.path
[ Attributes.d "M57.0092,77.49 C53.2358,77.49 49.5404,76.88062 45.9932,75.6775 L37.5049,84.1658 C34.6104,87.0642 30.7627,88.658 26.6649,88.658 C22.5711,88.658 18.7235,87.0642 15.8249,84.1658 C12.9265,81.2713 11.3327,77.4236 11.3327,73.3258 C11.3327,69.232 12.9265,65.3805 15.8249,62.4818 L32.3209,45.9858 C35.2193,43.0913 39.0631,41.5014 43.1609,41.5014 C47.2625,41.5014 51.1062,43.0952 54.0049,45.9858 C55.4151,47.396 56.5166,49.0327 57.2783,50.8139 C59.165,50.716244 60.9267,49.92718 62.2666,48.5873 L66.6533,44.1928 C65.4502,41.9584 63.9033,39.8608 62.0205,37.974 C51.6065,27.56 34.7195,27.56 24.3095,37.974 L7.8135,54.478 C-2.6045,64.892 -2.6045,81.771 7.8135,92.189 C18.2275,102.603 35.1065,102.603 45.5205,92.189 L60.3755,77.334 C59.2661,77.43556 58.1489,77.49416 57.02,77.49416 L57.0092,77.49 Z" ]
[]
]
|> Nri.Ui.Svg.V1.fromHtml
@ -1610,6 +1795,25 @@ checklist =
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
checklistComplete : Nri.Ui.Svg.V1.Svg
checklistComplete =
Svg.svg
[ Attributes.width "100%"
, Attributes.height "100%"
, Attributes.fill "currentcolor"
, Attributes.viewBox "0 0 27 27"
]
[ Svg.path [ Attributes.d "M11.0772,5.46017143 L25.1094857,5.46017143 C25.8126171,5.46017143 26.3851457,4.88761714 26.3851457,4.18451143 C26.3851457,3.48138 25.8125914,2.90885143 25.1094857,2.90885143 L11.0772,2.90885143 C10.3740686,2.90885143 9.80154,3.48140571 9.80154,4.18451143 C9.80154,4.88764286 10.3740943,5.46017143 11.0772,5.46017143 Z" ] []
, Svg.path [ Attributes.d "M25.1094857,11.8386 L11.0772,11.8386 C10.3740686,11.8386 9.80154,12.4111543 9.80154,13.11426 C9.80154,13.8173657 10.3740943,14.38992 11.0772,14.38992 L25.1094857,14.38992 C25.8126171,14.38992 26.3851457,13.8173657 26.3851457,13.11426 C26.3851457,12.4111543 25.8125914,11.8386 25.1094857,11.8386 Z" ] []
, Svg.path [ Attributes.d "M25.1094857,20.7684 L11.0772,20.7684 C10.3740686,20.7684 9.80154,21.3409543 9.80154,22.04406 C9.80154,22.7471914 10.3740943,23.31972 11.0772,23.31972 L25.1094857,23.31972 C25.8126171,23.31972 26.3851457,22.7471657 26.3851457,22.04406 C26.3851457,21.3409286 25.8125914,20.7684 25.1094857,20.7684 Z" ] []
, Svg.path [ Attributes.d "M6.34628571,18.14451429 L3.42334286,21.06745723 L3.05169429,20.6958086 C2.55448286,20.19859723 1.74589714,20.19859723 1.24868571,20.6958086 C0.751474286,21.19301997 0.751474286,22.00160566 1.24868571,22.4988173 L2.52434571,23.77447723 C2.77546114,24.0255926 3.10191429,24.14612566 3.42835714,24.14612566 C3.7548,24.14612566 4.08126857,24.02056797 4.33236857,23.77447723 L8.15942571,19.94742 C8.65663714,19.4502086 8.65663714,18.64162291 8.15942571,18.14441142 C7.65216,17.64719999 6.84858857,17.64719999 6.34636286,18.14441142 L6.34628571,18.14451429 Z" ] []
, Svg.path [ Attributes.d "M6.34628571,9.6588 L3.42334286,12.5817429 L3.05169429,12.2100943 C2.55448286,11.7128829 1.74589714,11.7128829 1.24868571,12.2100943 C0.751474286,12.7073057 0.751474286,13.5158914 1.24868571,14.0131029 L2.52434571,15.2887629 C2.77546114,15.5398783 3.10191429,15.6604114 3.42835714,15.6604114 C3.7548,15.6604114 4.08126857,15.5348537 4.33236857,15.2887629 L8.15942571,11.4617057 C8.65663714,10.9644943 8.65663714,10.1559086 8.15942571,9.65869714 C7.65216,9.16148571 6.84858857,9.16148571 6.34636286,9.65869714 L6.34628571,9.6588 Z" ] []
, Svg.path [ Attributes.d "M6.34628571,1.17308571 L3.42334286,4.09602857 L3.05169429,3.72438 C2.55448286,3.22716857 1.74589714,3.22716857 1.24868571,3.72438 C0.751474286,4.22159143 0.751474286,5.03017714 1.24868571,5.52738857 L2.52434571,6.80304857 C2.77546114,7.054164 3.10191429,7.17469714 3.42835714,7.17469714 C3.7548,7.17469714 4.08126857,7.04913943 4.33236857,6.80304857 L8.15942571,2.97599143 C8.65663714,2.47878 8.65663714,1.67019429 8.15942571,1.17298286 C7.65216,0.675771429 6.84858857,0.675771429 6.34636286,1.17298286 L6.34628571,1.17308571 Z" ] []
]
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
openBook : Nri.Ui.Svg.V1.Svg
openBook =
@ -1857,3 +2061,60 @@ tada =
]
]
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
openInNewTab : Nri.Ui.Svg.V1.Svg
openInNewTab =
Svg.svg
[ Attributes.viewBox "0 0 74 74"
, Attributes.width "100%"
, Attributes.height "100%"
]
[ Svg.g
[ Attributes.fill "currentcolor"
, Attributes.fillRule "nonzero"
]
[ Svg.path
[ Attributes.d "M68.8007,0 L47.8007,0 L47.8007,9.1992 L57.6991,9.1992 L26.8011,40.1992 L33.3011,46.6992 L64.1991,15.8012 L64.1991,25.602 L73.3983,25.602 L73.3983,4.602 C73.3983,3.3793 72.91392,2.2114 72.0506,1.3481 C71.18732,0.48482 70.0194,0 68.8006,0 L68.8007,0 Z"
]
[]
, Svg.path
[ Attributes.d "M55.3007,64.301 L9.1987,64.301 L9.1987,18.199 L40.8007,18.199 L40.8007,8.9021 L4.5977,8.9021 C2.0586,8.9021 0,10.9607 0,13.4998 L0,68.9018 C0,71.4409 2.0586,73.4995 4.5977,73.4995 L59.8987,73.4995 C62.4417,73.4995 64.5003,71.4409 64.5003,68.9018 L64.5003,32.6988 L55.3011,32.6988 L55.3007,64.301 Z"
]
[]
]
]
|> Nri.Ui.Svg.V1.fromHtml
{-| -}
sync : Nri.Ui.Svg.V1.Svg
sync =
Svg.svg
[ Attributes.width "100%"
, Attributes.height "100%"
, Attributes.viewBox "0 0 62 62"
, Attributes.version "1.1"
]
[ Svg.g
[ Attributes.stroke "none"
, Attributes.strokeWidth "1"
, Attributes.fill "none"
, Attributes.fillRule "evenodd"
]
[ Svg.path
[ Attributes.d "M6.088844,32.9998 C5.999,32.33964 5.999,31.6717 5.999,30.9998 C5.999,23.9568 8.9678,17.2458 14.1787,12.5078 C19.3857,7.7695 26.3507,5.4453 33.3627,6.1133 C40.3705,6.77736 46.7727,10.3672 50.9997,16 L38.9997,16 L38.9997,22 L57.9997,22 C59.6559,22 60.9997,20.6562 60.9997,19 L60.9997,-3.55271368e-15 L54.9997,-3.55271368e-15 L54.9997,11.43 C49.5739,4.7933 41.6367,0.7 33.0817,0.121 C24.527,-0.45322 16.1167,2.539 9.8477,8.3905 C3.5782,14.2382 0.0157,22.4255 3.55271368e-15,30.9995 C3.55271368e-15,31.67138 3.55271368e-15,32.3393 0.070312,32.9998 L6.088844,32.9998 Z"
, Attributes.fill "currentcolor"
, Attributes.fillRule "nonzero"
]
[]
, Svg.path
[ Attributes.d "M22.998844,39.9998 L3.998844,39.9998 C2.342644,39.9998 0.998844,41.3436 0.998844,42.9998 L0.998844,61.9998 L6.998844,61.9998 L6.998844,50.5698 C12.424644,57.2065 20.361844,61.2998 28.916844,61.8788 C37.471544,62.45302 45.881844,59.4608 52.150844,53.6093 C58.420344,47.7616 61.982844,39.5743 61.998544,31.0003 C61.998544,30.32842 61.998544,29.6605 61.928232,29.0003 L55.928232,29.0003 C55.979013,29.66046 56.018075,30.3284 56.018075,31.0003 C56.025888,38.0472 53.057175,44.7733 47.846175,49.5163 C42.631375,54.2624 35.658175,56.5827 28.643175,55.9147 C21.627575,55.24673 15.221175,51.6452 10.998175,46.0006 L22.998175,46.0006 L22.998844,39.9998 Z"
, Attributes.fill "currentcolor"
, Attributes.fillRule "nonzero"
]
[]
]
]
|> Nri.Ui.Svg.V1.fromHtml

View File

@ -0,0 +1,78 @@
module Nri.Ui.WhenFocusLeaves.V1 exposing (toAttribute)
{-| Listen for when the focus leaves the area, and then do an action.
@docs toAttribute
-}
import Accessibility.Styled as Html
import Html.Styled.Events as Events
import Json.Decode as Decode exposing (Decoder)
{-| Attach this attribute to add a focus watcher to an HTML element and define
what to do in reponse to tab keypresses in a part of the UI.
The ids referenced here are expected to correspond to elements in the container
we are adding the attribute to.
-}
toAttribute :
{ firstId : String
, lastId : String
, tabBackAction : msg
, tabForwardAction : msg
}
-> Html.Attribute msg
toAttribute { firstId, lastId, tabBackAction, tabForwardAction } =
onTab <|
\elementId shiftKey ->
-- if the user tabs back while on the first id,
-- we execute the action
if elementId == firstId && shiftKey then
Decode.succeed
{ message = tabBackAction
, preventDefault = False
, stopPropagation = False
}
else if elementId == lastId && not shiftKey then
-- if the user tabs forward while on the last id,
-- we want to wrap around to the first id.
Decode.succeed
{ message = tabForwardAction
, preventDefault = False
, stopPropagation = False
}
else
Decode.fail "No need to intercept the key press"
onTab :
(String
-> Bool
-> Decoder { message : msg, preventDefault : Bool, stopPropagation : Bool }
)
-> Html.Attribute msg
onTab do =
Events.custom "keydown"
(Decode.andThen
(\( id, keyCode, shiftKey ) ->
if keyCode == 9 then
do id shiftKey
else
Decode.fail "No need to intercept the key press"
)
decodeKeydown
)
decodeKeydown : Decoder ( String, Int, Bool )
decodeKeydown =
Decode.map3 (\id keyCode shiftKey -> ( id, keyCode, shiftKey ))
(Decode.at [ "target", "id" ] Decode.string)
(Decode.field "keyCode" Decode.int)
(Decode.field "shiftKey" Decode.bool)

View File

@ -8,7 +8,6 @@ module TabsInternal exposing (Config, Tab, views)
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Role as Role
import Accessibility.Styled.Widget as Widget
import Css
import EventExtras
import Html.Styled as Html exposing (Attribute, Html)
@ -100,7 +99,7 @@ viewTab_ config tab =
(tagSpecificAttributes
++ tab.tabAttributes
++ [ Attributes.tabindex tabIndex
, Widget.selected isSelected
, Aria.selected isSelected
, Role.tab
, Attributes.id (tabToId tab.idString)
, Events.onFocus (config.onSelect tab.id)
@ -176,13 +175,13 @@ viewTabPanel tab selected =
, Attributes.id (tabToBodyId tab.idString)
]
++ (if selected then
[ Widget.hidden False
[ Aria.hidden False
, Attributes.tabindex 0
]
else
[ Attributes.css [ Css.display Css.none ]
, Widget.hidden True
, Aria.hidden True
, Attributes.tabindex -1
]
)

View File

@ -12,7 +12,6 @@ module TabsInternal.V2 exposing
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Role as Role
import Accessibility.Styled.Widget as Widget
import Css
import EventExtras
import Html.Styled as Html exposing (Attribute, Html)
@ -21,7 +20,7 @@ import Html.Styled.Events as Events
import Html.Styled.Keyed as Keyed
import Json.Decode
import Nri.Ui.Html.Attributes.V2 as AttributesExtra
import Nri.Ui.Tooltip.V2 as Tooltip
import Nri.Ui.Tooltip.V3 as Tooltip
import Nri.Ui.Util exposing (dashify)
@ -135,7 +134,7 @@ viewTab_ config index tab =
-- be able to focus on the current tab with the
-- keyboard.
Attributes.disabled (not isSelected && tab.disabled)
, Widget.selected isSelected
, Aria.selected isSelected
, Role.tab
, Attributes.id (tabToId tab.idString)
, Events.on "keyup" <|
@ -249,13 +248,13 @@ viewTabPanel tab selected =
, Attributes.id (tabToBodyId tab.idString)
]
++ (if selected then
[ Widget.hidden False
[ Aria.hidden False
, Attributes.tabindex 0
]
else
[ Attributes.css [ Css.display Css.none ]
, Widget.hidden True
, Aria.hidden True
, Attributes.tabindex -1
]
)

405
styleguide-app/App.elm Normal file
View File

@ -0,0 +1,405 @@
module App exposing (Effect(..), Model, Msg(..), init, perform, subscriptions, update, view)
import Accessibility.Styled as Html exposing (Html)
import Accessibility.Styled.Key as Key
import Browser exposing (Document, UrlRequest(..))
import Browser.Dom
import Browser.Navigation exposing (Key)
import Category exposing (Category)
import Css exposing (..)
import Css.Media exposing (withMedia)
import Dict exposing (Dict)
import Example exposing (Example)
import Examples
import Html.Styled.Attributes exposing (..)
import Http
import Json.Decode as Decode
import Nri.Ui.CssVendorPrefix.V1 as VendorPrefixed
import Nri.Ui.Heading.V2 as Heading
import Nri.Ui.MediaQuery.V1 exposing (mobile)
import Nri.Ui.Page.V3 as Page
import Nri.Ui.SideNav.V3 as SideNav
import Nri.Ui.Sprite.V1 as Sprite
import Nri.Ui.UiIcon.V1 as UiIcon
import Routes
import Sort.Set as Set
import Task
import Url exposing (Url)
type alias Route =
Routes.Route Examples.State Examples.Msg
type alias Model key =
{ -- Global UI
route : Route
, previousRoute : Maybe Route
, moduleStates : Dict String (Example Examples.State Examples.Msg)
, navigationKey : key
, elliePackageDependencies : Result Http.Error (Dict String String)
}
init : () -> Url -> key -> ( Model key, Effect )
init () url key =
let
moduleStates =
Dict.fromList
(List.map (\example -> ( example.name, example )) Examples.all)
in
( { route = Routes.fromLocation moduleStates url
, previousRoute = Nothing
, moduleStates = moduleStates
, navigationKey = key
, elliePackageDependencies = Ok Dict.empty
}
, Cmd.batch
[ loadPackage
, loadApplicationDependencies
]
|> Command
)
type Msg
= UpdateModuleStates String Examples.Msg
| OnUrlRequest Browser.UrlRequest
| OnUrlChange Url
| ChangeRoute Route
| SkipToMainContent
| LoadedPackages (Result Http.Error (Dict String String))
| Focused (Result Browser.Dom.Error ())
update : Msg -> Model key -> ( Model key, Effect )
update action model =
case action of
UpdateModuleStates key exampleMsg ->
case Dict.get key model.moduleStates of
Just example ->
example.update exampleMsg example.state
|> Tuple.mapFirst
(\newState ->
let
newExample =
{ example | state = newState }
in
{ model
| moduleStates = Dict.insert key newExample model.moduleStates
, route =
Maybe.withDefault model.route
(Routes.updateExample newExample model.route)
}
)
|> Tuple.mapSecond (Cmd.map (UpdateModuleStates key) >> Command)
Nothing ->
( model, None )
OnUrlRequest request ->
case request of
Internal loc ->
( model, GoToUrl loc )
External loc ->
( model, Load loc )
OnUrlChange location ->
let
route =
Routes.fromLocation model.moduleStates location
in
( { model | route = route, previousRoute = Just model.route }
, Maybe.map FocusOn (Routes.headerId route)
|> Maybe.withDefault None
)
ChangeRoute route ->
( model
, GoToRoute route
)
SkipToMainContent ->
( model
, FocusOn "maincontent"
)
LoadedPackages newPackagesResult ->
let
-- Ellie gets really slow to compile if we include all the packages, unfortunately!
-- feel free to adjust the settings here if you need more packages for a particular example.
removedPackages =
[ "avh4/elm-debug-controls"
, "BrianHicks/elm-particle"
, "elm-community/random-extra"
, "elm/browser"
, "elm/http"
, "elm/json"
, "elm/parser"
, "elm/random"
, "elm/regex"
, "elm/svg"
, "elm/url"
, "elm-community/string-extra"
, "Gizra/elm-keyboard-event"
, "pablohirafuji/elm-markdown"
, "rtfeldman/elm-sorter-experiment"
, "tesk9/accessible-html-with-css"
, "tesk9/palette"
, "wernerdegroot/listzipper"
]
in
( { model
| elliePackageDependencies =
List.foldl (\name -> Result.map (Dict.remove name))
(Result.map2 Dict.union model.elliePackageDependencies newPackagesResult)
removedPackages
}
, None
)
Focused _ ->
( model, None )
type Effect
= GoToRoute Route
| GoToUrl Url
| Load String
| FocusOn String
| None
| Command (Cmd Msg)
perform : Key -> Effect -> Cmd Msg
perform navigationKey effect =
case effect of
GoToRoute route ->
Browser.Navigation.pushUrl navigationKey (Routes.toString route)
GoToUrl url ->
Browser.Navigation.pushUrl navigationKey (Url.toString url)
Load loc ->
Browser.Navigation.load loc
FocusOn id ->
Task.attempt Focused (Browser.Dom.focus id)
None ->
Cmd.none
Command cmd ->
cmd
subscriptions : Model key -> Sub Msg
subscriptions model =
Dict.values model.moduleStates
|> List.map (\example -> Sub.map (UpdateModuleStates example.name) (example.subscriptions example.state))
|> Sub.batch
view : Model key -> Document Msg
view model =
let
toBody view_ =
List.map Html.toUnstyled
[ view_
, Html.map never Sprite.attach
]
in
case model.route of
Routes.Doodad example ->
{ title = example.name ++ " in the NoRedInk Style Guide"
, body = viewExample model example |> toBody
}
Routes.CategoryDoodad _ example ->
{ title = example.name ++ " in the NoRedInk Style Guide"
, body = viewExample model example |> toBody
}
Routes.NotFound name ->
{ title = name ++ " was not found in the NoRedInk Style Guide"
, body = toBody notFound
}
Routes.Category category ->
{ title = Category.forDisplay category ++ " Category in the NoRedInk Style Guide"
, body = toBody (viewCategory model category)
}
Routes.All ->
{ title = "NoRedInk Style Guide"
, body = toBody (viewAll model)
}
viewExample : Model key -> Example a Examples.Msg -> Html Msg
viewExample model example =
Example.view { packageDependencies = model.elliePackageDependencies } example
|> Html.map (UpdateModuleStates example.name)
|> withSideNav model
notFound : Html Msg
notFound =
Page.notFound
{ link = ChangeRoute Routes.All
, recoveryText = Page.ReturnTo "Component Library"
}
viewAll : Model key -> Html Msg
viewAll model =
withSideNav model <|
viewPreviews "all"
{ navigate = Routes.Doodad >> ChangeRoute
, exampleHref = Routes.Doodad >> Routes.toString
}
(Dict.values model.moduleStates)
viewCategory : Model key -> Category -> Html Msg
viewCategory model category =
withSideNav model
(model.moduleStates
|> Dict.values
|> List.filter
(\doodad ->
Set.memberOf
(Set.fromList Category.sorter doodad.categories)
category
)
|> viewPreviews (Category.forId category)
{ navigate = Routes.CategoryDoodad category >> ChangeRoute
, exampleHref = Routes.CategoryDoodad category >> Routes.toString
}
)
withSideNav :
{ model | route : Route, moduleStates : Dict String (Example Examples.State Examples.Msg) }
-> Html Msg
-> Html Msg
withSideNav model content =
Html.div
[ css
[ displayFlex
, withMedia [ mobile ] [ flexDirection column, alignItems stretch ]
, alignItems flexStart
, maxWidth (Css.px 1400)
, margin auto
]
]
[ navigation model
, Html.main_
[ css
[ flexGrow (int 1)
, margin2 (px 40) zero
, Css.minHeight (Css.vh 100)
]
, id "maincontent"
, Key.tabbable False
]
[ Html.div [ css [ Css.marginBottom (Css.px 30) ] ]
[ Routes.viewBreadCrumbs model.route
]
, content
]
]
viewPreviews :
String
->
{ navigate : Example Examples.State Examples.Msg -> Msg
, exampleHref : Example Examples.State Examples.Msg -> String
}
-> List (Example Examples.State Examples.Msg)
-> Html Msg
viewPreviews containerId navConfig examples =
examples
|> List.map (Example.preview navConfig)
|> Html.div
[ id containerId
, css
[ Css.displayFlex
, Css.flexWrap Css.wrap
, Css.property "gap" "10px"
]
]
navigation :
{ model | route : Route, moduleStates : Dict String (Example Examples.State Examples.Msg) }
-> Html Msg
navigation { moduleStates, route } =
let
examples =
Dict.values moduleStates
exampleEntriesForCategory category =
List.filter (\{ categories } -> List.any ((==) category) categories) examples
|> List.map
(\example ->
SideNav.entry example.name
[ SideNav.href (Routes.CategoryDoodad category example)
]
)
categoryNavLinks : List (SideNav.Entry Route Msg)
categoryNavLinks =
List.map
(\category ->
SideNav.entryWithChildren (Category.forDisplay category)
[ SideNav.href (Routes.Category category)
]
(exampleEntriesForCategory category)
)
Category.all
in
SideNav.view
{ isCurrentRoute = (==) route
, routeToString = Routes.toString
, onSkipNav = SkipToMainContent
}
[ SideNav.navNotMobileCss
[ VendorPrefixed.value "position" "sticky"
, top (px 55)
]
]
(SideNav.entry "Usage Guidelines"
[ SideNav.linkExternal "https://paper.dropbox.com/doc/UI-Style-Guide-and-Caveats--BhJHYronm1RGM1hRfnkvhrZMAg-PvOLxeX3oyujYEzdJx5pu"
, SideNav.icon UiIcon.openInNewTab
]
:: SideNav.entry "All" [ SideNav.href Routes.All ]
:: categoryNavLinks
)
loadPackage : Cmd Msg
loadPackage =
Http.get
{ url = "/package.json"
, expect =
Http.expectJson
LoadedPackages
(Decode.map2 Dict.singleton
(Decode.field "name" Decode.string)
(Decode.field "version" Decode.string)
)
}
loadApplicationDependencies : Cmd Msg
loadApplicationDependencies =
Http.get
{ url = "/application.json"
, expect =
Http.expectJson
LoadedPackages
(Decode.at [ "dependencies", "direct" ] (Decode.dict Decode.string))
}

View File

@ -1,7 +1,7 @@
module Category exposing
( Category(..)
, fromString
, forDisplay, forId
, forDisplay, forId, forRoute
, all
, sorter
)
@ -10,7 +10,7 @@ module Category exposing
@docs Category
@docs fromString
@docs forDisplay, forId
@docs forDisplay, forId, forRoute
@docs all
@docs sorter
@ -107,6 +107,12 @@ forDisplay category =
"Animations"
{-| -}
forRoute : Category -> String
forRoute =
Debug.toString
{-| -}
sorter : Sorter Category
sorter =

43
styleguide-app/Code.elm Normal file
View File

@ -0,0 +1,43 @@
module Code exposing
( string, maybeString
, maybeFloat
, bool
)
{-|
@docs string, maybeString
@docs maybeFloat
@docs bool
-}
{-| -}
string : String -> String
string s =
"\"" ++ s ++ "\""
{-| -}
maybe : Maybe String -> String
maybe =
Maybe.map (\s -> "Just " ++ s) >> Maybe.withDefault "Nothing"
{-| -}
maybeString : Maybe String -> String
maybeString =
maybe << Maybe.map string
{-| -}
maybeFloat : Maybe Float -> String
maybeFloat =
maybe << Maybe.map String.fromFloat
{-| -}
bool : Bool -> String
bool =
Debug.toString

View File

@ -3,8 +3,8 @@ module CommonControls exposing
, choice
, icon, iconNotCheckedByDefault, uiIcon
, content
, quickBrownFox, longPangrams, romeoAndJulietQuotation, markdown, exampleHtml, httpError
, disabledListItem, premiumDisplay, premiumLevel
, httpError
, disabledListItem, premiumDisplay
)
{-|
@ -17,7 +17,7 @@ module CommonControls exposing
### Content
@docs content
@docs quickBrownFox, longPangrams, romeoAndJulietQuotation, markdown, exampleHtml, httpError
@docs httpError
-}
@ -25,30 +25,20 @@ import Css
import Debug.Control as Control exposing (Control)
import Debug.Control.Extra as ControlExtra
import Html.Styled as Html exposing (Html)
import Html.Styled.Attributes as Attributes
import Http
import Nri.Ui.ClickableText.V3 as ClickableText
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Data.PremiumDisplay as PremiumDisplay exposing (PremiumDisplay)
import Nri.Ui.Data.PremiumLevel exposing (PremiumLevel(..))
import Nri.Ui.Svg.V1 exposing (Svg)
import Nri.Ui.UiIcon.V1 as UiIcon
premiumLevel : Control ( String, PremiumLevel )
premiumLevel =
choice "PremiumLevel"
[ ( "Free", Free )
, ( "PremiumWithWriting", PremiumWithWriting )
]
premiumDisplay : Control ( String, PremiumDisplay )
premiumDisplay =
Control.choice
[ ( "Free", Control.value ( "Free", PremiumDisplay.Free ) )
, ( "Premium Locked", Control.value ( "PremiumLocked", PremiumDisplay.PremiumLocked ) )
, ( "Premium Unlocked", Control.value ( "PremiumUnlocked", PremiumDisplay.PremiumUnlocked ) )
[ ( "Free", Control.value ( "PremiumDisplay.Free", PremiumDisplay.Free ) )
, ( "Premium Locked", Control.value ( "PremiumDisplay.PremiumLocked", PremiumDisplay.PremiumLocked ) )
, ( "Premium Unlocked", Control.value ( "PremiumDisplay.PremiumUnlocked", PremiumDisplay.PremiumUnlocked ) )
]
@ -91,7 +81,7 @@ httpError =
content :
{ moduleName : String
, plaintext : String -> attribute
, markdown : String -> attribute
, markdown : Maybe (String -> attribute)
, html : List (Html msg) -> attribute
, httpError : Maybe (Http.Error -> attribute)
}
@ -125,23 +115,30 @@ content ({ moduleName } as config) =
)
)
)
, ( "markdown"
, Control.string markdown
|> Control.map
(\str ->
( moduleName ++ ".markdown \"" ++ str ++ "\""
, config.markdown str
)
)
)
, ( "HTML"
, Control.value
( moduleName ++ ".html [ ... ]"
, config.html exampleHtml
)
)
]
++ (case config.httpError of
++ (case config.markdown of
Just markdown_ ->
[ ( "markdown"
, Control.string markdown
|> Control.map
(\str ->
( moduleName ++ ".markdown \"" ++ str ++ "\""
, markdown_ str
)
)
)
]
Nothing ->
[]
)
++ ( "HTML"
, Control.value
( moduleName ++ ".html [ ... ]"
, config.html exampleHtml
)
)
:: (case config.httpError of
Just httpError_ ->
[ ( "httpError"
, Control.map

View File

@ -1,12 +1,18 @@
module Debug.Control.Extra exposing
( float, int
, list, listItem, optionalListItem, optionalListItemDefaultChecked, optionalBoolListItem
, list, listItem, optionalListItem, optionalListItemDefaultChecked
, optionalBoolListItem, optionalBoolListItemDefaultTrue
, bool
, string
)
{-|
@docs float, int
@docs list, listItem, optionalListItem, optionalListItemDefaultChecked, optionalBoolListItem
@docs list, listItem, optionalListItem, optionalListItemDefaultChecked
@docs optionalBoolListItem, optionalBoolListItemDefaultTrue
@docs bool
@docs string
-}
@ -41,10 +47,14 @@ list =
{-| -}
listItem : String -> Control a -> Control (List a) -> Control (List a)
listItem name accessor accumulator =
Control.field name
(Control.map List.singleton accessor)
(Control.map (++) accumulator)
listItem name accessor =
listItems name (Control.map List.singleton accessor)
{-| -}
listItems : String -> Control (List a) -> Control (List a) -> Control (List a)
listItems name accessor accumulator =
Control.field name accessor (Control.map (++) accumulator)
{-| -}
@ -82,3 +92,41 @@ optionalBoolListItem name f accumulator =
(Control.bool False)
)
(Control.map (++) accumulator)
optionalBoolListItemDefaultTrue : String -> a -> Control (List a) -> Control (List a)
optionalBoolListItemDefaultTrue name f accumulator =
Control.field name
(Control.map
(\value ->
if not value then
[ f ]
else
[]
)
(Control.bool True)
)
(Control.map (++) accumulator)
{-| -}
bool : Bool -> Control ( String, Bool )
bool default =
Control.map
(\val ->
( if val then
"True"
else
"False"
, val
)
)
(Control.bool default)
{-| -}
string : String -> Control ( String, String )
string default =
Control.map (\val -> ( "\"" ++ val ++ "\"", val )) (Control.string default)

Some files were not shown because too many files have changed in this diff Show More