mirror of
https://github.com/NoRedInk/noredink-ui.git
synced 2024-11-23 08:27:11 +03:00
Merge remote-tracking branch 'origin/master' into add-button-ternary
This commit is contained in:
commit
079f76157e
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -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
130
README.md
@ -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
|
||||
|
75
Shakefile.hs
75
Shakefile.hs
@ -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)
|
||||
|
@ -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
|
||||
|
|
28
elm.json
28
elm.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -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";
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
require('./TextArea/V3')
|
||||
require('./TextArea/V4')
|
||||
require("./TextArea/V3");
|
||||
require("./TextArea/V4");
|
||||
|
||||
exports.CustomElement = require('./CustomElement')
|
||||
exports.CustomElement = require("./CustomElement");
|
||||
|
@ -1,3 +1,6 @@
|
||||
[build.environment]
|
||||
NODE_ENV = "production"
|
||||
|
||||
[build]
|
||||
command = "script/netlify.sh"
|
||||
publish = "public"
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
7443
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -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
35
review/elm.json
Normal 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": {}
|
||||
}
|
||||
}
|
41
review/src/ReviewConfig.elm
Normal file
41
review/src/ReviewConfig.elm
Normal 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
|
||||
]
|
@ -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);
|
||||
});
|
@ -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"))
|
||||
"
|
@ -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.
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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())
|
||||
})
|
||||
});
|
@ -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
4
script/prettier-fix-all.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
git ls-files | grep -E '.js$' | xargs node_modules/.bin/prettier --write
|
3
script/puppeteer-tests-no-percy.sh
Executable file
3
script/puppeteer-tests-no-percy.sh
Executable 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
|
3
script/puppeteer-tests-percy.sh
Executable file
3
script/puppeteer-tests-percy.sh
Executable 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
187
script/puppeteer-tests.js
Normal 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();
|
||||
});
|
||||
});
|
@ -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
360
src/CheckboxIcons.elm
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
@ -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
|
||||
|
372
src/Nri/Ui/BreadCrumbs/V1.elm
Normal file
372
src/Nri/Ui/BreadCrumbs/V1.elm
Normal 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
|
||||
]
|
@ -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
|
||||
|
64
src/Nri/Ui/Carousel/V1.elm
Normal file
64
src/Nri/Ui/Carousel/V1.elm
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
]
|
||||
]
|
||||
|
@ -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
|
||||
]
|
@ -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
|
@ -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"
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
]
|
||||
]
|
@ -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
|
||||
|
@ -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 ]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -10,7 +10,7 @@ module Nri.Ui.Palette.V1 exposing
|
||||
|
||||
-}
|
||||
|
||||
import Css exposing (..)
|
||||
import Css
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
|
||||
|
||||
|
@ -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 ""
|
||||
]
|
@ -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
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
@ -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)
|
||||
]
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
@ -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)
|
||||
|
@ -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 ] ] )
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
@ -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) ]
|
||||
]
|
@ -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
|
||||
|
386
src/Nri/Ui/SortableTable/V3.elm
Normal file
386
src/Nri/Ui/SortableTable/V3.elm
Normal 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
|
@ -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
|
||||
]
|
||||
|
@ -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
|
@ -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)
|
||||
]
|
@ -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 ]
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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 (..)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 ) ->
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
]
|
||||
[]
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
78
src/Nri/Ui/WhenFocusLeaves/V1.elm
Normal file
78
src/Nri/Ui/WhenFocusLeaves/V1.elm
Normal 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)
|
@ -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
|
||||
]
|
||||
)
|
||||
|
@ -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
405
styleguide-app/App.elm
Normal 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))
|
||||
}
|
@ -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
43
styleguide-app/Code.elm
Normal 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
|
@ -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
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user