├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .prettierrc ├── .versionrc ├── ACKNOWLEDGMENTS ├── CHANGELOG.md ├── LICENSE ├── README.md ├── demo ├── assets │ └── og.png ├── favicon.js └── styles.css ├── index.html ├── package-lock.json ├── package.json ├── rollup.config.js ├── screenshot.png ├── scripts ├── build-styles.cjs └── build-web-types.cjs ├── src ├── hex-alpha-color-picker.ts ├── hex-color-picker.ts ├── hex-input.ts ├── hsl-color-picker.ts ├── hsl-string-color-picker.ts ├── hsla-color-picker.ts ├── hsla-string-color-picker.ts ├── hsv-color-picker.ts ├── hsv-string-color-picker.ts ├── hsva-color-picker.ts ├── hsva-string-color-picker.ts ├── lib │ ├── components │ │ ├── alpha-color-picker.ts │ │ ├── alpha.ts │ │ ├── color-picker.ts │ │ ├── hue.ts │ │ ├── saturation.ts │ │ └── slider.ts │ ├── entrypoints │ │ ├── hex-alpha.ts │ │ ├── hex-input.ts │ │ ├── hex.ts │ │ ├── hsl-string.ts │ │ ├── hsl.ts │ │ ├── hsla-string.ts │ │ ├── hsla.ts │ │ ├── hsv-string.ts │ │ ├── hsv.ts │ │ ├── hsva-string.ts │ │ ├── hsva.ts │ │ ├── rgb-string.ts │ │ ├── rgb.ts │ │ ├── rgba-string.ts │ │ └── rgba.ts │ ├── styles │ │ ├── alpha.css │ │ ├── color-picker.css │ │ ├── hue.css │ │ └── saturation.css │ ├── types.ts │ └── utils │ │ ├── compare.ts │ │ ├── convert.ts │ │ ├── dom.ts │ │ ├── math.ts │ │ └── validate.ts ├── rgb-color-picker.ts ├── rgb-string-color-picker.ts ├── rgba-color-picker.ts ├── rgba-string-color-picker.ts └── test │ ├── a11y.test.ts │ ├── color-picker.test.ts │ ├── hex-input.test.ts │ ├── utils.test.ts │ └── visual │ ├── screenshots │ └── Chrome │ │ └── baseline │ │ ├── hex-alpha.png │ │ ├── hex.png │ │ ├── hsl-string.png │ │ ├── hsl.png │ │ ├── hsla-string.png │ │ ├── hsla.png │ │ ├── hsv-string.png │ │ ├── hsv.png │ │ ├── hsva-string.png │ │ ├── hsva.png │ │ ├── rgb-string.png │ │ ├── rgb.png │ │ ├── rgba-string.png │ │ └── rgba.png │ └── visual.test.ts ├── tsconfig.eslint.json ├── tsconfig.json └── web-test-runner.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@typescript-eslint/eslint-recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier" 6 | ], 7 | "plugins": ["@typescript-eslint", "prettier"], 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "sourceType": "module", 11 | "ecmaVersion": 2018, 12 | "project": "./tsconfig.eslint.json" 13 | }, 14 | "env": { 15 | "browser": true 16 | }, 17 | "rules": { 18 | "@typescript-eslint/no-unsafe-declaration-merging": "off" 19 | }, 20 | "overrides": [ 21 | { 22 | "files": [ "src/test/*.test.ts" ], 23 | "rules": { 24 | "@typescript-eslint/no-non-null-assertion": "off", 25 | "@typescript-eslint/no-unused-expressions": "off", 26 | "@typescript-eslint/ban-ts-comment": ["error", { 27 | "ts-expect-error": false 28 | }] 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # do not show lock file while doing git diff 2 | package-lock.json -diff 3 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 16 21 | 22 | - run: npm ci 23 | 24 | - run: npm run lint 25 | 26 | size: 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - uses: actions/checkout@v3 31 | 32 | - uses: actions/setup-node@v3 33 | with: 34 | node-version: 16 35 | 36 | - name: npm install 37 | run: npm ci 38 | 39 | - name: Size limit 40 | run: npm run size 41 | 42 | unit-tests: 43 | runs-on: ubuntu-latest 44 | 45 | steps: 46 | - uses: actions/checkout@v3 47 | 48 | - uses: actions/setup-node@v3 49 | with: 50 | node-version: 16 51 | 52 | - name: npm install 53 | run: npm ci 54 | 55 | - name: Unit tests 56 | run: npm run test 57 | 58 | visual-tests: 59 | runs-on: ubuntu-latest 60 | 61 | steps: 62 | - uses: actions/checkout@v3 63 | 64 | - uses: actions/setup-node@v3 65 | with: 66 | node-version: 16 67 | 68 | - name: npm install 69 | run: npm ci 70 | 71 | - name: Visual tests 72 | run: npm run test:visual 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /coverage 3 | /dist 4 | /lib 5 | /test/**/*.d.ts 6 | /test/**/*.d.ts.map 7 | /test/**/*.js 8 | /test/**/*.js.map 9 | /src/lib/styles/*.ts 10 | hex-input.js 11 | *-color-picker.js 12 | *.d.ts 13 | *.map 14 | custom-elements.json 15 | web-types.json 16 | web-types.lit.json 17 | .wireit 18 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 100, 4 | "trailingComma": "none" 5 | } 6 | -------------------------------------------------------------------------------- /.versionrc: -------------------------------------------------------------------------------- 1 | { 2 | "types": [ 3 | { "type": "feat", "section": "Features", "hidden": false }, 4 | { "type": "fix", "section": "Bug Fixes", "hidden": false }, 5 | { "type": "refactor", "section": "Internal Changes", "hidden": true } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /ACKNOWLEDGMENTS: -------------------------------------------------------------------------------- 1 | ## Acknowledgments 2 | 3 | - First of all, thanks to @omgovich who created the original `react-colorful` library (MIT License). 4 | - Special thanks @RyanChristian4427 who has rewritten the original library in TypeScript. 5 | - In order to avoid tree-shaking problems, and provide better TS support, `rgbToHex` and `hexToRgb` methods were copied from [@swiftcarrot/color-fns](https://github.com/swiftcarrot/color-fns) (MIT License). 6 | - `hsvToRgb` modified from https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c. 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.7.2](https://github.com/web-padawan/vanilla-colorful/compare/v0.7.1...v0.7.2) (2022-11-05) 6 | 7 | 8 | ### Features 9 | 10 | * add hex-alpha-color-picker component ([#81](https://github.com/web-padawan/vanilla-colorful/issues/81)) ([32c9c14](https://github.com/web-padawan/vanilla-colorful/commit/32c9c146bc1902ae9aff4eda184cbc60ee44d7c0)) 11 | * add prefixed property to hex-input ([#92](https://github.com/web-padawan/vanilla-colorful/issues/92)) ([45cdc57](https://github.com/web-padawan/vanilla-colorful/commit/45cdc57b4df5e1e3268c8da855d6337278c1bc7a)) 12 | * add support for alpha to hex-input ([#91](https://github.com/web-padawan/vanilla-colorful/issues/91)) ([f6a5d4c](https://github.com/web-padawan/vanilla-colorful/commit/f6a5d4cc61d1b17194d2ef9fa16d3a8e4d1a5261)) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * handle reattach and changing input ([#93](https://github.com/web-padawan/vanilla-colorful/issues/93)) ([901150b](https://github.com/web-padawan/vanilla-colorful/commit/901150bb2419ba16eb4cbb5aec0a3b0c31549419)) 18 | 19 | ### [0.7.1](https://github.com/web-padawan/vanilla-colorful/compare/v0.7.0...v0.7.1) (2022-07-29) 20 | 21 | 22 | ### Features 23 | 24 | * add Web Types support for IntelliJ ([#79](https://github.com/web-padawan/vanilla-colorful/issues/79)) ([787330d](https://github.com/web-padawan/vanilla-colorful/commit/787330d94fdb7b6590849eaf54f6c5dac109d6eb)) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * center pointer fill using flexbox ([#76](https://github.com/web-padawan/vanilla-colorful/issues/76)) ([8421a6e](https://github.com/web-padawan/vanilla-colorful/commit/8421a6e7aea802438cd9da1bbe40c3b7ed657854)) 30 | * do not restore value after clearing ([#77](https://github.com/web-padawan/vanilla-colorful/issues/77)) ([fefce0d](https://github.com/web-padawan/vanilla-colorful/commit/fefce0d1a36ff71bfc0e924427aa294bf673e8a5)) 31 | 32 | ## [0.7.0](https://github.com/web-padawan/vanilla-colorful/compare/v0.6.2...v0.7.0) (2022-07-02) 33 | 34 | 35 | ### ⚠ BREAKING CHANGES 36 | 37 | * do not fire event on setting color (#72) 38 | 39 | ### Features 40 | 41 | * set type in package.json to module ([#73](https://github.com/web-padawan/vanilla-colorful/issues/73)) ([f31219e](https://github.com/web-padawan/vanilla-colorful/commit/f31219e16d6641a029f2dafe82b868f08162f78a)) 42 | * use Custom Elements Manifest analyzer ([#74](https://github.com/web-padawan/vanilla-colorful/issues/74)) ([5422770](https://github.com/web-padawan/vanilla-colorful/commit/5422770e49d9c8a01595b013cb25427059c4200a)) 43 | 44 | 45 | ### Internal Changes 46 | 47 | * do not fire event on setting color ([#72](https://github.com/web-padawan/vanilla-colorful/issues/72)) ([de8ceeb](https://github.com/web-padawan/vanilla-colorful/commit/de8ceebc69beb6924da8496ddfd53ddac1362b47)) 48 | 49 | ### [0.6.2](https://github.com/web-padawan/vanilla-colorful/compare/v0.6.1...v0.6.2) (2021-08-07) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * focus the slider on mousedown ([#63](https://github.com/web-padawan/vanilla-colorful/issues/63)) ([602962c](https://github.com/web-padawan/vanilla-colorful/commit/602962cdb27056d8764a468425d66b254d12d940)) 55 | 56 | ### [0.6.1](https://github.com/web-padawan/vanilla-colorful/compare/v0.6.0...v0.6.1) (2021-05-31) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * resolve rounded corner rendering bug ([#54](https://github.com/web-padawan/vanilla-colorful/issues/54)) ([49c8a8e](https://github.com/web-padawan/vanilla-colorful/commit/49c8a8ebb8c7f678b4bc1376a01f7de606f60263)) 62 | 63 | ## [0.6.0](https://github.com/web-padawan/vanilla-colorful/compare/v0.5.3...v0.6.0) (2021-05-24) 64 | 65 | 66 | ### ⚠ BREAKING CHANGES 67 | 68 | * simplify internal elements (#25) 69 | 70 | ### Features 71 | 72 | * add color-changed event typings ([#31](https://github.com/web-padawan/vanilla-colorful/issues/31)) ([4776869](https://github.com/web-padawan/vanilla-colorful/commit/4776869d257fc180b1d47c86908e88cc9fc243b2)) 73 | * support modern CSS color notations ([#39](https://github.com/web-padawan/vanilla-colorful/issues/39)) ([db0eed3](https://github.com/web-padawan/vanilla-colorful/commit/db0eed3a4a986c263f8cf9fed17128af41440ace)) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * prevent hue from being reset ([#33](https://github.com/web-padawan/vanilla-colorful/issues/33)) ([02b1423](https://github.com/web-padawan/vanilla-colorful/commit/02b142380d1048b493f6628f7fb17d7aa96e8318)) 79 | 80 | 81 | ### Internal Changes 82 | 83 | * simplify internal elements ([#25](https://github.com/web-padawan/vanilla-colorful/issues/25)) ([0deca42](https://github.com/web-padawan/vanilla-colorful/commit/0deca42f6de5fef5831fcb584d4ae831e2dbc7a9)) 84 | 85 | ### [0.5.3](https://github.com/web-padawan/vanilla-colorful/compare/v0.5.2...v0.5.3) (2020-12-25) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * make color-changed event bubble ([#22](https://github.com/web-padawan/vanilla-colorful/issues/22)) ([574e032](https://github.com/web-padawan/vanilla-colorful/commit/574e032e68eeb6635c6eb2f402cd105568ed87f6)) 91 | 92 | ### [0.5.2](https://github.com/web-padawan/vanilla-colorful/compare/v0.5.1...v0.5.2) (2020-11-24) 93 | 94 | 95 | ### Bug Fixes 96 | 97 | * add hex-color-picker export ([#20](https://github.com/web-padawan/vanilla-colorful/issues/20)) ([968cf2e](https://github.com/web-padawan/vanilla-colorful/commit/968cf2e1ed4b78c16423fb0c47922c23a713eff4)) 98 | 99 | ### [0.5.1](https://github.com/web-padawan/vanilla-colorful/compare/v0.5.0...v0.5.1) (2020-10-22) 100 | 101 | 102 | ### Bug Fixes 103 | 104 | * round color output values ([a68c80a](https://github.com/web-padawan/vanilla-colorful/commit/a68c80ab923496e9268151db30bca136985b00f0)) 105 | 106 | ## [0.5.0](https://github.com/web-padawan/vanilla-colorful/compare/v0.4.0...v0.5.0) (2020-10-20) 107 | 108 | 109 | ### ⚠ BREAKING CHANGES 110 | 111 | * use Shadow DOM in hex-input (#16) 112 | 113 | ### Internal Changes 114 | 115 | * use Shadow DOM in hex-input ([#16](https://github.com/web-padawan/vanilla-colorful/issues/16)) ([2fefcef](https://github.com/web-padawan/vanilla-colorful/commit/2fefcefbf14f142525ca516c68f7c23a17169986)) 116 | 117 | ## [0.4.0](https://github.com/web-padawan/vanilla-colorful/compare/v0.3.1...v0.4.0) (2020-09-28) 118 | 119 | 120 | ### ⚠ BREAKING CHANGES 121 | 122 | * rename protected methods 123 | 124 | ### Features 125 | 126 | * implement accessibility support ([23805f5](https://github.com/web-padawan/vanilla-colorful/commit/23805f5789468dc4682d042ec53c4c37144ca831)) 127 | 128 | 129 | ### Internal Changes 130 | 131 | * hide internals with symbols ([0f1d7e8](https://github.com/web-padawan/vanilla-colorful/commit/0f1d7e8849802ff3a41d79d12b02a229bfa8c574)) 132 | 133 | ### [0.3.1](https://github.com/web-padawan/vanilla-colorful/compare/v0.3.0...v0.3.1) (2020-09-19) 134 | 135 | 136 | ### Bug Fixes 137 | 138 | * ignore mouse on touch devices ([0ea812f](https://github.com/web-padawan/vanilla-colorful/commit/0ea812fa3295932a46de9fea68755fe0e14825b7)) 139 | 140 | ## [0.3.0](https://github.com/web-padawan/vanilla-colorful/compare/v0.2.1...v0.3.0) (2020-09-15) 141 | 142 | 143 | ### ⚠ BREAKING CHANGES 144 | 145 | * update `ColorModel` to use `fromHsva` and `toHsva` instead of `fromHsv` and `toHsv` 146 | 147 | ### Features 148 | 149 | * add alpha color picker components ([248b216](https://github.com/web-padawan/vanilla-colorful/commit/248b216358607a3faab97d08f37224712da4e66b)) 150 | * hsv-string and hva-string pickers ([d067683](https://github.com/web-padawan/vanilla-colorful/commit/d0676839e9c8e0f2e98b9d1187b53400317e10e1)) 151 | 152 | ### [0.2.1](https://github.com/web-padawan/vanilla-colorful/compare/v0.2.0...v0.2.1) (2020-09-11) 153 | 154 | 155 | ### Bug Fixes 156 | 157 | * add hidden attribute styles ([0c490dd](https://github.com/web-padawan/vanilla-colorful/commit/0c490dd774279dd239544d5723a9ba5c5413331c)) 158 | 159 | ## [0.2.0](https://github.com/web-padawan/vanilla-colorful/compare/v0.1.2...v0.2.0) (2020-09-08) 160 | 161 | 162 | ### ⚠ BREAKING CHANGES 163 | 164 | * rename components and types (#5) 165 | 166 | ### Internal Changes 167 | 168 | * rename components and types ([#5](https://github.com/web-padawan/vanilla-colorful/issues/5)) ([52652e9](https://github.com/web-padawan/vanilla-colorful/commit/52652e94b4c7fa5bf1a0ec85b727c6487b716122)) 169 | 170 | ### [0.1.2](https://github.com/web-padawan/vanilla-colorful/compare/v0.1.1...v0.1.2) (2020-09-05) 171 | 172 | 173 | ### Bug Fixes 174 | 175 | * only fire event if color changes ([#4](https://github.com/web-padawan/vanilla-colorful/issues/4)) ([9440820](https://github.com/web-padawan/vanilla-colorful/commit/9440820baf838f68eda17e38eeccd1db29120693)) 176 | 177 | ### [0.1.1](https://github.com/web-padawan/vanilla-colorful/compare/v0.1.0...v0.1.1) (2020-09-03) 178 | 179 | 180 | ### Features 181 | 182 | * add base classes entrypoints ([#2](https://github.com/web-padawan/vanilla-colorful/issues/2)) ([31024ca](https://github.com/web-padawan/vanilla-colorful/commit/31024ca0be6adce4e4cdd9b4c5485cc4812559a6)) 183 | 184 | 185 | ### Bug Fixes 186 | 187 | * prevent exception in hex-input ([2c2b74c](https://github.com/web-padawan/vanilla-colorful/commit/2c2b74c501e12f42020c54236e42a11530ab0687)) 188 | * run internal setters properly ([#3](https://github.com/web-padawan/vanilla-colorful/issues/3)) ([fe32815](https://github.com/web-padawan/vanilla-colorful/commit/fe3281580d1587428e926fc685ab82f4499051c6)) 189 | 190 | ## 0.1.0 (2020-09-01) 191 | 192 | 193 | ### Features 194 | 195 | * add exports for relevant types ([69fac69](https://github.com/web-padawan/vanilla-colorful/commit/69fac69298089ffcc74152577f7822432aebb9b4)) 196 | * allow custom input in hex-input ([843d15d](https://github.com/web-padawan/vanilla-colorful/commit/843d15d5dc49aac93492270e6327ed66c8e4a17d)) 197 | * implement color picker component ([49bad73](https://github.com/web-padawan/vanilla-colorful/commit/49bad73f2c8b18fa8ac621d0c685dd02d4dfceea)) 198 | * implement hex-input component ([7d037a5](https://github.com/web-padawan/vanilla-colorful/commit/7d037a51b0aac4d53122c82ab8682e7243c9becd)) 199 | * implement hue and saturation ([379f82c](https://github.com/web-padawan/vanilla-colorful/commit/379f82c667c936e59296923bb9504750005b6b6a)) 200 | * use different pointers parts ([2aa7954](https://github.com/web-padawan/vanilla-colorful/commit/2aa7954098992c64e3fce9a4b9f3a86dedbdd954)) 201 | 202 | 203 | ### Bug Fixes 204 | 205 | * prevent selection in Safari ([43a2b2e](https://github.com/web-padawan/vanilla-colorful/commit/43a2b2e7daa5ebe435eef6bd40cd9b91ad4aafe4)) 206 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Serhii Kulykov 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Screenshot of the color picker 4 | 5 |
6 | 7 |
8 | 9 | npm 10 | 11 | 12 | build 13 | 14 | 15 | gzip size 16 | 17 | 18 | no dependencies 19 | 20 |
21 | 22 |
23 | vanilla-colorful is a port of react-colorful to vanilla Custom Elements. 24 |
25 | 26 | ## Features 27 | 28 | - 🗜 **Small**: Just 2,7 KB (minified and gzipped). [Size Limit](https://github.com/ai/size-limit) controls the size. 29 | - 🚀 **Fast**: Built with standards based Custom Elements. 30 | - 🛡 **Bulletproof**: Written in strict TypeScript and has 100% test coverage. 31 | - 🗂 **Typed**: Ships with [types included](#typescript-support). 32 | - 😍 **Simple**: The interface is straightforward and easy to use. 33 | - 💬 **Accessible**: Follows the [WAI-ARIA](https://www.w3.org/WAI/standards-guidelines/aria/) guidelines to support users of assistive technologies. 34 | - 📲 **Mobile-friendly**: Works well on mobile devices and touch screens. 35 | - 👫 **Framework-agnostic**: Can be used [with any framework](https://custom-elements-everywhere.com/). 36 | - 💨 **No dependencies** 37 | 38 | ## Live demos 39 | 40 | - [Website](https://web-padawan.github.io/vanilla-colorful/) 41 | - [Angular example](https://stackblitz.com/edit/angular-vanilla-colorful?file=src/app/example.component.ts) 42 | - [Lit example](https://lit.dev/playground/#project=W3sibmFtZSI6ImluZGV4Lmh0bWwiLCJjb250ZW50IjoiPCFET0NUWVBFIGh0bWw-XG48aGVhZD5cbiAgPHNjcmlwdCB0eXBlPVwibW9kdWxlXCIgc3JjPVwiLi9jb2xvci1leGFtcGxlLmpzXCI-PC9zY3JpcHQ-XG48L2hlYWQ-XG48Ym9keT5cbiAgPHN0eWxlPlxuICAgIC53cmFwcGVyIHtcbiAgICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xuICAgICAganVzdGlmeS1jb250ZW50OiBjZW50ZXI7XG4gICAgICBhbGlnbi1pdGVtczogY2VudGVyO1xuICAgICAgbWluLWhlaWdodDogMTAwdmg7XG4gICAgICBmb250LWZhbWlseTogLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCBcIlNlZ29lIFVJXCIsIFJvYm90bywgT3h5Z2VuLVNhbnMsIFVidW50dSwgQ2FudGFyZWxsLCBcIkhlbHZldGljYSBOZXVlXCIsIHNhbnMtc2VyaWY7ICAgIFxuICAgIH1cbiAgPC9zdHlsZT5cbiAgPGRpdiBjbGFzcz1cIndyYXBwZXJcIj5cbiAgICA8Y29sb3ItZXhhbXBsZT48L2NvbG9yLWV4YW1wbGU-XG4gIDwvZGl2PlxuPC9ib2R5PlxuIn0seyJuYW1lIjoiY29sb3ItZXhhbXBsZS5qcyIsImNvbnRlbnQiOiJpbXBvcnQgeyBMaXRFbGVtZW50LCBodG1sLCBjc3MgfSBmcm9tICdsaXQnO1xuaW1wb3J0ICd2YW5pbGxhLWNvbG9yZnVsJztcblxuZXhwb3J0IGNsYXNzIENvbG9yRXhhbXBsZSBleHRlbmRzIExpdEVsZW1lbnQge1xuICBzdGF0aWMgZ2V0IHByb3BlcnRpZXMoKSB7XG4gICAgcmV0dXJuIHtcbiAgICAgIGNvbG9yOiB7IHR5cGU6IFN0cmluZyB9LFxuICAgIH07XG4gIH1cblxuICBzdGF0aWMgZ2V0IHN0eWxlcygpIHtcbiAgICByZXR1cm4gY3NzYFxuICAgICAgb3V0cHV0IHtcbiAgICAgICAgZGlzcGxheTogYmxvY2s7XG4gICAgICAgIG1hcmdpbi10b3A6IDEwcHg7XG4gICAgICAgIGZvbnQtc2l6ZTogMS4yNXJlbTtcbiAgICAgICAgdGV4dC1hbGlnbjogY2VudGVyO1xuICAgICAgfVxuICAgIGA7XG4gIH1cblxuICBjb25zdHJ1Y3RvcigpIHtcbiAgICBzdXBlcigpO1xuICAgIHRoaXMuY29sb3IgPSAnIzFlODhlNSc7XG4gIH1cblxuICByZW5kZXIoKSB7XG4gICAgcmV0dXJuIGh0bWxgXG4gICAgICA8aGV4LWNvbG9yLXBpY2tlclxuICAgICAgICAuY29sb3I9XCIke3RoaXMuY29sb3J9XCJcbiAgICAgICAgQGNvbG9yLWNoYW5nZWQ9XCIke3RoaXMuaGFuZGxlQ29sb3JDaGFuZ2VkfVwiXG4gICAgICA-PC9oZXgtY29sb3ItcGlja2VyPlxuICAgICAgPG91dHB1dD4ke3RoaXMuY29sb3J9PC9vdXRwdXQ-XG4gICAgYDtcbiAgfVxuXG4gIGhhbmRsZUNvbG9yQ2hhbmdlZChldmVudCkge1xuICAgIHRoaXMuY29sb3IgPSBldmVudC5kZXRhaWwudmFsdWU7XG4gIH1cbn1cblxuY3VzdG9tRWxlbWVudHMuZGVmaW5lKCdjb2xvci1leGFtcGxlJywgQ29sb3JFeGFtcGxlKTtcbiJ9XQ) 43 | - [React example](https://components.studio/edit/dXQXpT6ggwihpoxPqioI) 44 | - [Svelte example](https://components.studio/edit/CpWY9ofL287dfvJaQJIA) 45 | - [Vue example](https://components.studio/edit/xACXVNs47cgdWFSafS70) 46 | 47 | ## Install 48 | 49 | ``` 50 | npm install vanilla-colorful --save 51 | ``` 52 | 53 | Or use one of the following content delivery networks: 54 | 55 | [unpkg.com CDN](https://unpkg.com/vanilla-colorful?module): 56 | 57 | ```html 58 | 59 | ``` 60 | 61 | [Skypack CDN](https://cdn.skypack.dev/vanilla-colorful): 62 | 63 | ```html 64 | 65 | ``` 66 | 67 | [JSPM CDN](https://jspm.org): 68 | 69 | ```html 70 | 71 | ``` 72 | 73 | [ESM CDN](https://esm.sh): 74 | 75 | ```html 76 | 77 | ``` 78 | 79 | ## Usage 80 | 81 | ```html 82 | 83 | 95 | ``` 96 | 97 | ## ES modules 98 | 99 | **vanilla-colorful** is authored using ES modules which are [natively supported](https://caniuse.com/es6-module) 100 | by modern browsers. However, all the code examples listed here use so-called "bare module specifiers": 101 | `import 'vanilla-colorful'`. 102 | 103 | There is now a feature in the HTML Standard called [import maps](https://html.spec.whatwg.org/multipage/webappapis.html#import-maps) 104 | that enables resolving bare module specifiers without requiring any tools. As of October 2022, import 105 | maps are not yet [shipped](https://caniuse.com/import-maps) in all browsers. 106 | 107 | In the meantime, we recommend using one of the tools that leverage ES modules based development, such as 108 | [`vite`](https://vitejs.dev), [`@web/dev-server`](https://modern-web.dev/docs/dev-server/overview/), 109 | or [`wmr`](https://www.npmjs.com/package/wmr). None of these tools are needed when importing from CDN. 110 | 111 | ## Supported color models 112 | 113 | The default **vanilla-colorful**'s input/output format is a HEX string (like `#ffffff`). In case if 114 | you need another color model, we provide 12 additional color picker bundles. 115 | 116 |
117 | How to use another color model 118 | 119 | #### Available pickers 120 | 121 | | File to import | HTML element | Value example | 122 | | ------------------------------- | ---------------------------- | ---------------------------------- | 123 | | `"hex-color-picker.js"` | `` | `"#ffffff"` | 124 | | `"hex-alpha-color-picker.js"` | `` | `"#ffffff88"` | 125 | | `"hsl-color-picker.js"` | `` | `{ h: 0, s: 0, l: 100 }` | 126 | | `"hsl-string-color-picker.js"` | `` | `"hsl(0, 0%, 100%)"` | 127 | | `"hsla-color-picker.js"` | `` | `{ h: 0, s: 0, l: 100, a: 1 }` | 128 | | `"hsla-string-color-picker.js"` | `` | `"hsla(0, 0%, 100%, 1)"` | 129 | | `"hsv-color-picker.js"` | `` | `{ h: 0, s: 0, v: 100 }` | 130 | | `"hsv-string-color-picker.js"` | `` | `"hsv(0, 0%, 100%)"` | 131 | | `"hsva-color-picker.js"` | `` | `{ h: 0, s: 0, v: 100, a: 1 }` | 132 | | `"hsva-string-color-picker.js"` | `` | `"hsva(0, 0%, 100%, 1)"` | 133 | | `"rgb-color-picker.js"` | `` | `{ r: 255, g: 255, b: 255 }` | 134 | | `"rgba-color-picker.js"` | `` | `{ r: 255, g: 255, b: 255, a: 1 }` | 135 | | `"rgb-string-color-picker.js"` | `` | `"rgb(255, 255, 255)"` | 136 | | `"rgba-string-color-picker.js"` | `` | `"rgba(255, 255, 255, 1)"` | 137 | 138 | #### Code example 139 | 140 | ```html 141 | 142 | 148 | ``` 149 | 150 |
151 | 152 | ## Overriding styles 153 | 154 | **vanilla-colorful** exposes [CSS Shadow Parts](https://developer.mozilla.org/en-US/docs/Web/CSS/::part) 155 | allowing to override the default styles: 156 | 157 | ```css 158 | hex-color-picker { 159 | height: 250px; 160 | } 161 | 162 | hex-color-picker::part(saturation) { 163 | bottom: 30px; 164 | border-radius: 3px 3px 0 0; 165 | } 166 | 167 | hex-color-picker::part(hue) { 168 | height: 30px; 169 | border-radius: 0 0 3px 3px; 170 | } 171 | 172 | hex-color-picker::part(saturation-pointer) { 173 | border-radius: 5px; 174 | } 175 | 176 | hex-color-picker::part(hue-pointer) { 177 | border-radius: 2px; 178 | width: 15px; 179 | height: inherit; 180 | } 181 | ``` 182 | 183 | ## HEX input 184 | 185 | **vanilla-colorful** provides an additional `` element that can be used to type a color: 186 | 187 | ```html 188 | 189 | 197 | ``` 198 | 199 | `` renders an unstyled `` element inside a slot and exposes it for styling using 200 | `part`. You can also pass your own `` element as a child if you want to fully configure it. 201 | 202 | In addition to `color` property, `` supports the following boolean properties: 203 | 204 | | Property | Default | Description | 205 | | ---------- | ------- | -------------------------------------------- | 206 | | `alpha` | `false` | Allows `#rgba` and `#rrggbbaa` color formats | 207 | | `prefixed` | `false` | Enables `#` prefix displaying | 208 | 209 | ## Base classes 210 | 211 | **vanilla-colorful** provides a set of base classes that can be imported without registering custom 212 | elements. This is useful if you want to create your own color picker with a different tag name. 213 | 214 | ```js 215 | import { RgbBase } from 'vanilla-colorful/lib/entrypoints/rgb.js'; 216 | 217 | customElements.define('custom-color-picker', class extends RgbBase {}); 218 | ``` 219 | 220 | ## Code Recipes 221 | 222 | - [Custom styles and layout](https://webcomponents.dev/edit/VRYGVWFu1LIQGN7aXO54/www/styles.css) 223 | - [Prevent flash of unstyled content](https://webcomponents.dev/edit/NpMKtEifbhOKOw91El9Z/www/index.html) 224 | - [Prevent flash of unstyled content (picker with alpha)](https://webcomponents.dev/edit/D3XAGOGkyc7yMVyWefkl/www/index.html) 225 | - [Text field to be able to type/copy/paste a color](https://webcomponents.dev/edit/zz7e9YqnJsqtmdkWL9GI/www/index.html) 226 | 227 | ## TypeScript support 228 | 229 | **vanilla-colorful** supports TypeScript and ships with types in the library itself; no need for any other install. 230 | 231 |
232 | How you can get the most from our TypeScript support 233 | 234 | ### Custom types 235 | 236 | While not only typing its own class methods and variables, it can also help you type yours. Depending on 237 | the element you are using, you can also import the type that is associated with the element. 238 | For example, if you are using our `` element, you can also import the `HslColor` type. 239 | 240 | ```ts 241 | import type { HslColor } from 'vanilla-colorful/hsl-color-picker'; 242 | 243 | const myHslValue: HslColor = { h: 0, s: 0, l: 0 }; 244 | ``` 245 | 246 | ### Typed events 247 | 248 | All the included custom elements provide overrides for `addEventListener` and `removeEventListener` methods 249 | to include typings for the `color-changed` custom event `detail` property: 250 | 251 | ```ts 252 | const picker = document.querySelector('rgba-color-picker'); 253 | 254 | picker.addEventListener('color-changed', (event) => { 255 | console.log(event.detail.value.a); // (property) RgbaColor.a: number 256 | }); 257 | ``` 258 | 259 | ### Lit plugin 260 | 261 | All the included custom elements are compatible with [lit-analyzer](https://www.npmjs.com/package/lit-analyzer) and 262 | [lit-plugin](https://marketplace.visualstudio.com/items?itemName=runem.lit-plugin) extension for Visual 263 | Studio Code, so you can benefit from type checking in [Lit](https://lit.dev) templates, for example 264 | [validating binding names](https://github.com/runem/lit-analyzer/blob/master/docs/readme/rules.md#validating-binding-names). 265 | 266 |
267 | 268 | ## Browser support 269 | 270 | **vanilla-colorful** uses [Custom Elements](https://caniuse.com/#feat=custom-elementsv1) and [Shadow DOM](https://caniuse.com/#feat=shadowdomv1), 271 | and does not support IE11 or legacy Edge. 272 | 273 | ## Why vanilla-colorful? 274 | 275 | **vanilla-colorful** has all the benefits of [react-colorful](https://github.com/omgovich/react-colorful#why-react-colorful) 276 | with one important difference. 277 | 278 | While `react-colorful` claims to have zero dependencies, it still expects you to use React or [Preact](https://github.com/omgovich/react-colorful#usage-with-preact). 279 | This means that Angular, Vue, Svelte or vanilla JS users would have an **extra** dependency in their apps. 280 | 281 | Now when all the evergreen browsers support standards based [Custom Elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements), 282 | it's perfect time to build such tiny and lightweight UI controls as web components rather than framework components. 283 | -------------------------------------------------------------------------------- /demo/assets/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-padawan/vanilla-colorful/b81455e7d1cb81979d2cf6dd151147766062fa79/demo/assets/og.png -------------------------------------------------------------------------------- /demo/favicon.js: -------------------------------------------------------------------------------- 1 | import { throttle } from '../node_modules/throttle-debounce/esm/index.js'; 2 | 3 | const ICON_SIZE = 64; 4 | const OUTLINE_RADIUS = 28; 5 | const POINTER_RADIUS = 22; 6 | 7 | const createCanvas = () => { 8 | const canvas = document.createElement('canvas'); 9 | canvas.width = ICON_SIZE; 10 | canvas.height = ICON_SIZE; 11 | return canvas; 12 | }; 13 | 14 | const createBackgroundCanvas = () => { 15 | const canvas = createCanvas(); 16 | const ctx = canvas.getContext('2d'); 17 | 18 | ctx.beginPath(); 19 | ctx.arc(ICON_SIZE * 0.5, ICON_SIZE * 0.5, OUTLINE_RADIUS, 0, 2 * Math.PI, false); 20 | ctx.closePath(); 21 | 22 | ctx.shadowColor = 'rgba(0, 0, 0, 0.4)'; 23 | ctx.shadowOffsetY = 1; 24 | ctx.shadowBlur = 6; 25 | ctx.fillStyle = '#fff'; 26 | ctx.fill(); 27 | 28 | return canvas; 29 | }; 30 | 31 | let faviconNode; 32 | let canvas; 33 | let backgroundCanvas; 34 | 35 | // generate a favicon only once on mobiles in order to improve performance 36 | const shouldReplace = () => { 37 | if (window.innerWidth < 768 && faviconNode) return false; 38 | return true; 39 | }; 40 | 41 | export const setFaviconColor = (color) => { 42 | // create canvases only once 43 | if (!canvas) { 44 | canvas = createCanvas(); 45 | backgroundCanvas = createBackgroundCanvas(); 46 | } 47 | 48 | return throttle(500, () => { 49 | if (shouldReplace()) { 50 | // draw a new favicon and replace `link` tag 51 | const ctx = canvas.getContext('2d'); 52 | 53 | // wipe out the canvas 54 | ctx.clearRect(0, 0, ICON_SIZE, ICON_SIZE); 55 | 56 | // draw the cached background 57 | ctx.drawImage(backgroundCanvas, 0, 0); 58 | 59 | // draw a pointer 60 | ctx.beginPath(); 61 | ctx.arc(ICON_SIZE * 0.5, ICON_SIZE * 0.5, POINTER_RADIUS, 0, 2 * Math.PI, false); 62 | ctx.closePath(); 63 | ctx.fillStyle = color; 64 | ctx.fill(); 65 | 66 | // create a new favicon tag 67 | const link = document.createElement('link'); 68 | link.rel = 'shortcut icon'; 69 | link.href = canvas.toDataURL('image/x-icon'); 70 | 71 | // remove the old favicon from the document 72 | if (faviconNode) document.head.removeChild(faviconNode); 73 | 74 | document.head.appendChild(link); 75 | faviconNode = link; 76 | } 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /demo/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | border: none; 6 | font: inherit; 7 | } 8 | 9 | body { 10 | background-color: #1e88e5; 11 | color: #fff; 12 | font: normal 20px/1.4 "Recursive", Arial, Helvetica, sans-serif; 13 | } 14 | 15 | .wrapper { 16 | transition: color 0.15s; 17 | } 18 | 19 | .demo { 20 | position: relative; 21 | width: 200px; 22 | flex-shrink: 0; 23 | } 24 | 25 | rgba-color-picker:not(:defined) { 26 | display: block; 27 | position: relative; 28 | height: 200px; 29 | width: 200px; 30 | border-radius: 9px; 31 | box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2); 32 | background-position: 33 | top 0px left 0px, 34 | top 0px left 0px, 35 | top 140px left 0px, 36 | top 152px left 0px; 37 | background-size: 200px 140px, 200px 140px, 200px 12px, 200px 24px; 38 | background-repeat: no-repeat; 39 | background-color: rgb(30, 136, 229); 40 | background-image: 41 | linear-gradient(to top, #000, rgba(0, 0, 0, 0)), 42 | linear-gradient(to right, #fff, rgba(255, 255, 255, 0)), 43 | linear-gradient(to right, #000, #000), 44 | linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%); 45 | } 46 | 47 | rgba-color-picker:not(:defined)::after { 48 | display: block; 49 | content: ""; 50 | position: absolute; 51 | border-radius: 0 0 9px 9px; 52 | top: 176px; 53 | width: 200px; 54 | height: 24px; 55 | background-image: linear-gradient(to right, rgba(102, 51, 153, 0), rgba(30, 136, 229, 1)); 56 | } 57 | 58 | rgba-color-picker:not(:defined)::before { 59 | display: block; 60 | content: ""; 61 | position: absolute; 62 | border-radius: 0 0 9px 9px; 63 | top: 176px; 64 | width: 200px; 65 | height: 24px; 66 | background-color: #fff; 67 | background-image: url('data:image/svg+xml,'); 68 | } 69 | 70 | rgba-color-picker { 71 | border-radius: 9px; 72 | box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2); 73 | } 74 | 75 | .header { 76 | display: flex; 77 | align-items: center; 78 | width: 100%; 79 | max-width: 720px; 80 | margin: 0 auto; 81 | min-height: 100vh; 82 | padding: 120px 32px; 83 | } 84 | 85 | .header-content { 86 | flex-grow: 1; 87 | margin-left: 40px; 88 | } 89 | 90 | .header-title { 91 | margin-bottom: 16px; 92 | font-size: 44px; 93 | line-height: 1; 94 | } 95 | 96 | .header-description { 97 | max-width: 18em; 98 | } 99 | 100 | .links { 101 | margin-top: 24px; 102 | } 103 | 104 | a { 105 | color: inherit; 106 | text-decoration: none; 107 | border-bottom: 2px solid currentColor; 108 | opacity: 0.8; 109 | } 110 | 111 | a:hover { 112 | opacity: 1; 113 | } 114 | 115 | .links a { 116 | margin-right: 16px; 117 | } 118 | 119 | @media (max-width: 767px) { 120 | .header { 121 | max-width: 360px; 122 | padding: 100px 20px 0; 123 | flex-direction: column; 124 | } 125 | 126 | .header-content { 127 | text-align: center; 128 | margin-top: 40px; 129 | margin-left: 0; 130 | } 131 | 132 | .header-title { 133 | font-size: 32px; 134 | } 135 | 136 | .header-description { 137 | margin-left: auto; 138 | margin-right: auto; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vanilla-colorful 6 | 10 | 11 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 |
26 |
27 |
28 | 29 |
30 |
31 |

Vanilla Colorful 🎨

32 |

33 | A tiny framework agnostic color picker. Port of 34 | React Colorful 37 | to vanilla Custom Elements. 38 |

39 | 62 |
63 |
64 |
65 | 66 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-colorful", 3 | "version": "0.7.2", 4 | "description": "A tiny framework agnostic color picker element for modern web apps", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/web-padawan/vanilla-colorful.git" 9 | }, 10 | "author": "Serhii Kulykov ", 11 | "homepage": "https://web-padawan.github.io/vanilla-colorful/", 12 | "bugs": { 13 | "url": "https://github.com/web-padawan/vanilla-colorful/issues" 14 | }, 15 | "main": "hex-color-picker.js", 16 | "module": "hex-color-picker.js", 17 | "type": "module", 18 | "exports": { 19 | ".": "./hex-color-picker.js", 20 | "./hex-alpha-color-picker.js": "./hex-alpha-color-picker.js", 21 | "./hex-color-picker.js": "./hex-color-picker.js", 22 | "./hex-input.js": "./hex-input.js", 23 | "./hsl-color-picker.js": "./hsl-color-picker.js", 24 | "./hsl-string-color-picker.js": "./hsl-string-color-picker.js", 25 | "./hsla-color-picker.js": "./hsla-color-picker.js", 26 | "./hsla-string-color-picker.js": "./hsla-string-color-picker.js", 27 | "./hsv-color-picker.js": "./hsv-color-picker.js", 28 | "./hsv-string-color-picker.js": "./hsv-string-color-picker.js", 29 | "./hsva-color-picker.js": "./hsva-color-picker.js", 30 | "./hsva-string-color-picker.js": "./hsva-string-color-picker.js", 31 | "./rgb-color-picker.js": "./rgb-color-picker.js", 32 | "./rgb-string-color-picker.js": "./rgb-string-color-picker.js", 33 | "./rgba-color-picker.js": "./rgba-color-picker.js", 34 | "./rgba-string-color-picker.js": "./rgba-string-color-picker.js", 35 | "./lib/entrypoints/*": "./lib/entrypoints/*.js", 36 | "./package.json": "./package.json" 37 | }, 38 | "scripts": { 39 | "analyze": "wireit", 40 | "build": "wireit", 41 | "clean": "wireit", 42 | "deploy": "wireit", 43 | "dev": "wireit", 44 | "dist": "wireit", 45 | "generate:types": "wireit", 46 | "lint": "wireit", 47 | "prepack": "npm run generate:types", 48 | "release": "standard-version", 49 | "serve": "wireit", 50 | "size": "wireit", 51 | "start": "wireit", 52 | "styles": "wireit", 53 | "test": "wireit", 54 | "test:update": "wireit", 55 | "test:visual": "wireit", 56 | "watch": "wireit" 57 | }, 58 | "files": [ 59 | "*-color-picker.js", 60 | "*.d.ts", 61 | "*.d.ts.map", 62 | "*.js.map", 63 | "/lib", 64 | "ACKNOWLEDGMENTS", 65 | "custom-elements.json", 66 | "hex-input.js", 67 | "web-types.json", 68 | "web-types.lit.json" 69 | ], 70 | "keywords": [ 71 | "webcomponents", 72 | "web-components", 73 | "webcomponent", 74 | "web-component", 75 | "custom-element", 76 | "customelement", 77 | "colorpicker", 78 | "hex", 79 | "color", 80 | "color-picker", 81 | "accessible", 82 | "accessibility", 83 | "aria", 84 | "a11y", 85 | "wai-aria" 86 | ], 87 | "devDependencies": { 88 | "@custom-elements-manifest/analyzer": "^0.10.3", 89 | "@open-wc/testing-helpers": "^3.0.1", 90 | "@rollup/plugin-node-resolve": "^15.2.3", 91 | "@rollup/plugin-terser": "^0.4.4", 92 | "@size-limit/preset-small-lib": "^11.1.4", 93 | "@types/chai": "^4.3.19", 94 | "@types/mocha": "^10.0.7", 95 | "@types/sinon": "^17.0.3", 96 | "@typescript-eslint/eslint-plugin": "^8.4.0", 97 | "@typescript-eslint/parser": "^8.4.0", 98 | "@web/dev-server": "^0.4.6", 99 | "@web/dev-server-esbuild": "^1.0.2", 100 | "@web/rollup-plugin-html": "^2.3.0", 101 | "@web/test-runner": "^0.18.3", 102 | "@web/test-runner-commands": "^0.9.0", 103 | "@web/test-runner-visual-regression": "^0.9.0", 104 | "chai": "^5.1.1", 105 | "csso": "^5.0.5", 106 | "eslint": "^8.57.0", 107 | "eslint-config-prettier": "^9.1.0", 108 | "eslint-plugin-prettier": "^5.2.1", 109 | "gh-pages": "^6.1.1", 110 | "glob": "^10.4.5", 111 | "lint-staged": "^15.2.10", 112 | "lit-html": "^3.2.0", 113 | "prettier": "^3.3.3", 114 | "rimraf": "^5.0.1", 115 | "rollup": "^4.21.2", 116 | "simple-git-hooks": "^2.11.1", 117 | "sinon": "^18.0.0", 118 | "size-limit": "^11.1.4", 119 | "standard-version": "^9.5.0", 120 | "throttle-debounce": "^5.0.2", 121 | "tsc-watch": "^6.2.0", 122 | "typescript": "^5.5.4", 123 | "wireit": "^0.14.9" 124 | }, 125 | "types": "hex-color-picker.d.ts", 126 | "customElements": "custom-elements.json", 127 | "lint-staged": { 128 | "*.ts": [ 129 | "eslint --fix", 130 | "prettier --write" 131 | ] 132 | }, 133 | "sideEffects": [ 134 | "hex-alpha-color-picker.js", 135 | "hex-color-picker.js", 136 | "hex-input.js", 137 | "hsl-color-picker.js", 138 | "hsl-string-color-picker.js", 139 | "hsla-color-picker.js", 140 | "hsla-string-color-picker.js", 141 | "hsv-color-picker.js", 142 | "hsv-string-color-picker.js", 143 | "hsva-color-picker.js", 144 | "hsva-string-color-picker.js", 145 | "rgb-color-picker.js", 146 | "rgb-string-color-picker.js", 147 | "rgba-color-picker.js", 148 | "rgba-string-color-picker.js" 149 | ], 150 | "simple-git-hooks": { 151 | "pre-commit": "npx lint-staged" 152 | }, 153 | "size-limit": [ 154 | { 155 | "path": "hex-alpha-color-picker.js", 156 | "limit": "3.15 KB" 157 | }, 158 | { 159 | "path": "hex-color-picker.js", 160 | "limit": "2.8 KB" 161 | }, 162 | { 163 | "path": "hex-input.js", 164 | "limit": "1.1 KB" 165 | }, 166 | { 167 | "path": "hsl-color-picker.js", 168 | "limit": "2.5 KB" 169 | }, 170 | { 171 | "path": "hsl-string-color-picker.js", 172 | "limit": "2.6 KB" 173 | }, 174 | { 175 | "path": "hsla-color-picker.js", 176 | "limit": "2.8 KB" 177 | }, 178 | { 179 | "path": "hsla-string-color-picker.js", 180 | "limit": "2.95 KB" 181 | }, 182 | { 183 | "path": "hsv-color-picker.js", 184 | "limit": "2.5 KB" 185 | }, 186 | { 187 | "path": "hsv-string-color-picker.js", 188 | "limit": "2.6 KB" 189 | }, 190 | { 191 | "path": "hsva-color-picker.js", 192 | "limit": "2.75 KB" 193 | }, 194 | { 195 | "path": "hsva-string-color-picker.js", 196 | "limit": "2.95 KB" 197 | }, 198 | { 199 | "path": "rgb-color-picker.js", 200 | "limit": "2.6 KB" 201 | }, 202 | { 203 | "path": "rgb-string-color-picker.js", 204 | "limit": "2.8 KB" 205 | }, 206 | { 207 | "path": "rgba-color-picker.js", 208 | "limit": "2.9 KB" 209 | }, 210 | { 211 | "path": "rgba-string-color-picker.js", 212 | "limit": "3.1 KB" 213 | } 214 | ], 215 | "web-types": [ 216 | "web-types.json", 217 | "web-types.lit.json" 218 | ], 219 | "wireit": { 220 | "analyze": { 221 | "command": "cem analyze --globs '*.js' 'lib/components/*.js' 'lib/entrypoints/*.js'", 222 | "dependencies": [ 223 | "build" 224 | ] 225 | }, 226 | "build": { 227 | "command": "tsc", 228 | "dependencies": [ 229 | "styles" 230 | ] 231 | }, 232 | "clean": { 233 | "command": "rimraf dist" 234 | }, 235 | "deploy": { 236 | "command": "gh-pages -d dist", 237 | "dependencies": [ 238 | "dist" 239 | ] 240 | }, 241 | "dev": { 242 | "command": "web-dev-server --node-resolve --open", 243 | "dependencies": [ 244 | "watch" 245 | ] 246 | }, 247 | "dist": { 248 | "command": "rollup -c", 249 | "dependencies": [ 250 | "clean", 251 | "build" 252 | ] 253 | }, 254 | "generate:types": { 255 | "command": "node ./scripts/build-web-types.cjs", 256 | "dependencies": [ 257 | "analyze" 258 | ] 259 | }, 260 | "lint": { 261 | "command": "eslint . --ext .ts --ext .js --ignore-path .gitignore" 262 | }, 263 | "size": { 264 | "command": "size-limit", 265 | "dependencies": [ 266 | "build" 267 | ] 268 | }, 269 | "start": { 270 | "command": "web-dev-server --app-index dist/index.html --open" 271 | }, 272 | "styles": { 273 | "command": "node ./scripts/build-styles.cjs" 274 | }, 275 | "test": { 276 | "command": "wtr src/test/*.ts --coverage", 277 | "dependencies": [ 278 | "build" 279 | ] 280 | }, 281 | "test:update": { 282 | "command": "UPDATE_REFS=true wtr src/test/visual/*.ts", 283 | "dependencies": [ 284 | "build" 285 | ] 286 | }, 287 | "test:visual": { 288 | "command": "wtr src/test/visual/*.ts", 289 | "dependencies": [ 290 | "build" 291 | ] 292 | }, 293 | "watch": { 294 | "command": "tsc-watch", 295 | "service": true 296 | } 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | import terser from '@rollup/plugin-terser'; 3 | import { rollupPluginHTML as html } from '@web/rollup-plugin-html'; 4 | 5 | export default { 6 | input: './index.html', 7 | output: { 8 | dir: './dist' 9 | }, 10 | plugins: [ 11 | html(), 12 | nodeResolve(), 13 | terser() 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-padawan/vanilla-colorful/b81455e7d1cb81979d2cf6dd151147766062fa79/screenshot.png -------------------------------------------------------------------------------- /scripts/build-styles.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { globSync } = require('glob'); 3 | const util = require('util'); 4 | 5 | const csso = require('csso'); 6 | 7 | const writeFile = util.promisify(fs.writeFile); 8 | const readFile = util.promisify(fs.readFile); 9 | 10 | const delimiter = /<%\s*content\s*%>/; 11 | 12 | async function minifyCss(cssFile) { 13 | const data = await readFile(cssFile, 'utf8'); 14 | const result = csso.minify(data); 15 | return result.css; 16 | } 17 | 18 | const template = 'export default `<% content %>`;\n'; 19 | 20 | async function processFile(sourceFile) { 21 | const replacement = await minifyCss(sourceFile); 22 | const newContent = template.replace(delimiter, replacement); 23 | const outputFile = sourceFile.replace('.css', '.ts'); 24 | return writeFile(outputFile, newContent, 'utf-8'); 25 | } 26 | 27 | const files = globSync('./src/lib/styles/*.css'); 28 | files.forEach((file) => { 29 | processFile(file).catch((error) => { 30 | console.error(error); 31 | process.exit(-1); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /scripts/build-web-types.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | const PLAIN_WEB_TYPES_FILE = 'web-types.json'; 5 | const LIT_WEB_TYPES_FILE = 'web-types.lit.json'; 6 | 7 | function loadAnalysis() { 8 | const analysisPath = path.resolve('./custom-elements.json'); 9 | try { 10 | return JSON.parse(fs.readFileSync(analysisPath, 'utf8')); 11 | } catch (e) { 12 | throw new Error( 13 | `Could not find "custom-elements.json". Make sure to run the CEM Analyzer before generating web-types.` 14 | ); 15 | } 16 | } 17 | 18 | function getElementData(moduleInfo) { 19 | const elementInfo = moduleInfo.declarations[0]; 20 | 21 | return { 22 | name: moduleInfo.exports.find((entry) => entry.kind === 'custom-element-definition').name, 23 | description: elementInfo.description, 24 | properties: elementInfo.members.filter((member) => member.kind === 'field' && !!member.type), 25 | attributes: elementInfo.attributes.filter((attribute) => !!attribute.type), 26 | events: elementInfo.events 27 | }; 28 | } 29 | 30 | function createPlainElementDefinition(elementData) { 31 | const attributes = elementData.attributes.map((attribute) => ({ 32 | name: attribute.name, 33 | description: attribute.description, 34 | value: { 35 | type: [attribute.type.text] 36 | } 37 | })); 38 | 39 | const properties = elementData.properties.map((prop) => ({ 40 | name: prop.name, 41 | description: prop.description, 42 | value: { 43 | type: [prop.type.text] 44 | } 45 | })); 46 | 47 | const events = elementData.events.map((event) => ({ 48 | name: event.name, 49 | description: event.description 50 | })); 51 | 52 | return { 53 | name: elementData.name, 54 | description: elementData.description, 55 | attributes, 56 | js: { 57 | properties, 58 | events 59 | } 60 | }; 61 | } 62 | 63 | function createPlainWebTypes(packageJson, packageModules) { 64 | return { 65 | $schema: 'https://json.schemastore.org/web-types', 66 | name: packageJson.name, 67 | version: packageJson.version, 68 | 'description-markup': 'markdown', 69 | contributions: { 70 | html: { 71 | elements: packageModules.map((entry) => createPlainElementDefinition(getElementData(entry))) 72 | } 73 | } 74 | }; 75 | } 76 | 77 | function createLitElementDefinition(elementData) { 78 | const propertyAttributes = elementData.properties.map((prop) => ({ 79 | name: `.${prop.name}`, 80 | description: prop.description, 81 | value: { 82 | kind: 'expression' 83 | } 84 | })); 85 | 86 | const eventAttributes = elementData.events.map((event) => ({ 87 | name: `@${event.name}`, 88 | description: event.description, 89 | value: { 90 | kind: 'expression' 91 | } 92 | })); 93 | 94 | return { 95 | name: elementData.name, 96 | description: elementData.description, 97 | // Declare as extension to plain web type, this also means we don't have to 98 | // repeat the same stuff from the plain web-types.json again 99 | extension: true, 100 | // IntelliJ does not understand Lit template syntax, so 101 | // effectively everything has to be declared as attribute 102 | attributes: [...propertyAttributes, ...eventAttributes] 103 | }; 104 | } 105 | 106 | function createLitWebTypes(packageJson, modules) { 107 | return { 108 | $schema: 'https://json.schemastore.org/web-types', 109 | name: packageJson.name, 110 | version: packageJson.version, 111 | 'description-markup': 'markdown', 112 | framework: 'lit', 113 | 'framework-config': { 114 | 'enable-when': { 115 | 'node-packages': ['lit'] 116 | } 117 | }, 118 | contributions: { 119 | html: { 120 | elements: modules.map((entry) => createLitElementDefinition(getElementData(entry))) 121 | } 122 | } 123 | }; 124 | } 125 | 126 | /** 127 | * Create Web-Types definitions to enable IntelliJ IDEA code completion. 128 | * The definitions are split into two files, one containing "plain" types 129 | * for the web component, including attributes, properties and events. 130 | * The other file contains Lit-specific bindings, to bind properties, 131 | * attributes and events through their respective Lit attribute syntax. 132 | */ 133 | function buildWebTypes() { 134 | const analysis = loadAnalysis(); 135 | 136 | const packageJson = JSON.parse(fs.readFileSync(`./package.json`, 'utf8')); 137 | const entrypoints = analysis.modules.filter((el) => !el.path.startsWith('lib')); 138 | 139 | const plainWebTypes = createPlainWebTypes(packageJson, entrypoints); 140 | const plainWebTypesJson = JSON.stringify(plainWebTypes, null, 2); 141 | fs.writeFileSync(PLAIN_WEB_TYPES_FILE, plainWebTypesJson, 'utf8'); 142 | 143 | const litWebTypes = createLitWebTypes(packageJson, entrypoints); 144 | const litWebTypesJson = JSON.stringify(litWebTypes, null, 2); 145 | fs.writeFileSync(path.join(LIT_WEB_TYPES_FILE), litWebTypesJson, 'utf8'); 146 | } 147 | 148 | buildWebTypes(); 149 | -------------------------------------------------------------------------------- /src/hex-alpha-color-picker.ts: -------------------------------------------------------------------------------- 1 | import { HexAlphaBase } from './lib/entrypoints/hex-alpha.js'; 2 | 3 | /** 4 | * A color picker custom element that uses HEX format with alpha. 5 | * 6 | * @element hex-alpha-color-picker 7 | * 8 | * @prop {string} color - Selected color in HEX format. 9 | * @attr {string} color - Selected color in HEX format. 10 | * 11 | * @fires color-changed - Event fired when color property changes. 12 | * 13 | * @csspart hue - A hue selector container. 14 | * @csspart saturation - A saturation selector container 15 | * @csspart alpha - An alpha selector container. 16 | * @csspart hue-pointer - A hue pointer element. 17 | * @csspart saturation-pointer - A saturation pointer element. 18 | * @csspart alpha-pointer - An alpha pointer element. 19 | */ 20 | export class HexAlphaColorPicker extends HexAlphaBase {} 21 | 22 | customElements.define('hex-alpha-color-picker', HexAlphaColorPicker); 23 | 24 | declare global { 25 | interface HTMLElementTagNameMap { 26 | 'hex-alpha-color-picker': HexAlphaColorPicker; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/hex-color-picker.ts: -------------------------------------------------------------------------------- 1 | import { HexBase } from './lib/entrypoints/hex.js'; 2 | 3 | /** 4 | * A color picker custom element that uses HEX format. 5 | * 6 | * @element hex-color-picker 7 | * 8 | * @prop {string} color - Selected color in HEX format. 9 | * @attr {string} color - Selected color in HEX format. 10 | * 11 | * @fires color-changed - Event fired when color property changes. 12 | * 13 | * @csspart hue - A hue selector container. 14 | * @csspart saturation - A saturation selector container 15 | * @csspart hue-pointer - A hue pointer element. 16 | * @csspart saturation-pointer - A saturation pointer element. 17 | */ 18 | export class HexColorPicker extends HexBase {} 19 | 20 | customElements.define('hex-color-picker', HexColorPicker); 21 | 22 | declare global { 23 | interface HTMLElementTagNameMap { 24 | 'hex-color-picker': HexColorPicker; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/hex-input.ts: -------------------------------------------------------------------------------- 1 | import { HexInputBase } from './lib/entrypoints/hex-input.js'; 2 | 3 | /** 4 | * A custom element for entering color in HEX format. 5 | * 6 | * @element hex-input 7 | * 8 | * @prop {string} color - Color in HEX format. 9 | * @attr {string} color - Selected color in HEX format. 10 | * @prop {boolean} alpha - When true, `#rgba` and `#rrggbbaa` color formats are allowed. 11 | * @attr {boolean} alpha - Allows `#rgba` and `#rrggbbaa` color formats. 12 | * @prop {boolean} prefixed - When true, `#` prefix is displayed in the input. 13 | * @attr {boolean} prefixed - Enables `#` prefix displaying. 14 | * 15 | * @fires color-changed - Event fired when color is changed. 16 | * 17 | * @csspart input - A native input element. 18 | */ 19 | export class HexInput extends HexInputBase {} 20 | 21 | customElements.define('hex-input', HexInput); 22 | 23 | declare global { 24 | interface HTMLElementTagNameMap { 25 | 'hex-input': HexInput; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/hsl-color-picker.ts: -------------------------------------------------------------------------------- 1 | import { HslBase } from './lib/entrypoints/hsl.js'; 2 | export type { HslColor } from './lib/types'; 3 | 4 | /** 5 | * A color picker custom element that uses HSL object format. 6 | * 7 | * @element hsl-color-picker 8 | * 9 | * @prop {HslColor} color - Selected color in HSL object format. 10 | * 11 | * @fires color-changed - Event fired when color property changes. 12 | * 13 | * @csspart hue - A hue selector container. 14 | * @csspart saturation - A saturation selector container 15 | * @csspart hue-pointer - A hue pointer element. 16 | * @csspart saturation-pointer - A saturation pointer element. 17 | */ 18 | export class HslColorPicker extends HslBase {} 19 | 20 | customElements.define('hsl-color-picker', HslColorPicker); 21 | 22 | declare global { 23 | interface HTMLElementTagNameMap { 24 | 'hsl-color-picker': HslColorPicker; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/hsl-string-color-picker.ts: -------------------------------------------------------------------------------- 1 | import { HslStringBase } from './lib/entrypoints/hsl-string.js'; 2 | 3 | /** 4 | * A color picker custom element that uses HSL string format. 5 | * 6 | * @element hsl-string-color-picker 7 | * 8 | * @prop {string} color - Selected color in HSL string format. 9 | * @attr {string} color - Selected color in HSL string format. 10 | * 11 | * @fires color-changed - Event fired when color property changes. 12 | * 13 | * @csspart hue - A hue selector container. 14 | * @csspart saturation - A saturation selector container 15 | * @csspart hue-pointer - A hue pointer element. 16 | * @csspart saturation-pointer - A saturation pointer element. 17 | */ 18 | export class HslStringColorPicker extends HslStringBase {} 19 | 20 | customElements.define('hsl-string-color-picker', HslStringColorPicker); 21 | 22 | declare global { 23 | interface HTMLElementTagNameMap { 24 | 'hsl-string-color-picker': HslStringColorPicker; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/hsla-color-picker.ts: -------------------------------------------------------------------------------- 1 | import { HslaBase } from './lib/entrypoints/hsla.js'; 2 | export type { HslaColor } from './lib/types'; 3 | 4 | /** 5 | * A color picker custom element that uses HSLA object format. 6 | * 7 | * @element hsla-color-picker 8 | * 9 | * @prop {HslaColor} color - Selected color in HSLA object format. 10 | * 11 | * @fires color-changed - Event fired when color property changes. 12 | * 13 | * @csspart hue - A hue selector container. 14 | * @csspart saturation - A saturation selector container 15 | * @csspart alpha - An alpha selector container. 16 | * @csspart hue-pointer - A hue pointer element. 17 | * @csspart saturation-pointer - A saturation pointer element. 18 | * @csspart alpha-pointer - An alpha pointer element. 19 | */ 20 | export class HslaColorPicker extends HslaBase {} 21 | 22 | customElements.define('hsla-color-picker', HslaColorPicker); 23 | 24 | declare global { 25 | interface HTMLElementTagNameMap { 26 | 'hsla-color-picker': HslaColorPicker; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/hsla-string-color-picker.ts: -------------------------------------------------------------------------------- 1 | import { HslaStringBase } from './lib/entrypoints/hsla-string.js'; 2 | 3 | /** 4 | * A color picker custom element that uses HSLA string format. 5 | * 6 | * @element hsla-string-color-picker 7 | * 8 | * @prop {string} color - Selected color in HSLA string format. 9 | * @attr {string} color - Selected color in HSLA string format. 10 | * 11 | * @fires color-changed - Event fired when color property changes. 12 | * 13 | * @csspart hue - A hue selector container. 14 | * @csspart saturation - A saturation selector container 15 | * @csspart alpha - An alpha selector container. 16 | * @csspart hue-pointer - A hue pointer element. 17 | * @csspart saturation-pointer - A saturation pointer element. 18 | * @csspart alpha-pointer - An alpha pointer element. 19 | */ 20 | export class HslaStringColorPicker extends HslaStringBase {} 21 | 22 | customElements.define('hsla-string-color-picker', HslaStringColorPicker); 23 | 24 | declare global { 25 | interface HTMLElementTagNameMap { 26 | 'hsla-string-color-picker': HslaStringColorPicker; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/hsv-color-picker.ts: -------------------------------------------------------------------------------- 1 | import { HsvBase } from './lib/entrypoints/hsv.js'; 2 | export type { HsvColor } from './lib/types'; 3 | 4 | /** 5 | * A color picker custom element that uses HSV object format. 6 | * 7 | * @element hsv-color-picker 8 | * 9 | * @prop {HsvColor} color - Selected color in HSV object format. 10 | * 11 | * @fires color-changed - Event fired when color property changes. 12 | * 13 | * @csspart hue - A hue selector container. 14 | * @csspart saturation - A saturation selector container 15 | * @csspart hue-pointer - A hue pointer element. 16 | * @csspart saturation-pointer - A saturation pointer element. 17 | */ 18 | export class HsvColorPicker extends HsvBase {} 19 | 20 | customElements.define('hsv-color-picker', HsvColorPicker); 21 | 22 | declare global { 23 | interface HTMLElementTagNameMap { 24 | 'hsv-color-picker': HsvColorPicker; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/hsv-string-color-picker.ts: -------------------------------------------------------------------------------- 1 | import { HsvStringBase } from './lib/entrypoints/hsv-string.js'; 2 | 3 | /** 4 | * A color picker custom element that uses HSV string format. 5 | * 6 | * @element hsv-string-color-picker 7 | * 8 | * @prop {string} color - Selected color in HSV string format. 9 | * @attr {string} color - Selected color in HSV string format. 10 | * 11 | * @fires color-changed - Event fired when color property changes. 12 | * 13 | * @csspart hue - A hue selector container. 14 | * @csspart saturation - A saturation selector container 15 | * @csspart hue-pointer - A hue pointer element. 16 | * @csspart saturation-pointer - A saturation pointer element. 17 | */ 18 | export class HsvStringColorPicker extends HsvStringBase {} 19 | 20 | customElements.define('hsv-string-color-picker', HsvStringColorPicker); 21 | 22 | declare global { 23 | interface HTMLElementTagNameMap { 24 | 'hsv-string-color-picker': HsvStringColorPicker; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/hsva-color-picker.ts: -------------------------------------------------------------------------------- 1 | import { HsvaBase } from './lib/entrypoints/hsva.js'; 2 | export type { HsvaColor } from './lib/types'; 3 | 4 | /** 5 | * A color picker custom element that uses HSVA object format. 6 | * 7 | * @element hsva-color-picker 8 | * 9 | * @prop {HsvaColor} color - Selected color in HSVA object format. 10 | * 11 | * @fires color-changed - Event fired when color property changes. 12 | * 13 | * @csspart hue - A hue selector container. 14 | * @csspart saturation - A saturation selector container 15 | * @csspart alpha - An alpha selector container. 16 | * @csspart hue-pointer - A hue pointer element. 17 | * @csspart saturation-pointer - A saturation pointer element. 18 | * @csspart alpha-pointer - An alpha pointer element. 19 | */ 20 | export class HsvaColorPicker extends HsvaBase {} 21 | 22 | customElements.define('hsva-color-picker', HsvaColorPicker); 23 | 24 | declare global { 25 | interface HTMLElementTagNameMap { 26 | 'hsva-color-picker': HsvaColorPicker; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/hsva-string-color-picker.ts: -------------------------------------------------------------------------------- 1 | import { HsvaStringBase } from './lib/entrypoints/hsva-string.js'; 2 | 3 | /** 4 | * A color picker custom element that uses HSVA string format. 5 | * 6 | * @element hsva-string-color-picker 7 | * 8 | * @prop {string} color - Selected color in HSVA string format. 9 | * @attr {string} color - Selected color in HSVA string format. 10 | * 11 | * @fires color-changed - Event fired when color property changes. 12 | * 13 | * @csspart hue - A hue selector container. 14 | * @csspart saturation - A saturation selector container 15 | * @csspart alpha - An alpha selector container. 16 | * @csspart hue-pointer - A hue pointer element. 17 | * @csspart saturation-pointer - A saturation pointer element. 18 | * @csspart alpha-pointer - An alpha pointer element. 19 | */ 20 | export class HsvaStringColorPicker extends HsvaStringBase {} 21 | 22 | customElements.define('hsva-string-color-picker', HsvaStringColorPicker); 23 | 24 | declare global { 25 | interface HTMLElementTagNameMap { 26 | 'hsva-string-color-picker': HsvaStringColorPicker; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/components/alpha-color-picker.ts: -------------------------------------------------------------------------------- 1 | import { ColorPicker, Sliders, $css, $sliders } from './color-picker.js'; 2 | import type { AnyColor } from '../types'; 3 | import { Alpha } from './alpha.js'; 4 | import alphaCss from '../styles/alpha.js'; 5 | 6 | export abstract class AlphaColorPicker extends ColorPicker { 7 | protected get [$css](): string[] { 8 | return [...super[$css], alphaCss]; 9 | } 10 | 11 | protected get [$sliders](): Sliders { 12 | return [...super[$sliders], Alpha]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/components/alpha.ts: -------------------------------------------------------------------------------- 1 | import { Slider, Offset } from './slider.js'; 2 | import { hsvaToHslaString } from '../utils/convert.js'; 3 | import { clamp, round } from '../utils/math.js'; 4 | import type { HsvaColor } from '../types'; 5 | 6 | export class Alpha extends Slider { 7 | declare hsva: HsvaColor; 8 | 9 | constructor(root: ShadowRoot) { 10 | super(root, 'alpha', 'aria-label="Alpha" aria-valuemin="0" aria-valuemax="1"', false); 11 | } 12 | 13 | update(hsva: HsvaColor): void { 14 | this.hsva = hsva; 15 | const colorFrom = hsvaToHslaString({ ...hsva, a: 0 }); 16 | const colorTo = hsvaToHslaString({ ...hsva, a: 1 }); 17 | const value = hsva.a * 100; 18 | 19 | this.style([ 20 | { 21 | left: `${value}%`, 22 | color: hsvaToHslaString(hsva) 23 | }, 24 | { 25 | '--gradient': `linear-gradient(90deg, ${colorFrom}, ${colorTo}` 26 | } 27 | ]); 28 | 29 | const v = round(value); 30 | this.el.setAttribute('aria-valuenow', `${v}`); 31 | this.el.setAttribute('aria-valuetext', `${v}%`); 32 | } 33 | 34 | getMove(offset: Offset, key?: boolean): Record { 35 | // Alpha always fit into [0, 1] range 36 | return { a: key ? clamp(this.hsva.a + offset.x) : offset.x }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/components/color-picker.ts: -------------------------------------------------------------------------------- 1 | import { equalColorObjects } from '../utils/compare.js'; 2 | import { fire, render } from '../utils/dom.js'; 3 | import type { AnyColor, ColorModel, HsvaColor } from '../types'; 4 | import { Hue } from './hue.js'; 5 | import { Saturation } from './saturation.js'; 6 | import type { Slider } from './slider.js'; 7 | import css from '../styles/color-picker.js'; 8 | import hueCss from '../styles/hue.js'; 9 | import saturationCss from '../styles/saturation.js'; 10 | 11 | const $isSame = Symbol('same'); 12 | const $color = Symbol('color'); 13 | const $hsva = Symbol('hsva'); 14 | const $update = Symbol('update'); 15 | const $parts = Symbol('parts'); 16 | 17 | export const $css = Symbol('css'); 18 | export const $sliders = Symbol('sliders'); 19 | 20 | export type Sliders = Array Slider>; 21 | 22 | export abstract class ColorPicker extends HTMLElement { 23 | static get observedAttributes(): string[] { 24 | return ['color']; 25 | } 26 | 27 | protected get [$css](): string[] { 28 | return [css, hueCss, saturationCss]; 29 | } 30 | 31 | protected get [$sliders](): Sliders { 32 | return [Saturation, Hue]; 33 | } 34 | 35 | protected abstract get colorModel(): ColorModel; 36 | 37 | private declare [$hsva]: HsvaColor; 38 | 39 | private declare [$color]: C; 40 | 41 | private declare [$parts]: Slider[]; 42 | 43 | get color(): C { 44 | return this[$color]; 45 | } 46 | 47 | set color(newColor: C) { 48 | if (!this[$isSame](newColor)) { 49 | const newHsva = this.colorModel.toHsva(newColor); 50 | this[$update](newHsva); 51 | this[$color] = newColor; 52 | } 53 | } 54 | 55 | constructor() { 56 | super(); 57 | const root = this.attachShadow({ mode: 'open' }); 58 | render(root, ``); 59 | root.addEventListener('move', this); 60 | this[$parts] = this[$sliders].map((slider) => new slider(root)); 61 | } 62 | 63 | connectedCallback(): void { 64 | // A user may set a property on an _instance_ of an element, 65 | // before its prototype has been connected to this class. 66 | // If so, we need to run it through the proper class setter. 67 | if (this.hasOwnProperty('color')) { 68 | const value = this.color; 69 | delete this['color' as keyof this]; 70 | this.color = value; 71 | } else if (!this.color) { 72 | this.color = this.colorModel.defaultColor; 73 | } 74 | } 75 | 76 | attributeChangedCallback(_attr: string, _oldVal: string, newVal: string): void { 77 | const color = this.colorModel.fromAttr(newVal); 78 | if (!this[$isSame](color)) { 79 | this.color = color; 80 | } 81 | } 82 | 83 | handleEvent(event: CustomEvent): void { 84 | // Merge the current HSV color object with updated params. 85 | const oldHsva = this[$hsva]; 86 | const newHsva = { ...oldHsva, ...event.detail }; 87 | this[$update](newHsva); 88 | let newColor; 89 | if ( 90 | !equalColorObjects(newHsva, oldHsva) && 91 | !this[$isSame]((newColor = this.colorModel.fromHsva(newHsva))) 92 | ) { 93 | this[$color] = newColor; 94 | fire(this, 'color-changed', { value: newColor }); 95 | } 96 | } 97 | 98 | private [$isSame](color: C): boolean { 99 | return this.color && this.colorModel.equal(color, this.color); 100 | } 101 | 102 | private [$update](hsva: HsvaColor): void { 103 | this[$hsva] = hsva; 104 | this[$parts].forEach((part) => part.update(hsva)); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/lib/components/hue.ts: -------------------------------------------------------------------------------- 1 | import { Slider, Offset } from './slider.js'; 2 | import { hsvaToHslString } from '../utils/convert.js'; 3 | import { clamp, round } from '../utils/math.js'; 4 | import type { HsvaColor } from '../types'; 5 | 6 | export class Hue extends Slider { 7 | declare h: number; 8 | 9 | constructor(root: ShadowRoot) { 10 | super(root, 'hue', 'aria-label="Hue" aria-valuemin="0" aria-valuemax="360"', false); 11 | } 12 | 13 | update({ h }: HsvaColor): void { 14 | this.h = h; 15 | this.style([ 16 | { 17 | left: `${(h / 360) * 100}%`, 18 | color: hsvaToHslString({ h, s: 100, v: 100, a: 1 }) 19 | } 20 | ]); 21 | this.el.setAttribute('aria-valuenow', `${round(h)}`); 22 | } 23 | 24 | getMove(offset: Offset, key?: boolean): Record { 25 | // Hue measured in degrees of the color circle ranging from 0 to 360 26 | return { h: key ? clamp(this.h + offset.x * 360, 0, 360) : 360 * offset.x }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/components/saturation.ts: -------------------------------------------------------------------------------- 1 | import { Slider, Offset } from './slider.js'; 2 | import { hsvaToHslString } from '../utils/convert.js'; 3 | import { clamp, round } from '../utils/math.js'; 4 | import type { HsvaColor } from '../types'; 5 | 6 | export class Saturation extends Slider { 7 | declare hsva: HsvaColor; 8 | 9 | constructor(root: ShadowRoot) { 10 | super(root, 'saturation', 'aria-label="Color"', true); 11 | } 12 | 13 | update(hsva: HsvaColor): void { 14 | this.hsva = hsva; 15 | this.style([ 16 | { 17 | top: `${100 - hsva.v}%`, 18 | left: `${hsva.s}%`, 19 | color: hsvaToHslString(hsva) 20 | }, 21 | { 22 | 'background-color': hsvaToHslString({ h: hsva.h, s: 100, v: 100, a: 1 }) 23 | } 24 | ]); 25 | this.el.setAttribute( 26 | 'aria-valuetext', 27 | `Saturation ${round(hsva.s)}%, Brightness ${round(hsva.v)}%` 28 | ); 29 | } 30 | 31 | getMove(offset: Offset, key?: boolean): Record { 32 | // Saturation and brightness always fit into [0, 100] range 33 | return { 34 | s: key ? clamp(this.hsva.s + offset.x * 100, 0, 100) : offset.x * 100, 35 | v: key ? clamp(this.hsva.v - offset.y * 100, 0, 100) : Math.round(100 - offset.y * 100) 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/components/slider.ts: -------------------------------------------------------------------------------- 1 | import type { HsvaColor } from '../types.js'; 2 | import { fire, render } from '../utils/dom.js'; 3 | import { clamp } from '../utils/math.js'; 4 | 5 | export interface Offset { 6 | x: number; 7 | y: number; 8 | } 9 | 10 | let hasTouched = false; 11 | 12 | // Check if an event was triggered by touch 13 | const isTouch = (e: Event): e is TouchEvent => 'touches' in e; 14 | 15 | // Prevent mobile browsers from handling mouse events (conflicting with touch ones). 16 | // If we detected a touch interaction before, we prefer reacting to touch events only. 17 | const isValid = (event: Event): boolean => { 18 | if (hasTouched && !isTouch(event)) return false; 19 | if (!hasTouched) hasTouched = isTouch(event); 20 | return true; 21 | }; 22 | 23 | const pointerMove = (target: Slider, event: Event): void => { 24 | const pointer = isTouch(event) ? event.touches[0] : (event as MouseEvent); 25 | const rect = target.el.getBoundingClientRect(); 26 | 27 | fire( 28 | target.el, 29 | 'move', 30 | target.getMove({ 31 | x: clamp((pointer.pageX - (rect.left + window.pageXOffset)) / rect.width), 32 | y: clamp((pointer.pageY - (rect.top + window.pageYOffset)) / rect.height) 33 | }) 34 | ); 35 | }; 36 | 37 | const keyMove = (target: Slider, event: KeyboardEvent): void => { 38 | // We use `keyCode` instead of `key` to reduce the size of the library. 39 | const keyCode = event.keyCode; 40 | // Ignore all keys except arrow ones, Page Up, Page Down, Home and End. 41 | if (keyCode > 40 || (target.xy && keyCode < 37) || keyCode < 33) return; 42 | // Do not scroll page by keys when color picker element has focus. 43 | event.preventDefault(); 44 | // Send relative offset to the parent component. 45 | fire( 46 | target.el, 47 | 'move', 48 | target.getMove( 49 | { 50 | x: 51 | keyCode === 39 // Arrow Right 52 | ? 0.01 53 | : keyCode === 37 // Arrow Left 54 | ? -0.01 55 | : keyCode === 34 // Page Down 56 | ? 0.05 57 | : keyCode === 33 // Page Up 58 | ? -0.05 59 | : keyCode === 35 // End 60 | ? 1 61 | : keyCode === 36 // Home 62 | ? -1 63 | : 0, 64 | y: 65 | keyCode === 40 // Arrow down 66 | ? 0.01 67 | : keyCode === 38 // Arrow Up 68 | ? -0.01 69 | : 0 70 | }, 71 | true 72 | ) 73 | ); 74 | }; 75 | 76 | export abstract class Slider { 77 | declare nodes: HTMLElement[]; 78 | 79 | declare el: HTMLElement; 80 | 81 | declare xy: boolean; 82 | 83 | constructor(root: ShadowRoot, part: string, aria: string, xy: boolean) { 84 | render( 85 | root, 86 | `
` 87 | ); 88 | 89 | const el = root.querySelector(`[part=${part}]`) as HTMLElement; 90 | el.addEventListener('mousedown', this); 91 | el.addEventListener('touchstart', this); 92 | el.addEventListener('keydown', this); 93 | this.el = el; 94 | 95 | this.xy = xy; 96 | this.nodes = [el.firstChild as HTMLElement, el]; 97 | } 98 | 99 | set dragging(state: boolean) { 100 | const toggleEvent = state ? document.addEventListener : document.removeEventListener; 101 | toggleEvent(hasTouched ? 'touchmove' : 'mousemove', this); 102 | toggleEvent(hasTouched ? 'touchend' : 'mouseup', this); 103 | } 104 | 105 | handleEvent(event: Event): void { 106 | switch (event.type) { 107 | case 'mousedown': 108 | case 'touchstart': 109 | event.preventDefault(); 110 | // event.button is 0 in mousedown for left button activation 111 | if (!isValid(event) || (!hasTouched && (event as MouseEvent).button != 0)) return; 112 | this.el.focus(); 113 | pointerMove(this, event); 114 | this.dragging = true; 115 | break; 116 | case 'mousemove': 117 | case 'touchmove': 118 | event.preventDefault(); 119 | pointerMove(this, event); 120 | break; 121 | case 'mouseup': 122 | case 'touchend': 123 | this.dragging = false; 124 | break; 125 | case 'keydown': 126 | keyMove(this, event as KeyboardEvent); 127 | break; 128 | } 129 | } 130 | 131 | abstract getMove(offset: Offset, key?: boolean): Record; 132 | 133 | abstract update(hsva: HsvaColor): void; 134 | 135 | style(styles: Array>): void { 136 | styles.forEach((style, i) => { 137 | for (const p in style) { 138 | this.nodes[i].style.setProperty(p, style[p]); 139 | } 140 | }); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/lib/entrypoints/hex-alpha.ts: -------------------------------------------------------------------------------- 1 | import { AlphaColorPicker } from '../components/alpha-color-picker.js'; 2 | import type { ColorModel, ColorPickerEventListener, ColorPickerEventMap } from '../types'; 3 | import { hexToHsva, hsvaToHex } from '../utils/convert.js'; 4 | import { equalHex } from '../utils/compare.js'; 5 | 6 | const colorModel: ColorModel = { 7 | defaultColor: '#0001', 8 | toHsva: hexToHsva, 9 | fromHsva: hsvaToHex, 10 | equal: equalHex, 11 | fromAttr: (color) => color 12 | }; 13 | 14 | export interface HexAlphaBase { 15 | addEventListener>( 16 | type: T, 17 | listener: ColorPickerEventListener[T]>, 18 | options?: boolean | AddEventListenerOptions 19 | ): void; 20 | 21 | removeEventListener>( 22 | type: T, 23 | listener: ColorPickerEventListener[T]>, 24 | options?: boolean | EventListenerOptions 25 | ): void; 26 | } 27 | 28 | export class HexAlphaBase extends AlphaColorPicker { 29 | protected get colorModel(): ColorModel { 30 | return colorModel; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/entrypoints/hex-input.ts: -------------------------------------------------------------------------------- 1 | import type { ColorPickerEventListener, ColorPickerEventMap } from '../types'; 2 | import { validHex } from '../utils/validate.js'; 3 | import { render } from '../utils/dom.js'; 4 | 5 | // Escapes all non-hexadecimal characters including "#" 6 | const escape = (hex: string, alpha: boolean) => 7 | hex.replace(/([^0-9A-F]+)/gi, '').substring(0, alpha ? 8 : 6); 8 | 9 | const $alpha = Symbol('alpha'); 10 | const $color = Symbol('color'); 11 | const $saved = Symbol('saved'); 12 | const $input = Symbol('input'); 13 | const $init = Symbol('init'); 14 | const $prefix = Symbol('prefix'); 15 | const $update = Symbol('update'); 16 | 17 | export interface HexInputBase { 18 | addEventListener>( 19 | type: T, 20 | listener: ColorPickerEventListener[T]>, 21 | options?: boolean | AddEventListenerOptions 22 | ): void; 23 | 24 | removeEventListener>( 25 | type: T, 26 | listener: ColorPickerEventListener[T]>, 27 | options?: boolean | EventListenerOptions 28 | ): void; 29 | } 30 | 31 | export class HexInputBase extends HTMLElement { 32 | static get observedAttributes(): string[] { 33 | return ['alpha', 'color', 'prefixed']; 34 | } 35 | 36 | private declare [$color]: string; 37 | 38 | private declare [$alpha]: boolean; 39 | 40 | private declare [$prefix]: boolean; 41 | 42 | private declare [$saved]: string; 43 | 44 | private declare [$input]: HTMLInputElement; 45 | 46 | get color(): string { 47 | return this[$color]; 48 | } 49 | 50 | set color(hex: string) { 51 | this[$color] = hex; 52 | this[$update](hex); 53 | } 54 | 55 | get alpha(): boolean { 56 | return this[$alpha]; 57 | } 58 | 59 | set alpha(alpha: boolean) { 60 | this[$alpha] = alpha; 61 | this.toggleAttribute('alpha', alpha); 62 | 63 | // When alpha set to false, update color 64 | const color = this.color; 65 | if (color && !validHex(color, alpha)) { 66 | this.color = color.startsWith('#') 67 | ? color.substring(0, color.length === 5 ? 4 : 7) 68 | : color.substring(0, color.length === 4 ? 3 : 6); 69 | } 70 | } 71 | 72 | get prefixed(): boolean { 73 | return this[$prefix]; 74 | } 75 | 76 | set prefixed(prefixed: boolean) { 77 | this[$prefix] = prefixed; 78 | this.toggleAttribute('prefixed', prefixed); 79 | this[$update](this.color); 80 | } 81 | 82 | constructor() { 83 | super(); 84 | 85 | const root = this.attachShadow({ mode: 'open' }); 86 | render(root, ''); 87 | const slot = root.firstElementChild as HTMLSlotElement; 88 | slot.addEventListener('slotchange', () => this[$init](root)); 89 | } 90 | 91 | connectedCallback(): void { 92 | this[$init](this.shadowRoot as ShadowRoot); 93 | 94 | // A user may set a property on an _instance_ of an element, 95 | // before its prototype has been connected to this class. 96 | // If so, we need to run it through the proper class setter. 97 | if (this.hasOwnProperty('alpha')) { 98 | const value = this.alpha; 99 | delete this['alpha' as keyof this]; 100 | this.alpha = value; 101 | } else { 102 | this.alpha = this.hasAttribute('alpha'); 103 | } 104 | 105 | if (this.hasOwnProperty('prefixed')) { 106 | const value = this.prefixed; 107 | delete this['prefixed' as keyof this]; 108 | this.prefixed = value; 109 | } else { 110 | this.prefixed = this.hasAttribute('prefixed'); 111 | } 112 | 113 | if (this.hasOwnProperty('color')) { 114 | const value = this.color; 115 | delete this['color' as keyof this]; 116 | this.color = value; 117 | } else if (this.color == null) { 118 | this.color = this.getAttribute('color') || ''; 119 | } else if (this[$color]) { 120 | this[$update](this[$color]); 121 | } 122 | } 123 | 124 | handleEvent(event: Event): void { 125 | const target = event.target as HTMLInputElement; 126 | const { value } = target; 127 | switch (event.type) { 128 | case 'input': 129 | const hex = escape(value, this.alpha); 130 | this[$saved] = this.color; 131 | if (validHex(hex, this.alpha) || value === '') { 132 | this.color = hex; 133 | this.dispatchEvent( 134 | new CustomEvent('color-changed', { 135 | bubbles: true, 136 | detail: { value: hex ? '#' + hex : '' } 137 | }) 138 | ); 139 | } 140 | break; 141 | case 'blur': 142 | if (value && !validHex(value, this.alpha)) { 143 | this.color = this[$saved]; 144 | } 145 | } 146 | } 147 | 148 | attributeChangedCallback(attr: string, _oldVal: string, newVal: string): void { 149 | if (attr === 'color' && this.color !== newVal) { 150 | this.color = newVal; 151 | } 152 | 153 | const hasBooleanAttr = newVal != null; 154 | if (attr === 'alpha') { 155 | if (this.alpha !== hasBooleanAttr) { 156 | this.alpha = hasBooleanAttr; 157 | } 158 | } 159 | 160 | if (attr === 'prefixed') { 161 | if (this.prefixed !== hasBooleanAttr) { 162 | this.prefixed = hasBooleanAttr; 163 | } 164 | } 165 | } 166 | 167 | private [$init](root: ShadowRoot): void { 168 | let input = this.querySelector('input'); 169 | if (!input) { 170 | // remove all child node if no input found 171 | let c; 172 | while ((c = this.firstChild)) { 173 | c.remove(); 174 | } 175 | 176 | input = root.querySelector('input') as HTMLInputElement; 177 | } 178 | input.addEventListener('input', this); 179 | input.addEventListener('blur', this); 180 | this[$input] = input; 181 | this[$update](this.color); 182 | } 183 | 184 | private [$update](hex: string): void { 185 | if (this[$input]) { 186 | this[$input].value = 187 | hex == null || hex == '' ? '' : (this.prefixed ? '#' : '') + escape(hex, this.alpha); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/lib/entrypoints/hex.ts: -------------------------------------------------------------------------------- 1 | import type { ColorModel, ColorPickerEventListener, ColorPickerEventMap } from '../types'; 2 | import { ColorPicker } from '../components/color-picker.js'; 3 | import { hexToHsva, hsvaToHex } from '../utils/convert.js'; 4 | import { equalHex } from '../utils/compare.js'; 5 | 6 | const colorModel: ColorModel = { 7 | defaultColor: '#000', 8 | toHsva: hexToHsva, 9 | fromHsva: ({ h, s, v }) => hsvaToHex({ h, s, v, a: 1 }), 10 | equal: equalHex, 11 | fromAttr: (color) => color 12 | }; 13 | 14 | export interface HexBase { 15 | addEventListener>( 16 | type: T, 17 | listener: ColorPickerEventListener[T]>, 18 | options?: boolean | AddEventListenerOptions 19 | ): void; 20 | 21 | removeEventListener>( 22 | type: T, 23 | listener: ColorPickerEventListener[T]>, 24 | options?: boolean | EventListenerOptions 25 | ): void; 26 | } 27 | 28 | export class HexBase extends ColorPicker { 29 | protected get colorModel(): ColorModel { 30 | return colorModel; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/entrypoints/hsl-string.ts: -------------------------------------------------------------------------------- 1 | import type { ColorModel, ColorPickerEventListener, ColorPickerEventMap } from '../types'; 2 | import { ColorPicker } from '../components/color-picker.js'; 3 | import { hslStringToHsva, hsvaToHslString } from '../utils/convert.js'; 4 | import { equalColorString } from '../utils/compare.js'; 5 | 6 | const colorModel: ColorModel = { 7 | defaultColor: 'hsl(0, 0%, 0%)', 8 | toHsva: hslStringToHsva, 9 | fromHsva: hsvaToHslString, 10 | equal: equalColorString, 11 | fromAttr: (color) => color 12 | }; 13 | 14 | export interface HslStringBase { 15 | addEventListener>( 16 | type: T, 17 | listener: ColorPickerEventListener[T]>, 18 | options?: boolean | AddEventListenerOptions 19 | ): void; 20 | 21 | removeEventListener>( 22 | type: T, 23 | listener: ColorPickerEventListener[T]>, 24 | options?: boolean | EventListenerOptions 25 | ): void; 26 | } 27 | 28 | export class HslStringBase extends ColorPicker { 29 | protected get colorModel(): ColorModel { 30 | return colorModel; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/entrypoints/hsl.ts: -------------------------------------------------------------------------------- 1 | import type { ColorModel, ColorPickerEventListener, ColorPickerEventMap, HslColor } from '../types'; 2 | import { ColorPicker } from '../components/color-picker.js'; 3 | import { hslaToHsva, hsvaToHsla, hslaToHsl } from '../utils/convert.js'; 4 | import { equalColorObjects } from '../utils/compare.js'; 5 | 6 | const colorModel: ColorModel = { 7 | defaultColor: { h: 0, s: 0, l: 0 }, 8 | toHsva: ({ h, s, l }) => hslaToHsva({ h, s, l, a: 1 }), 9 | fromHsva: (hsva) => hslaToHsl(hsvaToHsla(hsva)), 10 | equal: equalColorObjects, 11 | fromAttr: (color) => JSON.parse(color) 12 | }; 13 | 14 | export interface HslBase { 15 | addEventListener>( 16 | type: T, 17 | listener: ColorPickerEventListener[T]>, 18 | options?: boolean | AddEventListenerOptions 19 | ): void; 20 | 21 | removeEventListener>( 22 | type: T, 23 | listener: ColorPickerEventListener[T]>, 24 | options?: boolean | EventListenerOptions 25 | ): void; 26 | } 27 | 28 | export class HslBase extends ColorPicker { 29 | protected get colorModel(): ColorModel { 30 | return colorModel; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/entrypoints/hsla-string.ts: -------------------------------------------------------------------------------- 1 | import type { ColorModel, ColorPickerEventListener, ColorPickerEventMap } from '../types'; 2 | import { AlphaColorPicker } from '../components/alpha-color-picker.js'; 3 | import { hslaStringToHsva, hsvaToHslaString } from '../utils/convert.js'; 4 | import { equalColorString } from '../utils/compare.js'; 5 | 6 | const colorModel: ColorModel = { 7 | defaultColor: 'hsla(0, 0%, 0%, 1)', 8 | toHsva: hslaStringToHsva, 9 | fromHsva: hsvaToHslaString, 10 | equal: equalColorString, 11 | fromAttr: (color) => color 12 | }; 13 | 14 | export interface HslaStringBase { 15 | addEventListener>( 16 | type: T, 17 | listener: ColorPickerEventListener[T]>, 18 | options?: boolean | AddEventListenerOptions 19 | ): void; 20 | 21 | removeEventListener>( 22 | type: T, 23 | listener: ColorPickerEventListener[T]>, 24 | options?: boolean | EventListenerOptions 25 | ): void; 26 | } 27 | 28 | export class HslaStringBase extends AlphaColorPicker { 29 | protected get colorModel(): ColorModel { 30 | return colorModel; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/entrypoints/hsla.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ColorModel, 3 | ColorPickerEventListener, 4 | ColorPickerEventMap, 5 | HslaColor 6 | } from '../types'; 7 | import { AlphaColorPicker } from '../components/alpha-color-picker.js'; 8 | import { hslaToHsva, hsvaToHsla } from '../utils/convert.js'; 9 | import { equalColorObjects } from '../utils/compare.js'; 10 | 11 | const colorModel: ColorModel = { 12 | defaultColor: { h: 0, s: 0, l: 0, a: 1 }, 13 | toHsva: hslaToHsva, 14 | fromHsva: hsvaToHsla, 15 | equal: equalColorObjects, 16 | fromAttr: (color) => JSON.parse(color) 17 | }; 18 | 19 | export interface HslaBase { 20 | addEventListener>( 21 | type: T, 22 | listener: ColorPickerEventListener[T]>, 23 | options?: boolean | AddEventListenerOptions 24 | ): void; 25 | 26 | removeEventListener>( 27 | type: T, 28 | listener: ColorPickerEventListener[T]>, 29 | options?: boolean | EventListenerOptions 30 | ): void; 31 | } 32 | 33 | export class HslaBase extends AlphaColorPicker { 34 | protected get colorModel(): ColorModel { 35 | return colorModel; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/entrypoints/hsv-string.ts: -------------------------------------------------------------------------------- 1 | import type { ColorModel, ColorPickerEventListener, ColorPickerEventMap } from '../types'; 2 | import { ColorPicker } from '../components/color-picker.js'; 3 | import { hsvStringToHsva, hsvaToHsvString } from '../utils/convert.js'; 4 | import { equalColorString } from '../utils/compare.js'; 5 | 6 | const colorModel: ColorModel = { 7 | defaultColor: 'hsv(0, 0%, 0%)', 8 | toHsva: hsvStringToHsva, 9 | fromHsva: hsvaToHsvString, 10 | equal: equalColorString, 11 | fromAttr: (color) => color 12 | }; 13 | 14 | export interface HsvStringBase { 15 | addEventListener>( 16 | type: T, 17 | listener: ColorPickerEventListener[T]>, 18 | options?: boolean | AddEventListenerOptions 19 | ): void; 20 | 21 | removeEventListener>( 22 | type: T, 23 | listener: ColorPickerEventListener[T]>, 24 | options?: boolean | EventListenerOptions 25 | ): void; 26 | } 27 | 28 | export class HsvStringBase extends ColorPicker { 29 | protected get colorModel(): ColorModel { 30 | return colorModel; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/entrypoints/hsv.ts: -------------------------------------------------------------------------------- 1 | import type { ColorModel, ColorPickerEventListener, ColorPickerEventMap, HsvColor } from '../types'; 2 | import { ColorPicker } from '../components/color-picker.js'; 3 | import { hsvaToHsv } from '../utils/convert.js'; 4 | import { equalColorObjects } from '../utils/compare.js'; 5 | 6 | const colorModel: ColorModel = { 7 | defaultColor: { h: 0, s: 0, v: 0 }, 8 | toHsva: ({ h, s, v }) => ({ h, s, v, a: 1 }), 9 | fromHsva: hsvaToHsv, 10 | equal: equalColorObjects, 11 | fromAttr: (color) => JSON.parse(color) 12 | }; 13 | 14 | export interface HsvBase { 15 | addEventListener>( 16 | type: T, 17 | listener: ColorPickerEventListener[T]>, 18 | options?: boolean | AddEventListenerOptions 19 | ): void; 20 | 21 | removeEventListener>( 22 | type: T, 23 | listener: ColorPickerEventListener[T]>, 24 | options?: boolean | EventListenerOptions 25 | ): void; 26 | } 27 | 28 | export class HsvBase extends ColorPicker { 29 | protected get colorModel(): ColorModel { 30 | return colorModel; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/entrypoints/hsva-string.ts: -------------------------------------------------------------------------------- 1 | import type { ColorModel, ColorPickerEventListener, ColorPickerEventMap } from '../types'; 2 | import { AlphaColorPicker } from '../components/alpha-color-picker.js'; 3 | import { hsvaStringToHsva, hsvaToHsvaString } from '../utils/convert.js'; 4 | import { equalColorString } from '../utils/compare.js'; 5 | 6 | const colorModel: ColorModel = { 7 | defaultColor: 'hsva(0, 0%, 0%, 1)', 8 | toHsva: hsvaStringToHsva, 9 | fromHsva: hsvaToHsvaString, 10 | equal: equalColorString, 11 | fromAttr: (color) => color 12 | }; 13 | 14 | export interface HsvaStringBase { 15 | addEventListener>( 16 | type: T, 17 | listener: ColorPickerEventListener[T]>, 18 | options?: boolean | AddEventListenerOptions 19 | ): void; 20 | 21 | removeEventListener>( 22 | type: T, 23 | listener: ColorPickerEventListener[T]>, 24 | options?: boolean | EventListenerOptions 25 | ): void; 26 | } 27 | 28 | export class HsvaStringBase extends AlphaColorPicker { 29 | protected get colorModel(): ColorModel { 30 | return colorModel; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/entrypoints/hsva.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ColorModel, 3 | ColorPickerEventListener, 4 | ColorPickerEventMap, 5 | HsvaColor 6 | } from '../types'; 7 | import { AlphaColorPicker } from '../components/alpha-color-picker.js'; 8 | import { equalColorObjects } from '../utils/compare.js'; 9 | import { roundHsva } from '../utils/convert.js'; 10 | 11 | const colorModel: ColorModel = { 12 | defaultColor: { h: 0, s: 0, v: 0, a: 1 }, 13 | toHsva: (hsva) => hsva, 14 | fromHsva: roundHsva, 15 | equal: equalColorObjects, 16 | fromAttr: (color) => JSON.parse(color) 17 | }; 18 | 19 | export interface HsvaBase { 20 | addEventListener>( 21 | type: T, 22 | listener: ColorPickerEventListener[T]>, 23 | options?: boolean | AddEventListenerOptions 24 | ): void; 25 | 26 | removeEventListener>( 27 | type: T, 28 | listener: ColorPickerEventListener[T]>, 29 | options?: boolean | EventListenerOptions 30 | ): void; 31 | } 32 | 33 | export class HsvaBase extends AlphaColorPicker { 34 | protected get colorModel(): ColorModel { 35 | return colorModel; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/entrypoints/rgb-string.ts: -------------------------------------------------------------------------------- 1 | import type { ColorModel, ColorPickerEventListener, ColorPickerEventMap } from '../types'; 2 | import { ColorPicker } from '../components/color-picker.js'; 3 | import { rgbStringToHsva, hsvaToRgbString } from '../utils/convert.js'; 4 | import { equalColorString } from '../utils/compare.js'; 5 | 6 | const colorModel: ColorModel = { 7 | defaultColor: 'rgb(0, 0, 0)', 8 | toHsva: rgbStringToHsva, 9 | fromHsva: hsvaToRgbString, 10 | equal: equalColorString, 11 | fromAttr: (color) => color 12 | }; 13 | 14 | export interface RgbStringBase { 15 | addEventListener>( 16 | type: T, 17 | listener: ColorPickerEventListener[T]>, 18 | options?: boolean | AddEventListenerOptions 19 | ): void; 20 | 21 | removeEventListener>( 22 | type: T, 23 | listener: ColorPickerEventListener[T]>, 24 | options?: boolean | EventListenerOptions 25 | ): void; 26 | } 27 | 28 | export class RgbStringBase extends ColorPicker { 29 | protected get colorModel(): ColorModel { 30 | return colorModel; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/entrypoints/rgb.ts: -------------------------------------------------------------------------------- 1 | import type { ColorModel, ColorPickerEventListener, ColorPickerEventMap, RgbColor } from '../types'; 2 | import { ColorPicker } from '../components/color-picker.js'; 3 | import { rgbaToHsva, hsvaToRgba, rgbaToRgb } from '../utils/convert.js'; 4 | import { equalColorObjects } from '../utils/compare.js'; 5 | 6 | const colorModel: ColorModel = { 7 | defaultColor: { r: 0, g: 0, b: 0 }, 8 | toHsva: ({ r, g, b }) => rgbaToHsva({ r, g, b, a: 1 }), 9 | fromHsva: (hsva) => rgbaToRgb(hsvaToRgba(hsva)), 10 | equal: equalColorObjects, 11 | fromAttr: (color) => JSON.parse(color) 12 | }; 13 | 14 | export interface RgbBase { 15 | addEventListener>( 16 | type: T, 17 | listener: ColorPickerEventListener[T]>, 18 | options?: boolean | AddEventListenerOptions 19 | ): void; 20 | 21 | removeEventListener>( 22 | type: T, 23 | listener: ColorPickerEventListener[T]>, 24 | options?: boolean | EventListenerOptions 25 | ): void; 26 | } 27 | 28 | export class RgbBase extends ColorPicker { 29 | protected get colorModel(): ColorModel { 30 | return colorModel; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/entrypoints/rgba-string.ts: -------------------------------------------------------------------------------- 1 | import type { ColorModel, ColorPickerEventListener, ColorPickerEventMap } from '../types'; 2 | import { AlphaColorPicker } from '../components/alpha-color-picker.js'; 3 | import { rgbaStringToHsva, hsvaToRgbaString } from '../utils/convert.js'; 4 | import { equalColorString } from '../utils/compare.js'; 5 | 6 | const colorModel: ColorModel = { 7 | defaultColor: 'rgba(0, 0, 0, 1)', 8 | toHsva: rgbaStringToHsva, 9 | fromHsva: hsvaToRgbaString, 10 | equal: equalColorString, 11 | fromAttr: (color) => color 12 | }; 13 | 14 | export interface RgbaStringBase { 15 | addEventListener>( 16 | type: T, 17 | listener: ColorPickerEventListener[T]>, 18 | options?: boolean | AddEventListenerOptions 19 | ): void; 20 | 21 | removeEventListener>( 22 | type: T, 23 | listener: ColorPickerEventListener[T]>, 24 | options?: boolean | EventListenerOptions 25 | ): void; 26 | } 27 | 28 | export class RgbaStringBase extends AlphaColorPicker { 29 | protected get colorModel(): ColorModel { 30 | return colorModel; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/entrypoints/rgba.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ColorModel, 3 | ColorPickerEventListener, 4 | ColorPickerEventMap, 5 | RgbaColor 6 | } from '../types'; 7 | import { AlphaColorPicker } from '../components/alpha-color-picker.js'; 8 | import { rgbaToHsva, hsvaToRgba } from '../utils/convert.js'; 9 | import { equalColorObjects } from '../utils/compare.js'; 10 | 11 | const colorModel: ColorModel = { 12 | defaultColor: { r: 0, g: 0, b: 0, a: 1 }, 13 | toHsva: rgbaToHsva, 14 | fromHsva: hsvaToRgba, 15 | equal: equalColorObjects, 16 | fromAttr: (color) => JSON.parse(color) 17 | }; 18 | 19 | export interface RgbaBase { 20 | addEventListener>( 21 | type: T, 22 | listener: ColorPickerEventListener[T]>, 23 | options?: boolean | AddEventListenerOptions 24 | ): void; 25 | 26 | removeEventListener>( 27 | type: T, 28 | listener: ColorPickerEventListener[T]>, 29 | options?: boolean | EventListenerOptions 30 | ): void; 31 | } 32 | 33 | export class RgbaBase extends AlphaColorPicker { 34 | protected get colorModel(): ColorModel { 35 | return colorModel; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/styles/alpha.css: -------------------------------------------------------------------------------- 1 | [part='alpha'] { 2 | flex: 0 0 24px; 3 | } 4 | 5 | [part='alpha']::after { 6 | display: block; 7 | content: ''; 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | right: 0; 12 | bottom: 0; 13 | border-radius: inherit; 14 | background-image: var(--gradient); 15 | /* Improve rendering on light backgrounds */ 16 | box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.05); 17 | } 18 | 19 | [part^='alpha'] { 20 | /* Chessboard pattern */ 21 | background-color: #fff; 22 | background-image: url('data:image/svg+xml,'); 23 | } 24 | 25 | [part='alpha-pointer'] { 26 | top: 50%; 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/styles/color-picker.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | position: relative; 5 | width: 200px; 6 | height: 200px; 7 | -webkit-user-select: none; 8 | user-select: none; 9 | cursor: default; 10 | } 11 | 12 | :host([hidden]) { 13 | display: none !important; 14 | } 15 | 16 | [role='slider'] { 17 | position: relative; 18 | touch-action: none; 19 | -webkit-user-select: none; 20 | user-select: none; 21 | outline: none; 22 | } 23 | 24 | /* Round bottom corners of the last element: `Hue` or `Alpha` */ 25 | [role='slider']:last-child { 26 | border-radius: 0 0 8px 8px; 27 | } 28 | 29 | [part$='pointer'] { 30 | position: absolute; 31 | z-index: 1; 32 | box-sizing: border-box; 33 | width: 28px; 34 | height: 28px; 35 | display: flex; 36 | place-content: center center; 37 | transform: translate(-50%, -50%); 38 | background-color: #fff; 39 | border: 2px solid #fff; 40 | border-radius: 50%; 41 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 42 | } 43 | 44 | [part$='pointer']::after { 45 | content: ''; 46 | width: 100%; 47 | height: 100%; 48 | border-radius: inherit; 49 | background-color: currentColor; 50 | } 51 | 52 | [role='slider']:focus [part$='pointer'] { 53 | transform: translate(-50%, -50%) scale(1.1); 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/styles/hue.css: -------------------------------------------------------------------------------- 1 | [part='hue'] { 2 | flex: 0 0 24px; 3 | background: linear-gradient( 4 | to right, 5 | #f00 0%, 6 | #ff0 17%, 7 | #0f0 33%, 8 | #0ff 50%, 9 | #00f 67%, 10 | #f0f 83%, 11 | #f00 100% 12 | ); 13 | } 14 | 15 | [part='hue-pointer'] { 16 | top: 50%; 17 | /* Display the hue pointer over the alpha one */ 18 | z-index: 2; 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/styles/saturation.css: -------------------------------------------------------------------------------- 1 | [part='saturation'] { 2 | flex-grow: 1; 3 | border-color: transparent; 4 | border-bottom: 12px solid #000; 5 | border-radius: 8px 8px 0 0; 6 | background-image: linear-gradient(to top, #000, rgba(0, 0, 0, 0)), 7 | linear-gradient(to right, #fff, rgba(255, 255, 255, 0)); 8 | /* Improve rendering on light backgrounds */ 9 | box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.05); 10 | } 11 | 12 | /* Display the saturation pointer over the hue one */ 13 | [part='saturation-pointer'] { 14 | z-index: 3; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export interface RgbColor { 2 | r: number; 3 | g: number; 4 | b: number; 5 | } 6 | 7 | export interface RgbaColor extends RgbColor { 8 | a: number; 9 | } 10 | 11 | export interface HslColor { 12 | h: number; 13 | s: number; 14 | l: number; 15 | } 16 | 17 | export interface HslaColor extends HslColor { 18 | a: number; 19 | } 20 | 21 | export interface HsvColor { 22 | h: number; 23 | s: number; 24 | v: number; 25 | } 26 | 27 | export interface HsvaColor extends HsvColor { 28 | a: number; 29 | } 30 | 31 | export type ObjectColor = RgbColor | HslColor | HsvColor | RgbaColor | HslaColor | HsvaColor; 32 | 33 | export type AnyColor = string | ObjectColor; 34 | 35 | export interface ColorModel { 36 | defaultColor: T; 37 | toHsva: (color: T) => HsvaColor; 38 | fromHsva: (hsva: HsvaColor) => T; 39 | equal: (first: T, second: T) => boolean; 40 | fromAttr: (attr: string) => T; 41 | } 42 | 43 | export interface ColorChangedEventListener { 44 | (evt: T): void; 45 | } 46 | 47 | export interface ColorChangedEventListenerObject { 48 | handleEvent(evt: T): void; 49 | } 50 | 51 | export interface ColorPickerEventMap extends HTMLElementEventMap { 52 | 'color-changed': CustomEvent<{ value: T }>; 53 | } 54 | 55 | export type ColorPickerEventListener = 56 | | ColorChangedEventListener 57 | | ColorChangedEventListenerObject; 58 | -------------------------------------------------------------------------------- /src/lib/utils/compare.ts: -------------------------------------------------------------------------------- 1 | import { hexToRgba } from './convert.js'; 2 | import type { ObjectColor } from '../types'; 3 | 4 | export const equalColorObjects = (first: ObjectColor, second: ObjectColor): boolean => { 5 | if (first === second) return true; 6 | 7 | for (const prop in first) { 8 | // The following allows for a type-safe calling of this function (first & second have to be HSL, HSV, or RGB) 9 | // with type-unsafe iterating over object keys. TS does not allow this without an index (`[key: string]: number`) 10 | // on an object to define how iteration is normally done. To ensure extra keys are not allowed on our types, 11 | // we must cast our object to unknown (as RGB demands `r` be a key, while `Record` does not care if 12 | // there is or not), and then as a type TS can iterate over. 13 | if ( 14 | (first as unknown as Record)[prop] !== 15 | (second as unknown as Record)[prop] 16 | ) 17 | return false; 18 | } 19 | 20 | return true; 21 | }; 22 | 23 | export const equalColorString = (first: string, second: string): boolean => { 24 | return first.replace(/\s/g, '') === second.replace(/\s/g, ''); 25 | }; 26 | 27 | export const equalHex = (first: string, second: string): boolean => { 28 | if (first.toLowerCase() === second.toLowerCase()) return true; 29 | 30 | // To compare colors like `#FFF` and `ffffff` we convert them into RGB objects 31 | return equalColorObjects(hexToRgba(first), hexToRgba(second)); 32 | }; 33 | -------------------------------------------------------------------------------- /src/lib/utils/convert.ts: -------------------------------------------------------------------------------- 1 | import { RgbaColor, RgbColor, HslaColor, HslColor, HsvaColor, HsvColor } from '../types'; 2 | import { round } from './math.js'; 3 | 4 | /** 5 | * Valid CSS units. 6 | * https://developer.mozilla.org/en-US/docs/Web/CSS/angle 7 | */ 8 | const angleUnits: Record = { 9 | grad: 360 / 400, 10 | turn: 360, 11 | rad: 360 / (Math.PI * 2) 12 | }; 13 | 14 | export const hexToHsva = (hex: string): HsvaColor => rgbaToHsva(hexToRgba(hex)); 15 | 16 | export const hexToRgba = (hex: string): RgbaColor => { 17 | if (hex[0] === '#') hex = hex.substring(1); 18 | 19 | if (hex.length < 6) { 20 | return { 21 | r: parseInt(hex[0] + hex[0], 16), 22 | g: parseInt(hex[1] + hex[1], 16), 23 | b: parseInt(hex[2] + hex[2], 16), 24 | a: hex.length === 4 ? round(parseInt(hex[3] + hex[3], 16) / 255, 2) : 1 25 | }; 26 | } 27 | 28 | return { 29 | r: parseInt(hex.substring(0, 2), 16), 30 | g: parseInt(hex.substring(2, 4), 16), 31 | b: parseInt(hex.substring(4, 6), 16), 32 | a: hex.length === 8 ? round(parseInt(hex.substring(6, 8), 16) / 255, 2) : 1 33 | }; 34 | }; 35 | 36 | export const parseHue = (value: string, unit = 'deg'): number => { 37 | return Number(value) * (angleUnits[unit] || 1); 38 | }; 39 | 40 | export const hslaStringToHsva = (hslString: string): HsvaColor => { 41 | const matcher = 42 | /hsla?\(?\s*(-?\d*\.?\d+)(deg|rad|grad|turn)?[,\s]+(-?\d*\.?\d+)%?[,\s]+(-?\d*\.?\d+)%?,?\s*[/\s]*(-?\d*\.?\d+)?(%)?\s*\)?/i; 43 | const match = matcher.exec(hslString); 44 | 45 | if (!match) return { h: 0, s: 0, v: 0, a: 1 }; 46 | 47 | return hslaToHsva({ 48 | h: parseHue(match[1], match[2]), 49 | s: Number(match[3]), 50 | l: Number(match[4]), 51 | a: match[5] === undefined ? 1 : Number(match[5]) / (match[6] ? 100 : 1) 52 | }); 53 | }; 54 | 55 | export const hslStringToHsva = hslaStringToHsva; 56 | 57 | export const hslaToHsva = ({ h, s, l, a }: HslaColor): HsvaColor => { 58 | s *= (l < 50 ? l : 100 - l) / 100; 59 | 60 | return { 61 | h: h, 62 | s: s > 0 ? ((2 * s) / (l + s)) * 100 : 0, 63 | v: l + s, 64 | a 65 | }; 66 | }; 67 | 68 | export const hsvaToHex = (hsva: HsvaColor): string => rgbaToHex(hsvaToRgba(hsva)); 69 | 70 | export const hsvaToHsla = ({ h, s, v, a }: HsvaColor): HslaColor => { 71 | const hh = ((200 - s) * v) / 100; 72 | 73 | return { 74 | h: round(h), 75 | s: round(hh > 0 && hh < 200 ? ((s * v) / 100 / (hh <= 100 ? hh : 200 - hh)) * 100 : 0), 76 | l: round(hh / 2), 77 | a: round(a, 2) 78 | }; 79 | }; 80 | 81 | export const hsvaToHsvString = (hsva: HsvaColor): string => { 82 | const { h, s, v } = roundHsva(hsva); 83 | return `hsv(${h}, ${s}%, ${v}%)`; 84 | }; 85 | 86 | export const hsvaToHsvaString = (hsva: HsvaColor): string => { 87 | const { h, s, v, a } = roundHsva(hsva); 88 | return `hsva(${h}, ${s}%, ${v}%, ${a})`; 89 | }; 90 | 91 | export const hsvaToHslString = (hsva: HsvaColor): string => { 92 | const { h, s, l } = hsvaToHsla(hsva); 93 | return `hsl(${h}, ${s}%, ${l}%)`; 94 | }; 95 | 96 | export const hsvaToHslaString = (hsva: HsvaColor): string => { 97 | const { h, s, l, a } = hsvaToHsla(hsva); 98 | return `hsla(${h}, ${s}%, ${l}%, ${a})`; 99 | }; 100 | 101 | export const hsvaToRgba = ({ h, s, v, a }: HsvaColor): RgbaColor => { 102 | h = (h / 360) * 6; 103 | s = s / 100; 104 | v = v / 100; 105 | 106 | const hh = Math.floor(h), 107 | b = v * (1 - s), 108 | c = v * (1 - (h - hh) * s), 109 | d = v * (1 - (1 - h + hh) * s), 110 | module = hh % 6; 111 | 112 | return { 113 | r: round([v, c, b, b, d, v][module] * 255), 114 | g: round([d, v, v, c, b, b][module] * 255), 115 | b: round([b, b, d, v, v, c][module] * 255), 116 | a: round(a, 2) 117 | }; 118 | }; 119 | 120 | export const hsvaToRgbString = (hsva: HsvaColor): string => { 121 | const { r, g, b } = hsvaToRgba(hsva); 122 | return `rgb(${r}, ${g}, ${b})`; 123 | }; 124 | 125 | export const hsvaToRgbaString = (hsva: HsvaColor): string => { 126 | const { r, g, b, a } = hsvaToRgba(hsva); 127 | return `rgba(${r}, ${g}, ${b}, ${a})`; 128 | }; 129 | 130 | export const hsvaStringToHsva = (hsvString: string): HsvaColor => { 131 | const matcher = 132 | /hsva?\(?\s*(-?\d*\.?\d+)(deg|rad|grad|turn)?[,\s]+(-?\d*\.?\d+)%?[,\s]+(-?\d*\.?\d+)%?,?\s*[/\s]*(-?\d*\.?\d+)?(%)?\s*\)?/i; 133 | const match = matcher.exec(hsvString); 134 | 135 | if (!match) return { h: 0, s: 0, v: 0, a: 1 }; 136 | 137 | return roundHsva({ 138 | h: parseHue(match[1], match[2]), 139 | s: Number(match[3]), 140 | v: Number(match[4]), 141 | a: match[5] === undefined ? 1 : Number(match[5]) / (match[6] ? 100 : 1) 142 | }); 143 | }; 144 | 145 | export const hsvStringToHsva = hsvaStringToHsva; 146 | 147 | export const rgbaStringToHsva = (rgbaString: string): HsvaColor => { 148 | const matcher = 149 | /rgba?\(?\s*(-?\d*\.?\d+)(%)?[,\s]+(-?\d*\.?\d+)(%)?[,\s]+(-?\d*\.?\d+)(%)?,?\s*[/\s]*(-?\d*\.?\d+)?(%)?\s*\)?/i; 150 | const match = matcher.exec(rgbaString); 151 | 152 | if (!match) return { h: 0, s: 0, v: 0, a: 1 }; 153 | 154 | return rgbaToHsva({ 155 | r: Number(match[1]) / (match[2] ? 100 / 255 : 1), 156 | g: Number(match[3]) / (match[4] ? 100 / 255 : 1), 157 | b: Number(match[5]) / (match[6] ? 100 / 255 : 1), 158 | a: match[7] === undefined ? 1 : Number(match[7]) / (match[8] ? 100 : 1) 159 | }); 160 | }; 161 | 162 | export const rgbStringToHsva = rgbaStringToHsva; 163 | 164 | const format = (number: number) => { 165 | const hex = number.toString(16); 166 | return hex.length < 2 ? '0' + hex : hex; 167 | }; 168 | 169 | export const rgbaToHex = ({ r, g, b, a }: RgbaColor): string => { 170 | const alphaHex = a < 1 ? format(round(a * 255)) : ''; 171 | return '#' + format(r) + format(g) + format(b) + alphaHex; 172 | }; 173 | 174 | export const rgbaToHsva = ({ r, g, b, a }: RgbaColor): HsvaColor => { 175 | const max = Math.max(r, g, b); 176 | const delta = max - Math.min(r, g, b); 177 | 178 | // prettier-ignore 179 | const hh = delta 180 | ? max === r 181 | ? (g - b) / delta 182 | : max === g 183 | ? 2 + (b - r) / delta 184 | : 4 + (r - g) / delta 185 | : 0; 186 | 187 | return { 188 | h: round(60 * (hh < 0 ? hh + 6 : hh)), 189 | s: round(max ? (delta / max) * 100 : 0), 190 | v: round((max / 255) * 100), 191 | a 192 | }; 193 | }; 194 | 195 | export const roundHsva = (hsva: HsvaColor): HsvaColor => ({ 196 | h: round(hsva.h), 197 | s: round(hsva.s), 198 | v: round(hsva.v), 199 | a: round(hsva.a, 2) 200 | }); 201 | 202 | export const rgbaToRgb = ({ r, g, b }: RgbaColor): RgbColor => ({ r, g, b }); 203 | 204 | export const hslaToHsl = ({ h, s, l }: HslaColor): HslColor => ({ h, s, l }); 205 | 206 | export const hsvaToHsv = (hsva: HsvaColor): HsvColor => { 207 | const { h, s, v } = roundHsva(hsva); 208 | return { h, s, v }; 209 | }; 210 | -------------------------------------------------------------------------------- /src/lib/utils/dom.ts: -------------------------------------------------------------------------------- 1 | const cache: Record = {}; 2 | 3 | export const render = (root: ShadowRoot, html: string): void => { 4 | let template = cache[html]; 5 | if (!template) { 6 | template = document.createElement('template'); 7 | template.innerHTML = html; 8 | cache[html] = template; 9 | } 10 | root.appendChild(template.content.cloneNode(true)); 11 | }; 12 | 13 | export const fire = (target: HTMLElement, type: string, detail: Record): void => { 14 | target.dispatchEvent( 15 | new CustomEvent(type, { 16 | bubbles: true, 17 | detail 18 | }) 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/lib/utils/math.ts: -------------------------------------------------------------------------------- 1 | // Clamps a value between an upper and lower bound. 2 | // We use ternary operators because it makes the minified code 3 | // 2 times shorter then `Math.min(Math.max(a,b),c)` 4 | export const clamp = (number: number, min = 0, max = 1): number => { 5 | return number > max ? max : number < min ? min : number; 6 | }; 7 | 8 | export const round = (number: number, digits = 0, base = Math.pow(10, digits)): number => { 9 | return Math.round(base * number) / base; 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/utils/validate.ts: -------------------------------------------------------------------------------- 1 | const matcher = /^#?([0-9A-F]{3,8})$/i; 2 | 3 | export const validHex = (value: string, alpha?: boolean): boolean => { 4 | const match = matcher.exec(value); 5 | const length = match ? match[1].length : 0; 6 | 7 | return ( 8 | length === 3 || // '#rgb' format 9 | length === 6 || // '#rrggbb' format 10 | (!!alpha && length === 4) || // '#rgba' format 11 | (!!alpha && length === 8) // '#rrggbbaa' format 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/rgb-color-picker.ts: -------------------------------------------------------------------------------- 1 | import { RgbBase } from './lib/entrypoints/rgb.js'; 2 | export type { RgbColor } from './lib/types'; 3 | 4 | /** 5 | * A color picker custom element that uses RGB object format. 6 | * 7 | * @element rgb-color-picker 8 | * 9 | * @prop {RgbColor} color - Selected color in RGB object format. 10 | * 11 | * @fires color-changed - Event fired when color property changes. 12 | * 13 | * @csspart hue - A hue selector container. 14 | * @csspart saturation - A saturation selector container 15 | * @csspart hue-pointer - A hue pointer element. 16 | * @csspart saturation-pointer - A saturation pointer element. 17 | */ 18 | export class RgbColorPicker extends RgbBase {} 19 | 20 | customElements.define('rgb-color-picker', RgbColorPicker); 21 | 22 | declare global { 23 | interface HTMLElementTagNameMap { 24 | 'rgb-color-picker': RgbColorPicker; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/rgb-string-color-picker.ts: -------------------------------------------------------------------------------- 1 | import { RgbStringBase } from './lib/entrypoints/rgb-string.js'; 2 | 3 | /** 4 | * A color picker custom element that uses RGB string format. 5 | * 6 | * @element rgb-string-color-picker 7 | * 8 | * @prop {string} color - Selected color in RGB string format. 9 | * @attr {string} color - Selected color in RGB string format. 10 | * 11 | * @fires color-changed - Event fired when color property changes. 12 | * 13 | * @csspart hue - A hue selector container. 14 | * @csspart saturation - A saturation selector container 15 | * @csspart hue-pointer - A hue pointer element. 16 | * @csspart saturation-pointer - A saturation pointer element. 17 | */ 18 | export class RgbStringColorPicker extends RgbStringBase {} 19 | 20 | customElements.define('rgb-string-color-picker', RgbStringColorPicker); 21 | 22 | declare global { 23 | interface HTMLElementTagNameMap { 24 | 'rgb-string-color-picker': RgbStringColorPicker; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/rgba-color-picker.ts: -------------------------------------------------------------------------------- 1 | import { RgbaBase } from './lib/entrypoints/rgba.js'; 2 | export type { RgbaColor } from './lib/types'; 3 | 4 | /** 5 | * A color picker custom element that uses RGBA object format. 6 | * 7 | * @element rgba-color-picker 8 | * 9 | * @prop {RgbaColor} color - Selected color in RGBA object format. 10 | * 11 | * @fires color-changed - Event fired when color property changes. 12 | * 13 | * @csspart hue - A hue selector container. 14 | * @csspart saturation - A saturation selector container 15 | * @csspart alpha - An alpha selector container. 16 | * @csspart hue-pointer - A hue pointer element. 17 | * @csspart saturation-pointer - A saturation pointer element. 18 | * @csspart alpha-pointer - An alpha pointer element. 19 | */ 20 | export class RgbaColorPicker extends RgbaBase {} 21 | 22 | customElements.define('rgba-color-picker', RgbaColorPicker); 23 | 24 | declare global { 25 | interface HTMLElementTagNameMap { 26 | 'rgba-color-picker': RgbaColorPicker; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/rgba-string-color-picker.ts: -------------------------------------------------------------------------------- 1 | import { RgbaStringBase } from './lib/entrypoints/rgba-string.js'; 2 | 3 | /** 4 | * A color picker custom element that uses RGBA string format. 5 | * 6 | * @element rgba-string-color-picker 7 | * 8 | * @prop {string} color - Selected color in RGBA string format. 9 | * @attr {string} color - Selected color in RGBA string format. 10 | * 11 | * @fires color-changed - Event fired when color property changes. 12 | * 13 | * @csspart hue - A hue selector container. 14 | * @csspart saturation - A saturation selector container 15 | * @csspart alpha - An alpha selector container. 16 | * @csspart hue-pointer - A hue pointer element. 17 | * @csspart saturation-pointer - A saturation pointer element. 18 | * @csspart alpha-pointer - An alpha pointer element. 19 | */ 20 | export class RgbaStringColorPicker extends RgbaStringBase {} 21 | 22 | customElements.define('rgba-string-color-picker', RgbaStringColorPicker); 23 | 24 | declare global { 25 | interface HTMLElementTagNameMap { 26 | 'rgba-string-color-picker': RgbaStringColorPicker; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/a11y.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { sendKeys } from '@web/test-runner-commands'; 3 | import { fixture, html } from '@open-wc/testing-helpers'; 4 | import type { RgbaColorPicker } from '../rgba-color-picker'; 5 | import '../rgba-color-picker.js'; 6 | 7 | describe('accessibility', () => { 8 | let picker: RgbaColorPicker; 9 | 10 | beforeEach(async () => { 11 | picker = await fixture(html``); 12 | picker.color = { r: 30, g: 136, b: 230, a: 1 }; 13 | }); 14 | 15 | describe('saturation', () => { 16 | let element: HTMLElement; 17 | 18 | beforeEach(() => { 19 | const root = picker.shadowRoot as ShadowRoot; 20 | element = root.querySelector('[part="saturation"]') as HTMLElement; 21 | }); 22 | 23 | describe('WAI-ARIA', () => { 24 | it('should set role attribute to slider', () => { 25 | expect(element.getAttribute('role')).to.equal('slider'); 26 | }); 27 | 28 | it('should set tabindex attribute to 0', () => { 29 | expect(element.getAttribute('tabindex')).to.equal('0'); 30 | }); 31 | 32 | it('should set aria-label attribute', () => { 33 | expect(element.getAttribute('aria-label')).to.equal('Color'); 34 | }); 35 | 36 | it('should set aria-valuetext attribute', () => { 37 | expect(element.getAttribute('aria-valuetext')).to.equal('Saturation 87%, Brightness 90%'); 38 | }); 39 | 40 | it('should update aria-valuetext on color change', () => { 41 | picker.color = { r: 60, g: 95, b: 138, a: 1 }; 42 | expect(element.getAttribute('aria-valuetext')).to.equal('Saturation 57%, Brightness 54%'); 43 | }); 44 | }); 45 | 46 | describe('keyboard navigation', () => { 47 | const color = { r: 30, g: 136, b: 230, a: 1 }; 48 | 49 | beforeEach(() => { 50 | picker.color = color; 51 | element.focus(); 52 | }); 53 | 54 | it('should update color on ArrowLeft key', async () => { 55 | await sendKeys({ press: 'ArrowLeft' }); 56 | expect(picker.color).to.deep.equal({ ...color, r: 32, g: 137 }); 57 | }); 58 | 59 | it('should update color on ArrowRight key', async () => { 60 | await sendKeys({ press: 'ArrowRight' }); 61 | expect(picker.color).to.deep.equal({ ...color, r: 28, g: 135 }); 62 | }); 63 | 64 | it('should update color on ArrowDown key', async () => { 65 | await sendKeys({ press: 'ArrowDown' }); 66 | expect(picker.color).to.deep.equal({ ...color, g: 135, b: 227 }); 67 | }); 68 | 69 | it('should update color on ArrowUp key', async () => { 70 | await sendKeys({ press: 'ArrowUp' }); 71 | expect(picker.color).to.deep.equal({ ...color, g: 138, b: 232 }); 72 | }); 73 | 74 | it('should not update color on PageUp key', async () => { 75 | await sendKeys({ press: 'PageUp' }); 76 | expect(picker.color).to.deep.equal(color); 77 | }); 78 | 79 | it('should not update color on PageDown key', async () => { 80 | await sendKeys({ press: 'PageDown' }); 81 | expect(picker.color).to.deep.equal(color); 82 | }); 83 | 84 | it('should not update color on Home key', async () => { 85 | await sendKeys({ press: 'Home' }); 86 | expect(picker.color).to.deep.equal(color); 87 | }); 88 | 89 | it('should not update color on End key', async () => { 90 | await sendKeys({ press: 'End' }); 91 | expect(picker.color).to.deep.equal(color); 92 | }); 93 | }); 94 | }); 95 | 96 | describe('hue', () => { 97 | let element: HTMLElement; 98 | 99 | beforeEach(() => { 100 | const root = picker.shadowRoot as ShadowRoot; 101 | element = root.querySelector('[part="hue"]') as HTMLElement; 102 | }); 103 | 104 | describe('WAI-ARIA', () => { 105 | it('should set role attribute to slider', () => { 106 | expect(element.getAttribute('role')).to.equal('slider'); 107 | }); 108 | 109 | it('should set tabindex attribute to 0', () => { 110 | expect(element.getAttribute('tabindex')).to.equal('0'); 111 | }); 112 | 113 | it('should set aria-label attribute', () => { 114 | expect(element.getAttribute('aria-label')).to.equal('Hue'); 115 | }); 116 | 117 | it('should set aria-valuemin attribute', () => { 118 | expect(element.getAttribute('aria-valuemin')).to.equal('0'); 119 | }); 120 | 121 | it('should set aria-valuemax attribute', () => { 122 | expect(element.getAttribute('aria-valuemax')).to.equal('360'); 123 | }); 124 | 125 | it('should set aria-valuenow attribute', () => { 126 | expect(element.getAttribute('aria-valuenow')).to.equal('208'); 127 | }); 128 | 129 | it('should update aria-valuenow on color change', () => { 130 | picker.color = { r: 196, g: 154, b: 64, a: 1 }; 131 | expect(element.getAttribute('aria-valuenow')).to.equal('41'); 132 | }); 133 | }); 134 | 135 | describe('keyboard navigation', () => { 136 | const color = { r: 30, g: 136, b: 230, a: 1 }; 137 | 138 | beforeEach(() => { 139 | picker.color = color; 140 | element.focus(); 141 | }); 142 | 143 | it('should update color on ArrowLeft key', async () => { 144 | await sendKeys({ press: 'ArrowLeft' }); 145 | expect(picker.color).to.deep.equal({ ...color, g: 148 }); 146 | }); 147 | 148 | it('should update color on ArrowRight key', async () => { 149 | await sendKeys({ press: 'ArrowRight' }); 150 | expect(picker.color).to.deep.equal({ ...color, g: 124 }); 151 | }); 152 | 153 | it('should not update color on ArrowDown key', async () => { 154 | await sendKeys({ press: 'ArrowDown' }); 155 | expect(picker.color).to.deep.equal(color); 156 | }); 157 | 158 | it('should not update color on ArrowUp key', async () => { 159 | await sendKeys({ press: 'ArrowUp' }); 160 | expect(picker.color).to.deep.equal(color); 161 | }); 162 | 163 | it('should update color on PageUp key', async () => { 164 | await sendKeys({ press: 'PageUp' }); 165 | expect(picker.color).to.deep.equal({ ...color, g: 196 }); 166 | }); 167 | 168 | it('should update color on PageDown key', async () => { 169 | await sendKeys({ press: 'PageDown' }); 170 | expect(picker.color).to.deep.equal({ ...color, g: 76 }); 171 | }); 172 | 173 | it('should update color on Home key', async () => { 174 | await sendKeys({ press: 'Home' }); 175 | expect(picker.color).to.deep.equal({ r: 230, g: 30, b: 30, a: 1 }); 176 | }); 177 | 178 | it('should update color on End key', async () => { 179 | await sendKeys({ press: 'End' }); 180 | expect(picker.color).to.deep.equal({ r: 230, g: 30, b: 30, a: 1 }); 181 | }); 182 | }); 183 | }); 184 | 185 | describe('alpha', () => { 186 | let element: HTMLElement; 187 | 188 | beforeEach(() => { 189 | const root = picker.shadowRoot as ShadowRoot; 190 | element = root.querySelector('[part="alpha"]') as HTMLElement; 191 | }); 192 | 193 | describe('WAI-ARIA', () => { 194 | it('should set role attribute to slider', () => { 195 | expect(element.getAttribute('role')).to.equal('slider'); 196 | }); 197 | 198 | it('should set tabindex attribute to 0', () => { 199 | expect(element.getAttribute('tabindex')).to.equal('0'); 200 | }); 201 | 202 | it('should set aria-label attribute', () => { 203 | expect(element.getAttribute('aria-label')).to.equal('Alpha'); 204 | }); 205 | 206 | it('should set aria-valuemin attribute', () => { 207 | expect(element.getAttribute('aria-valuemin')).to.equal('0'); 208 | }); 209 | 210 | it('should set aria-valuemax attribute', () => { 211 | expect(element.getAttribute('aria-valuemax')).to.equal('1'); 212 | }); 213 | }); 214 | 215 | describe('keyboard navigation', () => { 216 | const color = { r: 30, g: 136, b: 230, a: 0.5 }; 217 | 218 | beforeEach(() => { 219 | picker.color = color; 220 | element.focus(); 221 | }); 222 | 223 | it('should update color on ArrowLeft key', async () => { 224 | await sendKeys({ press: 'ArrowLeft' }); 225 | expect(picker.color).to.deep.equal({ ...color, a: 0.49 }); 226 | }); 227 | 228 | it('should update color on ArrowRight key', async () => { 229 | await sendKeys({ press: 'ArrowRight' }); 230 | expect(picker.color).to.deep.equal({ ...color, a: 0.51 }); 231 | }); 232 | 233 | it('should not update color on ArrowDown key', async () => { 234 | await sendKeys({ press: 'ArrowDown' }); 235 | expect(picker.color).to.deep.equal(color); 236 | }); 237 | 238 | it('should not update color on ArrowUp key', async () => { 239 | await sendKeys({ press: 'ArrowUp' }); 240 | expect(picker.color).to.deep.equal(color); 241 | }); 242 | 243 | it('should update color on PageUp key', async () => { 244 | await sendKeys({ press: 'PageUp' }); 245 | expect(picker.color).to.deep.equal({ ...color, a: 0.45 }); 246 | }); 247 | 248 | it('should update color on PageDown key', async () => { 249 | await sendKeys({ press: 'PageDown' }); 250 | expect(picker.color).to.deep.equal({ ...color, a: 0.55 }); 251 | }); 252 | 253 | it('should update color on Home key', async () => { 254 | await sendKeys({ press: 'Home' }); 255 | expect(picker.color).to.deep.equal({ ...color, a: 0 }); 256 | }); 257 | 258 | it('should update color on End key', async () => { 259 | await sendKeys({ press: 'End' }); 260 | expect(picker.color).to.deep.equal({ ...color, a: 1 }); 261 | }); 262 | }); 263 | }); 264 | }); 265 | -------------------------------------------------------------------------------- /src/test/color-picker.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import { fixture, html, nextFrame } from '@open-wc/testing-helpers'; 4 | import { hsvaToRgbString, rgbaToHsva } from '../lib/utils/convert'; 5 | import type { HexAlphaColorPicker } from '../hex-alpha-color-picker'; 6 | import type { HexColorPicker } from '../hex-color-picker'; 7 | import type { RgbaColorPicker } from '../rgba-color-picker'; 8 | import '../hex-alpha-color-picker.js'; 9 | import '../rgba-color-picker.js'; 10 | 11 | class FakeMouseEvent extends MouseEvent { 12 | constructor(type: string, values: { pageX: number; pageY: number }) { 13 | const { pageX, pageY } = values; 14 | super(type, { bubbles: true, composed: true }); 15 | 16 | Object.defineProperties(this, { 17 | pageX: { 18 | value: pageX || 0 19 | }, 20 | pageY: { 21 | value: pageY || 0 22 | } 23 | }); 24 | } 25 | } 26 | 27 | class FakeTouchEvent extends TouchEvent { 28 | constructor(type: string, touches: Array<{ pageX: number; pageY: number }>) { 29 | super(type, { bubbles: true, composed: true }); 30 | 31 | Object.defineProperty(this, 'touches', { 32 | get() { 33 | return touches; 34 | } 35 | }); 36 | } 37 | } 38 | 39 | const middleOfNode = (node: Element) => { 40 | const bcr = node.getBoundingClientRect(); 41 | return { y: bcr.top + bcr.height / 2, x: bcr.left + bcr.width / 2 }; 42 | }; 43 | 44 | describe('hex-color-picker', () => { 45 | let picker: HexColorPicker; 46 | 47 | describe('lazy upgrade', () => { 48 | it('should work with color property set before upgrade', async () => { 49 | picker = document.createElement('hex-color-picker'); 50 | document.body.appendChild(picker); 51 | picker.color = '#123'; 52 | await import('../hex-color-picker'); 53 | expect(picker.color).to.equal('#123'); 54 | document.body.removeChild(picker); 55 | }); 56 | }); 57 | 58 | describe('default', () => { 59 | beforeEach(async () => { 60 | picker = await fixture(html``); 61 | }); 62 | 63 | it('should set default color property value', () => { 64 | expect(picker.color).to.equal('#000'); 65 | }); 66 | 67 | it('should not reflect default color to attribute', () => { 68 | expect(picker.getAttribute('color')).to.equal(null); 69 | }); 70 | 71 | it('should change display to none when hidden is set', () => { 72 | picker.setAttribute('hidden', ''); 73 | expect(getComputedStyle(picker).display).to.equal('none'); 74 | }); 75 | }); 76 | 77 | describe('color property', () => { 78 | beforeEach(async () => { 79 | picker = await fixture(html``); 80 | }); 81 | 82 | it('should accept color set as a property', () => { 83 | expect(picker.color).to.equal('#ccc'); 84 | }); 85 | 86 | it('should not reflect color property to attribute', () => { 87 | expect(picker.getAttribute('color')).to.equal(null); 88 | }); 89 | 90 | it('should not fire color-changed event when property changes', () => { 91 | const spy = sinon.spy(); 92 | picker.addEventListener('color-changed', spy); 93 | picker.color = '#123'; 94 | expect(spy.called).to.be.false; 95 | }); 96 | }); 97 | 98 | describe('color attribute', () => { 99 | beforeEach(async () => { 100 | picker = document.createElement('hex-color-picker'); 101 | picker.setAttribute('color', '#488'); 102 | await nextFrame(); 103 | document.body.appendChild(picker); 104 | }); 105 | 106 | afterEach(() => { 107 | document.body.removeChild(picker); 108 | }); 109 | 110 | it('should set color based on the attribute value', () => { 111 | expect(picker.color).to.equal('#488'); 112 | }); 113 | 114 | it('should not update attribute when property changes', () => { 115 | picker.color = '#ccc'; 116 | expect(picker.getAttribute('color')).to.equal('#488'); 117 | }); 118 | 119 | it('should update property when attribute changes', () => { 120 | picker.setAttribute('color', '#ccc'); 121 | expect(picker.color).to.equal('#ccc'); 122 | }); 123 | 124 | it('should not fire color-changed event when attribute changes', () => { 125 | const spy = sinon.spy(); 126 | picker.addEventListener('color-changed', spy); 127 | picker.setAttribute('color', '#123'); 128 | expect(spy.called).to.be.false; 129 | }); 130 | }); 131 | 132 | describe('interaction', () => { 133 | let hue: HTMLElement; 134 | 135 | beforeEach(async () => { 136 | const root = picker.shadowRoot as ShadowRoot; 137 | hue = root.querySelector('[part="hue"]') as HTMLElement; 138 | }); 139 | 140 | it('should dispatch color-changed event on mousedown', () => { 141 | const spy = sinon.spy(); 142 | picker.addEventListener('color-changed', spy); 143 | const { x, y } = middleOfNode(hue); 144 | hue.dispatchEvent(new FakeMouseEvent('mousedown', { pageX: x + 10, pageY: y })); 145 | hue.dispatchEvent(new FakeMouseEvent('mouseup', { pageX: x + 10, pageY: y })); 146 | expect(spy.callCount).to.equal(1); 147 | }); 148 | }); 149 | }); 150 | 151 | describe('hex-alpha-color-picker', () => { 152 | let picker: HexAlphaColorPicker; 153 | let alpha: HTMLElement; 154 | 155 | beforeEach(async () => { 156 | picker = document.createElement('hex-alpha-color-picker'); 157 | picker.setAttribute('color', '#112233'); 158 | document.body.appendChild(picker); 159 | await nextFrame(); 160 | const root = picker.shadowRoot as ShadowRoot; 161 | alpha = root.querySelector('[part="alpha"]') as HTMLElement; 162 | }); 163 | 164 | afterEach(() => { 165 | document.body.removeChild(picker); 166 | }); 167 | 168 | it('should use #rrggbbaa format if alpha channel value is less than 1', () => { 169 | const { x, y } = middleOfNode(alpha); 170 | alpha.dispatchEvent(new FakeMouseEvent('mousedown', { pageX: x, pageY: y })); 171 | alpha.dispatchEvent(new FakeMouseEvent('mousemove', { pageX: x + 20, pageY: y })); 172 | alpha.dispatchEvent(new FakeMouseEvent('mouseup', { pageX: x + 20, pageY: y })); 173 | expect(picker.color).to.equal('#11223399'); 174 | }); 175 | }); 176 | 177 | describe('rgba-color-picker', () => { 178 | let picker: RgbaColorPicker; 179 | 180 | let hue: HTMLElement; 181 | let saturation: HTMLElement; 182 | let alpha: HTMLElement; 183 | 184 | beforeEach(async () => { 185 | picker = document.createElement('rgba-color-picker'); 186 | picker.setAttribute('color', JSON.stringify({ r: 68, b: 136, g: 136, a: 1 })); 187 | await nextFrame(); 188 | document.body.appendChild(picker); 189 | const root = picker.shadowRoot as ShadowRoot; 190 | hue = root.querySelector('[part="hue"]') as HTMLElement; 191 | saturation = root.querySelector('[part="saturation"]') as HTMLElement; 192 | alpha = root.querySelector('[part="alpha"]') as HTMLElement; 193 | }); 194 | 195 | afterEach(() => { 196 | document.body.removeChild(picker); 197 | }); 198 | 199 | describe('pointers', () => { 200 | it('should set saturation background color', () => { 201 | const hsva = rgbaToHsva(picker.color); 202 | const bgColor = hsvaToRgbString({ h: hsva.h, s: 100, v: 100, a: 1 }); 203 | expect(getComputedStyle(saturation).backgroundColor).to.equal(bgColor); 204 | }); 205 | 206 | it('should set saturation pointer color', () => { 207 | const pointer = saturation.firstChild as HTMLElement; 208 | expect(getComputedStyle(pointer).color).to.equal('rgb(68, 136, 136)'); 209 | }); 210 | 211 | it('should set saturation pointer coordinates', () => { 212 | const hsva = rgbaToHsva(picker.color); 213 | const pointer = saturation.firstChild as HTMLElement; 214 | expect(pointer.style.top).to.equal(`${100 - hsva.v}%`); 215 | expect(pointer.style.left).to.equal(`${hsva.s}%`); 216 | }); 217 | 218 | it('should set hue pointer color', () => { 219 | const hsva = rgbaToHsva(picker.color); 220 | const bgColor = hsvaToRgbString({ h: hsva.h, s: 100, v: 100, a: 1 }); 221 | const pointer = hue.firstChild as HTMLElement; 222 | expect(getComputedStyle(pointer).color).to.equal(bgColor); 223 | }); 224 | 225 | it('should set hue pointer coordinate', () => { 226 | const hsv = rgbaToHsva(picker.color); 227 | const pointer = hue.firstChild as HTMLElement; 228 | expect(pointer.style.left).to.equal(`${(hsv.h / 360) * 100}%`); 229 | }); 230 | 231 | it('should set alpha pointer coordinate', () => { 232 | const pointer = alpha.firstChild as HTMLElement; 233 | expect(pointer.style.left).to.equal('100%'); 234 | }); 235 | }); 236 | 237 | describe('interaction', () => { 238 | it('should focus the slider on mousedown', () => { 239 | const spy = sinon.spy(hue, 'focus'); 240 | const { x, y } = middleOfNode(hue); 241 | hue.dispatchEvent(new FakeMouseEvent('mousedown', { pageX: x + 10, pageY: y })); 242 | expect(spy.callCount).to.equal(1); 243 | }); 244 | 245 | it('should dispatch color-changed event on mousedown', () => { 246 | const spy = sinon.spy(); 247 | picker.addEventListener('color-changed', spy); 248 | const { x, y } = middleOfNode(hue); 249 | hue.dispatchEvent(new FakeMouseEvent('mousedown', { pageX: x + 10, pageY: y })); 250 | hue.dispatchEvent(new FakeMouseEvent('mouseup', { pageX: x + 10, pageY: y })); 251 | expect(spy.callCount).to.equal(1); 252 | }); 253 | 254 | it('should dispatch color-changed event on mousemove', () => { 255 | const spy = sinon.spy(); 256 | picker.addEventListener('color-changed', spy); 257 | const { x, y } = middleOfNode(hue); 258 | hue.dispatchEvent(new FakeMouseEvent('mousedown', { pageX: x + 10, pageY: y })); 259 | hue.dispatchEvent(new FakeMouseEvent('mousemove', { pageX: x + 20, pageY: y })); 260 | hue.dispatchEvent(new FakeMouseEvent('mouseup', { pageX: x + 20, pageY: y })); 261 | expect(spy.callCount).to.equal(2); 262 | }); 263 | 264 | it('should dispatch color-changed event on touchstart', () => { 265 | const spy = sinon.spy(); 266 | picker.addEventListener('color-changed', spy); 267 | const { x, y } = middleOfNode(saturation); 268 | saturation.dispatchEvent(new FakeTouchEvent('touchstart', [{ pageX: x + 10, pageY: y }])); 269 | saturation.dispatchEvent(new FakeTouchEvent('touchend', [{ pageX: x + 10, pageY: y }])); 270 | expect(spy.callCount).to.equal(1); 271 | }); 272 | 273 | it('should dispatch color-changed event on touchmove', () => { 274 | const spy = sinon.spy(); 275 | picker.addEventListener('color-changed', spy); 276 | const { x, y } = middleOfNode(saturation); 277 | saturation.dispatchEvent(new FakeTouchEvent('touchstart', [{ pageX: x + 10, pageY: y }])); 278 | saturation.dispatchEvent(new FakeTouchEvent('touchmove', [{ pageX: x + 20, pageY: y }])); 279 | saturation.dispatchEvent(new FakeTouchEvent('touchend', [{ pageX: x + 20, pageY: y }])); 280 | expect(spy.callCount).to.equal(2); 281 | }); 282 | 283 | it('should dispatch color-changed event on alpha interaction', () => { 284 | const spy = sinon.spy(); 285 | picker.addEventListener('color-changed', spy); 286 | const { x, y } = middleOfNode(alpha); 287 | alpha.dispatchEvent(new FakeTouchEvent('touchstart', [{ pageX: x, pageY: y }])); 288 | alpha.dispatchEvent(new FakeTouchEvent('touchend', [{ pageX: x, pageY: y }])); 289 | expect(spy.callCount).to.equal(1); 290 | }); 291 | 292 | it('should not dispatch event when hue changes for black', () => { 293 | picker.color = { r: 0, g: 0, b: 0, a: 1 }; 294 | const spy = sinon.spy(); 295 | picker.addEventListener('color-changed', spy); 296 | const { x, y } = middleOfNode(hue); 297 | hue.dispatchEvent(new FakeTouchEvent('touchstart', [{ pageX: x + 10, pageY: y }])); 298 | hue.dispatchEvent(new FakeTouchEvent('touchmove', [{ pageX: x + 20, pageY: y }])); 299 | hue.dispatchEvent(new FakeTouchEvent('touchend', [{ pageX: x + 20, pageY: y }])); 300 | expect(spy.callCount).to.equal(0); 301 | }); 302 | 303 | it('should not react on mouse events after a touch interaction', () => { 304 | picker.color = { r: 0, g: 0, b: 255, a: 1 }; 305 | const spy = sinon.spy(); 306 | picker.addEventListener('color-changed', spy); 307 | const { left, top, height } = hue.getBoundingClientRect(); 308 | const y = top + height / 2; 309 | hue.dispatchEvent(new FakeTouchEvent('touchstart', [{ pageX: left, pageY: y }])); // 1 (#ff0000) 310 | hue.dispatchEvent(new FakeTouchEvent('touchmove', [{ pageX: left + 50, pageY: y }])); // 2 (#00ffff) 311 | // Should be skipped 312 | hue.dispatchEvent(new FakeMouseEvent('mousedown', { pageX: left + 65, pageY: y })); // 3 313 | hue.dispatchEvent(new FakeMouseEvent('mousemove', { pageX: left + 125, pageY: y })); // 4 314 | expect(spy.callCount).to.equal(2); 315 | }); 316 | 317 | it('should not reset hue after saturation is changed', () => { 318 | picker.color = { r: 0, g: 0, b: 0, a: 1 }; 319 | 320 | const { x: hx, y: hy } = middleOfNode(hue); 321 | hue.dispatchEvent(new FakeTouchEvent('touchstart', [{ pageX: hx, pageY: hy }])); 322 | hue.dispatchEvent(new FakeTouchEvent('touchend', [{ pageX: hx, pageY: hy }])); 323 | 324 | const { x: sx, y: sy } = middleOfNode(saturation); 325 | saturation.dispatchEvent(new FakeTouchEvent('touchstart', [{ pageX: sx, pageY: sy }])); 326 | saturation.dispatchEvent(new FakeTouchEvent('touchend', [{ pageX: sx, pageY: sy }])); 327 | 328 | expect(picker.color).to.deep.equal({ r: 64, g: 128, b: 128, a: 1 }); 329 | }); 330 | }); 331 | }); 332 | -------------------------------------------------------------------------------- /src/test/hex-input.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { sendKeys } from '@web/test-runner-commands'; 3 | import sinon from 'sinon'; 4 | import { fixture, html, nextFrame } from '@open-wc/testing-helpers'; 5 | import type { HexInput } from '../hex-input'; 6 | 7 | describe('hex-input', () => { 8 | let input: HexInput; 9 | let target: HTMLInputElement; 10 | 11 | function getTarget(input: HexInput) { 12 | const root = input.shadowRoot as ShadowRoot; 13 | return root.querySelector('input') as HTMLInputElement; 14 | } 15 | 16 | describe('lazy upgrade', () => { 17 | it('should work with all properties set before upgrade', async () => { 18 | const element = document.createElement('hex-input'); 19 | document.body.appendChild(element); 20 | element.alpha = true; 21 | element.prefixed = true; 22 | element.color = '#123'; 23 | await import('../hex-input'); 24 | expect(element.color).to.equal('#123'); 25 | expect(element.alpha).to.be.true; 26 | expect(element.prefixed).to.be.true; 27 | target = getTarget(element); 28 | expect(target.value).to.equal('#123'); 29 | document.body.removeChild(element); 30 | }); 31 | }); 32 | 33 | describe('default', () => { 34 | beforeEach(async () => { 35 | input = await fixture(html``); 36 | target = getTarget(input); 37 | }); 38 | 39 | it('should set color property to empty string', () => { 40 | expect(input.color).to.equal(''); 41 | }); 42 | 43 | it('should set native input value to empty string', () => { 44 | expect(target.value).to.equal(''); 45 | }); 46 | 47 | it('should set part attribute on the native input', () => { 48 | expect(target.getAttribute('part')).to.equal('input'); 49 | }); 50 | 51 | it('should set spellcheck to false on the native input', () => { 52 | expect(target.getAttribute('spellcheck')).to.equal('false'); 53 | }); 54 | }); 55 | 56 | describe('initialization', () => { 57 | beforeEach(async () => { 58 | input = document.createElement('hex-input'); 59 | }); 60 | 61 | afterEach(() => { 62 | document.body.removeChild(input); 63 | }); 64 | 65 | it('should handle property set before adding to the DOM', () => { 66 | input.color = '#123'; 67 | document.body.appendChild(input); 68 | expect(getTarget(input).value).to.equal('123'); 69 | }); 70 | 71 | it('should handle attribute set before adding to the DOM', () => { 72 | input.setAttribute('color', '#123'); 73 | document.body.appendChild(input); 74 | expect(getTarget(input).value).to.equal('123'); 75 | }); 76 | 77 | it('should not throw when removing and adding from the DOM', async () => { 78 | input.color = '#123'; 79 | document.body.appendChild(input); 80 | document.body.removeChild(input); 81 | await nextFrame(); 82 | document.body.appendChild(input); 83 | expect(getTarget(input).value).to.equal('123'); 84 | }); 85 | }); 86 | 87 | describe('color property', () => { 88 | beforeEach(async () => { 89 | input = await fixture(html``); 90 | target = getTarget(input); 91 | }); 92 | 93 | it('should accept color set as a property', () => { 94 | expect(input.color).to.equal('#ccc'); 95 | }); 96 | 97 | it('should pass property value to native input', () => { 98 | expect(target.value).to.equal('ccc'); 99 | }); 100 | 101 | it('should not reflect property to attribute', () => { 102 | expect(input.getAttribute('color')).to.equal(null); 103 | }); 104 | }); 105 | 106 | describe('color attribute', () => { 107 | beforeEach(async () => { 108 | input = await fixture(html``); 109 | target = getTarget(input); 110 | }); 111 | 112 | it('should set color based on the attribute value', () => { 113 | expect(input.color).to.equal('#488'); 114 | }); 115 | 116 | it('should pass attribute value to native input', () => { 117 | expect(target.value).to.equal('488'); 118 | }); 119 | 120 | it('should not update attribute when property changes', () => { 121 | input.color = '#ccc'; 122 | expect(input.getAttribute('color')).to.equal('#488'); 123 | }); 124 | 125 | it('should update property when attribute changes', () => { 126 | input.setAttribute('color', '#ccc'); 127 | expect(input.color).to.equal('#ccc'); 128 | }); 129 | }); 130 | 131 | describe('empty value', () => { 132 | beforeEach(async () => { 133 | input = await fixture(html``); 134 | target = getTarget(input); 135 | }); 136 | 137 | it('should clean native input when color is set to empty string', () => { 138 | input.color = ''; 139 | expect(target.value).to.equal(''); 140 | }); 141 | 142 | it('should clean native input when color is set to null', () => { 143 | // @ts-expect-error 144 | input.color = null; 145 | expect(target.value).to.equal(''); 146 | }); 147 | 148 | it('should clean native input when color is set to undefined', () => { 149 | // @ts-expect-error 150 | input.color = undefined; 151 | expect(target.value).to.equal(''); 152 | }); 153 | }); 154 | 155 | describe('custom input', () => { 156 | beforeEach(async () => { 157 | input = await fixture(html``); 158 | target = input.querySelector('input') as HTMLInputElement; 159 | }); 160 | 161 | it('should pass attribute value to custom input', () => { 162 | expect(target.value).to.equal('488'); 163 | }); 164 | 165 | it('should pass property value to custom input', () => { 166 | input.color = '#ccc'; 167 | expect(target.value).to.equal('ccc'); 168 | }); 169 | 170 | it('should use default input if custom input removed', async () => { 171 | input.removeChild(target); 172 | target.value = ''; 173 | await nextFrame(); 174 | expect(getTarget(input).value).to.equal('488'); 175 | }); 176 | 177 | it('should update custom input if removed and added', async () => { 178 | input.removeChild(target); 179 | target.value = ''; 180 | await nextFrame(); 181 | input.appendChild(target); 182 | await nextFrame(); 183 | expect(target.value).to.equal('488'); 184 | }); 185 | }); 186 | 187 | describe('invalid content', () => { 188 | beforeEach(async () => { 189 | input = await fixture(html``); 190 | }); 191 | 192 | it('should remove invalid slotted content', () => { 193 | expect(input.querySelector('span')).to.be.not.ok; 194 | }); 195 | }); 196 | 197 | describe('events', () => { 198 | beforeEach(async () => { 199 | input = await fixture(html``); 200 | target = getTarget(input); 201 | target.focus(); 202 | }); 203 | 204 | it('should dispatch color-changed event on valid hex input', async () => { 205 | const spy = sinon.spy(); 206 | input.addEventListener('color-changed', spy); 207 | await sendKeys({ press: '3' }); 208 | await sendKeys({ press: '6' }); 209 | await sendKeys({ press: '9' }); 210 | expect(spy.callCount).to.equal(1); 211 | }); 212 | 213 | it('should not dispatch color-changed event on invalid input', async () => { 214 | const spy = sinon.spy(); 215 | input.addEventListener('color-changed', spy); 216 | await sendKeys({ press: '3' }); 217 | await sendKeys({ press: '6' }); 218 | expect(spy.callCount).to.equal(0); 219 | }); 220 | 221 | it('should restore color value on blur after invalid input', async () => { 222 | await sendKeys({ press: '3' }); 223 | await sendKeys({ press: '6' }); 224 | target.dispatchEvent(new Event('blur')); 225 | expect(input.color).to.equal('#488'); 226 | expect(target.value).to.equal('488'); 227 | }); 228 | 229 | it('should dispatch color-changed event on blur after clearing input', async () => { 230 | const spy = sinon.spy(); 231 | input.addEventListener('color-changed', spy); 232 | target.select(); 233 | await sendKeys({ press: 'Backspace' }); 234 | target.dispatchEvent(new Event('blur')); 235 | expect(spy.callCount).to.equal(1); 236 | }); 237 | 238 | it('should not restore color value on blur after clearing input', async () => { 239 | target.select(); 240 | await sendKeys({ press: 'Backspace' }); 241 | target.dispatchEvent(new Event('blur')); 242 | expect(input.color).to.equal(''); 243 | expect(target.value).to.equal(''); 244 | }); 245 | }); 246 | 247 | describe('alpha', () => { 248 | describe('property', () => { 249 | beforeEach(async () => { 250 | input = await fixture(html``); 251 | input.alpha = true; 252 | input.color = '#11223344'; 253 | target = getTarget(input); 254 | }); 255 | 256 | it('should allow setting 8 digits HEX when alpha is set with property', () => { 257 | expect(input.color).to.equal('#11223344'); 258 | expect(target.value).to.equal('11223344'); 259 | }); 260 | 261 | it('should set alpha attribute when property is set to true', () => { 262 | expect(input.hasAttribute('alpha')).to.be.true; 263 | }); 264 | 265 | it('should remove alpha attribute when property is set to false', () => { 266 | input.alpha = false; 267 | expect(input.hasAttribute('alpha')).to.be.false; 268 | }); 269 | 270 | it('should update input value to 6 digits when alpha is set to false', () => { 271 | input.alpha = false; 272 | expect(input.color).to.equal('#112233'); 273 | expect(target.value).to.equal('112233'); 274 | }); 275 | 276 | it('should update non-prefixed value to 6 digits when alpha is set to false', () => { 277 | input.color = '11223344'; 278 | input.alpha = false; 279 | expect(input.color).to.equal('112233'); 280 | expect(target.value).to.equal('112233'); 281 | }); 282 | 283 | it('should update shorthand value to 3 digits when alpha is set to false', () => { 284 | input.color = '#1234'; 285 | input.alpha = false; 286 | expect(input.color).to.equal('#123'); 287 | expect(target.value).to.equal('123'); 288 | }); 289 | 290 | it('should update non-prefixed shorthand value to 3 digits when alpha is set to false', () => { 291 | input.color = '1234'; 292 | input.alpha = false; 293 | expect(input.color).to.equal('123'); 294 | expect(target.value).to.equal('123'); 295 | }); 296 | 297 | it('should not allow using 8 digits HEX when alpha is set to false', async () => { 298 | input.alpha = false; 299 | input.focus(); 300 | 301 | await sendKeys({ press: '3' }); 302 | await sendKeys({ press: '6' }); 303 | target.dispatchEvent(new Event('blur')); 304 | 305 | expect(target.value).to.equal('112233'); 306 | }); 307 | }); 308 | 309 | describe('attribute', () => { 310 | beforeEach(async () => { 311 | input = await fixture(html``); 312 | target = getTarget(input); 313 | }); 314 | 315 | it('should allow setting 8 digits HEX when alpha is set with attribute', () => { 316 | expect(input.color).to.equal('#11223344'); 317 | expect(target.value).to.equal('11223344'); 318 | }); 319 | 320 | it('should set alpha property to false when attribute is removed', () => { 321 | input.removeAttribute('alpha'); 322 | expect(input.alpha).to.be.false; 323 | }); 324 | 325 | it('should update input value to 6 digits when attribute is removed', () => { 326 | input.removeAttribute('alpha'); 327 | expect(input.color).to.equal('#112233'); 328 | expect(target.value).to.equal('112233'); 329 | }); 330 | 331 | it('should update non-prefixed value to 6 digits when attribute is removed', () => { 332 | input.color = '11223344'; 333 | input.removeAttribute('alpha'); 334 | expect(input.color).to.equal('112233'); 335 | expect(target.value).to.equal('112233'); 336 | }); 337 | 338 | it('should update shorthand value to 3 digits when attribute is removed', () => { 339 | input.color = '#1234'; 340 | input.removeAttribute('alpha'); 341 | expect(input.color).to.equal('#123'); 342 | expect(target.value).to.equal('123'); 343 | }); 344 | 345 | it('should update non-prefixed shorthand value to 3 digits when attribute is removed', () => { 346 | input.color = '1234'; 347 | input.removeAttribute('alpha'); 348 | expect(input.color).to.equal('123'); 349 | expect(target.value).to.equal('123'); 350 | }); 351 | 352 | it('should not allow using 8 digits HEX when attribute is removed', async () => { 353 | input.removeAttribute('alpha'); 354 | input.focus(); 355 | 356 | await sendKeys({ press: '3' }); 357 | await sendKeys({ press: '6' }); 358 | target.dispatchEvent(new Event('blur')); 359 | 360 | expect(target.value).to.equal('112233'); 361 | }); 362 | }); 363 | }); 364 | 365 | describe('prefixed', () => { 366 | describe('property', () => { 367 | beforeEach(async () => { 368 | input = await fixture(html``); 369 | input.prefixed = true; 370 | input.color = '#112233'; 371 | target = getTarget(input); 372 | }); 373 | 374 | it('should set prefixed attribute when prefixed property is set', () => { 375 | expect(input.hasAttribute('prefixed')).to.be.true; 376 | }); 377 | 378 | it('should set # to the input when prefixed property is set', () => { 379 | expect(target.value).to.equal('#112233'); 380 | }); 381 | 382 | it('should remove prefixed attribute when prefixed property is set to false', () => { 383 | input.prefixed = false; 384 | expect(input.hasAttribute('prefixed')).to.be.false; 385 | }); 386 | 387 | it('should remove # from the input when prefixed property is set to false', () => { 388 | input.prefixed = false; 389 | expect(target.value).to.equal('112233'); 390 | }); 391 | }); 392 | 393 | describe('attribute', () => { 394 | beforeEach(async () => { 395 | input = await fixture(html``); 396 | target = getTarget(input); 397 | }); 398 | 399 | it('should set prefixed to true when prefixed attribute is set', () => { 400 | expect(input.prefixed).to.be.true; 401 | }); 402 | 403 | it('should set # to the input when prefixed attribute is set', () => { 404 | expect(target.value).to.equal('#112233'); 405 | }); 406 | 407 | it('should set prefixed to false when prefixed attribute is removed', () => { 408 | input.removeAttribute('prefixed'); 409 | expect(input.prefixed).to.be.false; 410 | }); 411 | 412 | it('should remove # from the input when prefixed attribute is removed', () => { 413 | input.removeAttribute('prefixed'); 414 | expect(target.value).to.equal('112233'); 415 | }); 416 | }); 417 | }); 418 | }); 419 | -------------------------------------------------------------------------------- /src/test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { clamp, round } from '../lib/utils/math.js'; 3 | import { 4 | hexToRgba, 5 | hexToHsva, 6 | hslaStringToHsva, 7 | hslaToHsl, 8 | hslaToHsva, 9 | hslStringToHsva, 10 | hsvaStringToHsva, 11 | hsvaToHex, 12 | hsvaToHsla, 13 | hsvaToHslaString, 14 | hsvaToHslString, 15 | hsvaToHsvaString, 16 | hsvaToHsv, 17 | hsvaToHsvString, 18 | hsvaToRgba, 19 | hsvaToRgbaString, 20 | hsvaToRgbString, 21 | hsvStringToHsva, 22 | rgbaStringToHsva, 23 | rgbaToHex, 24 | rgbaToHsva, 25 | rgbaToRgb, 26 | rgbStringToHsva, 27 | roundHsva 28 | } from '../lib/utils/convert.js'; 29 | import { equalColorObjects, equalColorString, equalHex } from '../lib/utils/compare.js'; 30 | import { validHex } from '../lib/utils/validate.js'; 31 | import type { HslaColor, HsvaColor, RgbaColor } from '../lib/types.js'; 32 | 33 | describe('Utils', () => { 34 | it('Converts HEX to HSV', () => { 35 | expect(hexToHsva('#ffffff')).to.deep.equal({ h: 0, s: 0, v: 100, a: 1 }); 36 | expect(hexToHsva('#ffff00')).to.deep.equal({ h: 60, s: 100, v: 100, a: 1 }); 37 | expect(hexToHsva('#ff0000')).to.deep.equal({ h: 0, s: 100, v: 100, a: 1 }); 38 | expect(hexToHsva('#000000')).to.deep.equal({ h: 0, s: 0, v: 0, a: 1 }); 39 | expect(hexToHsva('#c62182')).to.deep.equal({ h: 325, s: 83, v: 78, a: 1 }); 40 | }); 41 | 42 | it('Converts shorthand HEX to HSVA', () => { 43 | expect(hexToHsva('#FFF')).to.deep.equal({ h: 0, s: 0, v: 100, a: 1 }); 44 | expect(hexToHsva('#FF0')).to.deep.equal({ h: 60, s: 100, v: 100, a: 1 }); 45 | expect(hexToHsva('#F00')).to.deep.equal({ h: 0, s: 100, v: 100, a: 1 }); 46 | expect(hexToHsva('#ABC')).to.deep.equal({ h: 210, s: 17, v: 80, a: 1 }); 47 | }); 48 | 49 | it('Converts HEX with alpha to RGBA', () => { 50 | expect(hexToRgba('#11223399')).to.deep.equal({ r: 17, g: 34, b: 51, a: 0.6 }); 51 | expect(hexToRgba('#11223300')).to.deep.equal({ r: 17, g: 34, b: 51, a: 0 }); 52 | expect(hexToRgba('#112233')).to.deep.equal({ r: 17, g: 34, b: 51, a: 1 }); 53 | }); 54 | 55 | it('Converts shorthand HEX with alpha to RGBA', () => { 56 | expect(hexToRgba('#1239')).to.deep.equal({ r: 17, g: 34, b: 51, a: 0.6 }); 57 | expect(hexToRgba('#1230')).to.deep.equal({ r: 17, g: 34, b: 51, a: 0 }); 58 | expect(hexToRgba('#123')).to.deep.equal({ r: 17, g: 34, b: 51, a: 1 }); 59 | }); 60 | 61 | it('Converts HSV to HEX', () => { 62 | expect(hsvaToHex({ h: 0, s: 0, v: 100, a: 1 })).to.equal('#ffffff'); 63 | expect(hsvaToHex({ h: 60, s: 100, v: 100, a: 1 })).to.equal('#ffff00'); 64 | expect(hsvaToHex({ h: 0, s: 100, v: 100, a: 1 })).to.equal('#ff0000'); 65 | expect(hsvaToHex({ h: 0, s: 0, v: 0, a: 1 })).to.equal('#000000'); 66 | expect(hsvaToHex({ h: 284, s: 93, v: 73, a: 1 })).to.equal('#8c0dba'); 67 | }); 68 | 69 | it('Converts HSVA to HSLA', () => { 70 | const test = (input: HsvaColor, output: HslaColor) => 71 | expect(hsvaToHsla(input)).to.deep.equal(output); 72 | 73 | test({ h: 0, s: 0, v: 100, a: 1 }, { h: 0, s: 0, l: 100, a: 1 }); 74 | test({ h: 60, s: 100, v: 100, a: 1 }, { h: 60, s: 100, l: 50, a: 1 }); 75 | test({ h: 0, s: 100, v: 100, a: 1 }, { h: 0, s: 100, l: 50, a: 1 }); 76 | test({ h: 0, s: 0, v: 0, a: 1 }, { h: 0, s: 0, l: 0, a: 1 }); 77 | test({ h: 200, s: 40, v: 40, a: 1 }, { h: 200, s: 25, l: 32, a: 1 }); 78 | }); 79 | 80 | it('Converts HSLA to HSVA', () => { 81 | const test = (input: HslaColor, output: HsvaColor) => 82 | expect(hslaToHsva(input)).to.deep.equal(output); 83 | 84 | test({ h: 0, s: 0, l: 100, a: 1 }, { h: 0, s: 0, v: 100, a: 1 }); 85 | test({ h: 60, s: 100, l: 50, a: 1 }, { h: 60, s: 100, v: 100, a: 1 }); 86 | test({ h: 0, s: 100, l: 50, a: 1 }, { h: 0, s: 100, v: 100, a: 1 }); 87 | test({ h: 0, s: 0, l: 0, a: 1 }, { h: 0, s: 0, v: 0, a: 1 }); 88 | test({ h: 200, s: 25, l: 32, a: 1 }, { h: 200, s: 40, v: 40, a: 1 }); 89 | }); 90 | 91 | it('Converts HSVA to HSL string', () => { 92 | expect(hsvaToHslString({ h: 200, s: 40, v: 40, a: 1 })).to.equal('hsl(200, 25%, 32%)'); 93 | expect(hsvaToHslString({ h: 0, s: 0, v: 0, a: 1 })).to.equal('hsl(0, 0%, 0%)'); 94 | }); 95 | 96 | it('Converts HSVA to HSLA string', () => { 97 | expect(hsvaToHslaString({ h: 200, s: 40, v: 40, a: 0.5 })).to.equal('hsla(200, 25%, 32%, 0.5)'); 98 | expect(hsvaToHslaString({ h: 0, s: 0, v: 0, a: 0 })).to.equal('hsla(0, 0%, 0%, 0)'); 99 | }); 100 | 101 | it('Converts HSL string to HSV', () => { 102 | expect(hslStringToHsva('hsl(0, 0%, 100%)')).to.deep.equal({ h: 0, s: 0, v: 100, a: 1 }); 103 | expect(hslStringToHsva('hsl(0,0,100)')).to.deep.equal({ h: 0, s: 0, v: 100, a: 1 }); 104 | expect(hslStringToHsva('hsl(60, 100%, 50%)')).to.deep.equal({ h: 60, s: 100, v: 100, a: 1 }); 105 | expect(hslStringToHsva('hsl(0, 100%, 50%)')).to.deep.equal({ h: 0, s: 100, v: 100, a: 1 }); 106 | expect(hslStringToHsva('hsl(0, 0%, 0%)')).to.deep.equal({ h: 0, s: 0, v: 0, a: 1 }); 107 | expect(hslStringToHsva('hsl(200, 25%, 32%)')).to.deep.equal({ h: 200, s: 40, v: 40, a: 1 }); 108 | }); 109 | 110 | it('Converts HSLA string to HSVA', () => { 111 | const test = (input: string, output: HsvaColor) => 112 | expect(hslaStringToHsva(input)).to.deep.equal(output); 113 | 114 | test('hsla(0deg, 0%, 0%, 0.5)', { h: 0, s: 0, v: 0, a: 0.5 }); 115 | test('hsla(200, 25%, 32%, 1)', { h: 200, s: 40, v: 40, a: 1 }); 116 | test('hsla(.5turn 25% 32% / 50%)', { h: 180, s: 40, v: 40, a: 0.5 }); 117 | }); 118 | 119 | it('Converts HSVA to RGBA', () => { 120 | const test = (input: HsvaColor, output: RgbaColor) => 121 | expect(hsvaToRgba(input)).to.deep.equal(output); 122 | 123 | test({ h: 0, s: 0, v: 100, a: 1 }, { r: 255, g: 255, b: 255, a: 1 }); 124 | test({ h: 0, s: 100, v: 100, a: 0.5 }, { r: 255, g: 0, b: 0, a: 0.5 }); 125 | test({ h: 0, s: 100, v: 100, a: 0.567 }, { r: 255, g: 0, b: 0, a: 0.57 }); 126 | }); 127 | 128 | it('Converts RGBA to HSVA', () => { 129 | const test = (input: RgbaColor, output: HsvaColor) => 130 | expect(rgbaToHsva(input)).to.deep.equal(output); 131 | 132 | test({ r: 255, g: 255, b: 255, a: 1 }, { h: 0, s: 0, v: 100, a: 1 }); 133 | test({ r: 255, g: 0, b: 0, a: 1 }, { h: 0, s: 100, v: 100, a: 1 }); 134 | }); 135 | 136 | it('Converts RGBA to HEX', () => { 137 | expect(rgbaToHex({ r: 17, g: 34, b: 51, a: 0.6 })).to.deep.equal('#11223399'); 138 | expect(rgbaToHex({ r: 17, g: 34, b: 51, a: 0 })).to.deep.equal('#11223300'); 139 | expect(rgbaToHex({ r: 17, g: 34, b: 51, a: 1 })).to.deep.equal('#112233'); 140 | }); 141 | 142 | it('Converts RGB string to HSVA', () => { 143 | expect(rgbStringToHsva('rgb(255, 255, 255)')).to.deep.equal({ h: 0, s: 0, v: 100, a: 1 }); 144 | expect(rgbStringToHsva('rgb(0,0,0)')).to.deep.equal({ h: 0, s: 0, v: 0, a: 1 }); 145 | expect(rgbStringToHsva('rgb(61, 88, 102)')).to.deep.equal({ h: 200, s: 40, v: 40, a: 1 }); 146 | expect(rgbStringToHsva('rgb(100% 100% 100%)')).to.deep.equal({ h: 0, s: 0, v: 100, a: 1 }); 147 | expect(rgbStringToHsva('rgb(50% 45.9% 25%)')).to.deep.equal({ h: 50, s: 50, v: 50, a: 1 }); 148 | }); 149 | 150 | it('Converts HSVA to RGB string', () => { 151 | expect(hsvaToRgbString({ h: 0, s: 0, v: 100, a: 1 })).to.equal('rgb(255, 255, 255)'); 152 | expect(hsvaToRgbString({ h: 200, s: 40, v: 40, a: 1 })).to.equal('rgb(61, 88, 102)'); 153 | }); 154 | 155 | it('Converts RGBA string to HSVA', () => { 156 | const test = (input: string, output: HsvaColor) => 157 | expect(rgbaStringToHsva(input)).to.deep.equal(output); 158 | test('rgba(61, 88, 102, 0.5)', { h: 200, s: 40, v: 40, a: 0.5 }); 159 | test('rgba(23.9% 34.5% 40% / 99%)', { h: 200, s: 40, v: 40, a: 0.99 }); 160 | }); 161 | 162 | it('Converts HSVA to RGBA string', () => { 163 | expect(hsvaToRgbaString({ h: 0, s: 0, v: 100, a: 0.5 })).to.equal('rgba(255, 255, 255, 0.5)'); 164 | expect(hsvaToRgbaString({ h: 200, s: 40, v: 40, a: 0.5 })).to.equal('rgba(61, 88, 102, 0.5)'); 165 | }); 166 | 167 | it('Converts HSVA to HSVA string', () => { 168 | expect(hsvaToHsvaString({ h: 0, s: 0, v: 100, a: 1 })).to.equal('hsva(0, 0%, 100%, 1)'); 169 | expect(hsvaToHsvaString({ h: 200, s: 40, v: 40, a: 0 })).to.equal('hsva(200, 40%, 40%, 0)'); 170 | }); 171 | 172 | it('Converts HSVA to HSV string', () => { 173 | expect(hsvaToHsvString({ h: 0, s: 0, v: 100, a: 1 })).to.equal('hsv(0, 0%, 100%)'); 174 | expect(hsvaToHsvString({ h: 200, s: 40, v: 40, a: 1 })).to.equal('hsv(200, 40%, 40%)'); 175 | }); 176 | 177 | it('Converts HSV string to HSVA', () => { 178 | expect(hsvStringToHsva('hsv(0, 11%, 0%)')).to.deep.equal({ h: 0, s: 11, v: 0, a: 1 }); 179 | expect(hsvStringToHsva('hsv(90deg 20% 10%)')).to.deep.equal({ h: 90, s: 20, v: 10, a: 1 }); 180 | expect(hsvStringToHsva('hsv(100grad 20% 10%)')).to.deep.equal({ h: 90, s: 20, v: 10, a: 1 }); 181 | expect(hsvStringToHsva('hsv(0.25turn 20% 10%)')).to.deep.equal({ h: 90, s: 20, v: 10, a: 1 }); 182 | expect(hsvStringToHsva('hsv(1.5708rad 20% 10%)')).to.deep.equal({ h: 90, s: 20, v: 10, a: 1 }); 183 | }); 184 | 185 | it('Converts HSVA string to HSVA', () => { 186 | expect(hsvaStringToHsva('hsva(0, 11%, 0, 0.5)')).to.deep.equal({ h: 0, s: 11, v: 0, a: 0.5 }); 187 | expect(hsvaStringToHsva('hsva(5deg 9% 7% / 40%)')).to.deep.equal({ h: 5, s: 9, v: 7, a: 0.4 }); 188 | }); 189 | 190 | it('Converts HSVA to HSV', () => { 191 | expect(hsvaToHsv({ h: 200, s: 40, v: 40, a: 1 })).to.deep.equal({ h: 200, s: 40, v: 40 }); 192 | }); 193 | 194 | it('Converts HSLA to HSL', () => { 195 | expect(hslaToHsl({ h: 0, s: 0, l: 100, a: 1 })).to.deep.equal({ h: 0, s: 0, l: 100 }); 196 | }); 197 | 198 | it('Converts RGBA to RGB', () => { 199 | expect(rgbaToRgb({ r: 255, g: 255, b: 255, a: 1 })).to.deep.equal({ r: 255, g: 255, b: 255 }); 200 | }); 201 | 202 | it('Handles incorrect HSLA string', () => { 203 | expect(hslaStringToHsva('rgba(0,0,0,1)')).to.deep.equal({ h: 0, s: 0, v: 0, a: 1 }); 204 | }); 205 | 206 | it('Handles incorrect HSVA string', () => { 207 | expect(hsvaStringToHsva('hsla(0,0,0,1)')).to.deep.equal({ h: 0, s: 0, v: 0, a: 1 }); 208 | }); 209 | 210 | it('Handles incorrect RGBA string', () => { 211 | expect(rgbaStringToHsva('hsva(0,0,0,1)')).to.deep.equal({ h: 0, s: 0, v: 0, a: 1 }); 212 | }); 213 | 214 | it('Rounds HSVA', () => { 215 | const test = (input: HsvaColor, output: HsvaColor) => 216 | expect(roundHsva(input)).to.deep.equal(output); 217 | 218 | test({ h: 1, s: 1, v: 1, a: 1 }, { h: 1, s: 1, v: 1, a: 1 }); 219 | test({ h: 3.3333, s: 4.4444, v: 5.5555, a: 0.6789 }, { h: 3, s: 4, v: 6, a: 0.68 }); 220 | }); 221 | 222 | it('Compares two HEX colors', () => { 223 | expect(equalHex('#8c0dba', '#8c0dba')).to.equal(true); 224 | expect(equalHex('#FFFFFF', '#ffffff')).to.equal(true); 225 | expect(equalHex('#ABC', '#aabbcc')).to.equal(true); 226 | expect(equalHex('#abcdef', '#fedcbd')).to.equal(false); 227 | }); 228 | 229 | it('Compares two HSV colors', () => { 230 | expect(equalColorObjects({ h: 0, s: 0, v: 100 }, { h: 0, s: 0, v: 100 })).to.equal(true); 231 | expect(equalColorObjects({ h: 100, s: 50, v: 50 }, { h: 100, s: 50, v: 50 })).to.equal(true); 232 | expect(equalColorObjects({ h: 50, s: 0, v: 0 }, { h: 100, s: 0, v: 0 })).to.equal(false); 233 | expect(equalColorObjects({ h: 1, s: 2, v: 3 }, { h: 4, s: 5, v: 6 })).to.equal(false); 234 | }); 235 | 236 | it('Compares two equivalent objects', () => { 237 | const sameObject = { h: 0, s: 0, v: 100 }; 238 | expect(equalColorObjects(sameObject, sameObject)).to.equal(true); 239 | }); 240 | 241 | it('Compares two color strings', () => { 242 | expect(equalColorString('rgb(0, 100, 100)', 'rgb(0,100,100)')).to.equal(true); 243 | expect(equalColorString('hsl(0, 100%, 50%)', 'hsl(0,100%,50%)')).to.equal(true); 244 | }); 245 | 246 | it('Validates HEX colors', () => { 247 | // valid strings 248 | expect(validHex('#8c0dba')).to.equal(true); 249 | expect(validHex('aabbcc')).to.equal(true); 250 | expect(validHex('#ABC')).to.equal(true); 251 | expect(validHex('123')).to.equal(true); 252 | // out of [0-F] range 253 | expect(validHex('#eeffhh')).to.equal(false); 254 | // wrong length 255 | expect(validHex('#12')).to.equal(false); 256 | expect(validHex('#12345')).to.equal(false); 257 | // empty 258 | expect(validHex('')).to.equal(false); 259 | // @ts-expect-error 260 | expect(validHex(null)).to.equal(false); 261 | // @ts-expect-error 262 | expect(validHex()).to.equal(false); 263 | }); 264 | 265 | it('Clamps a number between bounds', () => { 266 | expect(clamp(0.5)).to.equal(0.5); 267 | expect(clamp(1.5)).to.equal(1); 268 | expect(clamp(-1)).to.equal(0); 269 | expect(clamp(50, -50, 100)).to.equal(50); 270 | expect(clamp(-500, -50, 100)).to.equal(-50); 271 | expect(clamp(500, -50, 100)).to.equal(100); 272 | }); 273 | 274 | it('Rounds a number', () => { 275 | expect(round(0)).to.equal(0); 276 | expect(round(1)).to.equal(1); 277 | expect(round(0.1)).to.equal(0); 278 | expect(round(0.9)).to.equal(1); 279 | expect(round(0.123, 2)).to.equal(0.12); 280 | expect(round(0.789, 2)).to.equal(0.79); 281 | expect(round(1, 10)).to.equal(1); 282 | expect(round(0.123, 10)).to.equal(0.123); 283 | }); 284 | }); 285 | -------------------------------------------------------------------------------- /src/test/visual/screenshots/Chrome/baseline/hex-alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-padawan/vanilla-colorful/b81455e7d1cb81979d2cf6dd151147766062fa79/src/test/visual/screenshots/Chrome/baseline/hex-alpha.png -------------------------------------------------------------------------------- /src/test/visual/screenshots/Chrome/baseline/hex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-padawan/vanilla-colorful/b81455e7d1cb81979d2cf6dd151147766062fa79/src/test/visual/screenshots/Chrome/baseline/hex.png -------------------------------------------------------------------------------- /src/test/visual/screenshots/Chrome/baseline/hsl-string.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-padawan/vanilla-colorful/b81455e7d1cb81979d2cf6dd151147766062fa79/src/test/visual/screenshots/Chrome/baseline/hsl-string.png -------------------------------------------------------------------------------- /src/test/visual/screenshots/Chrome/baseline/hsl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-padawan/vanilla-colorful/b81455e7d1cb81979d2cf6dd151147766062fa79/src/test/visual/screenshots/Chrome/baseline/hsl.png -------------------------------------------------------------------------------- /src/test/visual/screenshots/Chrome/baseline/hsla-string.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-padawan/vanilla-colorful/b81455e7d1cb81979d2cf6dd151147766062fa79/src/test/visual/screenshots/Chrome/baseline/hsla-string.png -------------------------------------------------------------------------------- /src/test/visual/screenshots/Chrome/baseline/hsla.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-padawan/vanilla-colorful/b81455e7d1cb81979d2cf6dd151147766062fa79/src/test/visual/screenshots/Chrome/baseline/hsla.png -------------------------------------------------------------------------------- /src/test/visual/screenshots/Chrome/baseline/hsv-string.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-padawan/vanilla-colorful/b81455e7d1cb81979d2cf6dd151147766062fa79/src/test/visual/screenshots/Chrome/baseline/hsv-string.png -------------------------------------------------------------------------------- /src/test/visual/screenshots/Chrome/baseline/hsv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-padawan/vanilla-colorful/b81455e7d1cb81979d2cf6dd151147766062fa79/src/test/visual/screenshots/Chrome/baseline/hsv.png -------------------------------------------------------------------------------- /src/test/visual/screenshots/Chrome/baseline/hsva-string.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-padawan/vanilla-colorful/b81455e7d1cb81979d2cf6dd151147766062fa79/src/test/visual/screenshots/Chrome/baseline/hsva-string.png -------------------------------------------------------------------------------- /src/test/visual/screenshots/Chrome/baseline/hsva.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-padawan/vanilla-colorful/b81455e7d1cb81979d2cf6dd151147766062fa79/src/test/visual/screenshots/Chrome/baseline/hsva.png -------------------------------------------------------------------------------- /src/test/visual/screenshots/Chrome/baseline/rgb-string.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-padawan/vanilla-colorful/b81455e7d1cb81979d2cf6dd151147766062fa79/src/test/visual/screenshots/Chrome/baseline/rgb-string.png -------------------------------------------------------------------------------- /src/test/visual/screenshots/Chrome/baseline/rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-padawan/vanilla-colorful/b81455e7d1cb81979d2cf6dd151147766062fa79/src/test/visual/screenshots/Chrome/baseline/rgb.png -------------------------------------------------------------------------------- /src/test/visual/screenshots/Chrome/baseline/rgba-string.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-padawan/vanilla-colorful/b81455e7d1cb81979d2cf6dd151147766062fa79/src/test/visual/screenshots/Chrome/baseline/rgba-string.png -------------------------------------------------------------------------------- /src/test/visual/screenshots/Chrome/baseline/rgba.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-padawan/vanilla-colorful/b81455e7d1cb81979d2cf6dd151147766062fa79/src/test/visual/screenshots/Chrome/baseline/rgba.png -------------------------------------------------------------------------------- /src/test/visual/visual.test.ts: -------------------------------------------------------------------------------- 1 | import { visualDiff } from '@web/test-runner-visual-regression'; 2 | import { fixture } from '@open-wc/testing-helpers'; 3 | import '../../hex-color-picker.js'; 4 | import '../../hex-alpha-color-picker.js'; 5 | import '../../hsl-color-picker.js'; 6 | import '../../hsl-string-color-picker.js'; 7 | import '../../hsla-color-picker.js'; 8 | import '../../hsla-string-color-picker.js'; 9 | import '../../hsv-color-picker.js'; 10 | import '../../hsv-string-color-picker.js'; 11 | import '../../hsva-color-picker.js'; 12 | import '../../hsva-string-color-picker.js'; 13 | import '../../rgb-color-picker.js'; 14 | import '../../rgb-string-color-picker.js'; 15 | import '../../rgba-color-picker.js'; 16 | import '../../rgba-string-color-picker.js'; 17 | 18 | describe('visual tests', () => { 19 | let picker: HTMLElement; 20 | 21 | [ 22 | 'hex', 23 | 'hex-alpha', 24 | 'hsl', 25 | 'hsl-string', 26 | 'hsla', 27 | 'hsla-string', 28 | 'hsv', 29 | 'hsv-string', 30 | 'hsva', 31 | 'hsva-string', 32 | 'rgb', 33 | 'rgb-string', 34 | 'rgba', 35 | 'rgba-string' 36 | ].forEach((type) => { 37 | describe(`${type}-color-picker`, () => { 38 | beforeEach(async () => { 39 | picker = await fixture(` 40 |
41 | <${type}-color-picker> 42 |
43 | `); 44 | }); 45 | 46 | it('should match screenshot', async () => { 47 | await visualDiff(picker, type); 48 | }); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts", "demo/*.js", "*.js"], 4 | "compilerOptions": { 5 | "checkJs": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./", 4 | "target": "esNext", 5 | "module": "esNext", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "sourceMap": true, 9 | "inlineSources": true, 10 | "lib": ["esnext", "es2017", "dom"], 11 | "moduleResolution": "node", 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "skipLibCheck": true, 19 | "strict": true, 20 | "allowSyntheticDefaultImports": true 21 | }, 22 | "include": ["src/**/*.ts"], 23 | "exclude": [] 24 | } 25 | -------------------------------------------------------------------------------- /web-test-runner.config.js: -------------------------------------------------------------------------------- 1 | import { esbuildPlugin } from '@web/dev-server-esbuild'; 2 | import { visualRegressionPlugin } from '@web/test-runner-visual-regression/plugin'; 3 | 4 | export default { 5 | nodeResolve: true, 6 | plugins: [ 7 | esbuildPlugin({ ts: true }), 8 | visualRegressionPlugin({ 9 | baseDir: 'src/test/visual/screenshots', 10 | diffOptions: { 11 | threshold: 0.2 12 | }, 13 | update: process.env.UPDATE_REFS === 'true' 14 | }) 15 | ], 16 | coverageConfig: { 17 | threshold: { 18 | statements: 100, 19 | branches: 100, 20 | functions: 100, 21 | lines: 100 22 | } 23 | }, 24 | }; 25 | --------------------------------------------------------------------------------