├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codeql-analysis.yml │ ├── main.yml │ └── pr.yml ├── .gitignore ├── .htmlvalidate.json ├── .nycrc.json ├── .stylelintrc.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── favicon.ico ├── index.html ├── logo.png ├── main.css ├── main.js └── no-js.css ├── package-lock.json ├── package.json ├── rollup-config ├── getModules.js ├── injectInnerHTML.js ├── plugins.js └── watcher.js ├── rollup.config.js ├── server.js ├── spec └── support │ └── jasmine.json └── src ├── zoo-modules ├── common │ └── register-components.js ├── form │ ├── checkbox │ │ ├── checkbox-a11y.spec.mjs │ │ ├── checkbox.css │ │ ├── checkbox.html │ │ ├── checkbox.js │ │ └── checkbox.spec.mjs │ ├── common │ │ └── FormElement.js │ ├── date-range │ │ ├── date-range.css │ │ ├── date-range.html │ │ ├── date-range.js │ │ └── date-range.spec.mjs │ ├── info │ │ ├── info.css │ │ ├── info.html │ │ └── info.js │ ├── input-tag │ │ ├── input-tag-option.css │ │ ├── input-tag-option.html │ │ ├── input-tag-option.js │ │ ├── input-tag.css │ │ ├── input-tag.html │ │ ├── input-tag.js │ │ └── input-tag.spec.mjs │ ├── input │ │ ├── input-a11y.spec.mjs │ │ ├── input.css │ │ ├── input.html │ │ ├── input.js │ │ └── input.spec.mjs │ ├── label │ │ ├── label.css │ │ ├── label.html │ │ └── label.js │ ├── quantity-control │ │ ├── quantity-contol-a11y.spec.mjs │ │ ├── quantity-control.css │ │ ├── quantity-control.html │ │ ├── quantity-control.js │ │ └── quantityControl.spec.mjs │ ├── radio │ │ ├── radio-a11y.spec.mjs │ │ ├── radio.css │ │ ├── radio.html │ │ ├── radio.js │ │ └── radio.spec.mjs │ ├── searchable-select │ │ ├── searchable-select-a11y.spec.mjs │ │ ├── searchable-select.css │ │ ├── searchable-select.html │ │ ├── searchable-select.js │ │ └── searchable-select.spec.mjs │ ├── select │ │ ├── select-a11y.spec.mjs │ │ ├── select.css │ │ ├── select.html │ │ ├── select.js │ │ └── select.spec.mjs │ └── toggle-switch │ │ ├── toggle-switch-a11y.spec.mjs │ │ ├── toggle-switch.css │ │ ├── toggle-switch.html │ │ ├── toggle-switch.js │ │ └── toggle-switch.spec.mjs ├── grid │ ├── grid-header │ │ ├── grid-header.css │ │ ├── grid-header.html │ │ ├── grid-header.js │ │ └── grid-header.spec.mjs │ ├── grid-row │ │ ├── grid-row.css │ │ ├── grid-row.html │ │ ├── grid-row.js │ │ └── grid-row.spec.mjs │ └── grid │ │ ├── grid.css │ │ ├── grid.html │ │ ├── grid.js │ │ └── grid.spec.mjs ├── helpers │ ├── debounce.js │ └── test.setup.mjs ├── icon │ ├── arrow-icon │ │ ├── arrow-icon.css │ │ ├── arrow-icon.html │ │ └── arrow-icon.js │ ├── attention-icon │ │ ├── attention-icon.css │ │ ├── attention-icon.html │ │ └── attention-icon.js │ ├── cross-icon │ │ ├── cross-icon.css │ │ ├── cross-icon.html │ │ └── cross-icon.js │ └── paw-icon │ │ ├── paw-icon.css │ │ ├── paw-icon.html │ │ └── paw-icon.js └── misc │ ├── button-group │ ├── button-group.css │ ├── button-group.html │ ├── button-group.js │ └── button-group.spec.mjs │ ├── button │ ├── button-a11y.spec.mjs │ ├── button.css │ ├── button.html │ ├── button.js │ └── button.spec.mjs │ ├── collapsable-list-item │ ├── collapsable-list-item.css │ ├── collapsable-list-item.html │ └── collapsable-list-item.js │ ├── collapsable-list │ ├── collapsable-list.css │ ├── collapsable-list.html │ ├── collapsable-list.js │ └── collapsable-list.spec.mjs │ ├── feedback │ ├── feeback.spec.mjs │ ├── feedback.css │ ├── feedback.html │ └── feedback.js │ ├── footer │ ├── footer-a11y.spec.mjs │ ├── footer.css │ ├── footer.html │ ├── footer.js │ └── footer.spec.mjs │ ├── header │ ├── header-a11y.spec.mjs │ ├── header.css │ ├── header.html │ ├── header.js │ └── header.spec.mjs │ ├── link │ ├── link-a11y.spec.mjs │ ├── link.css │ ├── link.html │ ├── link.js │ └── link.spec.mjs │ ├── modal │ ├── modal-a11y.spec.mjs │ ├── modal.css │ ├── modal.html │ ├── modal.js │ └── modal.spec.mjs │ ├── navigation │ ├── navigation-a11y.spec.mjs │ ├── navigation.css │ ├── navigation.html │ ├── navigation.js │ └── navigation.spec.mjs │ ├── paginator │ ├── paginator.css │ ├── paginator.html │ ├── paginator.js │ └── paginator.spec.mjs │ ├── preloader │ ├── preloader.css │ ├── preloader.html │ └── preloader.js │ ├── spinner │ ├── spinner.css │ ├── spinner.html │ └── spinner.js │ ├── tag │ ├── tag.css │ ├── tag.html │ └── tag.js │ ├── toast │ ├── toast-a11y.spec.mjs │ ├── toast.css │ ├── toast.html │ ├── toast.js │ └── toast.spec.mjs │ └── tooltip │ ├── tooltip.css │ ├── tooltip.html │ ├── tooltip.js │ └── tooltip.spec.mjs └── zoo-web-components.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2020": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 11, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | "tab" 15 | ], 16 | "quotes": [ 17 | "error", 18 | "single" 19 | ], 20 | "semi": [ 21 | "error", 22 | "always" 23 | ] 24 | }, 25 | "overrides": [ 26 | { 27 | "files": [ 28 | "*.spec.js" 29 | ], 30 | "rules": { 31 | "no-unused-expressions": "off", 32 | "no-undef": "off" 33 | } 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 1. Which component is affected? 12 | 2. A clear and concise description of what the bug is. 13 | 14 | **To Reproduce** 15 | Steps to reproduce the behavior: 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Desktop (please complete the following information):** 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 8.0.4] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '34 19 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # This workflow contains a single job called "build" 17 | build: 18 | # The type of runner that the job will run on 19 | runs-on: ubuntu-latest 20 | 21 | # Steps represent a sequence of tasks that will be executed as part of the job 22 | steps: 23 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 24 | - uses: actions/checkout@v2 25 | - name: install puppeteer libraries 26 | run: | 27 | sudo apt-get update 28 | sudo apt-get install -y libgbm1 29 | # Runs a single command using the runners shell 30 | - name: Test and build 31 | run: | 32 | npm install 33 | npm run lint 34 | npm test 35 | npm run build 36 | - name: Deploy 37 | uses: peaceiris/actions-gh-pages@v3 38 | with: 39 | github_token: ${{ secrets.GITHUB_TOKEN }} 40 | publish_dir: ./docs 41 | enable_jekyll: true 42 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: [ master ] 5 | 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: install puppeteer libraries 15 | run: | 16 | sudo apt-get update 17 | sudo apt-get install -y libgbm1 18 | - name: Test and build 19 | run: | 20 | npm install 21 | npm run lint 22 | npm test 23 | npm run build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test 3 | .out 4 | dist/ 5 | .DS_Store 6 | .idea 7 | docs/components 8 | coverage 9 | .nyc_output -------------------------------------------------------------------------------- /.htmlvalidate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "html-validate:recommended" 4 | ], 5 | "rules": { 6 | "close-order": "error", 7 | "svg-focusable": "off", 8 | "text-content": "off", 9 | "void-style": [ 10 | "error", 11 | { 12 | "style": "selfclosing" 13 | } 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "**/main.js", 4 | "**/main.css.js", 5 | "**/npm" 6 | ] 7 | } -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "block-no-empty": null, 5 | "color-no-invalid-hex": true, 6 | "comment-empty-line-before": [ 7 | "always", 8 | { 9 | "ignore": [ 10 | "stylelint-commands", 11 | "after-comment" 12 | ] 13 | } 14 | ], 15 | "rule-empty-line-before": [ 16 | "always", 17 | { 18 | "except": [ 19 | "first-nested" 20 | ], 21 | "ignore": [ 22 | "after-comment" 23 | ] 24 | } 25 | ], 26 | "declaration-block-no-redundant-longhand-properties": true, 27 | "declaration-no-important": null, 28 | "max-nesting-depth": 5, 29 | "no-duplicate-selectors": true, 30 | "selector-type-no-unknown": null 31 | } 32 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.quickSuggestions": null, 3 | "javascript.validate.enable": false 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019-2020 Zooplus AG. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zoo Web Components 2 | 3 | | **Dependencies** | 4 | | :-----------------------------------------------------------------------: | 5 | | ![David](https://img.shields.io/david/dev/zooplus/zoo-web-components.svg) | 6 | | ![David](https://img.shields.io/david/zooplus/zoo-web-components.svg) | 7 | 8 | - Set of extendable, reusable web-components which can be used in any modern UI framework (or without any). 9 | - 0 dependencies, built with [Vanilla JS](http://vanilla-js.com/) 10 | - Doesn't hide native HTML elements behind multiple levels of abstraction but rather enhances them via composition. 11 | 12 | ## Installation 13 | 14 | To use this library install it by running: 15 | 16 | ```bash 17 | npm i @zooplus/zoo-web-components --save 18 | ``` 19 | 20 | When there is a tree-shaking mechanism in your build pipeline, you can import only the components that you need, for example: 21 | 22 | ```JS 23 | import { Input, Select, registerComponents } from '@zooplus/zoo-web-components'; 24 | registerComponents(Input, Select); 25 | 26 | // or to import everything 27 | 28 | import * as zooComponents from '@zooplus/zoo-web-components'; 29 | zooComponents.registerComponents(zooComponents); 30 | ``` 31 | 32 | All dependencies needed by the components should be pulled automatically, so you don't have to worry about importing classes for `zoo-info`, `zoo-label` etc. 33 | 34 | Your build tool should remove all of the components that are not imported automatically (when using rollup, for example). 35 | 36 | In case you don't use any framework and/or any build tool you can import the whole library with the following: 37 | 38 | ```HTML 39 | 40 | ``` 41 | 42 | or only the components that you need, for example: 43 | 44 | ```HTML 45 | 46 | ``` 47 | 48 | Note, that IIFE components already include all sub-dependencies needed by the component. For example, the above `zoo-checkbox` requires also a `zoo-info` component, it is already in the same bundle as the checkbox. 49 | Other components that might also use `zoo-info` will not throw an error, since care is taken to not redefine same elements in the custom elements registry. 50 | 51 | Currently, we recommend to use ESM bundle only when you're using some kind of a bundler, which transforms your code into some js module format that is not ESM, since using pure ESM might trigger FOUC, and we do not want that. 52 | 53 | Remember to add CSS custom properties to your main styles file: 54 | 55 | ```CSS 56 | :root { 57 | --primary-mid: #3C9700; 58 | --primary-light: #66B100; 59 | --primary-dark: #286400; 60 | --primary-ultralight: #EBF4E5; 61 | --secondary-mid: #FF6200; 62 | --secondary-light: #F80; 63 | --secondary-dark: #CC4E00; 64 | --info-ultralight: #ECF5FA; 65 | --info-mid: #459FD0; 66 | --warning-ultralight: #FDE8E9; 67 | --warning-mid: #ED1C24; 68 | } 69 | ``` 70 | 71 | ## Examples integrating with various frameworks 72 | 73 | - [VueJS](https://github.com/GeorgeTailor/vue-wc-integration) 74 | - [Angular](https://github.com/GeorgeTailor/angular-wc-integration) 75 | - [React](https://github.com/GeorgeTailor/react-wc-integration) 76 | 77 | ## Documentation 78 | 79 | Landing page is available here: 80 | Documentation page is here: 81 | 82 | ## Note 83 | 84 | This library relies on attributes and/or slots. Usage of properties is not supported for simplicity. 85 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zooplus/zoo-web-components/9c386e61691e96b9c6703a30c488c70cea5bafc4/docs/favicon.ico -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zooplus/zoo-web-components/9c386e61691e96b9c6703a30c488c70cea5bafc4/docs/logo.png -------------------------------------------------------------------------------- /docs/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-mid: #368700; 3 | --primary-light: #66B100; 4 | --primary-dark: #286400; 5 | --primary-ultralight: #EBF4E5; 6 | --secondary-mid: #FF6200; 7 | --secondary-light: #F80; 8 | --secondary-dark: #CC4E00; 9 | --info-ultralight: #ECF5FA; 10 | --info-mid: #459FD0; 11 | --warning-ultralight: #FDE8E9; 12 | --warning-mid: #ED1C24; 13 | } 14 | 15 | body { 16 | margin: 0; 17 | font-family: Arial, Helvetica, sans-serif; 18 | } 19 | 20 | main { 21 | max-width: 1280px; 22 | width: 100%; 23 | margin: 0 auto; 24 | } 25 | 26 | .form-content { 27 | display: grid; 28 | grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); 29 | grid-gap: 20px; 30 | margin: 20px auto; 31 | padding: 0 10px; 32 | max-width: 1280px; 33 | } 34 | 35 | .form-content h3 { 36 | grid-column: 1 / -1; 37 | text-align: left; 38 | } 39 | 40 | h2 { 41 | color: var(--primary-mid); 42 | } 43 | 44 | hr { 45 | border-color: var(--primary-mid); 46 | margin: 45px 0; 47 | opacity: 0.3; 48 | } 49 | 50 | .buttons { 51 | max-width: 1280px; 52 | margin: 20px auto; 53 | display: grid; 54 | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); 55 | grid-gap: 15px; 56 | width: 90%; 57 | justify-items: center; 58 | } 59 | 60 | .buttons zoo-button { 61 | width: 100%; 62 | } 63 | 64 | .buttons .icon-btn { 65 | width: max-content; 66 | min-width: 45px; 67 | } 68 | 69 | .icon-btn button svg { 70 | fill: white; 71 | } 72 | 73 | #what { 74 | color: var(--primary-mid); 75 | text-align: center; 76 | } 77 | 78 | .buttons-holder { 79 | display: flex; 80 | justify-content: flex-end; 81 | flex-grow: 1; 82 | } 83 | 84 | .buttons-holder zoo-button { 85 | min-width: 120px; 86 | margin-left: 15px; 87 | } 88 | 89 | @media only screen and (max-width: 900px) { 90 | .buttons-holder { 91 | justify-content: initial; 92 | overflow-y: none; 93 | overflow-x: auto; 94 | } 95 | 96 | .form-content { 97 | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); 98 | } 99 | } 100 | 101 | h3 { 102 | color: var(--primary-mid); 103 | text-align: center; 104 | } 105 | 106 | zoo-grid { 107 | min-width: 1024px; 108 | } 109 | 110 | zoo-grid-row { 111 | --grid-row-content-height: 150px; 112 | } 113 | 114 | .grid-holder { 115 | max-width: 1280px; 116 | overflow: auto; 117 | box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); 118 | width: 95%; 119 | margin: 20px auto; 120 | } 121 | 122 | zoo-select[column="3"] { 123 | padding-right: 10px; 124 | } 125 | 126 | zoo-input zoo-button button { 127 | border-radius: 0 5px 5px 0; 128 | } 129 | 130 | #input-composition { 131 | border-radius: 5px 0 0 5px; 132 | } 133 | 134 | .button-group { 135 | display: flex; 136 | justify-content: center; 137 | } 138 | 139 | .grid-footer-content { 140 | display: flex; 141 | align-items: center; 142 | } 143 | 144 | .grid-footer-content zoo-button { 145 | margin-right: 10px; 146 | } 147 | 148 | .filter-row zoo-input input, 149 | .filter-row zoo-select select { 150 | padding: 0 5px; 151 | height: 25px; 152 | } 153 | 154 | .filter-row > *:not(button) { 155 | padding-right: 5px; 156 | } 157 | 158 | .filter-row input[type="date"]::-webkit-calendar-picker-indicator { 159 | margin: 0; 160 | } 161 | 162 | zoo-navigation { 163 | overflow-y: hidden; 164 | } 165 | 166 | zoo-input-tag-option { 167 | display: none; 168 | } 169 | -------------------------------------------------------------------------------- /docs/no-js.css: -------------------------------------------------------------------------------- 1 | zoo-input input, 2 | zoo-input textarea { 3 | width: 100%; 4 | font-size: 14px; 5 | line-height: 20px; 6 | padding: 13px 15px; 7 | margin: 0; 8 | border: 1px solid #767676; 9 | border-radius: 5px; 10 | color: #555; 11 | outline: none; 12 | box-sizing: border-box; 13 | overflow: hidden; 14 | text-overflow: ellipsis; 15 | grid-column: span 2; 16 | } 17 | 18 | zoo-input input[type="date"], 19 | zoo-input input[type="time"] { 20 | -webkit-logical-height: 48px; 21 | max-height: 48px; 22 | } 23 | 24 | zoo-input input::placeholder, 25 | zoo-input textarea::placeholder { 26 | color: #767676; 27 | } 28 | 29 | /* select */ 30 | 31 | zoo-select select { 32 | -webkit-appearance: none; 33 | -moz-appearance: none; 34 | width: 100%; 35 | font-size: 14px; 36 | line-height: 20px; 37 | padding: 13px 25px 13px 15px; 38 | border: 1px solid #767676; 39 | border-radius: 5px; 40 | color: #555; 41 | outline: none; 42 | box-sizing: border-box; 43 | grid-column: span 2; 44 | } 45 | 46 | zoo-select[labelposition="left"] { 47 | display: flex; 48 | grid-gap: 0 3px; 49 | } 50 | 51 | zoo-select[labelposition="left"] *[slot="label"] { 52 | display: flex; 53 | align-items: center; 54 | } 55 | 56 | /*checkbox*/ 57 | zoo-checkbox { 58 | display: grid; 59 | width: 100%; 60 | font-size: 14px; 61 | line-height: 20px; 62 | position: relative; 63 | 64 | --border: 0; 65 | --check-color: var(--primary-mid); 66 | } 67 | 68 | zoo-checkbox[highlighted] { 69 | --border: 1px solid var(--check-color); 70 | } 71 | 72 | zoo-checkbox input:invalid { 73 | --border: 2px solid var(--warning-mid); 74 | } 75 | 76 | zoo-checkbox input { 77 | display: flex; 78 | align-self: flex-start; 79 | height: 100%; 80 | cursor: pointer; 81 | margin: 0; 82 | border-radius: 3px; 83 | border: var(--border); 84 | } 85 | 86 | zoo-checkbox input:focus { 87 | border-width: 2px; 88 | } 89 | 90 | zoo-checkbox label { 91 | display: flex; 92 | align-self: center; 93 | cursor: pointer; 94 | margin-left: 5px; 95 | z-index: 1; 96 | } 97 | 98 | zoo-checkbox input:disabled { 99 | cursor: not-allowed; 100 | } 101 | 102 | 103 | /*common forms*/ 104 | 105 | zoo-input, zoo-select { 106 | display: grid; 107 | grid-gap: 3px 0; 108 | width: 100%; 109 | height: max-content; 110 | box-sizing: border-box; 111 | } 112 | zoo-input a, 113 | zoo-select a { 114 | text-align: right; 115 | max-width: max-content; 116 | justify-self: flex-end; 117 | padding: 0; 118 | } 119 | 120 | zoo-input input:disabled, 121 | zoo-input textarea:disabled, 122 | zoo-select select:disabled { 123 | border: 1px solid #E6E6E6; 124 | background: #F2F3F4; 125 | color: #767676; 126 | cursor: not-allowed; 127 | } 128 | 129 | zoo-input input:focus, 130 | zoo-input textarea:focus, 131 | zoo-select select:focus { 132 | border: 2px solid #555; 133 | padding: 12px 14px; 134 | } 135 | 136 | zoo-input input:invalid, 137 | zoo-input textarea:invalid, 138 | zoo-select select:invalid { 139 | border: 2px solid var(--warning-mid); 140 | padding: 12px 14px; 141 | } 142 | 143 | /*info + error*/ 144 | *[slot="info"], 145 | *[slot="error"] { 146 | padding: 2px; 147 | font-size: 12px; 148 | line-height: 16px; 149 | color: #555; 150 | align-items: center; 151 | grid-column: span 2; 152 | } 153 | 154 | *[slot="error"] { 155 | color: var(--warning-mid); 156 | display: none; 157 | } 158 | 159 | zoo-input input:invalid ~ *[slot="error"], 160 | zoo-input textarea:invalid ~ *[slot="error"], 161 | zoo-select select:invalid ~ *[slot="error"] { 162 | display: flex; 163 | } 164 | 165 | /*link*/ 166 | a[slot="link"] { 167 | contain: layout; 168 | display: flex; 169 | width: 100%; 170 | height: 100%; 171 | justify-content: center; 172 | align-items: center; 173 | position: relative; 174 | padding: 0 5px; 175 | font-size: 14px; 176 | line-height: 16px; 177 | 178 | --color-normal: var(--primary-mid); 179 | --color-active: var(--primary-dark); 180 | } 181 | 182 | a[slot="link"] { 183 | text-decoration: none; 184 | padding: 0 2px; 185 | color: var(--color-normal); 186 | width: 100%; 187 | } 188 | 189 | a[slot="link"]:hover, 190 | a[slot="link"]:focus, 191 | a[slot="link"]:active { 192 | color: var(--color-active); 193 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zooplus/zoo-web-components", 3 | "version": "10.4.0", 4 | "main": "dist/zoo-web-components.js", 5 | "sideEffects": false, 6 | "files": [ 7 | "dist/*.js", 8 | "dist/**/*.js" 9 | ], 10 | "type": "module", 11 | "module": "dist/esm/zoo-web-components.js", 12 | "description": "Set of web-components implementing zoo+ style guide", 13 | "person": "Yuriy Kravets ", 14 | "devDependencies": { 15 | "axe-core": "^4.3.3", 16 | "clean-css": "^5.1.5", 17 | "concurrently": "^6.2.1", 18 | "cross-env": "^7.0.3", 19 | "eslint": "^8.50.0", 20 | "eslint-config-prettier": "^9.0.0", 21 | "html-minifier": "^4.0.0", 22 | "html-validate": "^8.4.1", 23 | "jasmine": "^3.9.0", 24 | "nyc": "^15.1.0", 25 | "puppeteer": "^22.11.2", 26 | "puppeteer-to-istanbul": "^1.4.0", 27 | "rollup": "^2.79.1", 28 | "rollup-plugin-terser": "^7.0.2", 29 | "stylelint": "^15.10.3", 30 | "stylelint-config-standard": "^34.0.0" 31 | }, 32 | "scripts": { 33 | "start": "concurrently -k \"node server.js docs\" \"cross-env NODE_ENV=local rollup -c -w\"", 34 | "build": "rollup -c", 35 | "pretest": "cross-env NODE_ENV=local npm run build", 36 | "test": "concurrently -k -s first \"nyc --reporter=lcov --reporter=text-summary jasmine\" \"node server.js docs\"", 37 | "prepublishOnly": "npm run lint && npm test && npm run build", 38 | "lint": "eslint src/**/*.js && stylelint src/**/*.css && html-validate src/**/*.html" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/zooplus/zoo-web-components.git" 43 | }, 44 | "keywords": [ 45 | "web-components", 46 | "shadow-dom", 47 | "custom-elements", 48 | "javascript", 49 | "css", 50 | "html" 51 | ], 52 | "author": "Yuriy Kravets", 53 | "license": "MIT", 54 | "bugs": { 55 | "url": "https://github.com/zooplus/zoo-web-components/issues" 56 | }, 57 | "homepage": "https://github.com/zooplus/zoo-web-components#readme" 58 | } 59 | -------------------------------------------------------------------------------- /rollup-config/getModules.js: -------------------------------------------------------------------------------- 1 | import {plugins} from './plugins.js'; 2 | import fs from 'fs'; 3 | 4 | let dev = process.env.NODE_ENV == 'local'; 5 | 6 | const createConfig = (filePath) => { 7 | const fileName = filePath.replace('./src/zoo-modules/', ''); 8 | const shortName = fileName.substring(fileName.lastIndexOf('/') + 1, fileName.lastIndexOf('.')).split('-').join('') 9 | const shortFileName = fileName.substring(fileName.lastIndexOf('/') + 1, fileName.length); 10 | return { 11 | input: filePath, 12 | output: { 13 | file: dev ? `docs/components/${shortFileName}` : `dist/${shortFileName}`, 14 | format: 'iife', 15 | name: shortName, 16 | }, 17 | plugins 18 | } 19 | }; 20 | 21 | 22 | function getFiles(nextPath, modules) { 23 | if(fs.existsSync(nextPath) && fs.lstatSync(nextPath).isDirectory()) { 24 | const nextDirPath = fs.readdirSync(nextPath); 25 | nextDirPath.forEach(filePath => getFiles(`${nextPath}/${filePath}`, modules)); 26 | } else { 27 | if (nextPath.indexOf('.js') > -1) { 28 | modules.push(nextPath); 29 | } 30 | } 31 | } 32 | 33 | export function getModules() { 34 | let modules = []; 35 | getFiles('./src/zoo-modules', modules); 36 | return modules.map(modulePath => createConfig(modulePath)); 37 | } -------------------------------------------------------------------------------- /rollup-config/injectInnerHTML.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import CleanCSS from 'clean-css'; 3 | import minifyHTML from 'html-minifier'; 4 | 5 | export default function injectInnerHTML() { 6 | return { 7 | name: 'injectInnerHTML', 8 | 9 | transform(code, id) { 10 | if (code.indexOf('@injectHTML') > -1) { 11 | const htmlFile = id.replace('.js', '.html'); 12 | const cssFile = id.replace('.js', '.css'); 13 | const html = fs.readFileSync(htmlFile, 'utf8'); 14 | const minifiedHTML = minifyHTML.minify(html, {collapseWhitespace: true, collapseBooleanAttributes: true}); 15 | const css = fs.readFileSync(cssFile, 'utf8'); 16 | const minifiedCss = new CleanCSS({ level: { 2: { all: true } } }).minify(css); 17 | if (minifiedCss.errors && minifiedCss.errors.length > 0) { 18 | console.error(minifiedCss.errors); 19 | } 20 | if (minifiedCss.warnings && minifiedCss.warnings.length > 0) { 21 | console.warn(minifiedCss.warnings); 22 | } 23 | code = code.replace('super();', `super();this.attachShadow({mode:'open'}).innerHTML=\`${minifiedHTML}\`;`); 24 | 25 | // fs.appendFile('./docs/all.css', minifiedCss.styles, function (err) { 26 | // if (err) throw err; 27 | // console.log('Saved!'); 28 | // }); 29 | } 30 | return { 31 | code: code, 32 | map: null 33 | }; 34 | } 35 | }; 36 | } -------------------------------------------------------------------------------- /rollup-config/plugins.js: -------------------------------------------------------------------------------- 1 | import injectInnerHTML from './injectInnerHTML.js'; 2 | import { watcher, noOpWatcher } from './watcher.js'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | 5 | let dev = process.env.NODE_ENV == 'local'; 6 | 7 | export const plugins = [ 8 | injectInnerHTML(), 9 | dev ? watcher() : noOpWatcher(), 10 | dev ? noOpWatcher() : terser({ 11 | module: true, 12 | keep_classnames: true 13 | }), 14 | ]; -------------------------------------------------------------------------------- /rollup-config/watcher.js: -------------------------------------------------------------------------------- 1 | import glob from 'glob'; 2 | import path from 'path'; 3 | 4 | export function watcher() { 5 | return { 6 | buildStart() { 7 | let include = ['src/zoo-modules/**/*.html', 'src/zoo-modules/**/*.css']; 8 | for (const item of include) { 9 | glob.sync(path.resolve(item)).forEach(filename => this.addWatchFile(filename)); 10 | } 11 | }, 12 | options(options) { 13 | options.cache = {}; 14 | return options; 15 | } 16 | }; 17 | } 18 | 19 | export function noOpWatcher() { 20 | return { 21 | options(options) { 22 | return options; 23 | } 24 | }; 25 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { plugins } from './rollup-config/plugins.js'; 2 | import { getModules } from './rollup-config/getModules.js'; 3 | 4 | let dev = process.env.NODE_ENV == 'local'; 5 | 6 | const modules = !dev ? getModules() : []; 7 | export default [ 8 | { 9 | input: 'src/zoo-web-components.js', 10 | output: [ 11 | { 12 | sourcemap: true, 13 | format: 'iife', 14 | dir: dev ? 'docs/components' : 'dist', 15 | name: 'zooWebComponents' 16 | }, 17 | { 18 | sourcemap: true, 19 | format: 'esm', 20 | dir: dev ? 'docs/components/esm' : 'dist/esm', 21 | preserveModules: true 22 | } 23 | ], 24 | plugins: plugins 25 | }, 26 | ...modules 27 | ]; -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import url from 'url'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | const port = 5050; 6 | 7 | http.createServer(function (req, res) { 8 | console.log(`${req.method} ${req.url}`); 9 | 10 | if (req.url === '/') req.url = '/index.html'; 11 | // parse URL 12 | const parsedUrl = url.parse(req.url); 13 | // extract URL path 14 | let pathname = `./docs/${parsedUrl.pathname}`; 15 | // based on the URL path, extract the file extention. e.g. .js, .doc, ... 16 | const ext = path.parse(pathname).ext; 17 | // maps file extention to MIME typere 18 | const map = { 19 | '.ico': 'image/x-icon', 20 | '.html': 'text/html', 21 | '.js': 'text/javascript', 22 | '.json': 'application/json', 23 | '.css': 'text/css', 24 | '.png': 'image/png', 25 | '.jpg': 'image/jpeg', 26 | '.svg': 'image/svg+xml' 27 | }; 28 | 29 | fs.exists(pathname, function (exist) { 30 | if (!exist) { 31 | // if the file is not found, return 404 32 | res.statusCode = 404; 33 | res.end(`File ${pathname} not found!`); 34 | return; 35 | } 36 | // if is a directory search for index file matching the extention 37 | if (fs.statSync(pathname).isDirectory()) pathname += '/index' + ext; 38 | // read file from file system 39 | fs.readFile(pathname, function (err, data) { 40 | if (err) { 41 | res.statusCode = 500; 42 | res.end(`Error getting the file: ${err}.`); 43 | } else { 44 | // if the file is found, set Content-type and send data 45 | res.setHeader('Content-type', map[ext] || 'text/plain'); 46 | res.end(data); 47 | } 48 | }); 49 | }); 50 | }).listen(parseInt(port)); 51 | 52 | console.log(`Server listening on port ${port}`); -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "src/zoo-modules", 3 | "spec_files": [ 4 | "**/*[sS]pec.mjs" 5 | ], 6 | "helpers": [ 7 | "helpers/*.mjs" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": true 11 | } 12 | -------------------------------------------------------------------------------- /src/zoo-modules/common/register-components.js: -------------------------------------------------------------------------------- 1 | export function registerComponents (...args) { 2 | args ? '' : console.error('Please register your components!'); 3 | } -------------------------------------------------------------------------------- /src/zoo-modules/form/checkbox/checkbox-a11y.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo checkbox', () => { 2 | it('should be a11y', async () => { 3 | const results = await page.evaluate(async () => { 4 | document.body.innerHTML = ` 5 | 6 | 7 | 8 | `; 9 | return await axe.run('zoo-checkbox'); 10 | }); 11 | 12 | if (results.violations.length) { 13 | console.log('zoo-checkbox a11y violations ', results.violations); 14 | throw new Error('Accessibility issues found'); 15 | } 16 | }); 17 | }); -------------------------------------------------------------------------------- /src/zoo-modules/form/checkbox/checkbox.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | font-size: 14px; 6 | line-height: 20px; 7 | position: relative; 8 | 9 | --border: 0; 10 | --check-color: var(--primary-mid); 11 | } 12 | 13 | :host([disabled]) { 14 | --check-color: #767676; 15 | } 16 | 17 | :host([highlighted]) { 18 | --border: 1px solid var(--check-color); 19 | } 20 | 21 | :host([invalid]) { 22 | --check-color: var(--warning-mid); 23 | --border: 2px solid var(--warning-mid); 24 | } 25 | 26 | ::slotted(input) { 27 | width: 100%; 28 | height: 100%; 29 | top: 0; 30 | left: 0; 31 | position: absolute; 32 | display: flex; 33 | align-self: flex-start; 34 | appearance: none; 35 | cursor: pointer; 36 | margin: 0; 37 | border-radius: 3px; 38 | border: var(--border); 39 | } 40 | 41 | svg { 42 | border: 1px solid var(--check-color); 43 | fill: var(--check-color); 44 | border-radius: 3px; 45 | pointer-events: none; 46 | min-width: 24px; 47 | z-index: 1; 48 | padding: 1px; 49 | box-sizing: border-box; 50 | } 51 | 52 | svg path { 53 | display: none; 54 | } 55 | 56 | .indeterminate { 57 | display: none; 58 | background: var(--check-color); 59 | fill: white; 60 | } 61 | 62 | :host([checked]) svg path { 63 | display: flex; 64 | } 65 | 66 | :host([checked][indeterminate]) .indeterminate { 67 | display: flex; 68 | } 69 | 70 | :host([checked][indeterminate]) .checked { 71 | display: none; 72 | } 73 | 74 | :host(:focus-within) svg { 75 | border-width: 2px; 76 | } 77 | 78 | ::slotted(input:focus) { 79 | border-width: 2px; 80 | } 81 | 82 | :host([checked]) ::slotted(input) { 83 | border-width: 2px; 84 | } 85 | 86 | :host([disabled]) svg { 87 | background: var(--input-disabled, #F2F3F4); 88 | } 89 | 90 | .checkbox { 91 | display: flex; 92 | width: 100%; 93 | box-sizing: border-box; 94 | cursor: pointer; 95 | align-items: baseline; 96 | position: relative; 97 | } 98 | 99 | :host([highlighted]) .checkbox { 100 | padding: 11px 15px; 101 | } 102 | 103 | ::slotted(label) { 104 | display: flex; 105 | align-self: center; 106 | cursor: pointer; 107 | margin-left: 5px; 108 | z-index: 1; 109 | } 110 | 111 | ::slotted(input:disabled), 112 | :host([disabled]) ::slotted(label) { 113 | cursor: not-allowed; 114 | } 115 | -------------------------------------------------------------------------------- /src/zoo-modules/form/checkbox/checkbox.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/zoo-modules/form/checkbox/checkbox.js: -------------------------------------------------------------------------------- 1 | import { FormElement } from '../common/FormElement.js'; 2 | import { InfoMessage } from '../info/info.js'; 3 | import { registerComponents } from '../../common/register-components.js'; 4 | 5 | /** 6 | * @injectHTML 7 | */ 8 | export class Checkbox extends FormElement { 9 | constructor() { 10 | super(); 11 | registerComponents(InfoMessage); 12 | this.observer = new MutationObserver(mutationsList => { 13 | for (let mutation of mutationsList) { 14 | mutation.target.disabled ? this.setAttribute('disabled', '') : this.removeAttribute('disabled'); 15 | mutation.target.hasAttribute('indeterminate') ? this.setAttribute('indeterminate', '') : this.removeAttribute('indeterminate'); 16 | } 17 | }); 18 | this.shadowRoot.querySelector('slot[name="checkbox"]').addEventListener('slotchange', e => { 19 | let checkbox = [...e.target.assignedElements()].find(el => el.tagName === 'INPUT'); 20 | if (!checkbox) return; 21 | checkbox.addEventListener('change', () => { 22 | checkbox.checked 23 | ? this.setAttribute('checked', '') 24 | : this.removeAttribute('checked'); 25 | }); 26 | this.registerElementForValidation(checkbox); 27 | if (checkbox.disabled) this.setAttribute('disabled', ''); 28 | if (checkbox.checked) this.setAttribute('checked', ''); 29 | if (checkbox.hasAttribute('indeterminate')) this.setAttribute('indeterminate', ''); 30 | this.observer.observe(checkbox, { attributes: true, attributeFilter: ['disabled', 'indeterminate'] }); 31 | }); 32 | } 33 | 34 | disconnectedCallback() { 35 | this.observer.disconnect(); 36 | } 37 | } 38 | if (!window.customElements.get('zoo-checkbox')) { 39 | window.customElements.define('zoo-checkbox', Checkbox); 40 | } -------------------------------------------------------------------------------- /src/zoo-modules/form/checkbox/checkbox.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo checkbox', function () { 2 | it('should create highlighted checkbox', async () => { 3 | const inputLabelText = await page.evaluate(() => { 4 | document.body.innerHTML = ` 5 | 6 | 7 | 8 | 9 | `; 10 | let checkbox = document.querySelector('zoo-checkbox'); 11 | 12 | return checkbox.shadowRoot.querySelector('slot[name="label"]').assignedElements()[0].innerHTML; 13 | }); 14 | expect(inputLabelText).toEqual('label-text'); 15 | }); 16 | 17 | it('should create normal checkbox', async () => { 18 | const style = await page.evaluate(() => { 19 | document.body.innerHTML = ` 20 | 21 | 22 | 23 | 24 | `; 25 | const root = document.querySelector('zoo-checkbox'); 26 | const style = window.getComputedStyle(root); 27 | return { 28 | checkColor: style.getPropertyValue('--check-color').trim(), 29 | border: style.getPropertyValue('--border').trim() 30 | }; 31 | }); 32 | expect(style.checkColor).toEqual(colors.primaryMid); 33 | expect(style.border).toEqual('0'); 34 | }); 35 | 36 | it('should create highlighted checkbox', async () => { 37 | const style = await page.evaluate(() => { 38 | document.body.innerHTML = ` 39 | 40 | 41 | 42 | 43 | `; 44 | const root = document.querySelector('zoo-checkbox'); 45 | const style = window.getComputedStyle(root); 46 | return { 47 | checkColor: style.getPropertyValue('--check-color').trim(), 48 | border: style.getPropertyValue('--border').trim() 49 | }; 50 | }); 51 | expect(style.checkColor).toEqual(colors.primaryMid); 52 | expect(style.border).toEqual(`1px solid ${colors.primaryMid}`); 53 | }); 54 | 55 | it('should create disabled checkbox', async () => { 56 | const style = await page.evaluate(async () => { 57 | document.body.innerHTML = ` 58 | 59 | 60 | 61 | 62 | `; 63 | const root = document.querySelector('zoo-checkbox'); 64 | const style = window.getComputedStyle(root); 65 | await new Promise(r => setTimeout(r, 50)); // wait for internal callbacks to kick in 66 | return { 67 | checkColor: style.getPropertyValue('--check-color').trim(), 68 | border: style.getPropertyValue('--border').trim() 69 | }; 70 | }); 71 | expect(style.checkColor).toEqual('#767676'); 72 | expect(style.border).toEqual('1px solid #767676'); 73 | }); 74 | 75 | it('should create invalid checkbox', async () => { 76 | const style = await page.evaluate(() => { 77 | document.body.innerHTML = ` 78 | 79 | 80 | 81 | 82 | `; 83 | const root = document.querySelector('zoo-checkbox'); 84 | const style = window.getComputedStyle(root); 85 | return { 86 | checkColor: style.getPropertyValue('--check-color').trim(), 87 | border: style.getPropertyValue('--border').trim() 88 | }; 89 | }); 90 | expect(style.checkColor).toEqual(colors.warningMid); 91 | expect(style.border).toEqual(`2px solid ${colors.warningMid}`); 92 | }); 93 | 94 | it('should set checked attribute on host when checkbox is checked', async () => { 95 | const checked = await page.evaluate(async () => { 96 | document.body.innerHTML = ` 97 | 98 | 99 | 100 | 101 | `; 102 | const root = document.querySelector('zoo-checkbox'); 103 | root.shadowRoot.querySelector('slot[name="checkbox"]').assignedElements()[0].click(); 104 | await new Promise(r => setTimeout(r, 10)); 105 | return root.hasAttribute('checked'); 106 | }); 107 | expect(checked).toBeTrue(); 108 | }); 109 | 110 | it('should set disabled attribute on host when checkbox is disabled', async () => { 111 | const disabled = await page.evaluate(async () => { 112 | document.body.innerHTML = ` 113 | 114 | 115 | 116 | 117 | `; 118 | const root = document.querySelector('zoo-checkbox'); 119 | await new Promise(r => setTimeout(r, 10)); 120 | root.shadowRoot.querySelector('slot[name="checkbox"]').assignedElements()[0].disabled = true; 121 | await new Promise(r => setTimeout(r, 10)); 122 | 123 | return root.hasAttribute('disabled'); 124 | }); 125 | expect(disabled).toBeTrue(); 126 | }); 127 | 128 | it('should remove disabled attribute on host when checkbox is no longer disabled', async () => { 129 | const disabled = await page.evaluate(async () => { 130 | document.body.innerHTML = ` 131 | 132 | 133 | 134 | 135 | `; 136 | const root = document.querySelector('zoo-checkbox'); 137 | await new Promise(r => setTimeout(r, 10)); 138 | root.shadowRoot.querySelector('slot[name="checkbox"]').assignedElements()[0].disabled = false; 139 | await new Promise(r => setTimeout(r, 10)); 140 | 141 | return root.hasAttribute('disabled'); 142 | }); 143 | expect(disabled).toBeFalse(); 144 | }); 145 | 146 | it('should set checked attribute when input becomes checked', async () => { 147 | const checked = await page.evaluate(async () => { 148 | document.body.innerHTML = ` 149 | 150 | 151 | 152 | 153 | `; 154 | const root = document.querySelector('zoo-checkbox'); 155 | await new Promise(r => setTimeout(r, 10)); 156 | root.shadowRoot.querySelector('slot[name="checkbox"]').assignedElements()[0].click(); 157 | await new Promise(r => setTimeout(r, 10)); 158 | return root.hasAttribute('checked'); 159 | }); 160 | expect(checked).toBeTrue(); 161 | }); 162 | 163 | it('should set and then remove invalid attribute from host', async () => { 164 | const result = await page.evaluate(async () => { 165 | document.body.innerHTML = ` 166 | 167 | 168 | 169 | 170 | `; 171 | const result = []; 172 | let input = document.querySelector('zoo-checkbox'); 173 | const slottedInput = input.shadowRoot.querySelector('slot[name="checkbox"]').assignedElements()[0]; 174 | slottedInput.click(); 175 | await new Promise(r => setTimeout(r, 10)); 176 | result.push(input.hasAttribute('invalid')); 177 | 178 | slottedInput.click(); 179 | await new Promise(r => setTimeout(r, 10)); 180 | result.push(input.hasAttribute('invalid')); 181 | 182 | return result; 183 | }); 184 | expect(result[0]).toBeFalse(); 185 | expect(result[1]).toBeTrue(); 186 | }); 187 | }); -------------------------------------------------------------------------------- /src/zoo-modules/form/common/FormElement.js: -------------------------------------------------------------------------------- 1 | export class FormElement extends HTMLElement { 2 | constructor() { 3 | super(); 4 | } 5 | 6 | static get observedAttributes() { 7 | return ['invalid']; 8 | } 9 | 10 | registerElementForValidation(element) { 11 | element.addEventListener('invalid', () => { 12 | this.setInvalid(); 13 | this.toggleInvalidAttribute(element); 14 | }); 15 | element.addEventListener('input', () => { 16 | if (element.checkValidity()) { 17 | this.setValid(); 18 | } else { 19 | this.setInvalid(); 20 | } 21 | this.toggleInvalidAttribute(element); 22 | }); 23 | } 24 | 25 | setInvalid() { 26 | this.setAttribute('invalid', ''); 27 | this.setAttribute('aria-invalid', ''); 28 | } 29 | 30 | setValid() { 31 | this.removeAttribute('aria-invalid'); 32 | this.removeAttribute('invalid'); 33 | } 34 | 35 | toggleInvalidAttribute(element) { 36 | const errorMsg = this.shadowRoot.querySelector('zoo-info[role="alert"]'); 37 | element.validity.valid ? errorMsg.removeAttribute('invalid') : errorMsg.setAttribute('invalid', ''); 38 | } 39 | 40 | attributeChangedCallback() { 41 | const errorMsg = this.shadowRoot.querySelector('zoo-info[role="alert"]'); 42 | this.hasAttribute('invalid') ? errorMsg.setAttribute('invalid', '') : errorMsg.removeAttribute('invalid'); 43 | } 44 | } -------------------------------------------------------------------------------- /src/zoo-modules/form/date-range/date-range.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: grid; 3 | grid-gap: 3px; 4 | width: 100%; 5 | height: max-content; 6 | box-sizing: border-box; 7 | } 8 | 9 | fieldset { 10 | border: 0; 11 | padding: 0; 12 | margin: 0; 13 | position: relative; 14 | } 15 | 16 | :host([invalid]) ::slotted(input) { 17 | border: 2px solid var(--warning-mid); 18 | padding: 12px 14px; 19 | } 20 | 21 | .content { 22 | display: flex; 23 | justify-content: space-between; 24 | grid-column: span 2; 25 | } 26 | 27 | .content zoo-input { 28 | width: 49%; 29 | } 30 | 31 | zoo-info { 32 | grid-column: span 2; 33 | } 34 | -------------------------------------------------------------------------------- /src/zoo-modules/form/date-range/date-range.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
-------------------------------------------------------------------------------- /src/zoo-modules/form/date-range/date-range.js: -------------------------------------------------------------------------------- 1 | import { registerComponents } from '../../common/register-components.js'; 2 | import { FormElement } from '../common/FormElement.js'; 3 | import { InfoMessage } from '../info/info.js'; 4 | import { Label } from '../label/label.js'; 5 | import { Input } from '../input/input.js'; 6 | 7 | /** 8 | * @injectHTML 9 | */ 10 | export class DateRange extends FormElement { 11 | constructor() { 12 | super(); 13 | registerComponents(InfoMessage, Label, Input); 14 | const slottedInputs = {}; 15 | this.shadowRoot.querySelector('slot[name="date-from"]') 16 | .addEventListener('slotchange', e => this.handleAndSaveSlottedInputAs(e, 'dateFrom', slottedInputs)); 17 | this.shadowRoot.querySelector('slot[name="date-to"]') 18 | .addEventListener('slotchange', e => this.handleAndSaveSlottedInputAs(e, 'dateTo', slottedInputs)); 19 | this.addEventListener('input', () => { 20 | const dateInputFrom = slottedInputs.dateFrom; 21 | const dateInputTo = slottedInputs.dateTo; 22 | if (dateInputFrom.value && dateInputTo.value && dateInputFrom.value > dateInputTo.value) { 23 | this.setInvalid(); 24 | } else if (dateInputFrom.validity.valid && dateInputTo.validity.valid) { 25 | this.setValid(); 26 | } 27 | }); 28 | } 29 | 30 | handleAndSaveSlottedInputAs(e, propName, slottedInputs) { 31 | const input = [...e.target.assignedElements()].find(el => el.tagName === 'INPUT'); 32 | slottedInputs[propName] = input; 33 | input && this.registerElementForValidation(input); 34 | } 35 | } 36 | if (!window.customElements.get('zoo-date-range')) { 37 | window.customElements.define('zoo-date-range', DateRange); 38 | } -------------------------------------------------------------------------------- /src/zoo-modules/form/date-range/date-range.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo date range', function () { 2 | it('mark component as invalid when min > max', async () => { 3 | const invalid = await page.evaluate(async () => { 4 | document.body.innerHTML = ` 5 | 6 | 7 | 8 | 9 | 10 | `; 11 | await new Promise(r => setTimeout(r, 10)); 12 | const dateRange = document.querySelector('zoo-date-range'); 13 | 14 | const dateFrom = dateRange.shadowRoot.querySelector('slot[name="date-from"]').assignedElements()[0]; 15 | dateFrom.value = '2021-04-26'; 16 | dateFrom.dispatchEvent(new Event('input', {bubbles: true})); 17 | await new Promise(r => setTimeout(r, 10)); 18 | 19 | const dateTo = dateRange.shadowRoot.querySelector('slot[name="date-to"]').assignedElements()[0]; 20 | dateTo.value = '2021-04-25'; 21 | dateTo.dispatchEvent(new Event('input', {bubbles: true})); 22 | await new Promise(r => setTimeout(r, 10)); 23 | 24 | return document.querySelector('zoo-date-range').hasAttribute('invalid'); 25 | }); 26 | expect(invalid).toBeTrue(); 27 | }); 28 | 29 | it('mark component as invalid when max < min', async () => { 30 | const invalid = await page.evaluate(async () => { 31 | document.body.innerHTML = ` 32 | 33 | 34 | 35 | 36 | 37 | `; 38 | await new Promise(r => setTimeout(r, 10)); 39 | const dateRange = document.querySelector('zoo-date-range'); 40 | 41 | const dateTo = dateRange.shadowRoot.querySelector('slot[name="date-to"]').assignedElements()[0]; 42 | dateTo.value = '2021-04-27'; 43 | dateTo.dispatchEvent(new Event('input', {bubbles: true})); 44 | await new Promise(r => setTimeout(r, 10)); 45 | 46 | const dateFrom = dateRange.shadowRoot.querySelector('slot[name="date-from"]').assignedElements()[0]; 47 | dateFrom.value = '2021-04-28'; 48 | dateFrom.dispatchEvent(new Event('input', {bubbles: true})); 49 | await new Promise(r => setTimeout(r, 10)); 50 | 51 | return document.querySelector('zoo-date-range').hasAttribute('invalid'); 52 | }); 53 | expect(invalid).toBeTrue(); 54 | }); 55 | }); -------------------------------------------------------------------------------- /src/zoo-modules/form/info/info.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: none; 3 | padding: 2px; 4 | font-size: 12px; 5 | line-height: 16px; 6 | color: #555; 7 | align-items: center; 8 | } 9 | 10 | :host([shown]) { 11 | display: flex; 12 | } 13 | 14 | :host([role="alert"][shown]:not([invalid])) { 15 | display: none; 16 | } 17 | 18 | :host([role="alert"][invalid][shown]) { 19 | display: flex; 20 | 21 | --icon-color: var(--warning-mid); 22 | } 23 | 24 | zoo-attention-icon { 25 | align-self: flex-start; 26 | } 27 | -------------------------------------------------------------------------------- /src/zoo-modules/form/info/info.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/zoo-modules/form/info/info.js: -------------------------------------------------------------------------------- 1 | import { registerComponents } from '../../common/register-components.js'; 2 | import { AttentionIcon } from '../../icon/attention-icon/attention-icon.js'; 3 | 4 | /** 5 | * @injectHTML 6 | */ 7 | export class InfoMessage extends HTMLElement { 8 | constructor() { 9 | super(); 10 | registerComponents(AttentionIcon); 11 | this.shadowRoot.querySelector('slot').addEventListener('slotchange', e => { 12 | e.target.assignedElements({ flatten: true }).length > 0 ? this.setAttribute('shown', '') : this.removeAttribute('shown'); 13 | }); 14 | } 15 | } 16 | if (!window.customElements.get('zoo-info')) { 17 | window.customElements.define('zoo-info', InfoMessage); 18 | } -------------------------------------------------------------------------------- /src/zoo-modules/form/input-tag/input-tag-option.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | cursor: pointer; 5 | padding: 5px; 6 | overflow: auto; 7 | font-size: 12px; 8 | gap: 3px; 9 | } 10 | -------------------------------------------------------------------------------- /src/zoo-modules/form/input-tag/input-tag-option.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/zoo-modules/form/input-tag/input-tag-option.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class InputTagOption extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | } 9 | if (!window.customElements.get('zoo-input-tag-option')) { 10 | window.customElements.define('zoo-input-tag-option', InputTagOption); 11 | } -------------------------------------------------------------------------------- /src/zoo-modules/form/input-tag/input-tag.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: grid; 3 | grid-gap: 3px; 4 | width: 100%; 5 | height: max-content; 6 | box-sizing: border-box; 7 | 8 | --input-tag-padding-top-bottom-default: 13px; 9 | --input-tag-padding-left-right-default: 15px; 10 | --input-tag-padding-reduced: calc(var(--input-tag-padding-top-bottom, var(--input-tag-padding-top-bottom-default)) - 1px) calc(var(--input-tag-padding-left-right, var(--input-tag-padding-left-right-default)) - 1px); 11 | } 12 | 13 | #input-wrapper { 14 | display: flex; 15 | flex-wrap: wrap; 16 | align-items: center; 17 | height: max-content; 18 | gap: 5px; 19 | font-size: 14px; 20 | line-height: 20px; 21 | padding: var(--input-tag-padding-top-bottom, var(--input-tag-padding-top-bottom-default)) var(--input-tag-padding-left-right, var(--input-tag-padding-left-right-default)); 22 | border: 1px solid #767676; 23 | border-radius: 5px; 24 | color: #555; 25 | box-sizing: border-box; 26 | grid-column: span 2; 27 | position: relative; 28 | overflow: visible; 29 | } 30 | 31 | :host(:focus-within) #input-wrapper { 32 | border: 2px solid #555; 33 | padding: var(--input-tag-padding-reduced); 34 | } 35 | 36 | :host([show-tags]) #input-wrapper { 37 | z-index: 2; 38 | } 39 | 40 | :host([invalid]) #input-wrapper { 41 | border: 2px solid var(--warning-mid); 42 | padding: var(--input-tag-padding-reduced); 43 | } 44 | 45 | ::slotted(input) { 46 | border: 0; 47 | min-width: 50px; 48 | flex: 1 0 auto; 49 | outline: none; 50 | font-size: 14px; 51 | line-height: 20px; 52 | color: #555; 53 | } 54 | 55 | zoo-label { 56 | grid-row: 1; 57 | } 58 | 59 | #tag-options { 60 | display: none; 61 | position: absolute; 62 | flex-wrap: wrap; 63 | background: white; 64 | padding: 5px var(--input-tag-padding-left-right, var(--input-tag-padding-left-right-default)); 65 | border: 1px solid #555; 66 | border-radius: 0 0 3px 3px; 67 | left: -1px; 68 | top: calc(90% + 2px); 69 | border-top: 0; 70 | width: calc(100% + 2px); 71 | box-sizing: border-box; 72 | max-height: var(--input-tag-options-max-height, fit-content); 73 | overflow: var(--input-tag-options-overflow, auto); 74 | } 75 | 76 | :host(:focus-within) #tag-options, 77 | :host([invalid]) #tag-options { 78 | border-width: 2px; 79 | width: calc(100% + 4px); 80 | left: -2px; 81 | padding-left: calc(var(--input-tag-padding-left-right, var(--input-tag-padding-left-right-default)) - 1px); 82 | padding-right: calc(var(--input-tag-padding-left-right, var(--input-tag-padding-left-right-default)) - 1px); 83 | } 84 | 85 | :host([invalid]) #tag-options { 86 | border-color: var(--warning-mid); 87 | } 88 | 89 | :host([show-tags]) #tag-options { 90 | display: flex; 91 | } 92 | 93 | ::slotted(*[slot="select"]) { 94 | display: none; 95 | } 96 | 97 | zoo-info { 98 | grid-column: span 2; 99 | } 100 | 101 | zoo-cross-icon { 102 | cursor: pointer; 103 | 104 | --icon-color: var(--primary-mid); 105 | } 106 | 107 | ::slotted(zoo-input-tag-option) { 108 | box-sizing: border-box; 109 | width: 100%; 110 | } 111 | 112 | ::slotted(zoo-input-tag-option:hover), 113 | ::slotted(zoo-input-tag-option[selected]:hover) { 114 | background: var(--item-hovered, #E6E6E6); 115 | } 116 | 117 | ::slotted(zoo-input-tag-option[selected]) { 118 | background: var(--primary-ultralight); 119 | } 120 | 121 | -------------------------------------------------------------------------------- /src/zoo-modules/form/input-tag/input-tag.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/zoo-modules/form/input/input-a11y.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo input', function () { 2 | it('should be a11y', async () => { 3 | const results = await page.evaluate(async () => { 4 | document.body.innerHTML = ` 5 | 6 | 7 | 8 | `; 9 | return await axe.run('zoo-input'); 10 | }); 11 | 12 | if (results.violations.length) { 13 | console.log('zoo-input a11y violations ', results.violations); 14 | throw new Error('Accessibility issues found'); 15 | } 16 | }); 17 | }); -------------------------------------------------------------------------------- /src/zoo-modules/form/input/input.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: grid; 3 | grid-gap: 3px; 4 | width: 100%; 5 | height: max-content; 6 | box-sizing: border-box; 7 | } 8 | 9 | ::slotted(input), 10 | ::slotted(textarea) { 11 | width: 100%; 12 | font-size: 14px; 13 | line-height: 20px; 14 | padding: 13px 15px; 15 | margin: 0; 16 | border: 1px solid #767676; 17 | border-radius: 5px; 18 | color: #555; 19 | outline: none; 20 | box-sizing: border-box; 21 | overflow: hidden; 22 | text-overflow: ellipsis; 23 | } 24 | 25 | :host([invalid]) ::slotted(input), 26 | :host([invalid]) ::slotted(textarea) { 27 | border: 2px solid var(--warning-mid); 28 | padding: 12px 14px; 29 | } 30 | 31 | ::slotted(input[type="date"]), 32 | ::slotted(input[type="time"]) { 33 | -webkit-logical-height: 48px; 34 | max-height: 48px; 35 | } 36 | 37 | ::slotted(input::placeholder), 38 | ::slotted(textarea::placeholder) { 39 | color: #767676; 40 | } 41 | 42 | ::slotted(input:disabled), 43 | ::slotted(textarea:disabled) { 44 | border: 1px solid #E6E6E6; 45 | background: var(--input-disabled, #F2F3F4); 46 | color: #767676; 47 | cursor: not-allowed; 48 | } 49 | 50 | ::slotted(input:focus), 51 | ::slotted(textarea:focus) { 52 | border: 2px solid #555; 53 | padding: 12px 14px; 54 | } 55 | 56 | .content { 57 | display: flex; 58 | grid-column: span 2; 59 | } 60 | 61 | zoo-info { 62 | grid-column: span 2; 63 | } 64 | 65 | zoo-link { 66 | text-align: right; 67 | max-width: max-content; 68 | justify-self: flex-end; 69 | padding: 0; 70 | } 71 | 72 | :host([labelposition="left"]) zoo-link { 73 | grid-column: 2; 74 | } 75 | 76 | :host([labelposition="left"]) zoo-label, 77 | :host([labelposition="left"]) .content { 78 | display: flex; 79 | align-items: center; 80 | grid-row: 2; 81 | } 82 | 83 | :host([labelposition="left"]) zoo-info[role="status"] { 84 | grid-row: 3; 85 | grid-column: 2; 86 | } 87 | 88 | :host([labelposition="left"]) zoo-info[role="alert"] { 89 | grid-row: 4; 90 | grid-column: 2; 91 | } 92 | -------------------------------------------------------------------------------- /src/zoo-modules/form/input/input.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/zoo-modules/form/input/input.js: -------------------------------------------------------------------------------- 1 | import { FormElement } from '../common/FormElement.js'; 2 | import { registerComponents } from '../../common/register-components.js'; 3 | import { InfoMessage } from '../info/info.js'; 4 | import { Label } from '../label/label.js'; 5 | import { Link } from '../../misc/link/link.js'; 6 | 7 | /** 8 | * @injectHTML 9 | */ 10 | export class Input extends FormElement { 11 | constructor() { 12 | super(); 13 | registerComponents(InfoMessage, Label, Link); 14 | this.shadowRoot.querySelector('slot[name="input"]').addEventListener('slotchange', e => { 15 | let input = [...e.target.assignedElements()].find(el => el.tagName === 'INPUT'); 16 | input && this.registerElementForValidation(input); 17 | }); 18 | } 19 | } 20 | if (!window.customElements.get('zoo-input')) { 21 | window.customElements.define('zoo-input', Input); 22 | } -------------------------------------------------------------------------------- /src/zoo-modules/form/input/input.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo input', function () { 2 | it('should pass attributes to input label component', async () => { 3 | const labelText = await page.evaluate(() => { 4 | document.body.innerHTML = ` 5 | 6 | 7 | 8 | 9 | `; 10 | let input = document.querySelector('zoo-input'); 11 | const label = input.shadowRoot.querySelector('slot[name="label"]').assignedElements()[0]; 12 | return label.innerHTML; 13 | }); 14 | expect(labelText).toEqual('label'); 15 | }); 16 | 17 | it('should render input link', async () => { 18 | const linkAttrs = await page.evaluate(() => { 19 | document.body.innerHTML = ` 20 | 21 | 22 | 23 | Possible values: Dog, Cat, Small Pet, Bird, Aquatic 24 | Learn your HTML and don't overcomplicate 25 | 26 | `; 27 | let input = document.querySelector('zoo-input'); 28 | const linkAnchor = input.shadowRoot.querySelector('slot[name="link"]').assignedElements()[0]; 29 | return { 30 | linkText: linkAnchor.innerHTML, 31 | linkTarget: linkAnchor.getAttribute('target'), 32 | linkHref: linkAnchor.getAttribute('href') 33 | }; 34 | }); 35 | expect(linkAttrs.linkText).toEqual('Learn your HTML and don\'t overcomplicate'); 36 | expect(linkAttrs.linkHref).toEqual('https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist'); 37 | expect(linkAttrs.linkTarget).toEqual('about:blank'); 38 | }); 39 | 40 | it('should render input error', async () => { 41 | const errorDisplay = await page.evaluate(async () => { 42 | document.body.innerHTML = ` 43 | 44 | 45 | 46 | error 47 | 48 | `; 49 | let input = document.querySelector('zoo-input'); 50 | await new Promise(r => setTimeout(r, 10)); 51 | const error = input.shadowRoot.querySelector('zoo-info[role="alert"]'); 52 | return window.getComputedStyle(error).display; 53 | }); 54 | expect(errorDisplay).toEqual('flex'); 55 | }); 56 | 57 | it('should not render input error', async () => { 58 | const errorDisplay = await page.evaluate(async () => { 59 | document.body.innerHTML = ` 60 | 61 | 62 | 63 | error 64 | 65 | `; 66 | let input = document.querySelector('zoo-input'); 67 | await new Promise(r => setTimeout(r, 10)); 68 | const error = input.shadowRoot.querySelector('zoo-info[role="alert"]'); 69 | return window.getComputedStyle(error).display; 70 | }); 71 | expect(errorDisplay).toEqual('none'); 72 | }); 73 | 74 | it('should set and then remove invalid attribute from host', async () => { 75 | const result = await page.evaluate(async () => { 76 | document.body.innerHTML = ` 77 | 78 | 79 | 80 | error 81 | 82 | `; 83 | const result = []; 84 | let input = document.querySelector('zoo-input'); 85 | await new Promise(r => setTimeout(r, 10)); 86 | const slottedInput = input.shadowRoot.querySelector('slot[name="input"]').assignedElements()[0]; 87 | slottedInput.value = 123; 88 | slottedInput.dispatchEvent(new Event('input')); 89 | await new Promise(r => setTimeout(r, 10)); 90 | result.push(input.hasAttribute('invalid')); 91 | 92 | slottedInput.value = 'asd'; 93 | slottedInput.dispatchEvent(new Event('input')); 94 | await new Promise(r => setTimeout(r, 10)); 95 | result.push(input.hasAttribute('invalid')); 96 | 97 | return result; 98 | }); 99 | expect(result[0]).toBeTrue(); 100 | expect(result[1]).toBeFalse(); 101 | }); 102 | }); -------------------------------------------------------------------------------- /src/zoo-modules/form/label/label.css: -------------------------------------------------------------------------------- 1 | :host { 2 | font-size: 14px; 3 | line-height: 20px; 4 | font-weight: 700; 5 | color: #555; 6 | text-align: left; 7 | } 8 | -------------------------------------------------------------------------------- /src/zoo-modules/form/label/label.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/zoo-modules/form/label/label.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class Label extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | } 9 | if (!window.customElements.get('zoo-label')) { 10 | window.customElements.define('zoo-label', Label); 11 | } -------------------------------------------------------------------------------- /src/zoo-modules/form/quantity-control/quantity-contol-a11y.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo quantity control', function() { 2 | it('should pass accessibility tests', async() => { 3 | const results = await page.evaluate(async () => { 4 | document.body.innerHTML = ` 5 | 6 | 9 | 10 | 11 | 17 | `; 18 | return await axe.run('zoo-quantity-control'); 19 | }); 20 | if (results.violations.length) { 21 | console.log('zoo-quantity-control a11y violations ', results.violations); 22 | throw new Error('Accessibility issues found'); 23 | } 24 | }); 25 | }); -------------------------------------------------------------------------------- /src/zoo-modules/form/quantity-control/quantity-control.css: -------------------------------------------------------------------------------- 1 | :host { 2 | --input-length: 1ch; 3 | } 4 | 5 | div { 6 | height: 36px; 7 | display: flex; 8 | } 9 | 10 | ::slotted(button) { 11 | border-width: 0; 12 | min-width: 30px; 13 | min-height: 30px; 14 | background: var(--primary-mid); 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | padding: 4px; 19 | cursor: pointer; 20 | stroke-width: 1.5; 21 | stroke: #FFF; 22 | } 23 | 24 | ::slotted(button[slot="decrease"]) { 25 | border-radius: 5px 0 0 5px; 26 | } 27 | 28 | ::slotted(button[slot="increase"]) { 29 | border-radius: 0 5px 5px 0; 30 | } 31 | 32 | ::slotted(button:disabled) { 33 | background: var(--input-disabled, #F2F3F4); 34 | cursor: not-allowed; 35 | } 36 | 37 | ::slotted(input) { 38 | width: var(--input-length); 39 | min-width: 30px; 40 | font-size: 14px; 41 | line-height: 20px; 42 | margin: 0; 43 | border: none; 44 | color: #555; 45 | outline: none; 46 | box-sizing: border-box; 47 | appearance: textfield; 48 | text-align: center; 49 | } 50 | 51 | :host([labelposition="left"]) { 52 | display: grid; 53 | grid-gap: 3px; 54 | height: max-content; 55 | } 56 | 57 | :host([labelposition="left"]) zoo-link { 58 | grid-column: 2; 59 | } 60 | 61 | :host([labelposition="left"]) zoo-label, 62 | :host([labelposition="left"]) div { 63 | display: flex; 64 | align-items: center; 65 | grid-row: 1; 66 | } 67 | 68 | :host([labelposition="left"]) zoo-info[role="status"] { 69 | grid-row: 2; 70 | grid-column: 2; 71 | } 72 | 73 | :host([labelposition="left"]) zoo-info[role="alert"] { 74 | grid-row: 3; 75 | grid-column: 2; 76 | } 77 | -------------------------------------------------------------------------------- /src/zoo-modules/form/quantity-control/quantity-control.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/zoo-modules/form/quantity-control/quantity-control.js: -------------------------------------------------------------------------------- 1 | import { FormElement } from '../common/FormElement.js'; 2 | import { registerComponents } from '../../common/register-components.js'; 3 | import { InfoMessage } from '../info/info.js'; 4 | import { Label } from '../label/label.js'; 5 | 6 | /** 7 | * @injectHTML 8 | */ 9 | export class QuantityControl extends FormElement { 10 | constructor() { 11 | super(); 12 | registerComponents(InfoMessage, Label); 13 | this.shadowRoot.querySelector('slot[name="input"]').addEventListener('slotchange', e => { 14 | this.input = [...e.target.assignedElements()].find(el => el.tagName === 'INPUT'); 15 | if (!this.input) return; 16 | this.registerElementForValidation(this.input); 17 | this.setInputWidth(); 18 | }); 19 | 20 | this.shadowRoot.querySelector('slot[name="increase"]') 21 | .addEventListener('slotchange', e => this.handleClick(true, e.target.assignedElements()[0])); 22 | 23 | this.shadowRoot.querySelector('slot[name="decrease"]') 24 | .addEventListener('slotchange', e => this.handleClick(false, e.target.assignedElements()[0])); 25 | } 26 | 27 | setInputWidth() { 28 | const length = this.input.value ? this.input.value.length || 1 : 1; 29 | this.style.setProperty('--input-length', length + 1 + 'ch'); 30 | } 31 | 32 | handleClick(increment, el) { 33 | if (!el) return; 34 | el.addEventListener('click', () => { 35 | const step = this.input.step || 1; 36 | this.input.value = this.input.value || 0; 37 | this.input.value -= increment ? -step : step; 38 | this.input.dispatchEvent(new Event('change')); 39 | this.setInputWidth(); 40 | }); 41 | } 42 | } 43 | 44 | if (!window.customElements.get('zoo-quantity-control')) { 45 | window.customElements.define('zoo-quantity-control', QuantityControl); 46 | } -------------------------------------------------------------------------------- /src/zoo-modules/form/quantity-control/quantityControl.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo quantity control', function() { 2 | it('should increase input value when plus is clicked', async() => { 3 | const ret = await page.evaluate(async () => { 4 | document.body.innerHTML = ` 5 | 6 | 9 | 10 | 11 | 17 | `; 18 | 19 | await new Promise(r => setTimeout(r, 10)); 20 | 21 | const input = document.querySelector('zoo-quantity-control'); 22 | const increaseBtn = input.shadowRoot.querySelector('slot[name="increase"]').assignedElements()[0]; 23 | const slottedInput = input.shadowRoot.querySelector('slot[name="input"]').assignedElements()[0]; 24 | increaseBtn.click(); 25 | 26 | return slottedInput.value; 27 | }); 28 | expect(ret).toEqual('50'); 29 | }); 30 | 31 | it('should not increase input value when plus is clicked', async() => { 32 | const ret = await page.evaluate(async () => { 33 | document.body.innerHTML = ` 34 | 35 | 38 | 39 | 40 | 46 | `; 47 | 48 | await new Promise(r => setTimeout(r, 10)); 49 | 50 | const input = document.querySelector('zoo-quantity-control'); 51 | const slottedInput = input.shadowRoot.querySelector('slot[name="input"]').assignedElements()[0]; 52 | slottedInput.value = 50; 53 | const increaseBtn = input.shadowRoot.querySelector('slot[name="increase"]').assignedElements()[0]; 54 | increaseBtn.click(); 55 | 56 | return slottedInput.value; 57 | }); 58 | expect(ret).toEqual('50'); 59 | }); 60 | 61 | it('should decrease input value when minus is clicked', async() => { 62 | const ret = await page.evaluate(async () => { 63 | document.body.innerHTML = ` 64 | 65 | 68 | 69 | 70 | 76 | `; 77 | 78 | await new Promise(r => setTimeout(r, 10)); 79 | 80 | const input = document.querySelector('zoo-quantity-control'); 81 | const slottedInput = input.shadowRoot.querySelector('slot[name="input"]').assignedElements()[0]; 82 | const decreaseBtn = input.shadowRoot.querySelector('slot[name="decrease"]').assignedElements()[0]; 83 | decreaseBtn.click(); 84 | 85 | return slottedInput.value; 86 | }); 87 | expect(ret).toEqual('-50'); 88 | }); 89 | 90 | it('should not decrease input value when minus is clicked', async() => { 91 | const ret = await page.evaluate(async () => { 92 | document.body.innerHTML = ` 93 | 94 | 97 | 98 | 99 | 105 | `; 106 | 107 | await new Promise(r => setTimeout(r, 10)); 108 | 109 | const input = document.querySelector('zoo-quantity-control'); 110 | const slottedInput = input.shadowRoot.querySelector('slot[name="input"]').assignedElements()[0]; 111 | slottedInput.value = 50; 112 | const decreaseBtn = input.shadowRoot.querySelector('slot[name="decrease"]').assignedElements()[0]; 113 | decreaseBtn.click(); 114 | await new Promise(r => setTimeout(r, 10)); 115 | 116 | return slottedInput.value; 117 | }); 118 | expect(ret).toEqual('50'); 119 | }); 120 | 121 | it('should use default step of 1 when step is not defined', async() => { 122 | const ret = await page.evaluate(async () => { 123 | document.body.innerHTML = ` 124 | 125 | 128 | 129 | 130 | 136 | `; 137 | 138 | await new Promise(r => setTimeout(r, 10)); 139 | 140 | const input = document.querySelector('zoo-quantity-control'); 141 | const increaseBtn = input.shadowRoot.querySelector('slot[name="increase"]').assignedElements()[0]; 142 | const slottedInput = input.shadowRoot.querySelector('slot[name="input"]').assignedElements()[0]; 143 | increaseBtn.click(); 144 | 145 | return slottedInput.value; 146 | }); 147 | expect(ret).toEqual('1'); 148 | }); 149 | 150 | it('should set and then remove invalid attribute from host', async () => { 151 | const result = await page.evaluate(async () => { 152 | document.body.innerHTML = ` 153 | 154 | 157 | 158 | 159 | 165 | 166 | `; 167 | const result = []; 168 | let quantityControl = document.querySelector('zoo-quantity-control'); 169 | await new Promise(r => setTimeout(r, 10)); 170 | const slottedInput = quantityControl.shadowRoot.querySelector('slot[name="input"]').assignedElements()[0]; 171 | slottedInput.value = ''; 172 | slottedInput.dispatchEvent(new Event('input')); 173 | await new Promise(r => setTimeout(r, 10)); 174 | result.push(quantityControl.hasAttribute('invalid')); 175 | 176 | slottedInput.value = 2; 177 | slottedInput.dispatchEvent(new Event('input')); 178 | await new Promise(r => setTimeout(r, 10)); 179 | result.push(quantityControl.hasAttribute('invalid')); 180 | 181 | return result; 182 | }); 183 | expect(result[0]).toBeTrue(); 184 | expect(result[1]).toBeFalse(); 185 | }); 186 | }); -------------------------------------------------------------------------------- /src/zoo-modules/form/radio/radio-a11y.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo radio', function() { 2 | it('should pass accessibility tests', async() => { 3 | const results = await page.evaluate(async () => { 4 | document.body.innerHTML = ` 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | `; 13 | return await axe.run('zoo-radio'); 14 | }); 15 | if (results.violations.length) { 16 | console.log('zoo-radio a11y violations ', results.violations); 17 | throw new Error('Accessibility issues found'); 18 | } 19 | }); 20 | }); -------------------------------------------------------------------------------- /src/zoo-modules/form/radio/radio.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | font-size: 14px; 5 | line-height: 20px; 6 | 7 | --box-shadow-color: #767676; 8 | --box-shadow-width: 1px; 9 | --box-shadow-color2: transparent; 10 | --box-shadow-width2: 1px; 11 | } 12 | 13 | fieldset { 14 | border: 0; 15 | padding: 0; 16 | margin: 0; 17 | position: relative; 18 | } 19 | 20 | .radio-group { 21 | display: flex; 22 | padding: 11px 0; 23 | } 24 | 25 | :host([invalid]) { 26 | color: var(--warning-mid); 27 | } 28 | 29 | ::slotted(input) { 30 | position: relative; 31 | min-width: 24px; 32 | height: 24px; 33 | border-radius: 50%; 34 | margin: 0 2px 0 0; 35 | padding: 4px; 36 | background-clip: content-box; 37 | appearance: none; 38 | outline: none; 39 | cursor: pointer; 40 | box-shadow: inset 0 0 0 var(--box-shadow-width) var(--box-shadow-color), inset 0 0 0 var(--box-shadow-width2) var(--box-shadow-color2); 41 | } 42 | 43 | :host([invalid]) ::slotted(input) { 44 | --box-shadow-color: var(--warning-mid); 45 | } 46 | 47 | ::slotted(input:focus) { 48 | --box-shadow-color: var(--primary-mid); 49 | --box-shadow-width: 2px; 50 | } 51 | 52 | ::slotted(input:checked) { 53 | background-color: var(--primary-mid); 54 | 55 | --box-shadow-color: var(--primary-mid); 56 | --box-shadow-width: 2px; 57 | --box-shadow-width2: 4px; 58 | --box-shadow-color2: white; 59 | } 60 | 61 | 62 | :host([invalid]) ::slotted(input:checked) { 63 | background-color: var(--warning-mid); 64 | } 65 | 66 | ::slotted(input:disabled) { 67 | cursor: not-allowed; 68 | background-color: #555; 69 | 70 | --box-shadow-width: 2px; 71 | --box-shadow-width2: 5px; 72 | --box-shadow-color: #555 !important; 73 | } 74 | 75 | ::slotted(label) { 76 | cursor: pointer; 77 | margin: 0 5px; 78 | align-self: center; 79 | } 80 | 81 | :host([labelposition="left"]) fieldset { 82 | display: grid; 83 | grid-gap: 3px; 84 | } 85 | 86 | :host([labelposition="left"]) .radio-group { 87 | grid-column: 2; 88 | } 89 | 90 | :host([labelposition="left"]) legend, 91 | :host([labelposition="left"]) .radio-group { 92 | grid-row: 1; 93 | display: flex; 94 | align-items: center; 95 | } 96 | 97 | :host([labelposition="left"]) legend { 98 | display: contents; 99 | } 100 | 101 | :host([labelposition="left"]) legend zoo-label { 102 | display: flex; 103 | align-items: center; 104 | } 105 | 106 | :host([labelposition="left"]) zoo-info[role="status"] { 107 | grid-row: 2; 108 | grid-column: 2; 109 | } 110 | 111 | :host([labelposition="left"]) zoo-info[role="alert"] { 112 | grid-row: 3; 113 | grid-column: 2; 114 | } 115 | -------------------------------------------------------------------------------- /src/zoo-modules/form/radio/radio.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
-------------------------------------------------------------------------------- /src/zoo-modules/form/radio/radio.js: -------------------------------------------------------------------------------- 1 | import { registerComponents } from '../../common/register-components.js'; 2 | import { FormElement } from '../common/FormElement.js'; 3 | import { InfoMessage } from '../info/info.js'; 4 | import { Label } from '../label/label.js'; 5 | 6 | /** 7 | * @injectHTML 8 | */ 9 | export class Radio extends FormElement { 10 | constructor() { 11 | super(); 12 | registerComponents(InfoMessage, Label); 13 | this.shadowRoot.querySelector('.radio-group slot').addEventListener('slotchange', e => { 14 | e.target.assignedElements().forEach(e => e.tagName === 'INPUT' && this.registerElementForValidation(e)); 15 | }); 16 | } 17 | } 18 | if (!window.customElements.get('zoo-radio')) { 19 | window.customElements.define('zoo-radio', Radio); 20 | } -------------------------------------------------------------------------------- /src/zoo-modules/form/radio/radio.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo radio', function () { 2 | it('should pass attributes to input label component', async () => { 3 | const labelText = await page.evaluate(() => { 4 | document.body.innerHTML = ` 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | `; 13 | let input = document.querySelector('zoo-radio'); 14 | const label = input.shadowRoot.querySelector('slot[name="label"]').assignedElements()[0]; 15 | return label.innerHTML; 16 | }); 17 | expect(labelText).toEqual('label'); 18 | }); 19 | 20 | it('should render input error', async () => { 21 | const errorDisplay = await page.evaluate(async () => { 22 | document.body.innerHTML = ` 23 | 24 | 25 | 26 | 27 | 28 | 29 | error 30 | 31 | `; 32 | let input = document.querySelector('zoo-radio'); 33 | await new Promise(r => setTimeout(r, 10)); 34 | const error = input.shadowRoot.querySelector('zoo-info[role="alert"]'); 35 | return window.getComputedStyle(error).display; 36 | }); 37 | expect(errorDisplay).toEqual('flex'); 38 | }); 39 | 40 | it('should not render input error', async () => { 41 | const errorDisplay = await page.evaluate(() => { 42 | document.body.innerHTML = ` 43 | 44 | 45 | 46 | 47 | 48 | 49 | error 50 | 51 | `; 52 | let input = document.querySelector('zoo-radio'); 53 | const error = input.shadowRoot.querySelector('zoo-info[role="alert"]'); 54 | return window.getComputedStyle(error).display; 55 | }); 56 | expect(errorDisplay).toEqual('none'); 57 | }); 58 | 59 | it('should set and then remove invalid attribute from host', async () => { 60 | const result = await page.evaluate(async () => { 61 | document.body.innerHTML = ` 62 | 63 | 64 | 65 | 66 | 67 | 68 | error 69 | 70 | `; 71 | const result = []; 72 | let input = document.querySelector('zoo-radio'); 73 | await new Promise(r => setTimeout(r, 10)); 74 | const slottedEls = input.shadowRoot.querySelector('.radio-group slot').assignedElements(); 75 | const inputs = []; 76 | slottedEls.forEach(e => { 77 | if (e.tagName === 'INPUT') inputs.push(e); 78 | }); 79 | inputs[0].checkValidity(); 80 | await new Promise(r => setTimeout(r, 10)); 81 | result.push(input.hasAttribute('invalid')); 82 | 83 | inputs[0].click(); 84 | await new Promise(r => setTimeout(r, 10)); 85 | result.push(input.hasAttribute('invalid')); 86 | 87 | return result; 88 | }); 89 | expect(result[0]).toBeTrue(); 90 | expect(result[1]).toBeFalse(); 91 | }); 92 | }); -------------------------------------------------------------------------------- /src/zoo-modules/form/searchable-select/searchable-select-a11y.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo searchable select', function() { 2 | it('should pass accessibility tests', async() => { 3 | const results = await page.evaluate(async () => { 4 | document.body.innerHTML = ` 5 | 6 | Searchable multiple select legend 7 | 10 | 11 | 12 | 13 | `; 14 | return await axe.run('zoo-searchable-select'); 15 | }); 16 | 17 | if (results.violations.length) { 18 | console.log('zoo-searchable-select a11y violations ', results.violations); 19 | throw new Error('Accessibility issues found'); 20 | } 21 | }); 22 | }); -------------------------------------------------------------------------------- /src/zoo-modules/form/searchable-select/searchable-select.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: grid; 3 | grid-gap: 3px; 4 | width: 100%; 5 | height: max-content; 6 | box-sizing: border-box; 7 | } 8 | 9 | .cross { 10 | display: none; 11 | position: absolute; 12 | top: 12px; 13 | right: 14px; 14 | cursor: pointer; 15 | border: 0; 16 | padding: 0; 17 | background: transparent; 18 | } 19 | 20 | .cross.hidden, 21 | :host([value-selected]) .cross.hidden { 22 | display: none; 23 | } 24 | 25 | :host([value-selected]) .cross { 26 | display: flex; 27 | } 28 | 29 | zoo-tooltip { 30 | display: none; 31 | } 32 | 33 | :host(:hover) zoo-tooltip, 34 | :host(:focus) zoo-tooltip { 35 | display: grid; 36 | } 37 | 38 | zoo-select { 39 | border-top: none; 40 | position: absolute; 41 | z-index: 2; 42 | top: 59%; 43 | display: none; 44 | 45 | --icons-display: none; 46 | } 47 | 48 | :host(:focus-within) zoo-select { 49 | display: grid; 50 | } 51 | 52 | slot[name="selectlabel"] { 53 | display: none; 54 | } 55 | 56 | :host(:focus-within) slot[name="selectlabel"] { 57 | display: block; 58 | } 59 | 60 | :host(:focus-within) ::slotted(select) { 61 | border-top-left-radius: 0; 62 | border-top-right-radius: 0; 63 | border: 2px solid #555; 64 | border-top: none !important; 65 | } 66 | 67 | :host([invalid]) ::slotted(select) { 68 | border: 2px solid var(--warning-mid); 69 | } 70 | 71 | zoo-preloader { 72 | display: none; 73 | } 74 | 75 | :host([loading]) zoo-preloader { 76 | display: flex; 77 | } 78 | 79 | ::slotted(*[slot="inputlabel"]), 80 | ::slotted(*[slot="selectlabel"]) { 81 | position: absolute; 82 | overflow: hidden; 83 | clip: rect(0 0 0 0); 84 | height: 1px; 85 | width: 1px; 86 | margin: -1px; 87 | padding: 0; 88 | border: 0; 89 | } 90 | 91 | zoo-link { 92 | align-items: flex-start; 93 | text-align: right; 94 | max-width: max-content; 95 | justify-self: flex-end; 96 | padding: 0; 97 | } 98 | 99 | zoo-label, 100 | zoo-link { 101 | grid-row: 1; 102 | } 103 | 104 | zoo-input { 105 | grid-gap: 0; 106 | grid-column: span 2; 107 | position: relative; 108 | } 109 | 110 | :host(:focus-within) ::slotted(input) { 111 | border: 2px solid #555; 112 | padding: 12px 14px; 113 | } 114 | 115 | :host([invalid]) ::slotted(input) { 116 | border: 2px solid var(--warning-mid); 117 | padding: 12px 14px; 118 | } 119 | 120 | :host([labelposition="left"]) zoo-link { 121 | grid-column: 2; 122 | } 123 | 124 | :host([labelposition="left"]) zoo-label, 125 | :host([labelposition="left"]) zoo-input { 126 | display: flex; 127 | align-items: center; 128 | grid-row: 2; 129 | } 130 | 131 | :host([labelposition="left"]) zoo-info[role="status"] { 132 | grid-row: 3; 133 | grid-column: 2; 134 | } 135 | 136 | :host([labelposition="left"]) zoo-info[role="alert"] { 137 | grid-row: 4; 138 | grid-column: 2; 139 | } 140 | 141 | zoo-info { 142 | grid-column: span 2; 143 | } 144 | -------------------------------------------------------------------------------- /src/zoo-modules/form/searchable-select/searchable-select.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/zoo-modules/form/searchable-select/searchable-select.js: -------------------------------------------------------------------------------- 1 | import { registerComponents } from '../../common/register-components.js'; 2 | import { CrossIcon } from '../../icon/cross-icon/cross-icon.js'; 3 | import { FormElement } from '../common/FormElement.js'; 4 | import { Input } from '../input/input.js'; 5 | import { Select } from '../select/select.js'; 6 | import { Preloader } from '../../misc/preloader/preloader.js'; 7 | import { Tooltip } from '../../misc/tooltip/tooltip.js'; 8 | 9 | /** 10 | * @injectHTML 11 | */ 12 | export class SearchableSelect extends FormElement { 13 | constructor() { 14 | super(); 15 | registerComponents(Input, Select, Preloader, CrossIcon, Tooltip); 16 | this.observer = new MutationObserver(mutationsList => { 17 | for (let mutation of mutationsList) { 18 | this.input.disabled = mutation.target.disabled; 19 | const crossIcon = this.shadowRoot.querySelector('.cross'); 20 | if (mutation.target.disabled) { 21 | crossIcon.classList.add('hidden'); 22 | } else { 23 | crossIcon.classList.remove('hidden'); 24 | } 25 | } 26 | }); 27 | this.shadowRoot.querySelector('.cross').addEventListener('click', () => { 28 | if (this.select.disabled) return; 29 | this.select.value = null; 30 | this.select.dispatchEvent(new Event('change', { bubbles: true, cancelable: false })); 31 | }); 32 | 33 | this.shadowRoot.querySelector('slot[name="select"]').addEventListener('slotchange', e => { 34 | this.select = [...e.target.assignedElements()].find(el => el.tagName === 'SELECT'); 35 | if (!this.select) return; 36 | this.registerElementForValidation(this.select); 37 | this.select.addEventListener('change', () => { 38 | this.handleOptionChange(); 39 | this.valueChange(); 40 | }); 41 | this.select.size = 4; 42 | this.observer.observe(this.select, { attributes: true, attributeFilter: ['disabled'] }); 43 | this.valueChange(); 44 | this.slotChange(); 45 | }); 46 | 47 | this.shadowRoot.querySelector('slot[name="input"]').addEventListener('slotchange', e => { 48 | this.input = [...e.target.assignedElements()].find(el => el.tagName === 'INPUT'); 49 | if (!this.input) return; 50 | this.inputPlaceholderFallback = this.input.placeholder; 51 | this.input.addEventListener('input', () => this.handleSearchChange()); 52 | this.slotChange(); 53 | }); 54 | } 55 | 56 | static get observedAttributes() { 57 | return ['closeicontitle']; 58 | } 59 | 60 | slotChange() { 61 | if (this.input && this.select) { 62 | this.handleOptionChange(); 63 | this.input.disabled = this.select.disabled; 64 | } 65 | } 66 | 67 | valueChange() { 68 | this.select.value ? this.setAttribute('value-selected', '') : this.removeAttribute('value-selected'); 69 | } 70 | 71 | attributeChangedCallback(attrName, oldVal, newVal) { 72 | this.shadowRoot.querySelector('zoo-cross-icon').setAttribute('title', newVal); 73 | } 74 | 75 | handleSearchChange() { 76 | const inputVal = this.input.value.toLowerCase(); 77 | this.select.querySelectorAll('option').forEach(option => { 78 | if (option.text.toLowerCase().indexOf(inputVal) > -1) option.style.display = 'block'; 79 | else option.style.display = 'none'; 80 | }); 81 | } 82 | 83 | handleOptionChange() { 84 | let inputValString = [...this.select.selectedOptions].map(o => o.text).join(', \n'); 85 | this.input.placeholder = inputValString || this.inputPlaceholderFallback; 86 | if (inputValString) { 87 | this.input.value = null; 88 | this.tooltip = this.tooltip || this.createTooltip(); 89 | this.tooltip.textContent = inputValString; 90 | this.shadowRoot.querySelector('zoo-input').appendChild(this.tooltip); 91 | } else if (this.tooltip) { 92 | this.tooltip.remove(); 93 | } 94 | } 95 | 96 | createTooltip() { 97 | const tooltip = document.createElement('zoo-tooltip'); 98 | tooltip.slot = 'additional'; 99 | tooltip.setAttribute('position', 'right'); 100 | return tooltip; 101 | } 102 | 103 | disconnectedCallback() { 104 | this.observer.disconnect(); 105 | } 106 | } 107 | if (!window.customElements.get('zoo-searchable-select')) { 108 | window.customElements.define('zoo-searchable-select', SearchableSelect); 109 | } -------------------------------------------------------------------------------- /src/zoo-modules/form/select/select-a11y.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo select', function() { 2 | it('should pass accessibility tests', async() => { 3 | const results = await page.evaluate(async () => { 4 | document.body.innerHTML = ` 5 | 6 | 12 | 13 | `; 14 | return await axe.run('zoo-select'); 15 | }); 16 | if (results.violations.length) { 17 | console.log('zoo-select a11y violations ', results.violations); 18 | throw new Error('Accessibility issues found'); 19 | } 20 | }); 21 | }); -------------------------------------------------------------------------------- /src/zoo-modules/form/select/select.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: grid; 3 | grid-gap: 3px; 4 | width: 100%; 5 | height: max-content; 6 | box-sizing: border-box; 7 | 8 | --icons-display: flex; 9 | } 10 | 11 | zoo-arrow-icon { 12 | position: absolute; 13 | right: 10px; 14 | display: var(--icons-display); 15 | pointer-events: none; 16 | } 17 | 18 | :host([invalid]) zoo-arrow-icon { 19 | --icon-color: var(--warning-mid); 20 | } 21 | 22 | :host([disabled]) zoo-arrow-icon { 23 | --icon-color: #666; 24 | } 25 | 26 | ::slotted(select) { 27 | appearance: none; 28 | width: 100%; 29 | font-size: 14px; 30 | line-height: 20px; 31 | padding: 13px 25px 13px 15px; 32 | border: 1px solid #767676; 33 | border-radius: 5px; 34 | color: #555; 35 | outline: none; 36 | box-sizing: border-box; 37 | } 38 | 39 | ::slotted(select:disabled) { 40 | border: 1px solid #E6E6E6; 41 | background: var(--input-disabled, #F2F3F4); 42 | color: #666; 43 | } 44 | 45 | ::slotted(select:disabled:hover) { 46 | cursor: not-allowed; 47 | } 48 | 49 | ::slotted(select:focus) { 50 | border: 2px solid #555; 51 | padding: 12px 24px 12px 14px; 52 | } 53 | 54 | :host([invalid]) ::slotted(select) { 55 | border: 2px solid var(--warning-mid); 56 | padding: 12px 24px 12px 14px; 57 | } 58 | 59 | .content { 60 | display: flex; 61 | justify-content: stretch; 62 | align-items: center; 63 | position: relative; 64 | grid-column: span 2; 65 | } 66 | 67 | zoo-info { 68 | grid-column: span 2; 69 | } 70 | 71 | :host([multiple]) zoo-arrow-icon { 72 | display: none; 73 | } 74 | 75 | zoo-link { 76 | text-align: right; 77 | max-width: max-content; 78 | justify-self: flex-end; 79 | padding: 0; 80 | } 81 | 82 | zoo-preloader { 83 | display: none; 84 | } 85 | 86 | :host([loading]) zoo-preloader { 87 | display: flex; 88 | } 89 | 90 | :host([labelposition="left"]) zoo-link { 91 | grid-column: 2; 92 | } 93 | 94 | :host([labelposition="left"]) zoo-label, 95 | :host([labelposition="left"]) .content { 96 | display: flex; 97 | align-items: center; 98 | grid-row: 2; 99 | } 100 | 101 | :host([labelposition="left"]) zoo-info[role="status"] { 102 | grid-row: 3; 103 | grid-column: 2; 104 | } 105 | 106 | :host([labelposition="left"]) zoo-info[role="alert"] { 107 | grid-row: 4; 108 | grid-column: 2; 109 | } 110 | -------------------------------------------------------------------------------- /src/zoo-modules/form/select/select.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/zoo-modules/form/select/select.js: -------------------------------------------------------------------------------- 1 | import { registerComponents } from '../../common/register-components.js'; 2 | import { ArrowDownIcon } from '../../icon/arrow-icon/arrow-icon.js'; 3 | import { Link } from '../../misc/link/link.js'; 4 | import { Preloader } from '../../misc/preloader/preloader.js'; 5 | import { FormElement } from '../common/FormElement.js'; 6 | import { InfoMessage } from '../info/info.js'; 7 | import { Label } from '../label/label.js'; 8 | 9 | /** 10 | * @injectHTML 11 | */ 12 | export class Select extends FormElement { 13 | constructor() { 14 | super(); 15 | registerComponents(InfoMessage, Label, Link, Preloader, ArrowDownIcon); 16 | this.observer = new MutationObserver(mutationsList => { 17 | for(let mutation of mutationsList) { 18 | const attr = mutation.attributeName; 19 | mutation.target[attr] ? this.setAttribute(attr, '') : this.removeAttribute(attr); 20 | } 21 | }); 22 | this.shadowRoot.querySelector('slot[name="select"]').addEventListener('slotchange', e => { 23 | let select = [...e.target.assignedElements()].find(el => el.tagName === 'SELECT'); 24 | if (!select) return; 25 | if (select.multiple) this.setAttribute('multiple', ''); 26 | if (select.disabled) this.setAttribute('disabled', ''); 27 | this.registerElementForValidation(select); 28 | this.observer.observe(select, { attributes: true, attributeFilter: ['disabled', 'multiple'] }); 29 | }); 30 | } 31 | 32 | disconnectedCallback() { 33 | this.observer.disconnect(); 34 | } 35 | } 36 | if (!window.customElements.get('zoo-select')) { 37 | window.customElements.define('zoo-select', Select); 38 | } -------------------------------------------------------------------------------- /src/zoo-modules/form/select/select.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo select', function () { 2 | it('should create select', async () => { 3 | const label = await page.evaluate(() => { 4 | document.body.innerHTML = ` 5 | 6 | 9 | 10 | 11 | `; 12 | let select = document.querySelector('zoo-select'); 13 | return select.shadowRoot.querySelector('slot[name="label"]').assignedElements()[0].innerHTML; 14 | }); 15 | expect(label).toEqual('Multiselect'); 16 | }); 17 | 18 | it('should set disabled attribute on host when select is disabled', async () => { 19 | const disabled = await page.evaluate(async () => { 20 | document.body.innerHTML = ` 21 | 22 | 25 | 26 | 27 | `; 28 | let select = document.querySelector('zoo-select'); 29 | await new Promise(r => setTimeout(r, 10)); 30 | select.shadowRoot.querySelector('slot[name="select"]').assignedElements()[0].disabled = true; 31 | await new Promise(r => setTimeout(r, 10)); 32 | 33 | return select.hasAttribute('disabled'); 34 | }); 35 | expect(disabled).toBeTrue(); 36 | }); 37 | 38 | it('should remove disabled attribute on host when select is not disabled', async () => { 39 | const disabled = await page.evaluate(async () => { 40 | document.body.innerHTML = ` 41 | 42 | 45 | 46 | 47 | `; 48 | let select = document.querySelector('zoo-select'); 49 | await new Promise(r => setTimeout(r, 10)); 50 | select.shadowRoot.querySelector('slot[name="select"]').assignedElements()[0].disabled = false; 51 | await new Promise(r => setTimeout(r, 10)); 52 | 53 | return select.hasAttribute('disabled'); 54 | }); 55 | expect(disabled).toBeFalse(); 56 | }); 57 | 58 | it('should set multiple attribute on host when select is multiple', async () => { 59 | const multiple = await page.evaluate(async () => { 60 | document.body.innerHTML = ` 61 | 62 | 65 | 66 | 67 | `; 68 | let select = document.querySelector('zoo-select'); 69 | await new Promise(r => setTimeout(r, 10)); 70 | select.shadowRoot.querySelector('slot[name="select"]').assignedElements()[0].multiple = true; 71 | await new Promise(r => setTimeout(r, 10)); 72 | 73 | return select.hasAttribute('multiple'); 74 | }); 75 | expect(multiple).toBeTrue(); 76 | }); 77 | 78 | it('should remove multiple attribute on host when select is not multiple', async () => { 79 | const multiple = await page.evaluate(async () => { 80 | document.body.innerHTML = ` 81 | 82 | 85 | 86 | 87 | `; 88 | let select = document.querySelector('zoo-select'); 89 | await new Promise(r => setTimeout(r, 10)); 90 | select.shadowRoot.querySelector('slot[name="select"]').assignedElements()[0].multiple = false; 91 | await new Promise(r => setTimeout(r, 10)); 92 | 93 | return select.hasAttribute('multiple'); 94 | }); 95 | expect(multiple).toBeFalse(); 96 | }); 97 | 98 | it('should set and then remove invalid attribute from host', async () => { 99 | const result = await page.evaluate(async () => { 100 | document.body.innerHTML = ` 101 | 102 | 106 | 107 | 108 | `; 109 | const result = []; 110 | let select = document.querySelector('zoo-select'); 111 | await new Promise(r => setTimeout(r, 10)); 112 | const slottedSelect = select.shadowRoot.querySelector('slot[name="select"]').assignedElements()[0]; 113 | slottedSelect.value = ''; 114 | slottedSelect.dispatchEvent(new Event('input')); 115 | await new Promise(r => setTimeout(r, 10)); 116 | result.push(select.hasAttribute('invalid')); 117 | 118 | slottedSelect.value = '2'; 119 | slottedSelect.dispatchEvent(new Event('input')); 120 | await new Promise(r => setTimeout(r, 10)); 121 | result.push(select.hasAttribute('invalid')); 122 | 123 | return result; 124 | }); 125 | expect(result[0]).toBeTrue(); 126 | expect(result[1]).toBeFalse(); 127 | }); 128 | }); -------------------------------------------------------------------------------- /src/zoo-modules/form/toggle-switch/toggle-switch-a11y.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo toggle switch', function() { 2 | it('should pass accessibility tests', async() => { 3 | const results = await page.evaluate(async () => { 4 | document.body.innerHTML = ` 5 | 6 | 7 | 8 | `; 9 | return await axe.run('zoo-toggle-switch'); 10 | }); 11 | if (results.violations.length) { 12 | console.log('zoo-toggle-switch a11y violations ', results.violations); 13 | throw new Error('Accessibility issues found'); 14 | } 15 | }); 16 | }); -------------------------------------------------------------------------------- /src/zoo-modules/form/toggle-switch/toggle-switch.css: -------------------------------------------------------------------------------- 1 | :host { 2 | height: 100%; 3 | width: 100%; 4 | } 5 | 6 | div { 7 | display: flex; 8 | align-items: center; 9 | position: relative; 10 | height: 17px; 11 | width: 40px; 12 | background: #E6E6E6; 13 | border-radius: 10px; 14 | border-width: 0; 15 | margin: 5px 0; 16 | } 17 | 18 | ::slotted(input) { 19 | transition: transform 0.2s; 20 | transform: translateX(-30%); 21 | width: 60%; 22 | height: 24px; 23 | border: 1px solid #E6E6E6; 24 | border-radius: 50%; 25 | display: flex; 26 | appearance: none; 27 | outline: none; 28 | cursor: pointer; 29 | background: white; 30 | } 31 | 32 | ::slotted(input:checked) { 33 | transform: translateX(80%); 34 | background: var(--primary-mid); 35 | } 36 | 37 | ::slotted(input:focus) { 38 | border-width: 2px; 39 | border: 1px solid #767676; 40 | } 41 | 42 | ::slotted(input:disabled) { 43 | background: var(--input-disabled, #F2F3F4); 44 | cursor: not-allowed; 45 | } 46 | 47 | :host([labelposition="left"]) { 48 | display: grid; 49 | grid-gap: 3px; 50 | height: max-content; 51 | } 52 | 53 | :host([labelposition="left"]) zoo-link { 54 | grid-column: 2; 55 | } 56 | 57 | :host([labelposition="left"]) zoo-label, 58 | :host([labelposition="left"]) div { 59 | display: flex; 60 | align-items: center; 61 | grid-row: 1; 62 | } 63 | 64 | :host([labelposition="left"]) zoo-info[role="status"] { 65 | grid-row: 2; 66 | grid-column: 2; 67 | } 68 | 69 | :host([labelposition="left"]) zoo-info[role="alert"] { 70 | grid-row: 3; 71 | grid-column: 2; 72 | } 73 | -------------------------------------------------------------------------------- /src/zoo-modules/form/toggle-switch/toggle-switch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/zoo-modules/form/toggle-switch/toggle-switch.js: -------------------------------------------------------------------------------- 1 | import { registerComponents } from '../../common/register-components.js'; 2 | import { FormElement } from '../common/FormElement.js'; 3 | import { InfoMessage } from '../info/info.js'; 4 | import { Label } from '../label/label.js'; 5 | 6 | /** 7 | * @injectHTML 8 | */ 9 | export class ToggleSwitch extends FormElement { 10 | constructor() { 11 | super(); 12 | registerComponents(InfoMessage, Label); 13 | this.shadowRoot.querySelector('slot[name="input"]').addEventListener('slotchange', e => { 14 | const input = [...e.target.assignedElements()].find(el => el.tagName === 'INPUT'); 15 | if (!input) return; 16 | this.registerElementForValidation(input); 17 | 18 | e.target.parentNode.addEventListener('click', (e) => { 19 | if (e.target.classList.contains('toggle-wrapper')) { 20 | input.click(); 21 | } 22 | }); 23 | }); 24 | } 25 | } 26 | 27 | if (!window.customElements.get('zoo-toggle-switch')) { 28 | window.customElements.define('zoo-toggle-switch', ToggleSwitch); 29 | } -------------------------------------------------------------------------------- /src/zoo-modules/form/toggle-switch/toggle-switch.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo toggle switch', function() { 2 | it('should create checkbox', async () => { 3 | const inputLabelText = await page.evaluate(() => { 4 | document.body.innerHTML = ` 5 | 6 | 7 | 8 | 9 | `; 10 | let control = document.querySelector('zoo-toggle-switch'); 11 | 12 | return control.shadowRoot.querySelector('slot[name="label"]').assignedElements()[0].innerHTML; 13 | }); 14 | expect(inputLabelText).toEqual('Toggle switch'); 15 | }); 16 | 17 | it('should set and then remove invalid attribute from host', async () => { 18 | const result = await page.evaluate(async () => { 19 | document.body.innerHTML = ` 20 | 21 | 22 | 23 | 24 | `; 25 | const result = []; 26 | let input = document.querySelector('zoo-toggle-switch'); 27 | await new Promise(r => setTimeout(r, 10)); 28 | const slottedInput = input.shadowRoot.querySelector('slot[name="input"]').assignedElements()[0]; 29 | slottedInput.click(); 30 | await new Promise(r => setTimeout(r, 10)); 31 | result.push(input.hasAttribute('invalid')); 32 | 33 | slottedInput.click(); 34 | await new Promise(r => setTimeout(r, 10)); 35 | result.push(input.hasAttribute('invalid')); 36 | 37 | return result; 38 | }); 39 | expect(result[0]).toBeFalse(); 40 | expect(result[1]).toBeTrue(); 41 | }); 42 | 43 | it('should set and then remove invalid attribute from host on wrapper click', async () => { 44 | const result = await page.evaluate(async () => { 45 | document.body.innerHTML = ` 46 | 47 | 48 | 49 | 50 | `; 51 | const result = []; 52 | const input = document.querySelector('zoo-toggle-switch'); 53 | await new Promise(r => setTimeout(r, 10)); 54 | const toggleWrapper = input.shadowRoot.querySelector('.toggle-wrapper'); 55 | toggleWrapper.click(); 56 | await new Promise(r => setTimeout(r, 10)); 57 | result.push(input.hasAttribute('invalid')); 58 | 59 | toggleWrapper.click(); 60 | await new Promise(r => setTimeout(r, 10)); 61 | result.push(input.hasAttribute('invalid')); 62 | 63 | return result; 64 | }); 65 | expect(result[0]).toBeFalse(); 66 | expect(result[1]).toBeTrue(); 67 | }); 68 | }); -------------------------------------------------------------------------------- /src/zoo-modules/grid/grid-header/grid-header.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | align-items: center; 4 | width: 100%; 5 | height: 100%; 6 | } 7 | 8 | button { 9 | display: none; 10 | width: 24px; 11 | opacity: 0; 12 | transition: opacity 0.1s; 13 | margin-left: 5px; 14 | padding: 0; 15 | border: 0; 16 | cursor: pointer; 17 | border-radius: 5px; 18 | background: var(--input-disabled, #F2F3F4); 19 | 20 | --icon-color: black; 21 | } 22 | 23 | button:active { 24 | opacity: 0.5; 25 | transform: translateY(1px); 26 | } 27 | 28 | button:focus { 29 | opacity: 1; 30 | } 31 | 32 | :host(:hover) button { 33 | opacity: 1; 34 | } 35 | 36 | .swap { 37 | cursor: grab; 38 | } 39 | 40 | .swap:active { 41 | cursor: grabbing; 42 | } 43 | 44 | :host([sortable]) .sort, 45 | :host([reorderable]) .swap { 46 | display: flex; 47 | } 48 | 49 | :host([sortstate='asc']) .sort { 50 | transform: rotate(180deg); 51 | } 52 | 53 | :host([sortstate]) .sort { 54 | opacity: 1; 55 | background: #F2F3F4; 56 | } 57 | -------------------------------------------------------------------------------- /src/zoo-modules/grid/grid-header/grid-header.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/zoo-modules/grid/grid-header/grid-header.js: -------------------------------------------------------------------------------- 1 | import { ArrowDownIcon } from '../../icon/arrow-icon/arrow-icon.js'; 2 | import { registerComponents } from '../../common/register-components.js'; 3 | 4 | /** 5 | * @injectHTML 6 | */ 7 | export class GridHeader extends HTMLElement { 8 | constructor() { 9 | super(); 10 | registerComponents(ArrowDownIcon); 11 | this.addEventListener('dragend', () => this.removeAttribute('draggable')); 12 | this.shadowRoot.querySelector('.swap').addEventListener('mousedown', () => this.setAttribute('draggable', true)); 13 | this.shadowRoot.querySelector('.sort').addEventListener('click', () => this.handleSortClick()); 14 | } 15 | 16 | static get observedAttributes() { 17 | return ['sort-title', 'swap-title']; 18 | } 19 | 20 | handleSortClick() { 21 | if (!this.hasAttribute('sortstate')) { 22 | this.setAttribute('sortstate', 'desc'); 23 | } else if (this.getAttribute('sortstate') == 'desc') { 24 | this.setAttribute('sortstate', 'asc'); 25 | } else if (this.getAttribute('sortstate') == 'asc') { 26 | this.removeAttribute('sortstate'); 27 | } 28 | const detail = this.hasAttribute('sortstate') 29 | ? { property: this.getAttribute('sortableproperty'), direction: this.getAttribute('sortstate') } 30 | : undefined; 31 | this.dispatchEvent(new CustomEvent('sortChange', {detail: detail, bubbles: true, composed: true })); 32 | } 33 | 34 | attributeChangedCallback(attrName, oldVal, newVal) { 35 | if (attrName === 'sort-title') { 36 | this.shadowRoot.querySelector('zoo-arrow-icon').setAttribute('title', newVal); 37 | } else if (attrName === 'swap-title') { 38 | this.shadowRoot.querySelector('.swap title').textContent = newVal; 39 | this.shadowRoot.querySelector('.swap').setAttribute('title', newVal); 40 | } 41 | } 42 | } 43 | 44 | if (!window.customElements.get('zoo-grid-header')) { 45 | window.customElements.define('zoo-grid-header', GridHeader); 46 | } -------------------------------------------------------------------------------- /src/zoo-modules/grid/grid-header/grid-header.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo grid header', function() { 2 | it('should show sort arrow when sortable attribute is present', async() => { 3 | const ret = await page.evaluate(async () => { 4 | document.body.innerHTML = 'Valid'; 5 | 6 | const header = document.querySelector('zoo-grid-header'); 7 | const arrow = header.shadowRoot.querySelector('.sort'); 8 | 9 | const style = window.getComputedStyle(arrow); 10 | return style.display; 11 | }); 12 | expect(ret).toEqual('flex'); 13 | }); 14 | 15 | it('should not show sort arrow when sortable attribute is absent', async() => { 16 | const ret = await page.evaluate(async () => { 17 | document.body.innerHTML = 'Valid'; 18 | 19 | const header = document.querySelector('zoo-grid-header'); 20 | const arrow = header.shadowRoot.querySelector('.sort'); 21 | 22 | const style = window.getComputedStyle(arrow); 23 | return style.display; 24 | }); 25 | expect(ret).toEqual('none'); 26 | }); 27 | 28 | it('should show reorder arrows when reorderable attribute is present', async() => { 29 | const ret = await page.evaluate(async () => { 30 | document.body.innerHTML = 'Valid'; 31 | 32 | const header = document.querySelector('zoo-grid-header'); 33 | const swap = header.shadowRoot.querySelector('.swap'); 34 | 35 | const style = window.getComputedStyle(swap); 36 | return style.display; 37 | }); 38 | expect(ret).toEqual('flex'); 39 | }); 40 | 41 | it('should not show reorder arrows when reorderable attribute is absent', async() => { 42 | const ret = await page.evaluate(async () => { 43 | document.body.innerHTML = 'Valid'; 44 | 45 | const header = document.querySelector('zoo-grid-header'); 46 | const swap = header.shadowRoot.querySelector('.swap'); 47 | 48 | const style = window.getComputedStyle(swap); 49 | return style.display; 50 | }); 51 | expect(ret).toEqual('none'); 52 | }); 53 | 54 | it('should add and then remove draggable attribute when on mousedown and draend events', async() => { 55 | const ret = await page.evaluate(async () => { 56 | document.body.innerHTML = 'Valid'; 57 | 58 | const header = document.querySelector('zoo-grid-header'); 59 | const swap = header.shadowRoot.querySelector('.swap'); 60 | 61 | swap.dispatchEvent(new Event('mousedown')); 62 | await new Promise(r => setTimeout(r, 10)); 63 | const draggableFirst = header.hasAttribute('draggable'); 64 | 65 | header.dispatchEvent(new Event('dragend')); 66 | await new Promise(r => setTimeout(r, 10)); 67 | const draggableSecond = header.hasAttribute('draggable'); 68 | 69 | return [draggableFirst, draggableSecond]; 70 | }); 71 | expect(ret[0]).toBeTrue(); 72 | expect(ret[1]).toBeFalse(); 73 | }); 74 | 75 | it('should set sortstate attribute on click on arrow', async() => { 76 | const sortState = await page.evaluate(async () => { 77 | document.body.innerHTML = 'Valid'; 78 | 79 | const header = document.querySelector('zoo-grid-header'); 80 | const arrow = header.shadowRoot.querySelector('.sort'); 81 | 82 | const states = []; 83 | 84 | arrow.dispatchEvent(new Event('click')); 85 | await new Promise(r => setTimeout(r, 10)); 86 | states.push(header.getAttribute('sortState')); 87 | 88 | arrow.dispatchEvent(new Event('click')); 89 | await new Promise(r => setTimeout(r, 10)); 90 | states.push(header.getAttribute('sortState')); 91 | 92 | arrow.dispatchEvent(new Event('click')); 93 | await new Promise(r => setTimeout(r, 10)); 94 | states.push(header.getAttribute('sortState')); 95 | 96 | return states; 97 | }); 98 | expect(sortState).toEqual(['desc', 'asc', null]); 99 | }); 100 | }); -------------------------------------------------------------------------------- /src/zoo-modules/grid/grid-row/grid-row.css: -------------------------------------------------------------------------------- 1 | :host { 2 | contain: layout; 3 | position: relative; 4 | flex-wrap: wrap; 5 | 6 | --grid-column-sizes: 1fr; 7 | } 8 | 9 | ::slotted(*[slot="row-details"]) { 10 | display: var(--zoo-grid-row-display, grid); 11 | grid-template-columns: var(--grid-details-column-sizes, repeat(var(--grid-column-num), minmax(50px, 1fr))); 12 | min-height: 50px; 13 | align-items: center; 14 | flex: 1 0 100%; 15 | } 16 | 17 | ::slotted(*[slot="row-content"]) { 18 | height: 0; 19 | overflow: hidden; 20 | background-color: white; 21 | padding: 0 10px; 22 | width: 100%; 23 | } 24 | 25 | ::slotted(*[slot="row-content"][expanded]) { 26 | height: var(--grid-row-content-height, auto); 27 | border-bottom: 2px solid rgb(0 0 0 / 20%); 28 | padding: 10px; 29 | margin: 4px; 30 | } 31 | -------------------------------------------------------------------------------- /src/zoo-modules/grid/grid-row/grid-row.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/zoo-modules/grid/grid-row/grid-row.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class GridRow extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | } 9 | 10 | if (!window.customElements.get('zoo-grid-row')) { 11 | window.customElements.define('zoo-grid-row', GridRow); 12 | } -------------------------------------------------------------------------------- /src/zoo-modules/grid/grid-row/grid-row.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo grid row', function() { 2 | it('should properly render row details', async () => { 3 | const result = await page.evaluate(async () => { 4 | document.body.innerHTML = ` 5 | 6 |
7 |
Valid
8 |
2020-05-20
9 |
Grid Row Expand
10 |
5kg
11 |
12 |
13 | `; 14 | 15 | const row = document.querySelector('zoo-grid-row'); 16 | const rowDetails = row.shadowRoot.querySelector('*[name="row-details"]').assignedElements(); 17 | 18 | return rowDetails.map(singleRow => singleRow.textContent)[0]; 19 | }); 20 | expect(result).toContain('Valid'); 21 | expect(result).toContain('2020-05-20'); 22 | expect(result).toContain('Grid Row Expand'); 23 | expect(result).toContain('5kg'); 24 | }); 25 | 26 | it('should properly render expandable content if expanded attribute exists', async () => { 27 | const result = await page.evaluate(async () => { 28 | document.body.innerHTML = ` 29 | 30 |
31 |
Valid
32 |
2020-05-20
33 |
Grid Row Expand
34 |
5kg
35 |
36 |
37 |
Content Row
38 |
39 |
40 | `; 41 | 42 | const row = document.querySelector('zoo-grid-row'); 43 | const content = row.shadowRoot.querySelector('slot[name="row-content"]').assignedElements()[0]; 44 | 45 | 46 | const style = window.getComputedStyle(content); 47 | return style.height; 48 | }); 49 | expect(result).toEqual('150px'); 50 | }); 51 | 52 | it('should not render expandable content if expanded attribute does not exists', async () => { 53 | const result = await page.evaluate(async () => { 54 | document.body.innerHTML = ` 55 | 56 |
57 |
Valid
58 |
2020-05-20
59 |
Grid Row Expand
60 |
5kg
61 |
62 |
63 |
Content Row
64 |
65 |
66 | `; 67 | 68 | const row = document.querySelector('zoo-grid-row'); 69 | const content = row.shadowRoot.querySelector('slot[name="row-content"]').assignedElements()[0]; 70 | 71 | 72 | const style = window.getComputedStyle(content); 73 | return style.height; 74 | }); 75 | expect(result).toEqual('0px'); 76 | }); 77 | }); -------------------------------------------------------------------------------- /src/zoo-modules/grid/grid/grid.css: -------------------------------------------------------------------------------- 1 | :host { 2 | contain: layout; 3 | position: relative; 4 | display: block; 5 | } 6 | 7 | .loading-shade { 8 | display: none; 9 | position: absolute; 10 | left: 0; 11 | top: 0; 12 | right: 0; 13 | z-index: var(--zoo-grid-z-index, 9998); 14 | justify-content: center; 15 | height: 100%; 16 | background: rgb(0 0 0 / 15%); 17 | pointer-events: none; 18 | } 19 | 20 | :host([loading]) .loading-shade { 21 | display: flex; 22 | } 23 | 24 | .header-row { 25 | min-width: inherit; 26 | font-weight: 600; 27 | color: #555; 28 | box-sizing: border-box; 29 | z-index: 2; 30 | background: white; 31 | } 32 | 33 | .header-row, 34 | ::slotted(*[slot="row"]) { 35 | display: grid; 36 | grid-template-columns: var(--grid-column-sizes, repeat(var(--grid-column-num), minmax(50px, 1fr))); 37 | padding: 5px 10px; 38 | border-bottom: 1px solid rgb(0 0 0 / 20%); 39 | min-height: 50px; 40 | font-size: 14px; 41 | line-height: 20px; 42 | } 43 | 44 | ::slotted(*[slot="row"]) { 45 | overflow: visible; 46 | align-items: center; 47 | box-sizing: border-box; 48 | } 49 | 50 | :host([resizable]) { 51 | --zoo-grid-row-display: flex; 52 | } 53 | 54 | :host([resizable]) .header-row, 55 | :host([resizable]) ::slotted(*[slot="row"]) { 56 | display: flex; 57 | } 58 | 59 | :host([resizable]) ::slotted(*[slot="headercell"]) { 60 | overflow: auto; 61 | resize: horizontal; 62 | height: inherit; 63 | } 64 | 65 | ::slotted(.drag-over) { 66 | box-shadow: inset 0 0 1px 1px rgb(0 0 0 / 40%); 67 | } 68 | 69 | :host([stickyheader]) .header-row { 70 | top: var(--grid-stickyheader-position-top, 0); 71 | position: sticky; 72 | } 73 | 74 | ::slotted(*[slot="row"]:nth-child(odd)) { 75 | background: #F2F3F4; 76 | } 77 | 78 | ::slotted(*[slot="row"]:hover), 79 | ::slotted(*[slot="row"]:focus) { 80 | background: var(--item-hovered, #E6E6E6); 81 | } 82 | 83 | ::slotted(*[slot="norecords"]) { 84 | color: var(--warning-dark); 85 | grid-column: span var(--grid-column-num); 86 | text-align: center; 87 | padding: 10px 0; 88 | } 89 | 90 | .footer { 91 | display: flex; 92 | position: sticky; 93 | bottom: 0; 94 | z-index: 2; 95 | width: 100%; 96 | background: #FFF; 97 | border-top: 1px solid #E6E6E6; 98 | box-sizing: border-box; 99 | padding: 10px; 100 | } 101 | 102 | slot[name="footer-content"] { 103 | display: flex; 104 | flex-grow: 1; 105 | } 106 | 107 | ::slotted(*[slot="footer-content"]) { 108 | justify-self: flex-start; 109 | } 110 | 111 | zoo-paginator { 112 | position: sticky; 113 | right: 10px; 114 | justify-content: flex-end; 115 | } 116 | 117 | slot[name="pagesizeselector"] { 118 | display: block; 119 | margin-right: 20px; 120 | } 121 | -------------------------------------------------------------------------------- /src/zoo-modules/grid/grid/grid.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /src/zoo-modules/grid/grid/grid.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '../../helpers/debounce.js'; 2 | import { Paginator } from '../../misc/paginator/paginator.js'; 3 | import { GridHeader } from '../grid-header/grid-header.js'; 4 | import { GridRow } from '../grid-row/grid-row.js'; 5 | import { registerComponents } from '../../common/register-components.js'; 6 | /** 7 | * @injectHTML 8 | * https://github.com/whatwg/html/issues/6226 9 | * which leads to https://github.com/WICG/webcomponents/issues/59 10 | */ 11 | 12 | export class ZooGrid extends HTMLElement { 13 | constructor() { 14 | super(); 15 | registerComponents(Paginator, GridHeader, GridRow); 16 | const headerSlot = this.shadowRoot.querySelector('slot[name="headercell"]'); 17 | headerSlot.addEventListener('slotchange', debounce(() => { 18 | const headers = headerSlot.assignedElements(); 19 | this.style.setProperty('--grid-column-num', headers.length); 20 | headers.forEach((header, i) => { 21 | header.setAttribute('column', i+1); 22 | header.setAttribute('role', 'columnheader'); 23 | }); 24 | if (this.hasAttribute('reorderable')) { 25 | headers.forEach(header => this.handleDraggableHeader(header)); 26 | } 27 | if (this.hasAttribute('resizable')) { 28 | this.handleResizableAttributeChange(); 29 | } 30 | })); 31 | const rowSlot = this.shadowRoot.querySelector('slot[name="row"]'); 32 | rowSlot.addEventListener('slotchange', debounce(() => { 33 | rowSlot.assignedElements().forEach(row => { 34 | row.setAttribute('role', 'row'); 35 | if (row.tagName === 'ZOO-GRID-ROW') { 36 | [...row.querySelector('*[slot="row-details"]').children].forEach((child, i) => { 37 | child.setAttribute('column', i+1); 38 | child.setAttribute('role', 'cell'); 39 | }); 40 | } else { 41 | [...row.children].forEach((child, i) => { 42 | child.setAttribute('column', i+1); 43 | child.setAttribute('role', 'cell'); 44 | }); 45 | } 46 | }); 47 | })); 48 | 49 | this.addEventListener('sortChange', e => { 50 | if (this.prevSortedHeader && !e.target.isEqualNode(this.prevSortedHeader)) { 51 | this.prevSortedHeader.removeAttribute('sortstate'); 52 | } 53 | this.prevSortedHeader = e.target; 54 | }); 55 | } 56 | 57 | static get observedAttributes() { 58 | return ['currentpage', 'maxpages', 'resizable', 'reorderable', 'prev-page-title', 'next-page-title']; 59 | } 60 | 61 | attributeChangedCallback(attrName, oldVal, newVal) { 62 | if (attrName == 'resizable') { 63 | this.handleResizableAttributeChange(); 64 | } else if (attrName == 'reorderable' && this.hasAttribute('reorderable')) { 65 | this.shadowRoot.querySelector('slot[name="headercell"]').assignedElements().forEach(header => this.handleDraggableHeader(header)); 66 | } else if (['maxpages', 'currentpage', 'prev-page-title', 'next-page-title'].includes(attrName)) { 67 | this.shadowRoot.querySelector('zoo-paginator').setAttribute(attrName, newVal); 68 | } 69 | } 70 | 71 | resizeCallback(entries) { 72 | entries.forEach(entry => { 73 | const columnNum = entry.target.getAttribute('column'); 74 | const width = entry.contentRect.width; 75 | const columns = this.querySelectorAll(`[column="${columnNum}"]`); 76 | columns.forEach(columnEl => columnEl.style.width = `${width}px`); 77 | }); 78 | } 79 | 80 | handleResizableAttributeChange() { 81 | if (this.hasAttribute('resizable')) { 82 | this.resizeObserver = this.resizeObserver || new ResizeObserver(debounce(this.resizeCallback.bind(this))); 83 | this.shadowRoot.querySelector('slot[name="headercell"]').assignedElements().forEach(header => this.resizeObserver.observe(header)); 84 | } 85 | } 86 | 87 | handleDraggableHeader(header) { 88 | // avoid attaching multiple eventListeners to the same element 89 | if (header.hasAttribute('reorderable')) return; 90 | header.setAttribute('reorderable', ''); 91 | header.setAttribute('ondragover', 'event.preventDefault()'); 92 | header.setAttribute('ondrop', 'event.preventDefault()'); 93 | 94 | header.addEventListener('dragstart', e => e.dataTransfer.setData('text/plain', header.getAttribute('column'))); 95 | // drag enter fires before dragleave, so stagger this function 96 | header.addEventListener('dragenter', debounce(() => { 97 | header.classList.add('drag-over'); 98 | this.prevDraggedOverHeader = header; 99 | })); 100 | header.addEventListener('dragleave', () => header.classList.remove('drag-over')); 101 | header.addEventListener('drop', e => this.handleDrop(e)); 102 | } 103 | 104 | handleDrop(e) { 105 | this.prevDraggedOverHeader && this.prevDraggedOverHeader.classList.remove('drag-over'); 106 | const sourceColumn = e.dataTransfer.getData('text'); 107 | const targetColumn = e.target.getAttribute('column'); 108 | if (targetColumn == sourceColumn) return; 109 | // move columns 110 | this.querySelectorAll(`[column="${sourceColumn}"]`).forEach(source => { 111 | const target = source.parentElement.querySelector(`[column="${targetColumn}"]`); 112 | targetColumn > sourceColumn ? target.after(source) : target.before(source); 113 | }); 114 | // reassign indexes for row cells 115 | this.shadowRoot.querySelector('slot[name="row"]').assignedElements() 116 | .forEach(row => { 117 | if (row.tagName === 'ZOO-GRID-ROW') { 118 | [...row.shadowRoot.querySelector('slot[name="row-details"]').assignedElements()[0].children] 119 | .forEach((child, i) => child.setAttribute('column', i+1)); 120 | } else { 121 | [...row.children].forEach((child, i) => child.setAttribute('column', i+1)); 122 | } 123 | }); 124 | } 125 | 126 | disconnectedCallback() { 127 | if (this.resizeObserver) { 128 | this.resizeObserver.disconnect(); 129 | } 130 | } 131 | } 132 | 133 | if (!window.customElements.get('zoo-grid')) { 134 | window.customElements.define('zoo-grid', ZooGrid); 135 | } -------------------------------------------------------------------------------- /src/zoo-modules/helpers/debounce.js: -------------------------------------------------------------------------------- 1 | export function debounce(func, wait) { 2 | let timeout; 3 | return function() { 4 | const later = () => { 5 | timeout = null; 6 | func.apply(this, arguments); 7 | }; 8 | clearTimeout(timeout); 9 | timeout = setTimeout(later, wait); 10 | if (!timeout) func.apply(this, arguments); 11 | }; 12 | } -------------------------------------------------------------------------------- /src/zoo-modules/helpers/test.setup.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import puppeteer from 'puppeteer'; 3 | import jasmine from 'jasmine'; 4 | import axe from 'axe-core'; 5 | import pti from 'puppeteer-to-istanbul'; 6 | 7 | beforeAll(async () => { 8 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; 9 | global.browser = await puppeteer.launch({ 10 | headless: "new", 11 | args: ['--no-sandbox', '--disable-setuid-sandbox'] 12 | }); 13 | global.page = await browser.newPage(); 14 | global.colors = { 15 | primaryMid: '#368700', 16 | primaryLight: '#66B100', 17 | primaryDark: '#286400', 18 | primaryUltralight: '#EBF4E5', 19 | secondaryMid: '#FF6200', 20 | secondaryLight: '#F80', 21 | secondaryDark: '#CC4E00', 22 | infoUltralight: '#ECF5FA', 23 | infoMid: '#459FD0', 24 | warningUltralight: '#FDE8E9', 25 | warningMid: '#ED1C24' 26 | }; 27 | page.on('console', msg => console.log('PAGE LOG:', msg.text())); 28 | await Promise.all([page.coverage.startJSCoverage(), page.coverage.startCSSCoverage()]); 29 | 30 | await page.goto('http://localhost:5050'); 31 | await page.addScriptTag( {'url' : 'https://cdn.jsdelivr.net/npm/jasmine-core@3.6.0/lib/jasmine-core/jasmine.js'}); 32 | await page.addScriptTag( {'url' : 'https://cdn.jsdelivr.net/npm/jasmine-core@3.6.0/lib/jasmine-core/jasmine-html.js'}); 33 | await page.addScriptTag( {'url' : 'https://cdn.jsdelivr.net/npm/jasmine-core@3.6.0/lib/jasmine-core/boot.js'}); 34 | global.axeHandle = await page.evaluateHandle(`${axe.source}`); 35 | }); 36 | 37 | afterAll(async () => { 38 | const [jsCoverage, cssCoverage] = await Promise.all([ 39 | page.coverage.stopJSCoverage(), 40 | page.coverage.stopCSSCoverage(), 41 | ]); 42 | pti.write([...jsCoverage, ...cssCoverage], { storagePath: './.nyc_output' }); 43 | await global.axeHandle ? global.axeHandle.dispose() : new Promise(res => res()); 44 | await browser.close(); 45 | }); -------------------------------------------------------------------------------- /src/zoo-modules/icon/arrow-icon/arrow-icon.css: -------------------------------------------------------------------------------- 1 | svg { 2 | display: flex; 3 | width: var(--icon-width, 24px); 4 | height: var(--icon-height, 24px); 5 | fill: var(--icon-color, var(--primary-mid)); 6 | } 7 | -------------------------------------------------------------------------------- /src/zoo-modules/icon/arrow-icon/arrow-icon.html: -------------------------------------------------------------------------------- 1 | 2 | Arrow icon 3 | 4 | -------------------------------------------------------------------------------- /src/zoo-modules/icon/arrow-icon/arrow-icon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class ArrowDownIcon extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | static get observedAttributes() { 10 | return ['title']; 11 | } 12 | 13 | attributeChangedCallback(attrName, oldVal, newVal) { 14 | this.shadowRoot.querySelector('svg title').textContent = newVal; 15 | } 16 | } 17 | 18 | if (!window.customElements.get('zoo-arrow-icon')) { 19 | window.customElements.define('zoo-arrow-icon', ArrowDownIcon); 20 | } -------------------------------------------------------------------------------- /src/zoo-modules/icon/attention-icon/attention-icon.css: -------------------------------------------------------------------------------- 1 | svg { 2 | display: flex; 3 | padding-right: 5px; 4 | width: var(--icon-width, 18px); 5 | height: var(--icon-height, 18px); 6 | fill: var(--icon-color, var(--info-mid)); 7 | } 8 | -------------------------------------------------------------------------------- /src/zoo-modules/icon/attention-icon/attention-icon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/zoo-modules/icon/attention-icon/attention-icon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class AttentionIcon extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | } 9 | 10 | if (!window.customElements.get('zoo-attention-icon')) { 11 | window.customElements.define('zoo-attention-icon', AttentionIcon); 12 | } -------------------------------------------------------------------------------- /src/zoo-modules/icon/cross-icon/cross-icon.css: -------------------------------------------------------------------------------- 1 | svg { 2 | display: flex; 3 | width: var(--icon-width, 18px); 4 | height: var(--icon-height, 18px); 5 | fill: var(--icon-color, black); 6 | } 7 | -------------------------------------------------------------------------------- /src/zoo-modules/icon/cross-icon/cross-icon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/zoo-modules/icon/cross-icon/cross-icon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class CrossIcon extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | static get observedAttributes() { 10 | return ['title']; 11 | } 12 | 13 | attributeChangedCallback(attrName, oldVal, newVal) { 14 | this.shadowRoot.querySelector('svg title').textContent = newVal; 15 | } 16 | } 17 | 18 | if (!window.customElements.get('zoo-cross-icon')) { 19 | window.customElements.define('zoo-cross-icon', CrossIcon); 20 | } -------------------------------------------------------------------------------- /src/zoo-modules/icon/paw-icon/paw-icon.css: -------------------------------------------------------------------------------- 1 | svg { 2 | display: flex; 3 | width: var(--icon-width, 44px); 4 | height: var(--icon-height, 44px); 5 | fill: var(--icon-color, white); 6 | } 7 | 8 | .fade-in { 9 | opacity: 0; 10 | animation: toes-fade-in-animation 2.2s infinite ease-in-out; 11 | } 12 | 13 | .fade-in-two { 14 | animation-delay: 0.4s; 15 | } 16 | 17 | .fade-in-three { 18 | animation-delay: 0.7s; 19 | } 20 | 21 | .fade-in-four { 22 | animation-delay: 1s; 23 | } 24 | 25 | @keyframes toes-fade-in-animation { 26 | 0% { 27 | opacity: 0; 28 | } 29 | 30 | 50% { 31 | opacity: 1; 32 | } 33 | 34 | 100% { 35 | opacity: 0; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/zoo-modules/icon/paw-icon/paw-icon.html: -------------------------------------------------------------------------------- 1 | 2 | Loading paw icon 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/zoo-modules/icon/paw-icon/paw-icon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class PawIcon extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | static get observedAttributes() { 10 | return ['title']; 11 | } 12 | 13 | attributeChangedCallback(attrName, oldVal, newVal) { 14 | this.shadowRoot.querySelector('svg title').textContent = newVal; 15 | } 16 | } 17 | 18 | if (!window.customElements.get('zoo-paw-icon')) { 19 | window.customElements.define('zoo-paw-icon', PawIcon); 20 | } -------------------------------------------------------------------------------- /src/zoo-modules/misc/button-group/button-group.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | opacity: 0; 4 | border: 1px solid #B8B8B8; 5 | border-radius: 5px; 6 | padding: 2px 0; 7 | justify-content: flex-end; 8 | width: fit-content; 9 | } 10 | 11 | ::slotted(zoo-button) { 12 | min-width: 50px; 13 | padding: 0 2px; 14 | } 15 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/button-group/button-group.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/button-group/button-group.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '../../helpers/debounce.js'; 2 | import { Button } from '../button/button.js'; 3 | import { registerComponents } from '../../common/register-components.js'; 4 | /** 5 | * @injectHTML 6 | */ 7 | export class ButtonGroup extends HTMLElement { 8 | constructor() { 9 | super(); 10 | registerComponents(Button); 11 | } 12 | 13 | connectedCallback() { 14 | const buttonGroup = this.shadowRoot.querySelector('slot'); 15 | this.registerSlotChangeListener(buttonGroup); 16 | this.registerButtonChangeHandler(buttonGroup); 17 | } 18 | 19 | registerSlotChangeListener(buttonGroup) { 20 | buttonGroup.addEventListener('slotchange', debounce(() => { 21 | buttonGroup.assignedElements().forEach((button, index) => { 22 | this.handleButtonInitialState(button, index); 23 | }); 24 | this.style.opacity = '1'; 25 | })); 26 | } 27 | 28 | registerButtonChangeHandler(buttonGroup) { 29 | this.addEventListener('click', (ev) => { 30 | const buttonIndex = buttonGroup.assignedElements().indexOf(ev.target.parentNode); 31 | if (buttonIndex > -1 && this.activeIndex !== buttonIndex) { 32 | this.deactivateButton(buttonGroup.assignedElements()[this.activeIndex]); 33 | this.activateButton(ev.target.parentNode, buttonIndex); 34 | } 35 | }); 36 | } 37 | 38 | handleButtonInitialState(button, buttonIndex) { 39 | if (button.hasAttribute('data-active')) { 40 | this.activateButton(button, buttonIndex); 41 | } else { 42 | this.deactivateButton(button); 43 | } 44 | } 45 | 46 | activateButton(button, buttonIndex) { 47 | const activeType = this.getAttribute('active-type'); 48 | button.setAttribute('type', activeType); 49 | this.activeIndex = buttonIndex; 50 | } 51 | 52 | deactivateButton(button) { 53 | const inactiveType = this.getAttribute('inactive-type'); 54 | button.setAttribute('type', inactiveType); 55 | } 56 | } 57 | 58 | if (!window.customElements.get('zoo-button-group')) { 59 | window.customElements.define('zoo-button-group', ButtonGroup); 60 | } 61 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/button-group/button-group.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo button group', () => { 2 | it('should properly set initially active button', async () => { 3 | const result = await page.evaluate(async () => { 4 | document.body.innerHTML = ` 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | `; 15 | const firstBtn = document.querySelector('zoo-button-group').shadowRoot.querySelector('slot').assignedElements()[0]; 16 | const secondBtn = document.querySelector('zoo-button-group').shadowRoot.querySelector('slot').assignedElements()[1]; 17 | await new Promise(r => setTimeout(r, 10)); 18 | 19 | return { 20 | firstButtonType: firstBtn.getAttribute('type'), 21 | secondButtonType: secondBtn.getAttribute('type') 22 | }; 23 | }); 24 | expect(result.firstButtonType).toEqual('transparent'); 25 | expect(result.secondButtonType).toEqual('primary'); 26 | }); 27 | 28 | it('should properly change active button', async () => { 29 | const result = await page.evaluate(async () => { 30 | document.body.innerHTML = ` 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | `; 41 | await new Promise(r => setTimeout(r)); 42 | const firstBtn = document.querySelector('zoo-button-group'); 43 | const secondBtn = document.querySelector('zoo-button-group'); 44 | await new Promise(r => setTimeout(r)); 45 | 46 | firstBtn.shadowRoot.querySelector('slot').assignedElements()[0].shadowRoot.querySelector('slot').assignedElements()[0].click(); 47 | 48 | return { 49 | firstButtonType: firstBtn.shadowRoot.querySelector('slot').assignedElements()[0].getAttribute('type'), 50 | secondButtonType: secondBtn.shadowRoot.querySelector('slot').assignedElements()[1].getAttribute('type') 51 | }; 52 | }); 53 | expect(result.firstButtonType).toEqual('primary'); 54 | expect(result.secondButtonType).toEqual('transparent'); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/button/button-a11y.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo button', function () { 2 | it('should pass accessibility tests', async () => { 3 | const results = await page.evaluate(async () => { 4 | document.body.innerHTML = ` 5 | 6 | 7 | `; 8 | return await axe.run('zoo-button'); 9 | }); 10 | if (results.violations.length) { 11 | console.log('zoo-button a11y violations', results.violations); 12 | throw new Error('Accessibility issues found'); 13 | } 14 | }); 15 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/button/button.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | max-width: 330px; 4 | min-height: 36px; 5 | position: relative; 6 | 7 | --color-light: var(--primary-light); 8 | --color-mid: var(--primary-mid); 9 | --color-dark: var(--primary-dark); 10 | --text-normal: white; 11 | --text-active: white; 12 | --background: linear-gradient(to right, var(--color-mid), var(--color-light)); 13 | --border: 0; 14 | } 15 | 16 | :host([type="secondary"]) { 17 | --color-light: var(--secondary-light); 18 | --color-mid: var(--secondary-mid); 19 | --color-dark: var(--secondary-dark); 20 | } 21 | 22 | :host([type="hollow"]) { 23 | --text-normal: var(--color-mid); 24 | --background: transparent; 25 | --border: 2px solid var(--color-mid); 26 | } 27 | 28 | :host([type="grayscale"]) { 29 | --background: transparent; 30 | --color-mid: transparent; 31 | --color-dark: transparent; 32 | --border: 0; 33 | --text-normal: #767676; 34 | --text-active: #9E9E9E; 35 | } 36 | 37 | :host([type="transparent"]) { 38 | --text-normal: var(--color-mid); 39 | --background: transparent; 40 | } 41 | 42 | ::slotted(button) { 43 | display: flex; 44 | align-items: center; 45 | justify-content: center; 46 | color: var(--text-normal); 47 | border: var(--border); 48 | border-radius: 5px; 49 | cursor: pointer; 50 | width: 100%; 51 | min-height: 100%; 52 | font-size: 14px; 53 | line-height: 20px; 54 | font-weight: bold; 55 | background: var(--background); 56 | } 57 | 58 | ::slotted(button:hover), 59 | ::slotted(button:focus) { 60 | background: var(--color-mid); 61 | color: var(--text-active); 62 | } 63 | 64 | ::slotted(button:active) { 65 | background: var(--color-dark); 66 | color: var(--text-active); 67 | } 68 | 69 | ::slotted(button:disabled) { 70 | cursor: not-allowed; 71 | 72 | --background: var(--input-disabled, #F2F3F4); 73 | --color-mid: var(--input-disabled, #F2F3F4); 74 | --color-dark: var(--input-disabled, #F2F3F4); 75 | --text-normal: #767676; 76 | --text-active: #767676; 77 | --border: 1px solid #E6E6E6; 78 | } 79 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/button/button.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/button/button.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class Button extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | } 9 | if (!window.customElements.get('zoo-button')) { 10 | window.customElements.define('zoo-button', Button); 11 | } -------------------------------------------------------------------------------- /src/zoo-modules/misc/button/button.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo button', () => { 2 | it('should create disabled button', async () => { 3 | const style = await page.evaluate(() => { 4 | document.body.innerHTML = ` 5 | 6 | 7 | 8 | `; 9 | const nestedButton = document.querySelector('zoo-button').shadowRoot.querySelector('slot').assignedElements()[0]; 10 | const style = window.getComputedStyle(nestedButton); 11 | return { 12 | colorLight: style.getPropertyValue('--color-light').trim(), 13 | colorMid: style.getPropertyValue('--color-mid').trim(), 14 | colorDark: style.getPropertyValue('--color-dark').trim(), 15 | textNormal: style.getPropertyValue('--text-normal').trim(), 16 | textActive: style.getPropertyValue('--text-active').trim(), 17 | background: style.getPropertyValue('--background').trim(), 18 | border: style.getPropertyValue('--border').trim() 19 | }; 20 | }); 21 | expect(style.colorLight).toEqual(colors.primaryLight); 22 | expect(style.colorMid).toEqual('#F2F3F4'); 23 | expect(style.colorDark).toEqual('#F2F3F4'); 24 | expect(style.textNormal).toEqual('#767676'); 25 | expect(style.textActive).toEqual('#767676'); 26 | expect(style.background).toEqual('#F2F3F4'); 27 | expect(style.border).toEqual('1px solid #E6E6E6'); 28 | }); 29 | 30 | it('should create normal button', async () => { 31 | const style = await page.evaluate(() => { 32 | document.body.innerHTML = ` 33 | 34 | 35 | 36 | `; 37 | const nestedButton = document.querySelector('zoo-button').shadowRoot.querySelector('slot').assignedElements()[0]; 38 | const style = window.getComputedStyle(nestedButton); 39 | return { 40 | colorLight: style.getPropertyValue('--color-light').trim(), 41 | colorMid: style.getPropertyValue('--color-mid').trim(), 42 | colorDark: style.getPropertyValue('--color-dark').trim(), 43 | textNormal: style.getPropertyValue('--text-normal').trim(), 44 | textActive: style.getPropertyValue('--text-active').trim(), 45 | background: style.getPropertyValue('--background').trim(), 46 | border: style.getPropertyValue('--border').trim() 47 | }; 48 | }); 49 | expect(style.colorLight).toEqual(colors.primaryLight); 50 | expect(style.colorMid).toEqual(colors.primaryMid); 51 | expect(style.colorDark).toEqual(colors.primaryDark); 52 | expect(style.textNormal).toEqual('white'); 53 | expect(style.textActive).toEqual('white'); 54 | expect(style.background).toEqual(`linear-gradient(to right, ${colors.primaryMid}, ${colors.primaryLight})`); 55 | expect(style.border).toEqual('0'); 56 | }); 57 | 58 | it('should create secondary button', async () => { 59 | const style = await page.evaluate(() => { 60 | document.body.innerHTML = ` 61 | 62 | 63 | 64 | `; 65 | const nestedButton = document.body.querySelector('zoo-button').shadowRoot.querySelector('slot').assignedElements()[0]; 66 | const style = window.getComputedStyle(nestedButton); 67 | return { 68 | colorLight: style.getPropertyValue('--color-light').trim(), 69 | colorMid: style.getPropertyValue('--color-mid').trim(), 70 | colorDark: style.getPropertyValue('--color-dark').trim(), 71 | textNormal: style.getPropertyValue('--text-normal').trim(), 72 | textActive: style.getPropertyValue('--text-active').trim(), 73 | background: style.getPropertyValue('--background').trim(), 74 | border: style.getPropertyValue('--border').trim() 75 | }; 76 | }); 77 | expect(style.colorLight).toEqual(colors.secondaryLight); 78 | expect(style.colorMid).toEqual(colors.secondaryMid); 79 | expect(style.colorDark).toEqual(colors.secondaryDark); 80 | expect(style.textNormal).toEqual('white'); 81 | expect(style.textActive).toEqual('white'); 82 | expect(style.background).toEqual(`linear-gradient(to right, ${colors.secondaryMid}, ${colors.secondaryLight})`); 83 | expect(style.border).toEqual('0'); 84 | }); 85 | 86 | it('should create hollow button', async () => { 87 | const style = await page.evaluate(() => { 88 | document.body.innerHTML = ` 89 | 90 | 91 | 92 | `; 93 | const nestedButton = document.body.querySelector('zoo-button').shadowRoot.querySelector('slot').assignedElements()[0]; 94 | const style = window.getComputedStyle(nestedButton); 95 | return { 96 | colorLight: style.getPropertyValue('--color-light').trim(), 97 | colorMid: style.getPropertyValue('--color-mid').trim(), 98 | colorDark: style.getPropertyValue('--color-dark').trim(), 99 | textNormal: style.getPropertyValue('--text-normal').trim(), 100 | textActive: style.getPropertyValue('--text-active').trim(), 101 | background: style.getPropertyValue('--background').trim(), 102 | border: style.getPropertyValue('--border').trim() 103 | }; 104 | }); 105 | expect(style.colorLight).toEqual(colors.primaryLight); 106 | expect(style.colorMid).toEqual(colors.primaryMid); 107 | expect(style.colorDark).toEqual(colors.primaryDark); 108 | expect(style.textNormal).toEqual(colors.primaryMid); 109 | expect(style.textActive).toEqual('white'); 110 | expect(style.background).toEqual('transparent'); 111 | expect(style.border).toEqual(`2px solid ${colors.primaryMid}`); 112 | }); 113 | 114 | it('should create transparent button', async () => { 115 | const style = await page.evaluate(() => { 116 | document.body.innerHTML = ` 117 | 118 | 119 | 120 | `; 121 | const nestedButton = document.body.querySelector('zoo-button').shadowRoot.querySelector('slot').assignedElements()[0]; 122 | const style = window.getComputedStyle(nestedButton); 123 | return { 124 | colorLight: style.getPropertyValue('--color-light').trim(), 125 | colorMid: style.getPropertyValue('--color-mid').trim(), 126 | colorDark: style.getPropertyValue('--color-dark').trim(), 127 | textNormal: style.getPropertyValue('--text-normal').trim(), 128 | textActive: style.getPropertyValue('--text-active').trim(), 129 | background: style.getPropertyValue('--background').trim(), 130 | border: style.getPropertyValue('--border').trim() 131 | }; 132 | }); 133 | expect(style.colorLight).toEqual(colors.primaryLight); 134 | expect(style.colorMid).toEqual(colors.primaryMid); 135 | expect(style.colorDark).toEqual(colors.primaryDark); 136 | expect(style.textNormal).toEqual(colors.primaryMid); 137 | expect(style.textActive).toEqual('white'); 138 | expect(style.background).toEqual('transparent'); 139 | expect(style.border).toEqual('0'); 140 | }); 141 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/collapsable-list-item/collapsable-list-item.css: -------------------------------------------------------------------------------- 1 | :host { 2 | padding: 0 10px; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | :host([border-visible]) { 8 | margin: 8px 0; 9 | } 10 | 11 | details { 12 | padding: 10px; 13 | } 14 | 15 | :host([border-visible]) details { 16 | color: var(--primary-dark); 17 | border: 1px solid var(--primary-mid); 18 | border-radius: 3px; 19 | } 20 | 21 | details[open] { 22 | color: var(--primary-dark); 23 | border: 1px solid var(--primary-mid); 24 | border-radius: 3px; 25 | } 26 | 27 | summary { 28 | cursor: pointer; 29 | color: var(--primary-mid); 30 | font-weight: 700; 31 | } 32 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/collapsable-list-item/collapsable-list-item.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
-------------------------------------------------------------------------------- /src/zoo-modules/misc/collapsable-list-item/collapsable-list-item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class CollapsableListItem extends HTMLElement { 5 | constructor() { 6 | super(); 7 | this.details = this.shadowRoot.querySelector('details'); 8 | this.details.addEventListener('toggle', e => { 9 | this.shadowRoot.host.dispatchEvent(new CustomEvent('toggle', {detail: e.target.open, composed: true})); 10 | }); 11 | } 12 | 13 | close() { 14 | this.details.open = false; 15 | } 16 | } 17 | if (!window.customElements.get('zoo-collapsable-list-item')) { 18 | window.customElements.define('zoo-collapsable-list-item', CollapsableListItem); 19 | } -------------------------------------------------------------------------------- /src/zoo-modules/misc/collapsable-list/collapsable-list.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/collapsable-list/collapsable-list.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/collapsable-list/collapsable-list.js: -------------------------------------------------------------------------------- 1 | import { CollapsableListItem } from '../collapsable-list-item/collapsable-list-item.js'; 2 | import { registerComponents } from '../../common/register-components.js'; 3 | 4 | /** 5 | * @injectHTML 6 | */ 7 | export class CollapsableList extends HTMLElement { 8 | constructor() { 9 | super(); 10 | registerComponents(CollapsableListItem); 11 | const slot = this.shadowRoot.querySelector('slot'); 12 | slot.addEventListener('slotchange', () => { 13 | const items = slot.assignedElements(); 14 | 15 | items.forEach(item => item.addEventListener('toggle', e => { 16 | if (!e.detail || this.hasAttribute('disable-autoclose')) return; 17 | items.forEach(i => !i.isEqualNode(item) && i.close()); 18 | })); 19 | 20 | 21 | items.forEach((item) => { 22 | if (item.hasAttribute('opened-by-default')) { 23 | item.details.open = true; 24 | } 25 | }); 26 | }); 27 | } 28 | } 29 | if (!window.customElements.get('zoo-collapsable-list')) { 30 | window.customElements.define('zoo-collapsable-list', CollapsableList); 31 | } -------------------------------------------------------------------------------- /src/zoo-modules/misc/collapsable-list/collapsable-list.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo collapsable list', function () { 2 | it('should create default collapsable list', async () => { 3 | const ret = await page.evaluate(() => { 4 | document.body.innerHTML = ` 5 | 6 | 7 | header1 8 |
content
9 |
10 | 11 | Header2 12 |
content
13 |
14 |
15 | `; 16 | const listItem = document.querySelector('zoo-collapsable-list-item'); 17 | const header = listItem.shadowRoot.querySelector('slot[name="header"]').assignedElements()[0].innerHTML; 18 | const content = listItem.shadowRoot.querySelector('slot[name="content"]').assignedElements()[0].innerHTML; 19 | return { 20 | header: header, 21 | content: content 22 | }; 23 | }); 24 | expect(ret.header).toEqual('header1'); 25 | expect(ret.content).toEqual('content'); 26 | }); 27 | 28 | it('should close other items on toggle', async () => { 29 | const result = await page.evaluate(async () => { 30 | document.body.innerHTML = ` 31 | 32 | 33 | header1 34 |
content
35 |
36 | 37 | header2 38 |
content
39 |
40 |
41 | `; 42 | const result = []; 43 | const listItems = [...document.querySelectorAll('zoo-collapsable-list-item')]; 44 | await new Promise(r => setTimeout(r, 10)); 45 | const details = listItems.map(item => item.shadowRoot.querySelector('details')); 46 | 47 | result.push(details.map(d => d.open)); 48 | details[0].open = true; 49 | result.push(details.map(d => d.open)); 50 | await new Promise(r => setTimeout(r, 10)); 51 | details[1].open = true; 52 | await new Promise(r => setTimeout(r, 10)); 53 | result.push(details.map(d => d.open)); 54 | return result; 55 | }); 56 | expect(result[0]).toEqual([false, false]); 57 | expect(result[1]).toEqual([true, false]); 58 | expect(result[2]).toEqual([false, true]); 59 | }); 60 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/feedback/feeback.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo feedback', function () { 2 | it('should create default feedback', async () => { 3 | const retText = await page.evaluate(() => { 4 | let feedback = document.createElement('zoo-feedback'); 5 | const span = document.createElement('span'); 6 | span.innerHTML = 'example'; 7 | feedback.appendChild(span); 8 | document.body.appendChild(feedback); 9 | const text = feedback.shadowRoot.querySelector('slot').assignedElements()[0].innerHTML; 10 | return text; 11 | }); 12 | expect(retText).toEqual('example'); 13 | }); 14 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/feedback/feedback.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | align-items: center; 4 | box-sizing: border-box; 5 | font-size: 14px; 6 | line-height: 20px; 7 | border-left: 3px solid var(--info-mid); 8 | width: 100%; 9 | height: 100%; 10 | padding: 5px 0; 11 | background: var(--info-ultralight); 12 | border-radius: 5px; 13 | 14 | --svg-fill: var(--info-mid); 15 | } 16 | 17 | :host([type="error"]) { 18 | background: var(--warning-ultralight); 19 | border-color: var(--warning-mid); 20 | 21 | --svg-fill: var(--warning-mid); 22 | } 23 | 24 | :host([type="success"]) { 25 | background: var(--primary-ultralight); 26 | border-color: var(--primary-mid); 27 | 28 | --svg-fill: var(--primary-mid); 29 | } 30 | 31 | zoo-attention-icon { 32 | padding: 0 10px 0 15px; 33 | 34 | --icon-color: var(--svg-fill); 35 | --width: 30px; 36 | --height: 30px; 37 | } 38 | 39 | ::slotted(*) { 40 | display: flex; 41 | align-items: center; 42 | height: 100%; 43 | overflow: auto; 44 | box-sizing: border-box; 45 | padding: 5px 5px 5px 0; 46 | } 47 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/feedback/feedback.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/feedback/feedback.js: -------------------------------------------------------------------------------- 1 | import { AttentionIcon } from '../../icon/attention-icon/attention-icon.js'; 2 | import { registerComponents } from '../../common/register-components.js'; 3 | 4 | /** 5 | * @injectHTML 6 | */ 7 | export class Feedback extends HTMLElement { 8 | constructor() { 9 | super(); 10 | registerComponents(AttentionIcon); 11 | } 12 | } 13 | 14 | if (!window.customElements.get('zoo-feedback')) { 15 | window.customElements.define('zoo-feedback', Feedback); 16 | } -------------------------------------------------------------------------------- /src/zoo-modules/misc/footer/footer-a11y.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo footer', function() { 2 | it('should pass accessibility tests', async() => { 3 | const results = await page.evaluate(async () => { 4 | document.body.innerHTML = ` 5 | 6 | 7 | Github 8 | 9 | 10 | NPM 11 | 12 | `; 13 | return await axe.run('zoo-footer'); 14 | }); 15 | if (results.violations.length) { 16 | console.log('zoo-footer a11y violations ', results.violations); 17 | throw new Error('Accessibility issues found'); 18 | } 19 | }); 20 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/footer/footer.css: -------------------------------------------------------------------------------- 1 | :host { 2 | contain: style; 3 | } 4 | 5 | nav { 6 | display: flex; 7 | justify-content: center; 8 | background: linear-gradient(to right, var(--primary-mid), var(--primary-light)); 9 | padding: 10px 30px; 10 | } 11 | 12 | ::slotted(zoo-link) { 13 | width: max-content; 14 | } 15 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/footer/footer.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 |
-------------------------------------------------------------------------------- /src/zoo-modules/misc/footer/footer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class Footer extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | } 9 | 10 | if (!window.customElements.get('zoo-footer')) { 11 | window.customElements.define('zoo-footer', Footer); 12 | } -------------------------------------------------------------------------------- /src/zoo-modules/misc/footer/footer.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo footer', function () { 2 | it('should create two links given array of two', async () => { 3 | const linksLength = await page.evaluate(() => { 4 | let footer = document.createElement('zoo-footer'); 5 | const link1 = document.createElement('zoo-link'); 6 | link1.href = 'https://google.com'; 7 | link1.text = 'About us'; 8 | footer.appendChild(link1); 9 | const link2 = document.createElement('zoo-link'); 10 | link2.href = 'https://google.com'; 11 | link2.text = 'About us'; 12 | footer.appendChild(link2); 13 | document.body.appendChild(footer); 14 | const linkSlot = footer.shadowRoot.querySelector('slot'); 15 | return linkSlot.assignedNodes().length; 16 | }); 17 | expect(linksLength).toEqual(2); 18 | }); 19 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/header/header-a11y.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo header', function() { 2 | it('should pass accessibility tests', async() => { 3 | const results = await page.evaluate(async () => { 4 | document.body.innerHTML = ` 5 | 6 | Zooplus logo 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
`; 19 | return await axe.run('zoo-header'); 20 | }); 21 | if (results.violations.length) { 22 | console.log('zoo-header a11y violations ', results.violations); 23 | throw new Error('Accessibility issues found'); 24 | } 25 | }); 26 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/header/header.css: -------------------------------------------------------------------------------- 1 | :host { 2 | contain: style; 3 | } 4 | 5 | header { 6 | display: flex; 7 | align-items: center; 8 | padding: 0 25px; 9 | height: 70px; 10 | } 11 | 12 | ::slotted(img) { 13 | height: 46px; 14 | padding: 5px 25px 5px 0; 15 | cursor: pointer; 16 | } 17 | 18 | ::slotted(*[slot="headertext"]) { 19 | color: var(--primary-mid); 20 | } 21 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/header/header.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
-------------------------------------------------------------------------------- /src/zoo-modules/misc/header/header.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class Header extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | } 9 | 10 | if (!window.customElements.get('zoo-header')) { 11 | window.customElements.define('zoo-header', Header); 12 | } -------------------------------------------------------------------------------- /src/zoo-modules/misc/header/header.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo header', function () { 2 | it('should create header text', async () => { 3 | const headerText = await page.evaluate(() => { 4 | document.body.innerHTML = ` 5 | 6 | Zooplus logo 7 |

header-text

8 |
9 | `; 10 | let header = document.querySelector('zoo-header'); 11 | 12 | const text = header.shadowRoot.querySelector('slot[name="headertext"]').assignedNodes()[0]; 13 | return text.innerHTML; 14 | }); 15 | expect(headerText).toEqual('header-text'); 16 | }); 17 | 18 | it('should create image', async () => { 19 | const imageSrc = await page.evaluate(() => { 20 | document.body.innerHTML = ` 21 | 22 | Zooplus logo 23 |

header-text

24 |
25 | `; 26 | let header = document.querySelector('zoo-header'); 27 | 28 | const imageSlot = header.shadowRoot.querySelector('slot[name="img"]'); 29 | return imageSlot.assignedNodes()[0].getAttribute('src'); 30 | }); 31 | expect(imageSrc).toEqual('logo.png'); 32 | }); 33 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/link/link-a11y.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo link', function() { 2 | it('should pass accessibility tests', async() => { 3 | const results = await page.evaluate(async () => { 4 | document.body.innerHTML = ` 5 | 6 | Github 7 | `; 8 | // disable color-contrast check until design team changes it. 9 | return await axe.run('zoo-link', {rules: { 'color-contrast': { enabled: false } }}); 10 | }); 11 | if (results.violations.length) { 12 | console.log('zoo-link a11y violations ', results.violations); 13 | throw new Error('Accessibility issues found'); 14 | } 15 | }); 16 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/link/link.css: -------------------------------------------------------------------------------- 1 | :host { 2 | contain: layout; 3 | display: flex; 4 | width: 100%; 5 | height: 100%; 6 | justify-content: center; 7 | align-items: center; 8 | position: relative; 9 | padding: 0 5px; 10 | font-size: 14px; 11 | line-height: 20px; 12 | 13 | --color-normal: var(--primary-mid); 14 | --color-active: var(--primary-dark); 15 | } 16 | 17 | :host([type="negative"]) { 18 | --color-normal: white; 19 | --color-active: var(--primary-dark); 20 | } 21 | 22 | :host([type="grey"]) { 23 | --color-normal: #767676; 24 | --color-active: var(--primary-dark); 25 | } 26 | 27 | :host([type="warning"]) { 28 | --color-normal: var(--warning-mid); 29 | --color-active: var(--warning-dark); 30 | } 31 | 32 | :host([size="large"]) { 33 | font-size: 18px; 34 | line-height: 22px; 35 | font-weight: bold; 36 | } 37 | 38 | ::slotted(a) { 39 | text-decoration: none; 40 | padding: 0 2px; 41 | color: var(--color-normal); 42 | width: 100%; 43 | } 44 | 45 | ::slotted(a:hover), 46 | ::slotted(a:focus), 47 | ::slotted(a:active) { 48 | color: var(--color-active); 49 | } 50 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/link/link.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/link/link.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class Link extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | } 9 | if (!window.customElements.get('zoo-link')) { 10 | window.customElements.define('zoo-link', Link); 11 | } -------------------------------------------------------------------------------- /src/zoo-modules/misc/link/link.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo link', function () { 2 | it('should create default empty link', async () => { 3 | const linkAttrs = await page.evaluate(() => { 4 | let link = document.createElement('zoo-link'); 5 | document.body.appendChild(link); 6 | const linkBox = link.shadowRoot.querySelector('.link-box'); 7 | const linkAttrs = { 8 | linkBox: linkBox 9 | }; 10 | return linkAttrs; 11 | }); 12 | expect(linkAttrs.linkBox).toBeNull(); 13 | }); 14 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/modal/modal-a11y.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo modal', function() { 2 | it('should pass accessibility tests', async() => { 3 | const results = await page.evaluate(async() => { 4 | document.body.innerHTML = ` 5 | 6 | Your basket contains licensed items 7 |
some content
8 |
`; 9 | 10 | document.querySelector('zoo-modal').openModal(); 11 | // wait for animation to finish 12 | await new Promise(res => setTimeout(() => res(), 330)); 13 | return await axe.run('zoo-modal'); 14 | }); 15 | if (results.violations.length) { 16 | console.log('zoo-modal a11y violations ', results.violations); 17 | throw new Error('Accessibility issues found'); 18 | } 19 | }); 20 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/modal/modal.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: none; 3 | contain: style; 4 | } 5 | 6 | .box { 7 | position: fixed; 8 | width: 100%; 9 | height: 100%; 10 | background: rgb(0 0 0 / var(--zoo-modal-opacity, 0.8)); 11 | opacity: 0; 12 | transition: opacity 0.3s; 13 | z-index: var(--zoo-modal-z-index, 9999); 14 | left: 0; 15 | top: 0; 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | will-change: opacity; 20 | transform: translateZ(0); 21 | } 22 | 23 | .dialog-content { 24 | padding: 0 20px 20px; 25 | box-sizing: border-box; 26 | background: white; 27 | overflow-y: auto; 28 | max-height: 95%; 29 | border-radius: 5px; 30 | animation-name: anim-show; 31 | animation-duration: 0.3s; 32 | animation-fill-mode: forwards; 33 | } 34 | 35 | @media only screen and (width <= 544px) { 36 | .dialog-content { 37 | padding: 25px; 38 | } 39 | } 40 | 41 | @media only screen and (width <= 375px) { 42 | .dialog-content { 43 | width: 100%; 44 | height: 100%; 45 | top: 0; 46 | left: 0; 47 | transform: none; 48 | } 49 | } 50 | 51 | .heading { 52 | display: flex; 53 | align-items: flex-start; 54 | } 55 | 56 | ::slotted(*[slot="header"]) { 57 | font-size: 24px; 58 | line-height: 29px; 59 | font-weight: bold; 60 | margin: 30px 0; 61 | } 62 | 63 | .close { 64 | cursor: pointer; 65 | background: transparent; 66 | border: 0; 67 | padding: 0; 68 | margin: 30px 0 30px auto; 69 | 70 | --icon-color: var(--primary-mid); 71 | } 72 | 73 | .show { 74 | opacity: 1; 75 | } 76 | 77 | .hide .dialog-content { 78 | animation-name: anim-hide; 79 | } 80 | 81 | @keyframes anim-show { 82 | 0% { 83 | opacity: 0; 84 | transform: scale3d(0.9, 0.9, 1); 85 | } 86 | 87 | 100% { 88 | opacity: 1; 89 | transform: scale3d(1, 1, 1); 90 | } 91 | } 92 | 93 | @keyframes anim-hide { 94 | 0% { 95 | opacity: 1; 96 | } 97 | 98 | 100% { 99 | opacity: 0; 100 | transform: scale3d(0.9, 0.9, 1); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/modal/modal.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 8 |
9 |
10 | 11 |
12 |
13 |
-------------------------------------------------------------------------------- /src/zoo-modules/misc/modal/modal.js: -------------------------------------------------------------------------------- 1 | import { CrossIcon } from '../../icon/cross-icon/cross-icon.js'; 2 | import { registerComponents } from '../../common/register-components.js'; 3 | 4 | /** 5 | * @injectHTML 6 | */ 7 | export class Modal extends HTMLElement { 8 | 9 | constructor() { 10 | super(); 11 | registerComponents(CrossIcon); 12 | this.shadowRoot.querySelector('.close').addEventListener('click', () => this.closeModal()); 13 | 14 | const box = this.shadowRoot.querySelector('.box'); 15 | this.closeModalOnClickHandler = (clickEvent) => { 16 | if (clickEvent.target == box) this.closeModal(); 17 | }; 18 | box.addEventListener('click', this.closeModalOnClickHandler); 19 | 20 | // https://github.com/HugoGiraudel/a11y-dialog/blob/main/a11y-dialog.js 21 | this.focusableSelectors = [ 22 | 'a[href]:not([tabindex^="-"]):not([inert])', 23 | 'area[href]:not([tabindex^="-"]):not([inert])', 24 | 'input:not([disabled]):not([inert])', 25 | 'select:not([disabled]):not([inert])', 26 | 'textarea:not([disabled]):not([inert])', 27 | 'button:not([disabled]):not([inert])', 28 | 'iframe:not([tabindex^="-"]):not([inert])', 29 | 'audio[controls]:not([tabindex^="-"]):not([inert])', 30 | 'video[controls]:not([tabindex^="-"]):not([inert])', 31 | '[contenteditable]:not([tabindex^="-"]):not([inert])', 32 | '[tabindex]:not([tabindex^="-"]):not([inert])', 33 | ]; 34 | 35 | this.keyUpEventHandler = (event) => { 36 | if (event.key === 'Escape') this.closeModal(); 37 | if (event.key === 'Tab') this.maintainFocus(event.shiftKey); 38 | }; 39 | } 40 | 41 | connectedCallback() { 42 | this.hidden = true; 43 | } 44 | 45 | static get observedAttributes() { 46 | return ['closelabel', 'button-closeable']; 47 | } 48 | 49 | attributeChangedCallback(attrName, oldVal, newVal) { 50 | if (attrName === 'button-closeable') { 51 | if (this.hasAttribute('button-closeable')) { 52 | const box = this.shadowRoot.querySelector('.box'); 53 | box.removeEventListener('click', this.closeModalOnClickHandler); 54 | } else { 55 | this.shadowRoot.querySelector('.box').addEventListener('click', this.closeModalOnClickHandler); 56 | } 57 | 58 | } else if (attrName === 'closelabel') { 59 | this.shadowRoot.querySelector('zoo-cross-icon').setAttribute('title', newVal); 60 | } 61 | } 62 | 63 | openModal() { 64 | this.style.display = 'block'; 65 | this.toggleModalClass(); 66 | this.shadowRoot.querySelector('button').focus(); 67 | document.addEventListener('keyup', this.keyUpEventHandler); 68 | } 69 | 70 | maintainFocus(shiftKey) { 71 | const button = this.shadowRoot.querySelector('button'); 72 | const slottedFocusableElements = [...this.querySelectorAll(this.focusableSelectors.join(','))]; 73 | const focusNotInSlotted = !slottedFocusableElements.some(el => el.isEqualNode(document.activeElement)); 74 | const focusNotInShadowRoot = !button.isEqualNode(this.shadowRoot.activeElement); 75 | if (focusNotInSlotted && focusNotInShadowRoot) { 76 | if (shiftKey) { 77 | slottedFocusableElements[slottedFocusableElements.length - 1].focus(); 78 | } else { 79 | button.focus(); 80 | } 81 | } 82 | } 83 | 84 | closeModal() { 85 | if (this.timeoutVar) return; 86 | this.hidden = !this.hidden; 87 | this.toggleModalClass(); 88 | this.timeoutVar = setTimeout(() => { 89 | this.style.display = 'none'; 90 | this.dispatchEvent(new Event('modalClosed')); 91 | this.hidden = !this.hidden; 92 | this.timeoutVar = undefined; 93 | }, 300); 94 | } 95 | 96 | toggleModalClass() { 97 | const modalBox = this.shadowRoot.querySelector('.box'); 98 | if (!this.hidden) { 99 | modalBox.classList.add('hide'); 100 | modalBox.classList.remove('show'); 101 | } else { 102 | modalBox.classList.add('show'); 103 | modalBox.classList.remove('hide'); 104 | } 105 | } 106 | } 107 | 108 | if (!window.customElements.get('zoo-modal')) { 109 | window.customElements.define('zoo-modal', Modal); 110 | } -------------------------------------------------------------------------------- /src/zoo-modules/misc/modal/modal.spec.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | describe('Zoo modal', function () { 3 | beforeEach(async () => await page.evaluate(() => jasmine.clock().install())); 4 | afterEach(async () => await page.evaluate(() => jasmine.clock().uninstall())); 5 | it('should create opened modal', async () => { 6 | const modalHeadingText = await page.evaluate(() => { 7 | document.body.innerHTML = ` 8 | 9 | header-text 10 |
content
11 |
12 | `; 13 | let modal = document.querySelector('zoo-modal'); 14 | modal.style.display = 'block'; 15 | 16 | return modal.shadowRoot.querySelector('slot[name="header"]').assignedNodes()[0].innerHTML; 17 | }); 18 | expect(modalHeadingText).toEqual('header-text'); 19 | }); 20 | 21 | it('should create opened modal and close it', async () => { 22 | const modalDisplay = await page.evaluate(async () => { 23 | document.body.innerHTML = ` 24 | 25 | header-text 26 |
content
27 |
28 | `; 29 | let modal = document.querySelector('zoo-modal'); 30 | modal.style.display = 'block'; 31 | 32 | const closeButton = modal.shadowRoot.querySelector('.close'); 33 | closeButton.click(); 34 | jasmine.clock().tick(400); 35 | return modal.style.display; 36 | }); 37 | expect(modalDisplay).toEqual('none'); 38 | }); 39 | 40 | it('should close opened modal when outer box is clicked', async () => { 41 | const modalDisplay = await page.evaluate(async () => { 42 | document.body.innerHTML = ` 43 | 44 | header-text 45 |
content
46 |
47 | `; 48 | let modal = document.querySelector('zoo-modal'); 49 | modal.style.display = 'block'; 50 | 51 | const box = modal.shadowRoot.querySelector('.box'); 52 | box.dispatchEvent(new Event('click')); 53 | jasmine.clock().tick(400); 54 | return modal.style.display; 55 | }); 56 | expect(modalDisplay).toEqual('none'); 57 | }); 58 | 59 | it('should not close opened modal when button-closeable attribute is set and outer box is clicked', async () => { 60 | const modalDisplay = await page.evaluate(async () => { 61 | document.body.innerHTML = ` 62 | 63 | header-text 64 |
content
65 |
66 | `; 67 | let modal = document.querySelector('zoo-modal'); 68 | modal.style.display = 'block'; 69 | 70 | const box = modal.shadowRoot.querySelector('.box'); 71 | box.dispatchEvent(new Event('click')); 72 | jasmine.clock().tick(400); 73 | return modal.style.display; 74 | }); 75 | expect(modalDisplay).toEqual('block'); 76 | }); 77 | 78 | it('should close opened modal when escape is clicked', async () => { 79 | const modalDisplay = await page.evaluate(async () => { 80 | document.body.innerHTML = ` 81 | 82 | header-text 83 |
content
84 |
85 | `; 86 | let modal = document.querySelector('zoo-modal'); 87 | modal.openModal(); 88 | 89 | const event = new KeyboardEvent('keyup', { key: 'Escape' }); 90 | document.dispatchEvent(event); 91 | jasmine.clock().tick(400); 92 | return modal.style.display; 93 | }); 94 | expect(modalDisplay).toEqual('none'); 95 | }); 96 | 97 | it('should dispatch only one modalClosed event despite multiple calls to closeModal', async () => { 98 | const calledTimes = await page.evaluate(async () => { 99 | document.body.innerHTML = ` 100 | 101 | header-text 102 |
content
103 |
104 | `; 105 | let modal = document.querySelector('zoo-modal'); 106 | modal.openModal(); 107 | jasmine.clock().tick(310); 108 | 109 | let called = 0; 110 | modal.addEventListener('modalClosed', () => called += 1); 111 | 112 | modal.closeModal(); 113 | modal.closeModal(); 114 | jasmine.clock().tick(620); 115 | return called; 116 | }); 117 | expect(calledTimes).toEqual(1); 118 | }); 119 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/navigation/navigation-a11y.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo navigation', function () { 2 | it('should pass accessibility tests', async () => { 3 | const results = await page.evaluate(async () => { 4 | document.body.innerHTML = ` 5 | 6 | Can I use shadowdomv1? 7 | Can I use custom-elementsv1? 8 | Documentation 9 | `; 10 | return await axe.run('zoo-navigation'); 11 | }); 12 | 13 | if (results.violations.length) { 14 | console.log('zoo-navigation a11y violations ', results.violations); 15 | throw new Error('Accessibility issues found'); 16 | } 17 | }); 18 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/navigation/navigation.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | height: 56px; 4 | } 5 | 6 | nav { 7 | display: flex; 8 | width: 100%; 9 | padding: 0 20px; 10 | background: linear-gradient(to right, var(--primary-mid), var(--primary-light)); 11 | } 12 | 13 | :host([direction="vertical"]) nav { 14 | flex-direction: column; 15 | height: auto; 16 | width: max-content; 17 | background: transparent; 18 | padding: 0; 19 | } 20 | 21 | ::slotted(*) { 22 | cursor: pointer; 23 | display: inline-flex; 24 | text-decoration: none; 25 | align-items: center; 26 | height: 100%; 27 | color: white; 28 | padding: 0 15px; 29 | font-weight: bold; 30 | font-size: 14px; 31 | line-height: 20px; 32 | } 33 | 34 | ::slotted(*:hover), 35 | ::slotted(*:focus) { 36 | background: rgb(255 255 255 / 20%); 37 | } 38 | 39 | :host([direction="vertical"]) ::slotted(*) { 40 | padding: 10px 5px; 41 | color: initial; 42 | box-sizing: border-box; 43 | } 44 | 45 | :host([direction="vertical"]) ::slotted(*:hover), 46 | :host([direction="vertical"]) ::slotted(*:focus) { 47 | background: rgb(0 0 0 / 7%); 48 | } 49 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/navigation/navigation.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/navigation/navigation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class Navigation extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | } 9 | if (!window.customElements.get('zoo-navigation')) { 10 | window.customElements.define('zoo-navigation', Navigation); 11 | } -------------------------------------------------------------------------------- /src/zoo-modules/misc/navigation/navigation.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo navigation', function () { 2 | it('should create nav element with slotted element', async () => { 3 | const slottedElement = await page.evaluate(() => { 4 | let nav = document.createElement('zoo-navigation'); 5 | let element = document.createElement('span'); 6 | element.innerHTML = 'slotted'; 7 | nav.appendChild(element); 8 | document.body.appendChild(nav); 9 | const slot = nav.shadowRoot.querySelector('slot'); 10 | return slot.assignedNodes()[0].innerHTML; 11 | }); 12 | expect(slottedElement).toEqual('slotted'); 13 | }); 14 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/paginator/paginator.css: -------------------------------------------------------------------------------- 1 | :host { 2 | min-width: inherit; 3 | display: none; 4 | } 5 | 6 | .box { 7 | display: flex; 8 | align-items: center; 9 | font-size: 14px; 10 | width: max-content; 11 | position: var(--paginator-position, 'initial'); 12 | right: var(--right, 'unset'); 13 | } 14 | 15 | :host([currentpage]) { 16 | display: flex; 17 | } 18 | 19 | nav { 20 | display: flex; 21 | align-items: center; 22 | border: 1px solid #E6E6E6; 23 | border-radius: 5px; 24 | padding: 15px; 25 | } 26 | 27 | button { 28 | display: flex; 29 | cursor: pointer; 30 | opacity: 1; 31 | transition: opacity 0.1s; 32 | background: transparent; 33 | border: 0; 34 | padding: 0; 35 | font-size: inherit; 36 | border-radius: 5px; 37 | margin: 0 2px; 38 | } 39 | 40 | button:active { 41 | opacity: 0.5; 42 | } 43 | 44 | button:hover, 45 | button:focus { 46 | background: #F2F3F4; 47 | } 48 | 49 | button.hidden { 50 | display: none; 51 | } 52 | 53 | .page-element { 54 | padding: 4px 8px; 55 | } 56 | 57 | .page-element.active { 58 | background: var(--primary-ultralight); 59 | color: var(--primary-dark); 60 | } 61 | 62 | zoo-arrow-icon { 63 | pointer-events: none; 64 | } 65 | 66 | .prev zoo-arrow-icon { 67 | transform: rotate(90deg); 68 | } 69 | 70 | .next zoo-arrow-icon { 71 | transform: rotate(-90deg); 72 | } 73 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/paginator/paginator.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 11 |
12 | 15 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/paginator/paginator.js: -------------------------------------------------------------------------------- 1 | import { ArrowDownIcon } from '../../icon/arrow-icon/arrow-icon.js'; 2 | import { registerComponents } from '../../common/register-components.js'; 3 | 4 | /** 5 | * @injectHTML 6 | */ 7 | export class Paginator extends HTMLElement { 8 | constructor() { 9 | super(); 10 | registerComponents(ArrowDownIcon); 11 | this.prev = this.shadowRoot.querySelector('.prev'); 12 | this.next = this.shadowRoot.querySelector('.next'); 13 | this.dots = this.shadowRoot.querySelector('#dots').content; 14 | this.pages = this.shadowRoot.querySelector('#pages').content; 15 | 16 | this.shadowRoot.addEventListener('click', e => { 17 | const pageNumber = e.target.getAttribute('page'); 18 | if (pageNumber) { 19 | this.goToPage(pageNumber); 20 | } else if (e.target.classList.contains('prev')) { 21 | this.goToPage(+this.getAttribute('currentpage')-1); 22 | } else if (e.target.classList.contains('next')) { 23 | this.goToPage(+this.getAttribute('currentpage')+1); 24 | } 25 | }); 26 | } 27 | 28 | goToPage(pageNumber) { 29 | this.setAttribute('currentpage', pageNumber); 30 | this.dispatchEvent(new CustomEvent('pageChange', { 31 | detail: {pageNumber: pageNumber}, bubbles: true, composed: true 32 | })); 33 | } 34 | 35 | static get observedAttributes() { 36 | return ['maxpages', 'currentpage', 'prev-page-title', 'next-page-title']; 37 | } 38 | handleHideShowArrows() { 39 | if (this.getAttribute('currentpage') == 1) { 40 | this.prev.classList.add('hidden'); 41 | } else { 42 | this.prev.classList.remove('hidden'); 43 | } 44 | if (+this.getAttribute('currentpage') >= +this.getAttribute('maxpages')) { 45 | this.next.classList.add('hidden'); 46 | } else { 47 | this.next.classList.remove('hidden'); 48 | } 49 | } 50 | rerenderPageButtons() { 51 | this.shadowRoot.querySelectorAll('*[class^="page-element"]').forEach(n => n.remove()); 52 | const pageNum = +this.getAttribute('currentpage'); 53 | const maxPages = this.getAttribute('maxpages'); 54 | for (let page=maxPages;page>0;page--) { 55 | //first, previous, current, next or last page 56 | if (page == 1 || page == pageNum - 1 || page == pageNum || page == pageNum + 1 || page == maxPages) { 57 | const pageNode = this.pages.cloneNode(true).firstElementChild; 58 | pageNode.setAttribute('page', page); 59 | pageNode.setAttribute('title', page); 60 | if (pageNum == page) { 61 | pageNode.classList.add('active'); 62 | } 63 | pageNode.textContent = page; 64 | this.prev.after(pageNode); 65 | } else if (page == pageNum-2 || pageNum+2 == page) { 66 | this.prev.after(this.dots.cloneNode(true)); 67 | } 68 | } 69 | } 70 | attributeChangedCallback(attrName, oldVal, newVal) { 71 | if (attrName == 'currentpage' || attrName == 'maxpages') { 72 | this.handleHideShowArrows(); 73 | this.rerenderPageButtons(); 74 | } else if (attrName === 'prev-page-title') { 75 | this.shadowRoot.querySelector('.prev zoo-arrow-icon').setAttribute('title', newVal); 76 | } else if (attrName === 'next-page-title') { 77 | this.shadowRoot.querySelector('.next zoo-arrow-icon').setAttribute('title', newVal); 78 | } 79 | } 80 | } 81 | if (!window.customElements.get('zoo-paginator')) { 82 | window.customElements.define('zoo-paginator', Paginator); 83 | } -------------------------------------------------------------------------------- /src/zoo-modules/misc/paginator/paginator.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo paginator', function () { 2 | it('should create default paginator', async () => { 3 | const buttonsLength = await page.evaluate(() => { 4 | document.body.innerHTML = ``; 5 | const paginator = document.querySelector('zoo-paginator'); 6 | const buttons = paginator.shadowRoot.querySelectorAll('button'); 7 | return buttons.length; 8 | }); 9 | expect(buttonsLength).toEqual(5); 10 | }); 11 | 12 | it('should go to previous page', async () => { 13 | const currentpage = await page.evaluate(async () => { 14 | document.body.innerHTML = ``; 15 | const paginator = document.querySelector('zoo-paginator'); 16 | const prev = paginator.shadowRoot.querySelector('.prev'); 17 | prev.click(); 18 | await new Promise(r => setTimeout(r, 10)); 19 | 20 | return paginator.getAttribute('currentpage'); 21 | }); 22 | expect(currentpage).toEqual('1'); 23 | }); 24 | 25 | it('should go to next page', async () => { 26 | const currentpage = await page.evaluate(async () => { 27 | document.body.innerHTML = ``; 28 | const paginator = document.querySelector('zoo-paginator'); 29 | const next = paginator.shadowRoot.querySelector('.next'); 30 | next.click(); 31 | await new Promise(r => setTimeout(r, 10)); 32 | 33 | return paginator.getAttribute('currentpage'); 34 | }); 35 | expect(currentpage).toEqual('3'); 36 | }); 37 | 38 | it('should go to first page', async () => { 39 | const currentpage = await page.evaluate(async () => { 40 | document.body.innerHTML = ``; 41 | const paginator = document.querySelector('zoo-paginator'); 42 | const button = paginator.shadowRoot.querySelector('button[page="1"]'); 43 | button.click(); 44 | await new Promise(r => setTimeout(r, 10)); 45 | 46 | return paginator.getAttribute('currentpage'); 47 | }); 48 | expect(currentpage).toEqual('1'); 49 | }); 50 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/preloader/preloader.css: -------------------------------------------------------------------------------- 1 | :host { 2 | position: absolute; 3 | width: 100%; 4 | height: 100%; 5 | top: 0; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | pointer-events: none; 10 | z-index: 2; 11 | } 12 | 13 | .bounce { 14 | text-align: center; 15 | } 16 | 17 | .bounce > div { 18 | width: 10px; 19 | height: 10px; 20 | background-color: #333; 21 | border-radius: 100%; 22 | display: inline-block; 23 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 24 | } 25 | 26 | .bounce .bounce1 { 27 | animation-delay: -0.32s; 28 | } 29 | 30 | .bounce .bounce2 { 31 | animation-delay: -0.16s; 32 | } 33 | 34 | @keyframes sk-bouncedelay { 35 | 0%, 36 | 80%, 37 | 100% { 38 | transform: scale(0); 39 | } 40 | 41 | 40% { 42 | transform: scale(1); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/preloader/preloader.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
-------------------------------------------------------------------------------- /src/zoo-modules/misc/preloader/preloader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class Preloader extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | } 9 | if (!window.customElements.get('zoo-preloader')) { 10 | window.customElements.define('zoo-preloader', Preloader); 11 | } -------------------------------------------------------------------------------- /src/zoo-modules/misc/spinner/spinner.css: -------------------------------------------------------------------------------- 1 | :host { 2 | contain: layout; 3 | } 4 | 5 | svg { 6 | position: absolute; 7 | inset: calc(50% - 60px) 0 0 calc(50% - 60px); 8 | height: 120px; 9 | width: 120px; 10 | transform-origin: center center; 11 | animation: rotate 2s linear infinite; 12 | z-index: var(--zoo-spinner-z-index, 10002); 13 | } 14 | 15 | svg circle { 16 | animation: dash 1.5s ease-in-out infinite; 17 | stroke: var(--primary-mid); 18 | stroke-dasharray: 1, 200; 19 | stroke-dashoffset: 0; 20 | stroke-linecap: round; 21 | } 22 | 23 | @keyframes rotate { 24 | 100% { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @keyframes dash { 30 | 0% { 31 | stroke-dasharray: 1, 200; 32 | stroke-dashoffset: 0; 33 | } 34 | 35 | 50% { 36 | stroke-dasharray: 89, 200; 37 | stroke-dashoffset: -35px; 38 | } 39 | 40 | 100% { 41 | stroke-dasharray: 89, 200; 42 | stroke-dashoffset: -124px; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/spinner/spinner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/spinner/spinner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class Spinner extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | } 9 | 10 | if (!window.customElements.get('zoo-spinner')) { 11 | window.customElements.define('zoo-spinner', Spinner); 12 | } -------------------------------------------------------------------------------- /src/zoo-modules/misc/tag/tag.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | box-sizing: border-box; 4 | padding: 0 10px; 5 | align-items: center; 6 | width: max-content; 7 | color: var(--color); 8 | border-color: var(--color); 9 | max-width: var(--zoo-tag-max-width, 100px); 10 | border-radius: 3px; 11 | } 12 | 13 | :host(:hover) { 14 | background: var(--primary-ultralight); 15 | color: var(--primary-dark); 16 | } 17 | 18 | :host([type="info"]) { 19 | min-height: 20px; 20 | border-radius: 10px; 21 | border: 1px solid; 22 | } 23 | 24 | :host([type="cloud"]) { 25 | min-height: 46px; 26 | border-radius: 3px; 27 | border: 1px solid lightgray; 28 | } 29 | 30 | :host([type="tag"]) { 31 | border: 1px solid lightgray; 32 | } 33 | 34 | ::slotted(*[slot="content"]) { 35 | font-size: 12px; 36 | overflow-x: hidden; 37 | text-overflow: ellipsis; 38 | white-space: nowrap; 39 | } 40 | 41 | ::slotted(*[slot="pre"]) { 42 | margin-right: 5px; 43 | } 44 | 45 | ::slotted(*[slot="post"]) { 46 | margin-left: 5px; 47 | } 48 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/tag/tag.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/tag/tag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class Tag extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | } 9 | 10 | if (!window.customElements.get('zoo-tag')) { 11 | window.customElements.define('zoo-tag', Tag); 12 | } -------------------------------------------------------------------------------- /src/zoo-modules/misc/toast/toast-a11y.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo toast', function() { 2 | it('should pass accessibility tests', async() => { 3 | const results = await page.evaluate(async() => { 4 | document.body.innerHTML = ` 5 | Search for more than 8.000 products. 6 | `; 7 | document.querySelector('zoo-toast').show(); 8 | // wait for animation to finish 9 | await new Promise(res => setTimeout(() => res(), 330)); 10 | return await axe.run('zoo-toast'); 11 | }); 12 | if (results.violations.length) { 13 | console.log('zoo-toast a11y violations ', results.violations); 14 | throw new Error('Accessibility issues found'); 15 | } 16 | }); 17 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/toast/toast.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: none; 3 | top: 20px; 4 | right: 20px; 5 | position: fixed; 6 | z-index: var(--zoo-toast-z-index, 10001); 7 | contain: layout; 8 | 9 | --color-ultralight: var(--info-ultralight); 10 | --color-mid: var(--info-mid); 11 | --svg-padding: 0; 12 | } 13 | 14 | :host([type="error"]) { 15 | --color-ultralight: var(--warning-ultralight); 16 | --color-mid: var(--warning-mid); 17 | } 18 | 19 | :host([type="success"]) { 20 | --color-ultralight: var(--primary-ultralight); 21 | --color-mid: var(--primary-mid); 22 | } 23 | 24 | div { 25 | max-width: 330px; 26 | min-height: 50px; 27 | box-shadow: 0 5px 5px -3px rgb(0 0 0 / 20%), 0 8px 10px 1px rgb(0 0 0 / 14%), 0 3px 14px 2px rgb(0 0 0 / 12%); 28 | border-left: 3px solid var(--color-mid); 29 | display: flex; 30 | align-items: center; 31 | word-break: break-word; 32 | font-size: 14px; 33 | line-height: 20px; 34 | padding: 15px; 35 | transition: transform 0.3s, opacity 0.4s; 36 | opacity: 0; 37 | transform: translate3d(100%, 0, 0); 38 | background: var(--color-ultralight); 39 | border-radius: 5px; 40 | } 41 | 42 | svg { 43 | padding-right: 10px; 44 | min-width: 48px; 45 | fill: var(--color-mid); 46 | } 47 | 48 | .show { 49 | opacity: 1; 50 | transform: translate3d(0, 0, 0); 51 | } 52 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/toast/toast.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
-------------------------------------------------------------------------------- /src/zoo-modules/misc/toast/toast.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class Toast extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | connectedCallback() { 10 | this.hidden = true; 11 | this.timeout = this.getAttribute('timeout') || 3; 12 | this.setAttribute('role', 'alert'); 13 | } 14 | 15 | show() { 16 | if (!this.hidden) return; 17 | this.style.display = 'block'; 18 | this.timeoutVar = setTimeout(() => { 19 | this.hidden = !this.hidden; 20 | this.toggleToastClass(); 21 | this.timeoutVar = setTimeout(() => { 22 | if (this && !this.hidden) { 23 | this.hidden = !this.hidden; 24 | this.timeoutVar = setTimeout(() => {this.style.display = 'none';}, 300); 25 | this.toggleToastClass(); 26 | } 27 | }, this.timeout * 1000); 28 | }, 30); 29 | } 30 | close() { 31 | if (this.hidden) return; 32 | clearTimeout(this.timeoutVar); 33 | setTimeout(() => { 34 | if (this && !this.hidden) { 35 | this.hidden = !this.hidden; 36 | setTimeout(() => {this.style.display = 'none';}, 300); 37 | this.toggleToastClass(); 38 | } 39 | }, 30); 40 | } 41 | 42 | toggleToastClass() { 43 | const toast = this.shadowRoot.querySelector('div'); 44 | toast.classList.toggle('show'); 45 | } 46 | } 47 | 48 | if (!window.customElements.get('zoo-toast')) { 49 | window.customElements.define('zoo-toast', Toast); 50 | } -------------------------------------------------------------------------------- /src/zoo-modules/misc/toast/toast.spec.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | describe('Zoo toast', function () { 3 | beforeEach(async () => await page.evaluate(() => jasmine.clock().install())); 4 | afterEach(async () => await page.evaluate(() => jasmine.clock().uninstall())); 5 | it('should create default toast', async () => { 6 | const toasttext = await page.evaluate(() => { 7 | document.body.innerHTML = ` 8 | 9 | some-text 10 | 11 | `; 12 | const toast = document.querySelector('zoo-toast'); 13 | const toastBox = toast.shadowRoot.querySelector('slot[name="content"]').assignedElements()[0]; 14 | return toastBox.innerHTML; 15 | }); 16 | expect(toasttext).toEqual('some-text'); 17 | }); 18 | 19 | it('should show and then close toast after 330ms even when timeout is 5000ms', async () => { 20 | const styles = await page.evaluate(async () => { 21 | document.body.innerHTML = ` 22 | 23 | some-text 24 | 25 | `; 26 | const styles = []; 27 | const toast = document.querySelector('zoo-toast'); 28 | toast.show(); 29 | jasmine.clock().tick(45); 30 | styles.push(window.getComputedStyle(toast).display); 31 | 32 | toast.close(); 33 | jasmine.clock().tick(345); 34 | styles.push(window.getComputedStyle(toast).display); 35 | return styles; 36 | }); 37 | expect(styles[0]).toEqual('block'); 38 | expect(styles[1]).toEqual('none'); 39 | }); 40 | 41 | it('should show and then close toast after 330ms even when timeout is 5000ms and a button is clicked', async () => { 42 | const styles = await page.evaluate(async () => { 43 | document.body.innerHTML = ` 44 | 45 | some-text 46 | 47 | `; 48 | const styles = []; 49 | const toast = document.querySelector('zoo-toast'); 50 | toast.show(); 51 | jasmine.clock().tick(45); 52 | styles.push(window.getComputedStyle(toast).display); 53 | 54 | toast.close(); 55 | jasmine.clock().tick(355); 56 | styles.push(window.getComputedStyle(toast).display); 57 | return styles; 58 | }); 59 | expect(styles[0]).toEqual('block'); 60 | expect(styles[1]).toEqual('none'); 61 | }); 62 | 63 | it('should show and then close toast after 330ms automatically', async () => { 64 | const styles = await page.evaluate(async () => { 65 | document.body.innerHTML = ` 66 | 67 | some-text 68 | 69 | `; 70 | const styles = []; 71 | const toast = document.querySelector('zoo-toast'); 72 | toast.show(); 73 | jasmine.clock().tick(45); 74 | styles.push(window.getComputedStyle(toast).display); 75 | 76 | jasmine.clock().tick(1350); 77 | styles.push(window.getComputedStyle(toast).display); 78 | return styles; 79 | }); 80 | expect(styles[0]).toEqual('block'); 81 | expect(styles[1]).toEqual('none'); 82 | }); 83 | 84 | it('should ignore multiple calls to show', async () => { 85 | const calledTimes = await page.evaluate(async () => { 86 | document.body.innerHTML = ` 87 | 88 | some-text 89 | 90 | `; 91 | const toast = document.querySelector('zoo-toast'); 92 | let calledTimes = 0; 93 | toast.toggleToastClass = () => calledTimes += 1; 94 | toast.show(); 95 | jasmine.clock().tick(45); 96 | toast.show(); 97 | jasmine.clock().tick(45); 98 | 99 | return calledTimes; 100 | }); 101 | expect(calledTimes).toEqual(1); 102 | }); 103 | 104 | it('should ignore multiple calls to close', async () => { 105 | const calledTimes = await page.evaluate(async () => { 106 | document.body.innerHTML = ` 107 | 108 | some-text 109 | 110 | `; 111 | const toast = document.querySelector('zoo-toast'); 112 | let calledTimes = 0; 113 | toast.toggleToastClass = () => calledTimes += 1; 114 | toast.show(); 115 | jasmine.clock().tick(45); 116 | toast.close(); 117 | jasmine.clock().tick(45); 118 | toast.close(); 119 | jasmine.clock().tick(45); 120 | 121 | return calledTimes; 122 | }); 123 | expect(calledTimes).toEqual(2); 124 | }); 125 | }); -------------------------------------------------------------------------------- /src/zoo-modules/misc/tooltip/tooltip.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: grid; 3 | position: absolute; 4 | width: max-content; 5 | z-index: var(--zoo-tooltip-z-index, 9997); 6 | pointer-events: none; 7 | color: black; 8 | 9 | --tip-bottom: 0; 10 | --tip-right: unset; 11 | --tip-justify: center; 12 | } 13 | 14 | :host([position="top"]) { 15 | bottom: 170%; 16 | 17 | --tip-bottom: calc(0% - 8px); 18 | } 19 | 20 | :host([position="right"]) { 21 | justify-content: end; 22 | left: 102%; 23 | bottom: 25%; 24 | 25 | --tip-bottom: unset; 26 | --tip-justify: start; 27 | --tip-right: calc(100% - 8px); 28 | } 29 | 30 | :host([position="bottom"]) { 31 | bottom: -130%; 32 | 33 | --tip-bottom: calc(100% - 8px); 34 | } 35 | 36 | :host([position="left"]) { 37 | justify-content: start; 38 | left: -101%; 39 | bottom: 25%; 40 | 41 | --tip-bottom: unset; 42 | --tip-justify: end; 43 | --tip-right: -8px; 44 | } 45 | 46 | .tip { 47 | justify-self: var(--tip-justify); 48 | align-self: center; 49 | position: absolute; 50 | width: 16px; 51 | height: 16px; 52 | box-shadow: 0 4px 15px 0 rgb(0 0 0 / 10%); 53 | transform: rotate(45deg); 54 | z-index: -1; 55 | background: white; 56 | right: var(--tip-right); 57 | bottom: var(--tip-bottom); 58 | } 59 | 60 | .tooltip-content { 61 | display: grid; 62 | padding: 10px; 63 | font-size: 12px; 64 | line-height: 16px; 65 | font-weight: initial; 66 | position: relative; 67 | background: white; 68 | border-radius: 5px; 69 | pointer-events: initial; 70 | box-shadow: 0 4px 15px 0 rgb(0 0 0 / 10%); 71 | } 72 | 73 | .tooltip-content span { 74 | white-space: pre; 75 | } 76 | -------------------------------------------------------------------------------- /src/zoo-modules/misc/tooltip/tooltip.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
-------------------------------------------------------------------------------- /src/zoo-modules/misc/tooltip/tooltip.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @injectHTML 3 | */ 4 | export class Tooltip extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | } 9 | 10 | if (!window.customElements.get('zoo-tooltip')) { 11 | window.customElements.define('zoo-tooltip', Tooltip); 12 | } -------------------------------------------------------------------------------- /src/zoo-modules/misc/tooltip/tooltip.spec.mjs: -------------------------------------------------------------------------------- 1 | describe('Zoo tooltip', function () { 2 | it('should create default tooltip', async () => { 3 | const tooltipText = await page.evaluate(() => { 4 | document.body.innerHTML = ` 5 | 6 | 10 | 11 | `; 12 | let tooltip = document.querySelector('zoo-tooltip'); 13 | const tooltiptext = tooltip.shadowRoot.querySelector('slot').assignedNodes()[0]; 14 | return tooltiptext.innerHTML; 15 | }); 16 | expect(tooltipText).toEqual('some-text'); 17 | }); 18 | }); -------------------------------------------------------------------------------- /src/zoo-web-components.js: -------------------------------------------------------------------------------- 1 | export { InfoMessage } from './zoo-modules/form/info/info.js'; 2 | export { Label } from './zoo-modules/form/label/label.js'; 3 | export { Input } from './zoo-modules/form/input/input.js'; 4 | export { Checkbox } from './zoo-modules/form/checkbox/checkbox.js'; 5 | export { Radio } from './zoo-modules/form/radio/radio.js'; 6 | export { Select } from './zoo-modules/form/select/select.js'; 7 | export { SearchableSelect } from './zoo-modules/form/searchable-select/searchable-select.js'; 8 | export { QuantityControl } from './zoo-modules/form/quantity-control/quantity-control.js'; 9 | export { ToggleSwitch } from './zoo-modules/form/toggle-switch/toggle-switch.js'; 10 | export { DateRange } from './zoo-modules/form/date-range/date-range.js'; 11 | export { InputTag } from './zoo-modules/form/input-tag/input-tag.js'; 12 | 13 | export { ZooGrid } from './zoo-modules/grid/grid/grid.js'; 14 | export { GridHeader } from './zoo-modules/grid/grid-header/grid-header.js'; 15 | export { GridRow } from './zoo-modules/grid/grid-row/grid-row.js'; 16 | 17 | export { Button } from './zoo-modules/misc/button/button.js'; 18 | export { ButtonGroup } from './zoo-modules/misc/button-group/button-group.js'; 19 | export { Header } from './zoo-modules/misc/header/header.js'; 20 | export { Modal } from './zoo-modules/misc/modal/modal.js'; 21 | export { Footer } from './zoo-modules/misc/footer/footer.js'; 22 | export { Feedback } from './zoo-modules/misc/feedback/feedback.js'; 23 | export { Tooltip } from './zoo-modules/misc/tooltip/tooltip.js'; 24 | export { Link } from './zoo-modules/misc/link/link.js'; 25 | export { Navigation } from './zoo-modules/misc/navigation/navigation.js'; 26 | export { Toast } from './zoo-modules/misc/toast/toast.js'; 27 | export { CollapsableList } from './zoo-modules/misc/collapsable-list/collapsable-list.js'; 28 | export { CollapsableListItem } from './zoo-modules/misc/collapsable-list-item/collapsable-list-item.js'; 29 | export { Spinner } from './zoo-modules/misc/spinner/spinner.js'; 30 | export { Paginator } from './zoo-modules/misc/paginator/paginator.js'; 31 | export { Preloader } from './zoo-modules/misc/preloader/preloader.js'; 32 | export { Tag } from './zoo-modules/misc/tag/tag.js'; 33 | 34 | export { AttentionIcon } from './zoo-modules/icon/attention-icon/attention-icon.js'; 35 | export { ArrowDownIcon } from './zoo-modules/icon/arrow-icon/arrow-icon.js'; 36 | export { CrossIcon } from './zoo-modules/icon/cross-icon/cross-icon.js'; 37 | export { PawIcon } from './zoo-modules/icon/paw-icon/paw-icon.js'; 38 | 39 | export { registerComponents } from './zoo-modules/common/register-components.js'; --------------------------------------------------------------------------------