├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── build.yml ├── .gitignore ├── .husky └── pre-commit ├── LICENSE ├── README.md ├── bun.lockb ├── docs ├── .vitepress │ ├── components │ │ ├── InteractiveHomePageLayout.vue │ │ └── examples │ │ │ ├── Accessibility.vue │ │ │ ├── Basic.vue │ │ │ ├── NonCollapsingMode.vue │ │ │ ├── NonPrimary.vue │ │ │ └── Uncontrolled.vue │ ├── config.mjs │ └── theme │ │ └── index.js ├── api.md ├── examples.md ├── guide │ ├── faq.md │ ├── getting-started.md │ └── index.md ├── index.md └── public │ └── logo.png ├── examples ├── simple │ ├── README.md │ └── index.html ├── with-config │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── App.vue │ │ └── main.js │ └── vite.config.js ├── with-nuxt │ ├── .gitignore │ ├── README.md │ ├── app.vue │ ├── nuxt.config.ts │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── server │ │ └── tsconfig.json │ └── tsconfig.json └── with-uncontrolled-component │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ └── favicon.ico │ ├── src │ ├── App.vue │ └── main.js │ └── vite.config.js ├── favicon.ico ├── index.d.ts ├── index.html ├── package.json ├── prettier.config.js ├── screenshots └── thumbnail.png ├── scripts ├── build.js └── print-stats.js ├── src ├── App.vue ├── ColorPicker.vue └── main.js ├── tests └── unit │ ├── __snapshots__ │ └── index.spec.js.snap │ └── index.spec.js └── vite.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 0.75% 2 | last 2 major versions 3 | not ie <= 11 4 | not dead 5 | not op_mini all 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | insert_final_newline = false 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ['plugin:vue/vue3-essential'], 7 | rules: { 8 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 9 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.1, available at 118 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 119 | 120 | Community Impact Guidelines were inspired by 121 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 122 | 123 | For answers to common questions about this code of conduct, see the FAQ at 124 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 125 | at [https://www.contributor-covenant.org/translations][translations]. 126 | 127 | [homepage]: https://www.contributor-covenant.org 128 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 129 | [Mozilla CoC]: https://github.com/mozilla/diversity 130 | [FAQ]: https://www.contributor-covenant.org/faq 131 | [translations]: https://www.contributor-covenant.org/translations 132 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/radial-color-picker/vue-color-picker). 6 | 7 | ## Pull Requests 8 | 9 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 10 | 11 | - **Consider our release cycle** - We try to follow [SemVer](https://semver.org/). Randomly breaking public APIs is not an option. 12 | 13 | - **Create feature branches** - Don't ask us to pull from your main branch. 14 | 15 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 16 | 17 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 18 | 19 | **Happy coding**! -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | workflow_dispatch: 6 | 7 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 14 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 15 | concurrency: 16 | group: pages 17 | cancel-in-progress: false 18 | 19 | jobs: 20 | build: 21 | name: Code quality 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: oven-sh/setup-bun@v1 27 | - name: Install dependencies 28 | run: bun install 29 | - name: Build 30 | run: bun run build 31 | - name: Run linting 32 | run: bun run lint 33 | - name: Check Bundle Size 34 | run: bun run size-limit 35 | - name: Run tests 36 | run: bun run test:ci 37 | - name: Upload coverage to Codecov 38 | uses: codecov/codecov-action@v4 39 | with: 40 | token: ${{ secrets.CODECOV_TOKEN }} 41 | verbose: true 42 | - name: Setup Pages 43 | uses: actions/configure-pages@v4 44 | - name: Build with VitePress 45 | run: bun run docs:build 46 | - name: Upload artifact 47 | uses: actions/upload-pages-artifact@v3 48 | with: 49 | path: docs/.vitepress/dist 50 | 51 | deploy: 52 | environment: 53 | name: github-pages 54 | url: ${{ steps.deployment.outputs.page_url }} 55 | needs: build 56 | runs-on: ubuntu-latest 57 | name: Deploy 58 | steps: 59 | - name: Deploy to GitHub Pages 60 | id: deployment 61 | uses: actions/deploy-pages@v4 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | coverage 5 | 6 | # Log files 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | 19 | # vitepress folders 20 | .temp 21 | docs/.vitepress/cache 22 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | bun run lint-staged 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present, Rosen Kanev 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 | ## Radial Color Picker - Vue 2 | 3 |

screenshot

4 | 5 |

6 | 7 | Downloads 8 | 9 | 10 | Version 11 | 12 | 13 | License 14 | 15 | 16 | Code Coverage 17 | 18 |

19 | 20 | ## Introduction 21 | 22 | Great UX starts with two basic principles - ease of use and simplicity. Selecting a color should be as easy as moving a slider, clicking a checkbox or pressing a key just like other basic form elements behave. 23 | 24 | This is a flexible and minimalistic color picker. Developed with mobile devices and keyboard usage in mind. Key features: 25 | 26 | - Small size - 3.3 KB gzipped (JS and CSS combined) 27 | - Supports touch devices 28 | - Optimized animations 29 | - Ease of use 30 | - Screen reader support. 31 | - Tab to focus the picker. 32 | - or arrow key to increase hue. PgUp to go quicker. 33 | - or arrow key to decrease hue. PgDown to go quicker. 34 | - Enter to select a color and close the picker or to open it. 35 | - Mouse ScrollUp to increase and ScrollDown to decrease hue (Opt-in). 36 | - TypeScript support 37 | 38 | ## Documentation 39 | 40 | You can find the documentation on the [website](https://radial-color-picker.github.io/vue-color-picker/). 41 | The documentation is divided into several sections: 42 | 43 | - [Getting Started](https://radial-color-picker.github.io/vue-color-picker/guide/getting-started.html) 44 | - [Examples](https://radial-color-picker.github.io/vue-color-picker/examples.html) 45 | - [Config Reference](https://radial-color-picker.github.io/vue-color-picker/api.html) 46 | - [First Asked Questions](https://radial-color-picker.github.io/vue-color-picker/guide/faq.html) 47 | 48 | ## Ecosystem 49 | 50 | The right color picker, but not the framework you're looking for? 51 | 52 | - [Vue][link-vue-color-picker] - you're here! 53 | - [React][link-react-color-picker] 54 | - [AngularJs][link-angularjs-color-picker] 55 | - [Angular][link-angular-color-picker] 56 | 57 | ## Demos 58 | 59 | - Basic Example - [Codepen](https://codepen.io/rkunev/pen/zjEmwV/) 60 | - Other Examples in the [docs](https://radial-color-picker.github.io/vue-color-picker/examples.html) 61 | 62 | ## Usage 63 | 64 | Color Picker on [npm](https://www.npmjs.com/package/@radial-color-picker/vue-color-picker) 65 | 66 | ```bash 67 | npm install @radial-color-picker/vue-color-picker 68 | ``` 69 | 70 | And in your app: 71 | 72 | ```vue 73 | 76 | 77 | 101 | ``` 102 | 103 | ## Change log 104 | 105 | Please see [Releases][link-releases] for more information on what has changed recently. 106 | 107 | ## Vue 3 and `@vue/compat` 108 | 109 | If your app isn't ready to upgrade to Vue 3 you can slowly transition via `@vue/compat` and the [instructions provided there](https://www.npmjs.com/package/@vue/compat). Once you're done you might notice a warning message - `ATTR_FALSE_VALUE`. This is intended behavior and you can optionally silence the warning with `configureCompat({ ATTR_FALSE_VALUE: false })`. When using Vue 3 without the compat layer the warning will go away too. 110 | 111 |
112 | Details 113 |
114 | [Vue warn]: (deprecation ATTR_FALSE_VALUE) Attribute "aria-disabled" with v-bind value `false` will 
render aria-disabled="false" instead of removing it in Vue 3. To remove the attribute, use `null` or
`undefined` instead. If the usage is intended, you can disable the compat behavior and suppress this warning with:
115 | configureCompat({ ATTR_FALSE_VALUE: false })
116 | Details: https://v3.vuejs.org/guide/migration/attribute-coercion.html 117 | at <ColorPicker> 118 | at <App>
119 |
120 | 121 | ## Migration 122 | 123 | ### Migration from v3 124 | 125 | 1. Double-click to move the knob to the current position of the pointer is gone since this is now the default behavior as soon as the clicks on the palette. If you had a tooltip or a help section in your app that described the shortcut you should remove it. 126 | 127 | 2. With v4 the keyboard shortcuts are better aligned with the suggested keys for any [sliders](https://www.w3.org/TR/wai-aria-practices/#slider). This means that the Shift/Ctrl + ↑/→/Shift/Ctrl + ↓/← non-standard key combos are replaced by the simpler PageDown and PageUp. If you had a tooltip or a help section in your app that described the shortcut keys you should update it. 128 | 129 | 3. The `@change` event is now emitted when the user changes the color (knob drop, click on the palette, keyboard interaction, scroll) and a `@select` event is emitted when interacting with the color well (middle selector). 130 | 131 | ```diff 132 | 137 | 138 | 144 | ``` 145 | 146 | ## Contributing 147 | 148 | If you're interested in the project you can help out with feature requests, bugfixes, documentation improvements or any other helpful contributions. You can use the issue list of this repo for bug reports and feature requests and as well as for questions and support. 149 | 150 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) and [CODE_OF_CONDUCT](.github/CODE_OF_CONDUCT.md) for details. 151 | 152 | ## Credits 153 | 154 | - [Rosen Kanev][link-author] 155 | - [Dennis Dierkes](https://github.com/deen13) 156 | - [All Contributors][link-contributors] 157 | 158 | This component is based on the great work that was done for the [AngularJs color picker][link-angularjs-color-picker]. 159 | 160 | ## License 161 | 162 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 163 | 164 | [link-react-color-picker]: https://github.com/radial-color-picker/react-color-picker 165 | [link-vue-color-picker]: https://github.com/radial-color-picker/vue-color-picker 166 | [link-angular-color-picker]: https://github.com/radial-color-picker/angular-color-picker 167 | [link-angularjs-color-picker]: https://github.com/talamaska/angular-radial-color-picker 168 | [link-author]: https://github.com/rkunev 169 | [link-contributors]: ../../contributors 170 | [link-releases]: ../../releases -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radial-color-picker/vue-color-picker/76d7732df988effe04b8da6fe53b8aba934f7aa5/bun.lockb -------------------------------------------------------------------------------- /docs/.vitepress/components/InteractiveHomePageLayout.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 27 | 28 | 34 | -------------------------------------------------------------------------------- /docs/.vitepress/components/examples/Accessibility.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 38 | -------------------------------------------------------------------------------- /docs/.vitepress/components/examples/Basic.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /docs/.vitepress/components/examples/NonCollapsingMode.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /docs/.vitepress/components/examples/NonPrimary.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | -------------------------------------------------------------------------------- /docs/.vitepress/components/examples/Uncontrolled.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress'; 2 | 3 | export default defineConfig({ 4 | base: '/vue-color-picker/', 5 | title: 'Radial Color Picker', 6 | description: 'Minimalistic color picker with a focus on size, accessibility and performance.', 7 | head: [ 8 | ['link', { rel: 'icon', href: '/vue-color-picker/logo.png', type: 'image/png' }], 9 | ['meta', { property: 'og:image', content: '/vue-color-picker/logo.png' }], 10 | ['meta', { property: 'og:image:height', content: '640' }], 11 | ['meta', { property: 'og:image:width', content: '640' }], 12 | ['meta', { property: 'og:locale', content: 'en' }], 13 | ['meta', { property: 'og:site_name', content: 'Radial Color Picker' }], 14 | [ 15 | 'meta', 16 | { 17 | property: 'og:title', 18 | content: 19 | 'Radial Color Picker | Minimalistic color picker with a focus on size, accessibility and performance.', 20 | }, 21 | ], 22 | ['meta', { property: 'og:type', content: 'website' }], 23 | ['meta', { property: 'og:url', content: 'https://radial-color-picker.github.io/vue-color-picker/' }], 24 | ], 25 | 26 | themeConfig: { 27 | logo: '/logo.png', 28 | 29 | nav: [ 30 | { text: 'Guide', link: '/guide/' }, 31 | { text: 'Examples', link: '/examples' }, 32 | { text: 'Reference', link: '/api' }, 33 | ], 34 | 35 | sidebar: { 36 | '/guide': [ 37 | { 38 | text: 'Guide', 39 | items: [ 40 | { text: 'Introduction', link: '/guide/' }, 41 | { text: 'Getting Started', link: '/guide/getting-started' }, 42 | { text: 'Frequently Asked Questions', link: '/guide/faq' }, 43 | ], 44 | }, 45 | ], 46 | }, 47 | 48 | footer: { 49 | message: 'MIT Licensed', 50 | copyright: 'Copyright © 2018-present Rosen Kanev', 51 | }, 52 | 53 | socialLinks: [{ icon: 'github', link: 'https://github.com/radial-color-picker/vue-color-picker' }], 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme'; 2 | import ColorPicker from '../../../dist/vue-color-picker.es.js'; 3 | import '../../../dist/vue-color-picker.css'; 4 | 5 | import InteractiveHomePageLayout from '../components/InteractiveHomePageLayout.vue'; 6 | 7 | import ExampleBasic from '../components/examples/Basic.vue'; 8 | import ExampleAccessibility from '../components/examples/Accessibility.vue'; 9 | import ExampleNonCollapsingMode from '../components/examples/NonCollapsingMode.vue'; 10 | import ExampleNonPrimary from '../components/examples/NonPrimary.vue'; 11 | import ExampleUncontrolled from '../components/examples/Uncontrolled.vue'; 12 | 13 | export default { 14 | extends: DefaultTheme, 15 | Layout: InteractiveHomePageLayout, 16 | enhanceApp({ app }) { 17 | app.component('ColorPicker', ColorPicker); 18 | 19 | app.component('ExampleBasic', ExampleBasic); 20 | app.component('ExampleAccessibility', ExampleAccessibility); 21 | app.component('ExampleNonCollapsingMode', ExampleNonCollapsingMode); 22 | app.component('ExampleNonPrimary', ExampleNonPrimary); 23 | app.component('ExampleUncontrolled', ExampleUncontrolled); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # Config Reference 6 | 7 | ## Props 8 | 9 | ### hue 10 | 11 | - Type: `Number` 12 | - Default: `0` 13 | 14 | A number between `0-359`. Around 0º/360º are reds. 120º is where greens are and 240º are blues. 15 | 16 | ### saturation 17 | 18 | - Type: `Number` 19 | - Default: `100` 20 | - _Optional_ 21 | 22 | A number between `0-100`. The larger the percentage, the more "colorful" this color is. 0% is completely desaturated (grayscale). 100% is fully saturated (full color). 23 | 24 | ### luminosity 25 | 26 | - Type: `Number` 27 | - Default: `50` 28 | - _Optional_ 29 | 30 | A number between `0-100`. 0% is completely dark (black). 100% is completely light (white). 50% is average lightness. 31 | 32 | ### alpha 33 | 34 | - Type: `Number` 35 | - Default: `1` 36 | - _Optional_ 37 | 38 | A number between `0-1`. Opacity/Transparency value. 0 is fully transparent. 1 is fully opaque. 0.5 is 50% transparent. 39 | 40 | ### disabled 41 | 42 | - Type: `Boolean` 43 | - Default: `false` 44 | - _Optional_ 45 | 46 | A boolean to disable UI interactions. When `:disabled="true"` is used the color picker is rendered with a dimmer color to indicate that the field is not available for use. 47 | 48 | ### step 49 | 50 | - Type: `Number` 51 | - Default: `1` 52 | - _Optional_ 53 | 54 | Amount of degrees to rotate the picker with keyboard and/or wheel. 55 | 56 | ### variant 57 | 58 | - Type: `String` 59 | - Default: `collapsible` 60 | - _Optional_ 61 | 62 | The mode of the picker. By default it will close/open when the color well is clicked. Use `variant="persistent"` to prevent collapsing/closing and to keep the widget always open. 63 | 64 | ### initially-collapsed 65 | 66 | - Type: `Boolean` 67 | - Default: `false` 68 | - _Optional_ 69 | 70 | Hides the palette initially. Using `variant="persistent"` and `:initially-collapsed="true"` at the same time is not supported. 71 | 72 | ### mouse-scroll 73 | 74 | - Type: `Boolean` 75 | - Default: `false` 76 | - _Optional_ 77 | 78 | Use wheel (scroll) event to rotate. By default it's off to keep things simple. Add `:mouse-scroll="true"` to allow the user to change the color by scrolling with mouse/trackpad. 79 | 80 | ::: danger Here be dragons! 81 | Keep in mind that by turning the _scroll to rotate_ functionality on it may result in actually worse UX than without it (preventing page scroll while mouse pointer is over the picker). It's also another non-passive event listener that could potentially introduce jank on scroll. 82 | ::: 83 | 84 | ### aria-label 85 | 86 | - Type: `String` 87 | - Default: `color picker` 88 | - _Optional_ 89 | 90 | Defines a string value that labels the color picker. It provides the user with a recognizable name of the object. 91 | 92 | ::: tip 93 | When a user interface is translated into multiple languages, ensure that `aria-label` values are translated. 94 | ::: 95 | 96 | ### aria-roledescription 97 | 98 | - Type: `String` 99 | - Default: `radial slider` 100 | - _Optional_ 101 | 102 | Defines a human-readable, author-localized description for the role of the color picker. Users of assistive technologies depend on the presentation of the role name, such as "slider" for an understanding of the purpose of the element and, if it is a widget, how to interact with it. 103 | 104 | ::: tip 105 | When a user interface is translated into multiple languages, ensure that `aria-roledescription` values are translated. 106 | ::: 107 | 108 | ### aria-valuetext 109 | 110 | - Type: `String` 111 | - Default: `'red' | 'yellow' | 'green' | 'cyan' | 'blue' | 'magenta'` 112 | - _Optional_ 113 | 114 | Defines the human readable text alternative of the value for a range widget. You can bring your own color-name map if you want (e.g. "dark orange", "amber", "salmon") 115 | 116 | ::: tip 117 | Make sure you update the `aria-valuetext` with any color change and as other aria attributes, when a user interface is translated into multiple languages, ensure that `aria-valuetext` values are translated. 118 | ::: 119 | 120 | ### aria-label-color-well 121 | 122 | - Type: `String` 123 | - Default: `color well` 124 | - _Optional_ 125 | 126 | Defines a string value that labels the color well (middle selector). 127 | 128 | ::: tip 129 | When a user interface is translated into multiple languages, ensure that `aria-label-color-well` values are translated. 130 | ::: 131 | 132 | ## Events 133 | 134 | ### input 135 | 136 | - Type: `Function` 137 | - Params: `hue` (`Number`) 138 | 139 | Emitted every time the color updates. This could be a touchstart/mousedown event, when rotating the knob, keyboard shortcuts like , and scrolling if enabled. It's also the glue between the color picker component and the outside world. Use this to update the `hue` prop. 140 | 141 | <<< @/.vitepress/components/examples/Basic.vue{2,15-17} 142 | 143 | ### change 144 | 145 | - Type: `Function` 146 | - Params: `hue` (`Number`) 147 | - _Optional_ 148 | 149 | Emitted every time the color changes, but unlike `@input` this is not emitted while rotating the knob. `@change` is a less noisy version of `@input` which is useful if you want to react to knob rotation stop for example or to use the `` as an uncontrolled component. 150 | 151 | ### select 152 | 153 | - Type: `Function` 154 | - Params: `hue` (`Number`) 155 | - _Optional_ 156 | 157 | Emitted when the user dismisses the color picker (i.e. interacting with the middle color well). Can be used as a secondary confirmation step from the user that this is the color value to be saved for example. 158 | 159 | ```vue{2,15,16,17} 160 | 163 | 164 | 181 | ``` -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | :::tip 4 | Looking for a quicker way to try it out yourself? Checkout the [examples](https://github.com/radial-color-picker/vue-color-picker/tree/main/examples) folder in GitHub. There's an example with Vite and a vanilla example without a build step. 5 | 6 | There's a [codepen](https://codepen.io/rkunev/pen/zjEmwV/) as well. 7 | ::: 8 | 9 | ## Basic 10 | 11 | In it's most basic form the color picker can be used as below. 12 | 13 | 14 | 15 | <<< @/.vitepress/components/examples/Basic.vue{2,6,11,14-17} 16 | 17 | ## Working with non-primary colors 18 | 19 | The basic example assumes only the most saturated colors and uses the default values for `saturation`, `luminosity` and `alpha`, but we're not limitted to them. Here's an example with a less aggressive saturation and luminosity: 20 | 21 | 22 | 23 | <<< @/.vitepress/components/examples/NonPrimary.vue{2,6,11-17,19-22} 24 | 25 | ## Uncontrolled component 26 | 27 | If you only need to react to `@change` or `@select` events you can skip `hue` + `@input`. Here's an example 28 | 29 | 30 | 31 | <<< @/.vitepress/components/examples/Uncontrolled.vue 32 | 33 | ## Persistent mode 34 | 35 | It's not always convenient to show the picker in a modal window that is shown/hidden on demand. That's why the color picker has an inline mode where interacting with the color well in the middle will not collapse the palette and the picker will stay opened. 36 | 37 | 38 | 39 | <<< @/.vitepress/components/examples/NonCollapsingMode.vue{2} 40 | 41 | ## Accessibility 42 | 43 | The color picker already has built-in screen reader support, but if you wish to customize it further you're free to do so! In fact, if you're app has internationalization (e.g. it's translated in Spanish) you _should_ also translate the `aria-label`, `aria-roledescription`, `aria-valuetext`, and `aria-label-color-well`. The following example highlights how to tweak the `aria-valuetext` since it's a dynamic value. 44 | 45 | 46 | 47 | <<< @/.vitepress/components/examples/Accessibility.vue{2,8,19-26,30} 48 | 49 | 54 | -------------------------------------------------------------------------------- /docs/guide/faq.md: -------------------------------------------------------------------------------- 1 | # First Asked Questions 2 | 3 | ## Does it support server-side rendering/static site generation? 4 | 5 | The good news: yes, via Vite's server-side rendering. 6 | 7 | The bad news: As of this writing Nuxt v3 has been released, but it is still in Beta. Use at your own risk. 8 | 9 | ## What's the browser support? 10 | 11 | The **last two versions of major browsers** (Chrome, Safari, Firefox, Edge) are supported though it will probably work in other browsers, webviews and runtimes as well. 12 | 13 | ## How to select other shades of the solid colors? 14 | 15 | The saturation, luminosity and alpha props are **display-only** values - you can only change the hue through the picker. We suggest to add a custom slider for saturation and luminosity or use ``. 16 | 17 | ## Why HSL? 18 | 19 | Regular HEX color format is limitting (no alpha channel) and browser support for HSLA is great. It's also sometimes more intuitive to work with HSLA notation since hue and angles map 1:1. Primary red color is at 0º, primary green is at 120º and gold for example sits somewhere in between. When a user rotates the wheel the hue is updated respectively. 20 | 21 | ::: tip Setting The Value of input of type color 22 | The value of an `` element of type `"color"` is a 7-character string specifying an RGB color in hexadecimal format. In addition, colors with an alpha channel are not supported; specifying a color in 9-character hexadecimal notation (e.g. `#009900aa`) will also result in the color being set to `"#000000"`. 23 | ::: 24 | _[Source](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/color#Value)_ 25 | 26 | ## Why exactly `input`/`change` events? 27 | 28 | Event names are based on the HTML `` 29 | 30 | ::: tip Tracking color change of input of type color 31 | As is the case with other `` types, there are two events that can be used to detect changes to the color value: input and change. input is fired on the `` element every time the color changes. The change event is fired when the user dismisses the color picker. 32 | ::: 33 | _[Source](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/color#Tracking_color_changes)_ 34 | 35 | ## Why is the scroll-to-rotate functionality not turned on by default? 36 | 37 | It's another non-passive event that could potentially introduce jank on scroll. To rotate the color knob, but stay on the same scrolling position the `wheel` event is blocked with `preventDefault()`. Thus, if you really want this feature for your users you'll have to explicitly add `:mouse-scroll="true"`. 38 | 39 | ## Why am I getting a warning event in the console? 40 | 41 | The `[Violation] Added non-passive event listener to a scroll-blocking 'touchmove' event.` warning may appear in Google Chrome DevTools console. 42 | 43 | `touchmove` is used with `preventDefault()` to block scrolling on mobile while rotating the color knob. Even the [Web Incubator Community Group](https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#removing-the-need-to-cancel-events) acknowledges that in some cases a passive event listener can't be used. 44 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Using NPM 4 | 5 | Color Picker on [npm](https://www.npmjs.com/package/@radial-color-picker/vue-color-picker) 6 | 7 | ```bash 8 | npm install @radial-color-picker/vue-color-picker 9 | ``` 10 | 11 | And in your app: 12 | 13 | ```vue 14 | 17 | 18 | 41 | ``` 42 | 43 | Depending on your build tool of choice you may have to setup the appropriate loaders or plugins. Checkout the [examples](https://github.com/radial-color-picker/vue-color-picker/tree/main/examples) folder. There's an example with Vite and CSS. If you're using tools such as Vite, Vue CLI, or Poi you don't have to do anything else - these tools come preconfigured and support CSS import out of the box. 44 | 45 | ## Using the component globally 46 | 47 | If you don't want to register the component everywhere it's used you can instead register it globally: 48 | 49 | ```js 50 | // in your main.js file 51 | import { createApp } from 'vue'; 52 | import App from './App.vue'; 53 | 54 | import ColorPicker from '@radial-color-picker/vue-color-picker'; 55 | import '@radial-color-picker/vue-color-picker/dist/vue-color-picker.min.css'; 56 | 57 | const app = createApp(App); 58 | 59 | app.component('ColorPicker', ColorPicker); 60 | app.mount('#app'); 61 | ``` 62 | 63 | ## Using CDN 64 | 65 | You can also use the minified sources directly: 66 | 67 | ```html 68 | 69 | 70 | 71 | 75 | 76 | 77 |
78 | 79 |
80 | 81 | 105 | 106 | ``` -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Great UX starts with two basic principles - ease of use and simplicity. Selecting a color should be as easy as moving a slider, clicking a checkbox or pressing a key just like other basic form elements behave. 4 | 5 | This is a flexible and minimalistic color picker. Developed with mobile devices and keyboard usage in mind. 6 | 7 | ## Features 8 | 9 | - Small size - 3.3 KB gzipped (JS and CSS combined) 10 | - Supports touch devices 11 | - Optimized animations 12 | - Ease of use 13 | - Screen reader support. 14 | - Tab to focus the picker. 15 | - or arrow key to increase hue. PgUp to go quicker. 16 | - or arrow key to decrease hue. PgDown to go quicker. 17 | - Enter to select a color and close the picker or to open it. 18 | - Mouse ScrollUp to increase and ScrollDown to decrease hue (Opt-in). 19 | - TypeScript support 20 | 21 | ## Ecosystem 22 | 23 | The right color picker, but not the framework you're looking for? 24 | 25 | - [Vue](https://github.com/radial-color-picker/vue-color-picker) - you're here! 26 | - [React](https://github.com/radial-color-picker/react-color-picker) 27 | - [AngularJs](https://github.com/talamaska/angular-radial-color-picker) 28 | - [Angular](https://github.com/radial-color-picker/angular-color-picker) -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: 'Radial Color Picker' 6 | tagline: 'Minimalistic color picker with a focus on size, accessibility and performance.' 7 | actions: 8 | - theme: brand 9 | text: Get Started 10 | link: /guide/ 11 | - theme: alt 12 | text: View on GitHub 13 | link: https://github.com/radial-color-picker/vue-color-picker 14 | 15 | features: 16 | - title: Small size 17 | details: Say no to web bloat! Sitting at 2.3 KB gzipped JS it's probably one of the smallest color pickers out there. 18 | - title: Ease of use 19 | details: Developed with mobile devices and keyboard usage in mind. Screen reader support. 20 | - title: Performant 21 | details: Highly tuned touch interactions to stay above 30fps on lower end mobile devices. 22 | --- -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radial-color-picker/vue-color-picker/76d7732df988effe04b8da6fe53b8aba934f7aa5/docs/public/logo.png -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # Example - UMD build (simple) 2 | 3 | This is a basic example of how to use `vue-color-picker` with the UMD build (useful for quick prototyping in the browser for example). 4 | 5 | ## How to use 6 | 7 | Download the example [or clone the whole project](https://github.com/radial-color-picker/vue-color-picker.git): 8 | 9 | ```bash 10 | curl https://codeload.github.com/radial-color-picker/vue-color-picker/tar.gz/main | tar -xz --strip=2 vue-color-picker-main/examples/simple 11 | cd simple 12 | ``` 13 | 14 | ## Running the example 15 | 16 | Start an HTTP server with [`http-server`](https://www.npmjs.com/package/http-server), [`superstatic`](https://www.npmjs.com/package/superstatic) or if you prefer using Python follow [these instructions](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/set_up_a_local_testing_server) at MDN. 17 | 18 | ```bash 19 | # with http-server (starts the server on port 8000 and opens the default browser with the page) 20 | http-server -o 21 | 22 | # with superstatic (starts the server on port 3474) 23 | superstatic 24 | ``` -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Radial Color Picker | Vue | Simple 6 | 7 | 8 | 9 | 10 | 14 | 47 | 48 | 49 |
50 | 51 |

{{ msg }}

52 |
{{ color }}
53 |
54 | 55 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /examples/with-config/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | 5 | # Log files 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | yarn.lock 10 | package-lock.json 11 | 12 | # Editor directories and files 13 | .idea 14 | .vscode 15 | *.suo 16 | *.ntvs* 17 | *.njsproj 18 | *.sln 19 | *.sw* 20 | -------------------------------------------------------------------------------- /examples/with-config/README.md: -------------------------------------------------------------------------------- 1 | # Example - With Component Configuration 2 | 3 | Example shows how to further configure `vue-color-picker`. It's scaffolded with [Vite](https://vitejs.dev/) 4 | 5 | ## How to use 6 | 7 | Download the example [or clone the whole project](https://github.com/radial-color-picker/vue-color-picker.git): 8 | 9 | ```bash 10 | curl https://codeload.github.com/radial-color-picker/vue-color-picker/tar.gz/main | tar -xz --strip=2 vue-color-picker-main/examples/with-config 11 | cd with-config 12 | ``` 13 | 14 | ## Project setup 15 | 16 | ``` 17 | npm install 18 | ``` 19 | 20 | ### Compiles and hot-reloads for development 21 | 22 | ``` 23 | npm run dev 24 | ``` 25 | 26 | ### Compiles and minifies for production 27 | 28 | ``` 29 | npm run build 30 | ``` 31 | 32 | ### Customize configuration 33 | 34 | See [Configuration Reference](https://vitejs.dev/guide/). -------------------------------------------------------------------------------- /examples/with-config/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Radial Color Picker | Vue | With Component Configuration 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/with-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-with-config", 3 | "description": "Example with component config", 4 | "version": "4.0.0", 5 | "author": "Rosen Kanev ", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "dev": "vite", 10 | "build": "vite build", 11 | "serve": "vite preview" 12 | }, 13 | "dependencies": { 14 | "@radial-color-picker/vue-color-picker": "latest", 15 | "vue": "^3.4.21" 16 | }, 17 | "devDependencies": { 18 | "@vitejs/plugin-vue": "^5.0.4", 19 | "@vue/compiler-sfc": "^3.0.5", 20 | "vite": "^5.1.6" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/with-config/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radial-color-picker/vue-color-picker/76d7732df988effe04b8da6fe53b8aba934f7aa5/examples/with-config/public/favicon.ico -------------------------------------------------------------------------------- /examples/with-config/src/App.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 85 | 86 | 119 | -------------------------------------------------------------------------------- /examples/with-config/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | 4 | createApp(App).mount('#app'); 5 | -------------------------------------------------------------------------------- /examples/with-config/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | }); 8 | -------------------------------------------------------------------------------- /examples/with-nuxt/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | 26 | # Lock files 27 | yarn.lock 28 | package-lock.json 29 | 30 | # Editor directories and files 31 | .vscode 32 | *.suo 33 | *.ntvs* 34 | *.njsproj 35 | *.sln 36 | *.sw* 37 | -------------------------------------------------------------------------------- /examples/with-nuxt/README.md: -------------------------------------------------------------------------------- 1 | # Example - With Nuxt.js 2 | 3 | --- 4 | 5 | Example shows how to setup `vue-color-picker` with [Nuxt.js](https://v3.nuxtjs.org/). Setup process is the same as an app built with Vue CLI or Vite, but the CSS is configured globally for all pages in `next.config.ts`. 6 | 7 | ## How to use 8 | 9 | Download the example [or clone the whole project](https://github.com/radial-color-picker/vue-color-picker.git): 10 | 11 | ```bash 12 | curl https://codeload.github.com/radial-color-picker/vue-color-picker/tar.gz/main | tar -xz --strip=2 vue-color-picker-main/examples/with-nuxt 13 | cd with-nuxt 14 | ``` 15 | 16 | # Nuxt 3 Minimal Starter 17 | 18 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 19 | 20 | ## Setup 21 | 22 | Make sure to install the dependencies: 23 | 24 | ```bash 25 | # npm 26 | npm install 27 | 28 | # pnpm 29 | pnpm install 30 | 31 | # yarn 32 | yarn install 33 | 34 | # bun 35 | bun install 36 | ``` 37 | 38 | ## Development Server 39 | 40 | Start the development server on `http://localhost:3000`: 41 | 42 | ```bash 43 | # npm 44 | npm run dev 45 | 46 | # pnpm 47 | pnpm run dev 48 | 49 | # yarn 50 | yarn dev 51 | 52 | # bun 53 | bun run dev 54 | ``` 55 | 56 | ## Production 57 | 58 | Build the application for production: 59 | 60 | ```bash 61 | # npm 62 | npm run build 63 | 64 | # pnpm 65 | pnpm run build 66 | 67 | # yarn 68 | yarn build 69 | 70 | # bun 71 | bun run build 72 | ``` 73 | 74 | Locally preview production build: 75 | 76 | ```bash 77 | # npm 78 | npm run preview 79 | 80 | # pnpm 81 | pnpm run preview 82 | 83 | # yarn 84 | yarn preview 85 | 86 | # bun 87 | bun run preview 88 | ``` 89 | 90 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. -------------------------------------------------------------------------------- /examples/with-nuxt/app.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 32 | 33 | 66 | -------------------------------------------------------------------------------- /examples/with-nuxt/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | // Add the Color Picker component's CSS globally 3 | css: ['@radial-color-picker/vue-color-picker/dist/vue-color-picker.min.css'], 4 | }); 5 | -------------------------------------------------------------------------------- /examples/with-nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-with-nuxt", 3 | "version": "4.0.0", 4 | "description": "Example with Nuxt.js", 5 | "author": "Rosen Kanev", 6 | "private": true, 7 | "type": "module", 8 | "scripts": { 9 | "build": "nuxt build", 10 | "dev": "nuxt dev", 11 | "generate": "nuxt generate", 12 | "preview": "nuxt preview", 13 | "postinstall": "nuxt prepare" 14 | }, 15 | "dependencies": { 16 | "@radial-color-picker/vue-color-picker": "latest", 17 | "nuxt": "^3.10.3", 18 | "vue": "^3.4.21", 19 | "vue-router": "^4.3.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/with-nuxt/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radial-color-picker/vue-color-picker/76d7732df988effe04b8da6fe53b8aba934f7aa5/examples/with-nuxt/public/favicon.ico -------------------------------------------------------------------------------- /examples/with-nuxt/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /examples/with-nuxt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /examples/with-uncontrolled-component/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | 5 | # Log files 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | yarn.lock 10 | package-lock.json 11 | 12 | # Editor directories and files 13 | .idea 14 | .vscode 15 | *.suo 16 | *.ntvs* 17 | *.njsproj 18 | *.sln 19 | *.sw* 20 | -------------------------------------------------------------------------------- /examples/with-uncontrolled-component/README.md: -------------------------------------------------------------------------------- 1 | # Example - With Uncontrolled Component 2 | 3 | Example shows how to only listen for changes of the color and skip `hue` and `@input`. Useful only for simple cases and the recommended usage is still 4 | 5 | ```vue 6 | 7 | ``` 8 | 9 | ## How to use 10 | 11 | Download the example [or clone the whole project](https://github.com/radial-color-picker/vue-color-picker.git): 12 | 13 | ```bash 14 | curl https://codeload.github.com/radial-color-picker/vue-color-picker/tar.gz/main | tar -xz --strip=2 vue-color-picker-main/examples/with-uncontrolled-component 15 | cd with-uncontrolled-component 16 | ``` 17 | 18 | ## Project setup 19 | 20 | ``` 21 | npm install 22 | ``` 23 | 24 | ### Compiles and hot-reloads for development 25 | 26 | ``` 27 | npm run dev 28 | ``` 29 | 30 | ### Compiles and minifies for production 31 | 32 | ``` 33 | npm run build 34 | ``` 35 | 36 | ### Customize configuration 37 | 38 | See [Configuration Reference](https://vitejs.dev/guide/). -------------------------------------------------------------------------------- /examples/with-uncontrolled-component/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Radial Color Picker | Vue | With Uncontrolled Component 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/with-uncontrolled-component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-with-uncontrolled-component", 3 | "description": "Example with uncontrolled component", 4 | "version": "4.0.0", 5 | "author": "Rosen Kanev ", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "dev": "vite", 10 | "build": "vite build", 11 | "serve": "vite preview" 12 | }, 13 | "dependencies": { 14 | "@radial-color-picker/vue-color-picker": "latest", 15 | "vue": "^3.4.21" 16 | }, 17 | "devDependencies": { 18 | "@vitejs/plugin-vue": "^5.0.4", 19 | "@vue/compiler-sfc": "^3.0.5", 20 | "vite": "^5.1.6" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/with-uncontrolled-component/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radial-color-picker/vue-color-picker/76d7732df988effe04b8da6fe53b8aba934f7aa5/examples/with-uncontrolled-component/public/favicon.ico -------------------------------------------------------------------------------- /examples/with-uncontrolled-component/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 30 | 31 | 64 | -------------------------------------------------------------------------------- /examples/with-uncontrolled-component/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | 4 | createApp(App).mount('#app'); 5 | -------------------------------------------------------------------------------- /examples/with-uncontrolled-component/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | }); 8 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radial-color-picker/vue-color-picker/76d7732df988effe04b8da6fe53b8aba934f7aa5/favicon.ico -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { DefineComponent, ComponentOptionsMixin } from 'vue'; 2 | 3 | export type ColorPickerProps = { 4 | /** 5 | * A number between 0-359. Around 0º/360º are reds. 120º is where greens are and 240º are blues. Default: 0 6 | */ 7 | hue?: number; 8 | 9 | /** 10 | * A number between 0-100. The larger the percentage, the more "colorful" this color is. 0% is completely desaturated (grayscale). 100% is fully saturated (full color). Default: 100 11 | */ 12 | saturation?: number; 13 | 14 | /** 15 | * A number between 0-100. 0% is completely dark (black). 100% is completely light (white). 50% is average lightness. Default: 50 16 | */ 17 | luminosity?: number; 18 | 19 | /** 20 | * A number between 0-1. Opacity/Transparency value. 0 is fully transparent. 1 is fully opaque. 0.5 is 50% transparent. Default: 1 21 | */ 22 | alpha?: number; 23 | 24 | /** 25 | * A boolean to disable UI interactions. When :disabled="true" is used the color picker is rendered with a dimmer color to indicate that the field is not available for use. 26 | */ 27 | disabled?: boolean; 28 | 29 | /** 30 | * Amount of degrees to rotate the picker with keyboard and/or wheel. Default: 1 31 | */ 32 | step?: number; 33 | 34 | /** 35 | * The mode of the picker. By default it will close/open when the color well is clicked. Use variant="persistent" to prevent collapsing/closing and to keep the widget always open. Default: 'collapsible' 36 | */ 37 | variant?: 'collapsible' | 'persistent'; 38 | 39 | /** 40 | * Hides the palette initially. Using variant="persistent" and :initially-collapsed="true" at the same time is not supported. Default: false 41 | */ 42 | initiallyCollapsed?: boolean; 43 | 44 | /** 45 | * Use wheel (scroll) event to rotate. By default it's off to keep things simple. Add :mouse-scroll="true" to allow the user to change the color by scrolling with mouse/trackpad. Default: false 46 | */ 47 | mouseScroll?: boolean; 48 | 49 | /** 50 | * Defines a string value that labels the color well (middle selector). Default: color well 51 | */ 52 | ariaLabelColorWell?: string; 53 | }; 54 | 55 | export type ColorPickerEmits = { 56 | /** 57 | * Called every time the color updates. This could be a touchstart/mousedown event, when rotating the knob, keyboard shortcuts like ↑, and scrolling if enabled. It's also the glue between the color picker component and the outside world. Use this to update the hue prop. 58 | */ 59 | input: (hue: number) => void; 60 | 61 | /** 62 | * Called every time the color changes, but unlike onInput this is not called while rotating the knob. onChange is a less noisy version of `@input` which is useful if you want to react to knob rotation stop for example or to use the as an uncontrolled component. 63 | */ 64 | change: (hue: number) => void; 65 | 66 | /** 67 | * Called when the user dismisses the color picker (i.e. interacting with the middle color well). Can be used as a secondary confirmation step from the user that this is the color value to be saved for example. 68 | */ 69 | select: (hue: number) => void; 70 | }; 71 | 72 | declare const ColorPicker: DefineComponent< 73 | ColorPickerProps, 74 | {}, 75 | {}, 76 | {}, 77 | {}, 78 | ComponentOptionsMixin, 79 | ComponentOptionsMixin, 80 | ColorPickerEmits 81 | >; 82 | 83 | export default ColorPicker; 84 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue Color Picker 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@radial-color-picker/vue-color-picker", 3 | "version": "6.0.0", 4 | "description": "Radial Color Picker - Vue", 5 | "author": "Rosen Kanev ", 6 | "scripts": { 7 | "start": "vite", 8 | "build": "node scripts/build.js", 9 | "test": "vitest", 10 | "test:ci": "vitest run --coverage --no-file-parallelism --bail 1", 11 | "lint": "eslint --ext .js,vue src", 12 | "prepublishOnly": "bun run build", 13 | "docs:dev": "vitepress dev docs", 14 | "docs:build": "vitepress build docs", 15 | "docs:preview": "vitepress preview docs", 16 | "prepare": "husky" 17 | }, 18 | "main": "dist/vue-color-picker.cjs.js", 19 | "module": "dist/vue-color-picker.es.js", 20 | "unpkg": "dist/vue-color-picker.umd.min.js", 21 | "types": "index.d.ts", 22 | "files": [ 23 | "dist", 24 | "index.d.ts" 25 | ], 26 | "dependencies": { 27 | "@radial-color-picker/rotator": "3.0.2" 28 | }, 29 | "devDependencies": { 30 | "@size-limit/file": "11.0.2", 31 | "@vitejs/plugin-vue": "5.0.4", 32 | "@vitest/coverage-v8": "1.3.1", 33 | "@vue/compiler-sfc": "3.4.21", 34 | "@vue/test-utils": "2.4.4", 35 | "chalk": "4.1.2", 36 | "cliui": "7.0.4", 37 | "eslint": "8.57.0", 38 | "eslint-plugin-vue": "9.22.0", 39 | "fs-extra": "10.0.0", 40 | "husky": "9.0.11", 41 | "jsdom": "24.0.0", 42 | "lint-staged": "15.2.2", 43 | "prettier": "3.2.5", 44 | "size-limit": "11.0.2", 45 | "vite": "5.1.4", 46 | "vitepress": "1.0.0-rc.44", 47 | "vitest": "1.3.1", 48 | "vue": "3.4.21" 49 | }, 50 | "peerDependencies": { 51 | "vue": "^3.0.0" 52 | }, 53 | "contributors": [ 54 | "Dennis Dierkes " 55 | ], 56 | "homepage": "https://radial-color-picker.github.io/vue-color-picker/", 57 | "keywords": [ 58 | "vue", 59 | "radial color picker", 60 | "color picker", 61 | "color-picker", 62 | "vue-color-picker", 63 | "hue picker", 64 | "nuxt", 65 | "nuxt module" 66 | ], 67 | "license": "MIT", 68 | "lint-staged": { 69 | "*.{js,vue}": [ 70 | "bun run eslint", 71 | "bun run prettier --write" 72 | ] 73 | }, 74 | "size-limit": [ 75 | { 76 | "path": "./dist/vue-color-picker.umd.min.js", 77 | "gzip": true, 78 | "limit": "2.4 kB" 79 | }, 80 | { 81 | "path": "./dist/vue-color-picker.min.css", 82 | "gzip": true, 83 | "limit": "1 kB" 84 | } 85 | ], 86 | "repository": { 87 | "type": "git", 88 | "url": "https://github.com/radial-color-picker/vue-color-picker.git" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | singleQuote: true, 4 | tabWidth: 4, 5 | trailingComma: 'es5', 6 | endOfLine: 'auto', 7 | vueIndentScriptAndStyle: true, 8 | }; 9 | -------------------------------------------------------------------------------- /screenshots/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radial-color-picker/vue-color-picker/76d7732df988effe04b8da6fe53b8aba934f7aa5/screenshots/thumbnail.png -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const vue = require('@vitejs/plugin-vue'); 2 | const fse = require('fs-extra'); 3 | const path = require('path'); 4 | const { build } = require('vite'); 5 | const chalk = require('chalk'); 6 | const readline = require('readline'); 7 | 8 | const { printStats } = require('./print-stats'); 9 | 10 | const fileNameBase = 'vue-color-picker'; 11 | 12 | const getBuildConfig = ({ formats, minify }) => { 13 | return { 14 | define: { 15 | // When there's an if (process.env.NODE_ENV === 'development') {} statement in the code 16 | // this removes the whole if branch for UMD builds, but leaves it in for CJS/ES builds. 17 | // So a warning will be shown to consumers of the library while they are developing locally, 18 | // but it will be removed through dead code elimination in a production build. 19 | ...(formats.includes('umd') ? { 'process.env.NODE_ENV': '"production"' } : {}), 20 | }, 21 | logLevel: 'silent', 22 | configFile: false, 23 | plugins: [vue()], 24 | build: { 25 | emptyOutDir: false, 26 | minify, 27 | brotliSize: false, 28 | sourcemap: !!minify, 29 | lib: { 30 | entry: path.resolve(__dirname, '../src/ColorPicker.vue'), 31 | name: 'VueColorPicker', 32 | formats, 33 | }, 34 | rollupOptions: { 35 | external: ['vue'], 36 | output: { 37 | globals: { 38 | vue: 'Vue', 39 | }, 40 | entryFileNames: `${fileNameBase}.[format].${!!minify ? 'min.js' : 'js'}`, 41 | assetFileNames: `${fileNameBase}.${!!minify ? 'min.[ext]' : '[ext]'}`, 42 | }, 43 | }, 44 | }, 45 | }; 46 | }; 47 | 48 | (async () => { 49 | try { 50 | if (process.stdout.isTTY) { 51 | console.log('\n'.repeat(process.stdout.rows)); 52 | 53 | readline.cursorTo(process.stdout, 0, 0); 54 | readline.clearScreenDown(process.stdout); 55 | } 56 | 57 | await fse.remove('dist'); 58 | 59 | console.log('Building for production as library (commonjs, es, umd, umd-min)...'); 60 | 61 | await build(getBuildConfig({ formats: ['es', 'cjs'], minify: false })); 62 | await build(getBuildConfig({ formats: ['umd'], minify: false })); 63 | await build(getBuildConfig({ formats: ['umd'], minify: true })); 64 | 65 | printStats(); 66 | } catch (err) { 67 | console.error('\n' + chalk.red(err.message)); 68 | console.error(err.stack); 69 | process.exit(1); 70 | } 71 | })(); 72 | -------------------------------------------------------------------------------- /scripts/print-stats.js: -------------------------------------------------------------------------------- 1 | const fse = require('fs-extra'); 2 | const zlib = require('zlib'); 3 | const ui = require('cliui')({ width: 80 }); 4 | const chalk = require('chalk'); 5 | 6 | const outputDir = 'dist'; 7 | 8 | const isJS = (name) => /\.js$/.test(name); 9 | const isCSS = (name) => /\.css$/.test(name); 10 | const formatSize = (size) => (size / 1024).toFixed(2) + ' KiB'; 11 | const makeRow = (a, b, c) => ` ${a}\t ${b}\t ${c}`; 12 | 13 | const printStats = () => { 14 | const assets = fse 15 | .readdirSync(outputDir) 16 | .filter((fileName) => isCSS(fileName) || isJS(fileName)) 17 | .map((fileName) => { 18 | const filePath = `${outputDir}/${fileName}`; 19 | 20 | const { size } = fse.statSync(filePath); 21 | const gzipped = zlib.gzipSync(fse.readFileSync(filePath)).length; 22 | 23 | return { 24 | name: `${outputDir}/${fileName}`, 25 | size: formatSize(size), 26 | gzipped: formatSize(gzipped), 27 | }; 28 | }) 29 | .sort((a, b) => { 30 | // move CSS files to be first 31 | if (isCSS(a.name) && isJS(b.name)) return -1; 32 | 33 | // otherwise sort alphabetically 34 | return a.name.localeCompare(b.name); 35 | }); 36 | 37 | ui.div( 38 | makeRow(chalk.whiteBright.bold('File'), chalk.whiteBright.bold('Size'), chalk.whiteBright.bold('Gzipped')) + 39 | `\n\n` + 40 | assets 41 | .map(({ name, size, gzipped }) => 42 | makeRow(isJS(name) ? chalk.cyan(name) : chalk.magenta(name), size, gzipped) 43 | ) 44 | .join(`\n`) 45 | ); 46 | 47 | console.log(`\n${ui.toString()}\n\n ${chalk.gray(`Images and other types of assets omitted.`)}\n`); 48 | }; 49 | 50 | module.exports.printStats = printStats; 51 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | -------------------------------------------------------------------------------- /src/ColorPicker.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 255 | 256 | 468 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | 4 | createApp(App).mount('#app'); 5 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/index.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Init > renders correctly 1`] = ` 4 |
18 |
21 |
25 |
28 |
29 |
33 |
41 | `; 42 | -------------------------------------------------------------------------------- /tests/unit/index.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | import ColorPicker from '@/ColorPicker.vue'; 4 | 5 | describe('Init', () => { 6 | it('renders correctly', () => { 7 | const wrapper = mount(ColorPicker, { 8 | props: { 9 | hue: 0, 10 | }, 11 | }); 12 | 13 | expect(wrapper.element).toMatchSnapshot(); 14 | }); 15 | 16 | it('setups an optional event for updating the hue by scrolling', async () => { 17 | const wrapper = mount(ColorPicker, { 18 | props: { 19 | hue: 30, 20 | saturation: 100, 21 | luminosity: 50, 22 | alpha: 1, 23 | mouseScroll: true, 24 | }, 25 | }); 26 | 27 | await wrapper.get('.rcp__rotator').trigger('wheel', { deltaY: 120 }); 28 | 29 | expect(wrapper.emitted('input')[0]).toEqual([31]); 30 | }); 31 | }); 32 | 33 | describe('Core Methods', () => { 34 | it('closes the picker when the color well is clicked', async () => { 35 | const wrapper = mount(ColorPicker, { 36 | props: { 37 | hue: 30, 38 | }, 39 | }); 40 | 41 | const well = wrapper.get('.rcp__well'); 42 | 43 | await well.trigger('click'); 44 | 45 | expect(well.classes()).toContain('pressed'); 46 | expect(wrapper.emitted('select')[0]).toEqual([30]); 47 | }); 48 | 49 | it('closes the picker when the picker receives an Enter keyboard event', async () => { 50 | const wrapper = mount(ColorPicker, { 51 | props: { 52 | hue: 30, 53 | }, 54 | }); 55 | 56 | const well = wrapper.get('.rcp__well'); 57 | 58 | await well.trigger('keyup', { key: 'Enter' }); 59 | 60 | expect(well.classes()).toContain('pressed'); 61 | expect(wrapper.emitted('select')[0]).toEqual([30]); 62 | }); 63 | 64 | it('opens the picker when the color well is clicked', async () => { 65 | const wrapper = mount(ColorPicker, { 66 | props: { 67 | hue: 0, 68 | initiallyCollapsed: true, 69 | }, 70 | }); 71 | 72 | const well = wrapper.get('.rcp__well'); 73 | 74 | await well.trigger('click'); 75 | await well.trigger('click'); 76 | 77 | expect(wrapper.get('[role="slider"]').attributes('aria-expanded')).toBe('true'); 78 | }); 79 | 80 | it('triggers the knob out animation when the ripple animation ends', async () => { 81 | const wrapper = mount(ColorPicker, { 82 | props: { 83 | hue: 0, 84 | }, 85 | }); 86 | 87 | const well = wrapper.get('.rcp__well'); 88 | await well.trigger('animationend'); 89 | 90 | expect(wrapper.get('.rcp__knob').classes()).toContain('out'); 91 | }); 92 | 93 | it('triggers the knob in animation when the ripple animation ends', async () => { 94 | const wrapper = mount(ColorPicker, { 95 | props: { 96 | hue: 0, 97 | }, 98 | }); 99 | 100 | const well = wrapper.get('.rcp__well'); 101 | await well.trigger('animationend'); 102 | await well.trigger('animationend'); 103 | 104 | expect(wrapper.get('.rcp__knob').classes()).toContain('in'); 105 | }); 106 | 107 | it('keeps the picker opened when togglePicker is called and the picker is of variant persistent', async () => { 108 | const wrapper = mount(ColorPicker, { 109 | props: { 110 | hue: 0, 111 | variant: 'persistent', 112 | }, 113 | }); 114 | 115 | await wrapper.get('.rcp__well').trigger('animationend'); 116 | 117 | expect(wrapper.get('.rcp__knob').classes()).toContain('in'); 118 | }); 119 | 120 | it('prevents the palette from hiding when hidePalette is called and knob is not hidden yet', async () => { 121 | const wrapper = mount(ColorPicker, { 122 | props: { 123 | hue: 0, 124 | }, 125 | }); 126 | 127 | await wrapper.get('.rcp__knob').trigger('transitionend'); 128 | 129 | expect(wrapper.get('[role="slider"]').attributes('aria-expanded')).toBe('true'); 130 | }); 131 | 132 | it('changes the hue when you scroll and the picker is not animating', async () => { 133 | const wrapper = mount(ColorPicker, { 134 | props: { 135 | hue: 20, 136 | mouseScroll: true, 137 | }, 138 | }); 139 | 140 | await wrapper.get('.rcp__rotator').trigger('wheel', { deltaY: 120 }); 141 | await wrapper.get('.rcp__rotator').trigger('wheel', { deltaY: -120 }); 142 | 143 | expect(wrapper.emitted('input')[0]).toEqual([21]); 144 | expect(wrapper.emitted('change')[0]).toEqual([21]); 145 | 146 | expect(wrapper.emitted('input')[1]).toEqual([20]); 147 | expect(wrapper.emitted('change')[1]).toEqual([20]); 148 | }); 149 | 150 | it('changes the hue by 10 when PageUp/PageDown is pressed', async () => { 151 | const wrapper = mount(ColorPicker, { 152 | props: { 153 | hue: 30, 154 | }, 155 | }); 156 | 157 | const rcp = wrapper.get('[role="slider"]'); 158 | 159 | await rcp.trigger('keydown', { key: 'PageUp' }); 160 | 161 | expect(wrapper.emitted('input')[0]).toEqual([40]); 162 | expect(wrapper.emitted('change')[0]).toEqual([40]); 163 | expect(rcp.attributes('aria-valuenow')).toBe('40'); 164 | 165 | await rcp.trigger('keydown', { key: 'PageDown' }); 166 | 167 | expect(wrapper.emitted('input')[1]).toEqual([30]); 168 | expect(wrapper.emitted('change')[1]).toEqual([30]); 169 | expect(rcp.attributes('aria-valuenow')).toBe('30'); 170 | }); 171 | 172 | it('changes the hue by 1 when Arrow keys are pressed', async () => { 173 | const wrapper = mount(ColorPicker, { 174 | props: { 175 | hue: 30, 176 | }, 177 | }); 178 | 179 | const rcp = wrapper.get('[role="slider"]'); 180 | 181 | await rcp.trigger('keydown', { key: 'ArrowUp' }); 182 | 183 | expect(wrapper.emitted('input')[0]).toEqual([31]); 184 | expect(wrapper.emitted('change')[0]).toEqual([31]); 185 | expect(rcp.attributes('aria-valuenow')).toBe('31'); 186 | 187 | await rcp.trigger('keydown', { key: 'ArrowRight' }); 188 | 189 | expect(wrapper.emitted('input')[1]).toEqual([32]); 190 | expect(wrapper.emitted('change')[1]).toEqual([32]); 191 | expect(rcp.attributes('aria-valuenow')).toBe('32'); 192 | 193 | await rcp.trigger('keydown', { key: 'ArrowDown' }); 194 | 195 | expect(wrapper.emitted('input')[2]).toEqual([31]); 196 | expect(wrapper.emitted('change')[2]).toEqual([31]); 197 | expect(rcp.attributes('aria-valuenow')).toBe('31'); 198 | 199 | await rcp.trigger('keydown', { key: 'ArrowLeft' }); 200 | 201 | expect(wrapper.emitted('input')[3]).toEqual([30]); 202 | expect(wrapper.emitted('change')[3]).toEqual([30]); 203 | expect(rcp.attributes('aria-valuenow')).toBe('30'); 204 | }); 205 | 206 | it('sets the hue to 0 when Home is pressed', async () => { 207 | const wrapper = mount(ColorPicker, { 208 | props: { 209 | hue: 30, 210 | }, 211 | }); 212 | 213 | await wrapper.get('[role="slider"]').trigger('keydown', { key: 'Home' }); 214 | 215 | expect(wrapper.emitted('input')[0]).toEqual([0]); 216 | expect(wrapper.emitted('change')[0]).toEqual([0]); 217 | }); 218 | 219 | it('sets the hue to 359 when End is pressed', async () => { 220 | const wrapper = mount(ColorPicker, { 221 | props: { 222 | hue: 30, 223 | }, 224 | }); 225 | 226 | await wrapper.get('[role="slider"]').trigger('keydown', { key: 'End' }); 227 | 228 | expect(wrapper.emitted('input')[0]).toEqual([359]); 229 | expect(wrapper.emitted('change')[0]).toEqual([359]); 230 | }); 231 | 232 | it("doesn't change the hue on keyboard events when the picker is disabled", async () => { 233 | const wrapper = mount(ColorPicker, { 234 | props: { 235 | hue: 30, 236 | disabled: true, 237 | }, 238 | }); 239 | 240 | await wrapper.get('[role="slider"]').trigger('keydown', { key: 'PageUp' }); 241 | 242 | expect(wrapper.emitted('input')).toBeUndefined(); 243 | }); 244 | }); 245 | 246 | describe('Reactive Changes', () => { 247 | it('updates rotator based on hue prop', async () => { 248 | const wrapper = mount(ColorPicker, { 249 | props: { 250 | hue: 90, 251 | saturation: 100, 252 | luminosity: 50, 253 | alpha: 1, 254 | }, 255 | }); 256 | 257 | await wrapper.setProps({ hue: 60 }); 258 | 259 | expect(wrapper.get('[role="slider"]').attributes('aria-valuenow')).toBe('60'); 260 | }); 261 | 262 | it("doesn't update the hue if it is currently pressed", async () => { 263 | const wrapper = mount(ColorPicker, { 264 | props: { 265 | hue: 90, 266 | mouseScroll: true, 267 | }, 268 | }); 269 | 270 | const well = wrapper.get('.rcp__well'); 271 | 272 | await well.trigger('click'); 273 | await wrapper.get('.rcp__well').trigger('animationend'); 274 | await wrapper.get('.rcp__knob').trigger('transitionend'); 275 | 276 | await wrapper.get('.rcp__rotator').trigger('wheel', { deltaY: 120 }); 277 | 278 | expect(wrapper.vm.angle).toBe(90); 279 | expect(wrapper.emitted('input')).toBeUndefined(); 280 | }); 281 | 282 | it('hides the palette after the knob is hidden', async () => { 283 | const wrapper = mount(ColorPicker, { 284 | props: { 285 | hue: 90, 286 | }, 287 | }); 288 | 289 | await wrapper.get('.rcp__well').trigger('animationend'); 290 | await wrapper.get('.rcp__knob').trigger('transitionend'); 291 | 292 | expect(wrapper.get('[role="slider"]').attributes('aria-expanded')).toBe('false'); 293 | }); 294 | }); 295 | 296 | describe('Teardown', () => { 297 | it('cleans up Rotator unused object references & events', () => { 298 | const wrapper = mount(ColorPicker, { 299 | props: { 300 | hue: 0, 301 | }, 302 | }); 303 | 304 | wrapper.unmount(); 305 | 306 | expect(wrapper.vm.rcp).toBe(null); 307 | }); 308 | }); 309 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url'; 2 | import path from 'node:path'; 3 | import vue from '@vitejs/plugin-vue'; 4 | 5 | export default { 6 | plugins: [vue()], 7 | test: { 8 | environment: 'jsdom', 9 | coverage: { 10 | include: ['src/ColorPicker.vue'], 11 | }, 12 | alias: { 13 | '@': fileURLToPath(new URL('./src', import.meta.url)), 14 | }, 15 | }, 16 | }; 17 | --------------------------------------------------------------------------------