├── .eslintrc.js ├── .github └── workflows │ ├── ci.yml │ └── npm-publish.yml ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── babel.config.js ├── docs ├── .gitignore ├── LICENCE.md ├── README.md ├── content │ └── docs │ │ └── index.md ├── gridsome.config.js ├── gridsome.server.js ├── netlify.toml ├── package.json ├── src │ ├── assets │ │ └── favicon.png │ ├── components │ │ ├── LayoutHeader.vue │ │ ├── Logo.vue │ │ ├── NextPrevLinks.vue │ │ ├── OnThisPage.vue │ │ ├── Search.vue │ │ ├── Sidebar.vue │ │ └── ToggleDarkMode.vue │ ├── favicon.png │ ├── layouts │ │ └── Default.vue │ ├── main.js │ ├── pages │ │ ├── 404.vue │ │ └── Index.vue │ └── templates │ │ └── MarkdownPage.vue ├── static │ ├── README.md │ └── logo.jpg ├── tailwind.config.js └── yarn.lock ├── examples └── simple-modal │ ├── .gitignore │ ├── .vscode │ └── extensions.json │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ └── favicon.ico │ ├── src │ ├── App.vue │ ├── env.d.ts │ └── main.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── yarn.lock ├── jest.config.js ├── package.json ├── prettier.config.js ├── rollup.config.js ├── src ├── Box │ ├── Box.spec.tsx │ ├── Box.ts │ └── index.ts ├── Button │ ├── Button.spec.tsx │ ├── Button.ts │ ├── README.md │ └── index.ts ├── Clickable │ ├── Clickable.spec.tsx │ ├── Clickable.ts │ └── index.ts ├── Dialog │ ├── Dialog.ts │ ├── DialogDisclosure.ts │ ├── DialogState.ts │ ├── __tests__ │ │ ├── Dialog.spec.tsx │ │ ├── DialogDisclosure.spec.tsx │ │ └── index.spec.tsx │ └── index.ts ├── Disclosure │ ├── Disclosure.ts │ ├── DisclosureContent.ts │ ├── DisclosureState.ts │ ├── __tests__ │ │ ├── Disclosure.spec.tsx │ │ ├── DisclosureContent.spec.tsx │ │ ├── DisclosureState.spec.tsx │ │ └── index.spec.tsx │ └── index.ts ├── Modal │ ├── Modal.ts │ ├── ModalBackdrop.ts │ ├── ModalDisclosure.ts │ ├── ModalState.ts │ ├── README.md │ ├── __tests__ │ │ ├── Modal.spec.tsx │ │ ├── ModalBackdrop.spec.tsx │ │ ├── ModalDisclosure.spec.tsx │ │ └── index.spec.tsx │ └── index.ts ├── Popover │ ├── Popover.ts │ ├── PopoverDisclosure.ts │ ├── PopoverState.ts │ ├── README.md │ ├── __tests__ │ │ ├── Popover.spec.tsx │ │ ├── PopoverDisclosure.spec.tsx │ │ └── index.spec.tsx │ └── index.ts ├── Portal │ ├── Portal.spec.tsx │ ├── Portal.ts │ └── index.ts ├── Tabbable │ ├── Tabbable.spec.tsx │ ├── Tabbable.ts │ └── index.ts └── utils │ ├── component.ts │ ├── element.ts │ ├── index.ts │ └── visibility.ts ├── test ├── setup.ts └── utils │ ├── events.ts │ ├── index.ts │ └── render.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2020: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:vue/vue3-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier', 12 | ], 13 | parser: 'vue-eslint-parser', 14 | parserOptions: { 15 | ecmaVersion: 11, 16 | parser: '@typescript-eslint/parser', 17 | sourceType: 'module', 18 | }, 19 | plugins: ['vue', '@typescript-eslint'], 20 | rules: { 21 | '@typescript-eslint/no-var-requires': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | 'vue/multi-word-component-names': 'off', 24 | }, 25 | ignorePatterns: ['lib', 'docs', 'coverage'], 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v2 16 | 17 | - name: Set up Node 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 12 21 | 22 | - name: Install dependencies 23 | run: yarn --frozen-lockfile 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test --coverage 30 | 31 | - name: Upload coverage to Codecov 32 | env: 33 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 34 | run: bash <(curl -s https://codecov.io/bash) 35 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 14 15 | - name: Install dependencies 16 | run: yarn --frozen-lockfile 17 | 18 | - name: Lint 19 | run: yarn lint 20 | 21 | - name: Test 22 | run: yarn test 23 | 24 | - name: Build 25 | run: yarn build:publish 26 | 27 | publish-npm: 28 | needs: build 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: actions/setup-node@v1 33 | with: 34 | node-version: 14 35 | registry-url: https://registry.npmjs.org/ 36 | - run: yarn --frozen-lockfile 37 | - run: yarn build:publish 38 | - run: npm publish 39 | env: 40 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .DS_Store 4 | dist 5 | *.local 6 | .eslintcache 7 | lib -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .DS_Store 4 | dist 5 | *.local 6 | .eslintcache 7 | examples 8 | .vscode 9 | .github -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.codeActionsOnSave.mode": "all", 3 | "eslint.validate": [ 4 | "typescript", 5 | "typescriptreact", 6 | "javascript", 7 | "javascriptreact", 8 | "vue" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jörg Bayreuther 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 | # Ari 2 | 3 | [![CI](https://github.com/visualjerk/ari/workflows/CI/badge.svg)](https://github.com/visualjerk/ari/actions) 4 | [![Test Coverage](https://codecov.io/gh/visualjerk/ari/branch/master/graph/badge.svg)](https://codecov.io/gh/visualjerk/ari) 5 | 6 | Accessible unstyled Vue components inspired by Reakit. 7 | 8 | Try it on [Stackblitz](https://stackblitz.com/edit/vue-grjxen?file=src/App.vue). 9 | 10 | ## Installation 11 | 12 | ```bash 13 | npm i vue-ari 14 | ``` 15 | 16 | or 17 | 18 | ```bash 19 | yarn add vue-ari 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```vue 25 | 33 | 34 | 50 | ``` 51 | 52 | ## Styling 53 | 54 | Ari components don't include styling by default. This gives you the ability to add styles however you like. 55 | 56 | ### Example Using Tailwind 57 | 58 | ```vue 59 | 73 | 74 | 90 | ``` 91 | 92 | ## Reusable Components 93 | 94 | It would get pretty verbose to add the same styling classes wherever you like to use a `Popover`. So the recommended way is wrapping Ari components inside your own base components and use them inside your app. 95 | 96 | ```vue 97 | 105 | 106 | 117 | ``` 118 | 119 | ## Abstracting State 120 | 121 | If you would rather not create a modal state each time, just create a provider component. 122 | 123 | Provider component: 124 | 125 | ```vue 126 | 129 | 130 | 141 | ``` 142 | 143 | Base component for disclosure: 144 | 145 | ```vue 146 | 154 | 155 | 172 | ``` 173 | 174 | Base component for modal: 175 | 176 | ```vue 177 | 190 | 191 | 210 | ``` 211 | 212 | Inside your app: 213 | 214 | ```vue 215 | 225 | 226 | 237 | ``` 238 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['@ant-design-vue/babel-plugin-jsx'], 3 | presets: [ 4 | [ 5 | '@babel/preset-env', 6 | { 7 | targets: { 8 | node: 'current', 9 | }, 10 | }, 11 | ], 12 | ['@babel/preset-typescript'], 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .cache 3 | .DS_Store 4 | src/.temp 5 | node_modules 6 | dist 7 | !.env.example 8 | .env 9 | .env.* 10 | -------------------------------------------------------------------------------- /docs/LICENCE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Marco Reimann 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. -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 |

2 | Docc Logo 3 |

4 | 5 | A starter documentation theme for [Gridsome](https://gridsome.org/). 6 | 7 | ## Installation 8 | 9 | If you have the Gridsome CLI installed, simply run: 10 | 11 | `gridsome create your-project https://github.com/mrcrmn/docc` 12 | 13 | ## Documentation 14 | 15 | Documentation can be found [here](https://docc-theme.netlify.com/). 16 | -------------------------------------------------------------------------------- /docs/content/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: '' 3 | sidebar: 'docs' 4 | next: '/docs/button/' 5 | --- 6 | 7 | # Introduction 8 | 9 | Ari offers accessible Vue 3 components without styling. It helps building a11y compliant components without having to overwrite default styles. 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm i vue-ari 15 | ``` 16 | 17 | or 18 | 19 | ```bash 20 | yarn add vue-ari 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```html 26 | 34 | 35 | 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/gridsome.config.js: -------------------------------------------------------------------------------- 1 | // This is where project configuration and plugin options are located. 2 | // Learn more: https://gridsome.org/docs/config 3 | 4 | // Changes here require a server restart. 5 | // To restart press CTRL + C in terminal and run `gridsome develop` 6 | 7 | module.exports = { 8 | siteName: 'ari', 9 | icon: { 10 | favicon: './src/assets/favicon.png', 11 | touchicon: './src/assets/favicon.png', 12 | }, 13 | siteUrl: 'https://visualjerk.github.io', 14 | pathPrefix: '/ari', 15 | settings: { 16 | web: process.env.URL_WEB || false, 17 | twitter: process.env.URL_TWITTER || false, 18 | github: process.env.URL_GITHUB || 'https://github.com/visualjerk/ari', 19 | nav: { 20 | links: [{ path: '/docs/', title: 'Docs' }], 21 | }, 22 | sidebar: [ 23 | { 24 | name: 'docs', 25 | sections: [ 26 | { 27 | title: 'Getting Started', 28 | items: ['/docs/'], 29 | }, 30 | { 31 | title: 'Components', 32 | items: ['/docs/button/', '/docs/modal/', '/docs/popover/'], 33 | }, 34 | ], 35 | }, 36 | ], 37 | }, 38 | plugins: [ 39 | { 40 | use: '@gridsome/source-filesystem', 41 | options: { 42 | baseDir: './content', 43 | path: '**/*.md', 44 | typeName: 'MarkdownPage', 45 | remark: { 46 | externalLinksTarget: '_blank', 47 | externalLinksRel: ['noopener', 'noreferrer'], 48 | plugins: ['@gridsome/remark-prismjs'], 49 | }, 50 | }, 51 | }, 52 | { 53 | use: '@gridsome/source-filesystem', 54 | options: { 55 | baseDir: '../src', 56 | path: '**/*.md', 57 | pathPrefix: '/docs', 58 | index: ['README'], 59 | typeName: 'MarkdownPage', 60 | remark: { 61 | externalLinksTarget: '_blank', 62 | externalLinksRel: ['noopener', 'noreferrer'], 63 | plugins: ['@gridsome/remark-prismjs'], 64 | }, 65 | }, 66 | }, 67 | { 68 | use: 'gridsome-plugin-tailwindcss', 69 | options: { 70 | tailwindConfig: './tailwind.config.js', 71 | purgeConfig: { 72 | // Prevent purging of prism classes. 73 | whitelistPatternsChildren: [/token$/], 74 | }, 75 | }, 76 | }, 77 | { 78 | use: '@gridsome/plugin-sitemap', 79 | options: {}, 80 | }, 81 | ], 82 | } 83 | -------------------------------------------------------------------------------- /docs/gridsome.server.js: -------------------------------------------------------------------------------- 1 | // Server API makes it possible to hook into various parts of Gridsome 2 | // on server-side and add custom data to the GraphQL data layer. 3 | // Learn more: https://gridsome.org/docs/server-api/ 4 | 5 | // Changes here require a server restart. 6 | // To restart press CTRL + C in terminal and run `gridsome develop` 7 | 8 | module.exports = function (api) { 9 | api.loadSource(({ addCollection, addMetadata }) => { 10 | // Use the Data Store API here: https://gridsome.org/docs/data-store-api/ 11 | addMetadata('settings', require('./gridsome.config').settings); 12 | }); 13 | 14 | api.createPages(({ createPage }) => { 15 | // Use the Pages API here: https://gridsome.org/docs/pages-api/ 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /docs/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "gridsome build" 3 | publish = "dist/" -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "private": true, 4 | "scripts": { 5 | "build": "gridsome build", 6 | "dev": "gridsome develop", 7 | "explore": "gridsome explore", 8 | "predeploy": "npm run build", 9 | "deploy": "gh-pages -d dist" 10 | }, 11 | "dependencies": { 12 | "gridsome": "^0.7.0" 13 | }, 14 | "devDependencies": { 15 | "@gridsome/plugin-google-analytics": "^0.1.0", 16 | "@gridsome/plugin-sitemap": "^0.2.3", 17 | "@gridsome/remark-prismjs": "^0.3.0", 18 | "@gridsome/source-filesystem": "^0.6.2", 19 | "@gridsome/transformer-remark": "^0.5.0", 20 | "fuse.js": "^3.4.6", 21 | "gh-pages": "^3.1.0", 22 | "gridsome-plugin-tailwindcss": "^2.2.36", 23 | "node-sass": "^4.13.1", 24 | "prism-themes": "^1.3.0", 25 | "sass-loader": "^8.0.2", 26 | "tailwindcss": "^1.2.0", 27 | "vue-feather-icons": "^5.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visualjerk/ari/c0d5d2c720cd25250b892ff1217a8be98d7e4b09/docs/src/assets/favicon.png -------------------------------------------------------------------------------- /docs/src/components/LayoutHeader.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 79 | query { 80 | metadata { 81 | siteName 82 | settings { 83 | web 84 | github 85 | twitter 86 | nav { 87 | links { 88 | path 89 | title 90 | } 91 | } 92 | } 93 | } 94 | } 95 | 96 | 97 | 135 | 136 | 151 | -------------------------------------------------------------------------------- /docs/src/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 42 | 43 | 48 | -------------------------------------------------------------------------------- /docs/src/components/NextPrevLinks.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 58 | -------------------------------------------------------------------------------- /docs/src/components/OnThisPage.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 121 | -------------------------------------------------------------------------------- /docs/src/components/Search.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 73 | query Search { 74 | allMarkdownPage{ 75 | edges { 76 | node { 77 | id 78 | path 79 | title 80 | headings { 81 | depth 82 | value 83 | anchor 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | 91 | 177 | 178 | 180 | -------------------------------------------------------------------------------- /docs/src/components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 44 | query Sidebar { 45 | metadata { 46 | settings { 47 | sidebar { 48 | name 49 | sections { 50 | title 51 | items 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | 59 | 96 | -------------------------------------------------------------------------------- /docs/src/components/ToggleDarkMode.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 67 | 68 | -------------------------------------------------------------------------------- /docs/src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visualjerk/ari/c0d5d2c720cd25250b892ff1217a8be98d7e4b09/docs/src/favicon.png -------------------------------------------------------------------------------- /docs/src/layouts/Default.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 43 | query { 44 | metadata { 45 | siteName 46 | } 47 | } 48 | 49 | 50 | 122 | 123 | 323 | -------------------------------------------------------------------------------- /docs/src/main.js: -------------------------------------------------------------------------------- 1 | // This is the main.js file. Import global CSS and scripts here. 2 | // The Client API can be used here. Learn more: gridsome.org/docs/client-api 3 | import DefaultLayout from '~/layouts/Default.vue' 4 | 5 | export default function (Vue, { router, head, isClient }) { 6 | // Set default layout as a global component 7 | Vue.component('Layout', DefaultLayout) 8 | 9 | router.beforeEach((to, _from, next) => { 10 | head.meta.push({ 11 | key: 'og:url', 12 | name: 'og:url', 13 | content: process.env.GRIDSOME_BASE_PATH + to.path, 14 | }) 15 | next() 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /docs/src/pages/404.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /docs/src/pages/Index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 81 | 82 | 87 | -------------------------------------------------------------------------------- /docs/src/templates/MarkdownPage.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | query ($id: ID!) { 21 | markdownPage(id: $id) { 22 | id 23 | title 24 | description 25 | path 26 | timeToRead 27 | content 28 | sidebar 29 | next 30 | prev 31 | headings { 32 | depth 33 | value 34 | anchor 35 | } 36 | } 37 | allMarkdownPage{ 38 | edges { 39 | node { 40 | path 41 | title 42 | } 43 | } 44 | } 45 | } 46 | 47 | 48 | 95 | 96 | -------------------------------------------------------------------------------- /docs/static/README.md: -------------------------------------------------------------------------------- 1 | Add static files here. Files in this directory will be copied directly to `dist` folder during build. For example, /static/robots.txt will be located at https://yoursite.com/robots.txt. 2 | 3 | This file should be deleted. -------------------------------------------------------------------------------- /docs/static/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visualjerk/ari/c0d5d2c720cd25250b892ff1217a8be98d7e4b09/docs/static/logo.jpg -------------------------------------------------------------------------------- /docs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: { 4 | colors: { 5 | blue: { 6 | 400: '#0057FF', 7 | 500: '#004DE2', 8 | }, 9 | pink: { 10 | 500: '#F23E74', 11 | }, 12 | ui: { 13 | background: 'var(--color-ui-background)', 14 | sidebar: 'var(--color-ui-sidebar)', 15 | typo: 'var(--color-ui-typo)', 16 | primary: 'var(--color-ui-primary)', 17 | highlight: 'var(--color-ui-highlight)', 18 | border: 'var(--color-ui-border)', 19 | }, 20 | }, 21 | spacing: { 22 | sm: '24rem', 23 | }, 24 | screens: { 25 | xxl: '1400px', 26 | }, 27 | }, 28 | container: { 29 | center: true, 30 | padding: '1rem', 31 | }, 32 | }, 33 | variants: {}, 34 | plugins: [], 35 | } 36 | -------------------------------------------------------------------------------- /examples/simple-modal/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | -------------------------------------------------------------------------------- /examples/simple-modal/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["johnsoncodehk.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/simple-modal/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + Typescript + Vite 2 | 3 | This template should help get you started developing with Vue 3 and Typescript in Vite. The template uses Vue 3 ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/simple-modal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-modal", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vue-tsc --noEmit && vite build", 7 | "preview": "vite preview" 8 | }, 9 | "dependencies": { 10 | "vue": "^3.2.25", 11 | "vue-ari": "link:../.." 12 | }, 13 | "devDependencies": { 14 | "@vitejs/plugin-vue": "^2.0.0", 15 | "typescript": "^4.4.4", 16 | "vite": "^2.7.2", 17 | "vue-tsc": "^0.29.8" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/simple-modal/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visualjerk/ari/c0d5d2c720cd25250b892ff1217a8be98d7e4b09/examples/simple-modal/public/favicon.ico -------------------------------------------------------------------------------- /examples/simple-modal/src/App.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 53 | 54 | 239 | -------------------------------------------------------------------------------- /examples/simple-modal/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /examples/simple-modal/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /examples/simple-modal/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "lib": ["esnext", "dom"] 13 | }, 14 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 15 | } 16 | -------------------------------------------------------------------------------- /examples/simple-modal/vite.config.ts: -------------------------------------------------------------------------------- 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/simple-modal/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/helper-validator-identifier@^7.16.7": 6 | version "7.16.7" 7 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" 8 | integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== 9 | 10 | "@babel/parser@^7.16.4", "@babel/parser@^7.6.0", "@babel/parser@^7.9.6": 11 | version "7.16.10" 12 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.10.tgz#aba1b1cb9696a24a19f59c41af9cf17d1c716a88" 13 | integrity sha512-Sm/S9Or6nN8uiFsQU1yodyDW3MWXQhFeqzMPM+t8MJjM+pLsnFVxFZzkpXKvUXh+Gz9cbMoYYs484+Jw/NTEFQ== 14 | 15 | "@babel/types@^7.6.1", "@babel/types@^7.9.6": 16 | version "7.16.8" 17 | resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.8.tgz#0ba5da91dd71e0a4e7781a30f22770831062e3c1" 18 | integrity sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg== 19 | dependencies: 20 | "@babel/helper-validator-identifier" "^7.16.7" 21 | to-fast-properties "^2.0.0" 22 | 23 | "@emmetio/abbreviation@^2.2.2": 24 | version "2.2.2" 25 | resolved "https://registry.yarnpkg.com/@emmetio/abbreviation/-/abbreviation-2.2.2.tgz#746762fd9e7a8c2ea604f580c62e3cfe250e6989" 26 | integrity sha512-TtE/dBnkTCct8+LntkqVrwqQao6EnPAs1YN3cUgxOxTaBlesBCY37ROUAVZrRlG64GNnVShdl/b70RfAI3w5lw== 27 | dependencies: 28 | "@emmetio/scanner" "^1.0.0" 29 | 30 | "@emmetio/css-abbreviation@^2.1.4": 31 | version "2.1.4" 32 | resolved "https://registry.yarnpkg.com/@emmetio/css-abbreviation/-/css-abbreviation-2.1.4.tgz#90362e8a1122ce3b76f6c3157907d30182f53f54" 33 | integrity sha512-qk9L60Y+uRtM5CPbB0y+QNl/1XKE09mSO+AhhSauIfr2YOx/ta3NJw2d8RtCFxgzHeRqFRr8jgyzThbu+MZ4Uw== 34 | dependencies: 35 | "@emmetio/scanner" "^1.0.0" 36 | 37 | "@emmetio/scanner@^1.0.0": 38 | version "1.0.0" 39 | resolved "https://registry.yarnpkg.com/@emmetio/scanner/-/scanner-1.0.0.tgz#065b2af6233fe7474d44823e3deb89724af42b5f" 40 | integrity sha512-8HqW8EVqjnCmWXVpqAOZf+EGESdkR27odcMMMGefgKXtar00SoYNSryGv//TELI4T3QFsECo78p+0lmalk/CFA== 41 | 42 | "@popperjs/core@2.11.2": 43 | version "2.11.2" 44 | resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9" 45 | integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA== 46 | 47 | "@vitejs/plugin-vue@^2.0.0": 48 | version "2.1.0" 49 | resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-2.1.0.tgz#ddf5e0059f84f2ff649afc25ce5a59211e670542" 50 | integrity sha512-AZ78WxvFMYd8JmM/GBV6a6SGGTU0GgN/0/4T+FnMMsLzFEzTeAUwuraapy50ifHZsC+G5SvWs86bvaCPTneFlA== 51 | 52 | "@volar/code-gen@0.29.8": 53 | version "0.29.8" 54 | resolved "https://registry.yarnpkg.com/@volar/code-gen/-/code-gen-0.29.8.tgz#db1a4bf29caeba131265bed9dbe96a1a0b66ea35" 55 | integrity sha512-eohLLUqPChHRPDFT5gXn4V6pr/CeTri7Ou5GI26lUvBRRAbP8p+oYfQRcbMPGeKmVkYjfVj0chsxQGx6T8PQ4Q== 56 | dependencies: 57 | "@volar/shared" "0.29.8" 58 | "@volar/source-map" "0.29.8" 59 | 60 | "@volar/html2pug@0.29.8": 61 | version "0.29.8" 62 | resolved "https://registry.yarnpkg.com/@volar/html2pug/-/html2pug-0.29.8.tgz#2e97fa2968dcdfe0dbbc67b0cd2ab4c440018738" 63 | integrity sha512-bhSNXg8A2aD3w0B+CwmHjqCAaKtj5rORbE5C/q/UdGqptJbC6STCmi30KuRTdfPhR++Xb18Hauf3s/WCmtNAPA== 64 | dependencies: 65 | domelementtype "^2.2.0" 66 | domhandler "^4.2.2" 67 | htmlparser2 "^7.1.2" 68 | pug "^3.0.2" 69 | 70 | "@volar/shared@0.29.8": 71 | version "0.29.8" 72 | resolved "https://registry.yarnpkg.com/@volar/shared/-/shared-0.29.8.tgz#e635ddf2cbcf307da932eb4b98e33c320d3d2991" 73 | integrity sha512-Y1NN6irkIukD+T0wf4p/dHWYL90sacN2e2lYoDXxRlvoYxwANnHgw0J0Rcp+yw58ElWRScdG7/YntEIuZWeJsw== 74 | dependencies: 75 | upath "^2.0.1" 76 | vscode-jsonrpc "^8.0.0-next.2" 77 | vscode-uri "^3.0.2" 78 | 79 | "@volar/source-map@0.29.8": 80 | version "0.29.8" 81 | resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-0.29.8.tgz#3299a0ae86ae0b72b4db3e50499d8bb285d8e9b2" 82 | integrity sha512-7w+UoYtnc6UQu30CgMVvx0YN4dzDgP4TIsSmUaW62AGmxU9Lxwp3Kkn/4N8efi91z8ma5Z78v/HddyJPwAC3LA== 83 | dependencies: 84 | "@volar/shared" "0.29.8" 85 | 86 | "@volar/transforms@0.29.8": 87 | version "0.29.8" 88 | resolved "https://registry.yarnpkg.com/@volar/transforms/-/transforms-0.29.8.tgz#ef807010ac90772a065e7cf50509b6433b53e355" 89 | integrity sha512-o2hRa8CoDwYTO1Mu5KA47+1elUnYUjDaVhCvbyKlRfd8qpHea2llotArq7B6OORSL2M9DVs1IRJ5NGURBFeZ3Q== 90 | dependencies: 91 | "@volar/shared" "0.29.8" 92 | vscode-languageserver "^8.0.0-next.2" 93 | 94 | "@volar/vue-code-gen@0.29.8": 95 | version "0.29.8" 96 | resolved "https://registry.yarnpkg.com/@volar/vue-code-gen/-/vue-code-gen-0.29.8.tgz#32401d52e2570d775fcc6cbc83abefeef65c48cd" 97 | integrity sha512-E1e7P2oktNC/DzgDBditfla4s8+HlUlluZ+BtcLvEdbkl3QEjujkB0x1wxguWzXmpWgLIDPtrS3Jzll5cCOkTg== 98 | dependencies: 99 | "@volar/code-gen" "0.29.8" 100 | "@volar/shared" "0.29.8" 101 | "@volar/source-map" "0.29.8" 102 | "@vue/compiler-core" "^3.2.21" 103 | "@vue/compiler-dom" "^3.2.21" 104 | "@vue/shared" "^3.2.21" 105 | upath "^2.0.1" 106 | 107 | "@vscode/emmet-helper@^2.8.0": 108 | version "2.8.3" 109 | resolved "https://registry.yarnpkg.com/@vscode/emmet-helper/-/emmet-helper-2.8.3.tgz#f7c2b4a4751d03bcf2b421f0fce5b521a0f64a18" 110 | integrity sha512-dkTSL+BaBBS8gFgPm/GMOU+XfxaMyI+Fl1IUYxEi8Iv24RfHf9/q2eCpV2hs7sncLcoKWEbMYe5gv4Ppmp2Oxw== 111 | dependencies: 112 | emmet "^2.3.0" 113 | jsonc-parser "^2.3.0" 114 | vscode-languageserver-textdocument "^1.0.1" 115 | vscode-languageserver-types "^3.15.1" 116 | vscode-nls "^5.0.0" 117 | vscode-uri "^2.1.2" 118 | 119 | "@vue/compiler-core@3.2.28", "@vue/compiler-core@^3.2.21": 120 | version "3.2.28" 121 | resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.28.tgz#7f6aa4b167f0ae0413f3c36e507c898db06e8fe8" 122 | integrity sha512-mQpfEjmHVxmWKaup0HL6tLMv2HqjjJu7XT4/q0IoUXYXC4xKG8lIVn5YChJqxBTLPuQjzas7u7i9L4PAWJZRtA== 123 | dependencies: 124 | "@babel/parser" "^7.16.4" 125 | "@vue/shared" "3.2.28" 126 | estree-walker "^2.0.2" 127 | source-map "^0.6.1" 128 | 129 | "@vue/compiler-dom@3.2.28", "@vue/compiler-dom@^3.2.21": 130 | version "3.2.28" 131 | resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.28.tgz#cc32a987fee50673f25430df35ea943f252c23e6" 132 | integrity sha512-KA4yXceLteKC7VykvPnViUixemQw3A+oii+deSbZJOQKQKVh1HLosI10qxa8ImPCyun41+wG3uGR+tW7eu1W6Q== 133 | dependencies: 134 | "@vue/compiler-core" "3.2.28" 135 | "@vue/shared" "3.2.28" 136 | 137 | "@vue/compiler-sfc@3.2.28": 138 | version "3.2.28" 139 | resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.28.tgz#0a576c09abc72d6a76b153133de6fd7599c182c3" 140 | integrity sha512-zB0WznfEBb4CbGBHzhboHDKVO5nxbkbxxFo9iVlxObP7a9/qvA5kkZEuT7nXP52f3b3qEfmVTjIT23Lo1ndZdQ== 141 | dependencies: 142 | "@babel/parser" "^7.16.4" 143 | "@vue/compiler-core" "3.2.28" 144 | "@vue/compiler-dom" "3.2.28" 145 | "@vue/compiler-ssr" "3.2.28" 146 | "@vue/reactivity-transform" "3.2.28" 147 | "@vue/shared" "3.2.28" 148 | estree-walker "^2.0.2" 149 | magic-string "^0.25.7" 150 | postcss "^8.1.10" 151 | source-map "^0.6.1" 152 | 153 | "@vue/compiler-ssr@3.2.28": 154 | version "3.2.28" 155 | resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.28.tgz#411e8b3bdc3183b2acd35e6551734b34366d64e5" 156 | integrity sha512-z8rck1PDTu20iLyip9lAvIhaO40DUJrw3Zv0mS4Apfh3PlfWpF5dhsO5g0dgt213wgYsQIYVIlU9cfrYapqRgg== 157 | dependencies: 158 | "@vue/compiler-dom" "3.2.28" 159 | "@vue/shared" "3.2.28" 160 | 161 | "@vue/reactivity-transform@3.2.28": 162 | version "3.2.28" 163 | resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.28.tgz#e0abf86694f4d182f974fbac934fc3e23e0a6d9b" 164 | integrity sha512-zE8idNkOPnBDd2tKSIk84hOQZ+jXKvSy5FoIIVlcNEJHnCFnQ3maqeSJ9KoB2Rf6EXUhFTiTDNRlYlXmT2uHbQ== 165 | dependencies: 166 | "@babel/parser" "^7.16.4" 167 | "@vue/compiler-core" "3.2.28" 168 | "@vue/shared" "3.2.28" 169 | estree-walker "^2.0.2" 170 | magic-string "^0.25.7" 171 | 172 | "@vue/reactivity@3.2.28", "@vue/reactivity@^3.2.21": 173 | version "3.2.28" 174 | resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.28.tgz#1c3c7f434372edd867f937151897fca7efc4be18" 175 | integrity sha512-WamM5LGv7JIarW+EYAzYFqYonZXjTnOjNW0sBO93jRE9I1ReAwfH8NvQXkPA3JZ3fuF6SGDdG8Y9/+dKjd/1Gw== 176 | dependencies: 177 | "@vue/shared" "3.2.28" 178 | 179 | "@vue/runtime-core@3.2.28": 180 | version "3.2.28" 181 | resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.28.tgz#69d8eede42957a1660b964004aa002982ae36a41" 182 | integrity sha512-sVbBMFUt42JatTlXbdH6tVcLPw1eEOrrVQWI+j6/nJVzR852RURaT6DhdR0azdYscxq4xmmBctE0VQmlibBOFw== 183 | dependencies: 184 | "@vue/reactivity" "3.2.28" 185 | "@vue/shared" "3.2.28" 186 | 187 | "@vue/runtime-dom@3.2.28": 188 | version "3.2.28" 189 | resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.28.tgz#b5a0cf38daed5534edbc95790f4eeac97dff2003" 190 | integrity sha512-Jg7cxZanEXXGu1QnZILFLnDrM+MIFN8VAullmMZiJEZziHvhygRMpi0ahNy/8OqGwtTze1JNhLdHRBO+q2hbmg== 191 | dependencies: 192 | "@vue/runtime-core" "3.2.28" 193 | "@vue/shared" "3.2.28" 194 | csstype "^2.6.8" 195 | 196 | "@vue/server-renderer@3.2.28": 197 | version "3.2.28" 198 | resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.28.tgz#235944dc4d969fadd387f62acc2eb8b8d50008a2" 199 | integrity sha512-S+MhurgkPabRvhdDl8R6efKBmniJqBbbWIYTXADaJIKFLFLQCW4gcYUTbxuebzk6j3z485vpekhrHHymTF52Pg== 200 | dependencies: 201 | "@vue/compiler-ssr" "3.2.28" 202 | "@vue/shared" "3.2.28" 203 | 204 | "@vue/shared@3.2.28", "@vue/shared@^3.2.21": 205 | version "3.2.28" 206 | resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.28.tgz#5b0b1840432031d0ea1adff633b356a503e87048" 207 | integrity sha512-eMQ8s9j8FpbGHlgUAaj/coaG3Q8YtMsoWL/RIHTsE3Ex7PUTyr7V91vB5HqWB5Sn8m4RXTHGO22/skoTUYvp0A== 208 | 209 | acorn@^7.1.1: 210 | version "7.4.1" 211 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" 212 | integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== 213 | 214 | asap@~2.0.3: 215 | version "2.0.6" 216 | resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" 217 | integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= 218 | 219 | assert-never@^1.2.1: 220 | version "1.2.1" 221 | resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.2.1.tgz#11f0e363bf146205fb08193b5c7b90f4d1cf44fe" 222 | integrity sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw== 223 | 224 | babel-walk@3.0.0-canary-5: 225 | version "3.0.0-canary-5" 226 | resolved "https://registry.yarnpkg.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz#f66ecd7298357aee44955f235a6ef54219104b11" 227 | integrity sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw== 228 | dependencies: 229 | "@babel/types" "^7.9.6" 230 | 231 | body-scroll-lock@3.0.3: 232 | version "3.0.3" 233 | resolved "https://registry.yarnpkg.com/body-scroll-lock/-/body-scroll-lock-3.0.3.tgz#221d87435bcfb50e27ab5d4508735f622aed11a2" 234 | integrity sha512-EUryImgD6Gv87HOjJB/yB2WIGECiZMhmcUK+DrqVRFDDa64xR+FsK0LgvLPnBxZDTxIl+W80/KJ8i6gp2IwOHQ== 235 | 236 | call-bind@^1.0.2: 237 | version "1.0.2" 238 | resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" 239 | integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== 240 | dependencies: 241 | function-bind "^1.1.1" 242 | get-intrinsic "^1.0.2" 243 | 244 | character-parser@^2.2.0: 245 | version "2.2.0" 246 | resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0" 247 | integrity sha1-x84o821LzZdE5f/CxfzeHHMmH8A= 248 | dependencies: 249 | is-regex "^1.0.3" 250 | 251 | constantinople@^4.0.1: 252 | version "4.0.1" 253 | resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-4.0.1.tgz#0def113fa0e4dc8de83331a5cf79c8b325213151" 254 | integrity sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw== 255 | dependencies: 256 | "@babel/parser" "^7.6.0" 257 | "@babel/types" "^7.6.1" 258 | 259 | csstype@^2.6.8: 260 | version "2.6.19" 261 | resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.19.tgz#feeb5aae89020bb389e1f63669a5ed490e391caa" 262 | integrity sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ== 263 | 264 | doctypes@^1.1.0: 265 | version "1.1.0" 266 | resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9" 267 | integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk= 268 | 269 | dom-serializer@^1.0.1: 270 | version "1.3.2" 271 | resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" 272 | integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== 273 | dependencies: 274 | domelementtype "^2.0.1" 275 | domhandler "^4.2.0" 276 | entities "^2.0.0" 277 | 278 | domelementtype@^2.0.1, domelementtype@^2.2.0: 279 | version "2.2.0" 280 | resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" 281 | integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== 282 | 283 | domhandler@^4.2.0, domhandler@^4.2.2: 284 | version "4.3.0" 285 | resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626" 286 | integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g== 287 | dependencies: 288 | domelementtype "^2.2.0" 289 | 290 | domutils@^2.8.0: 291 | version "2.8.0" 292 | resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" 293 | integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== 294 | dependencies: 295 | dom-serializer "^1.0.1" 296 | domelementtype "^2.2.0" 297 | domhandler "^4.2.0" 298 | 299 | emmet@^2.3.0: 300 | version "2.3.5" 301 | resolved "https://registry.yarnpkg.com/emmet/-/emmet-2.3.5.tgz#7f80f9c3db6831d1ee2b458717b9c36a074b1a47" 302 | integrity sha512-LcWfTamJnXIdMfLvJEC5Ld3hY5/KHXgv1L1bp6I7eEvB0ZhacHZ1kX0BYovJ8FroEsreLcq7n7kZhRMsf6jkXQ== 303 | dependencies: 304 | "@emmetio/abbreviation" "^2.2.2" 305 | "@emmetio/css-abbreviation" "^2.1.4" 306 | 307 | entities@^2.0.0: 308 | version "2.2.0" 309 | resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" 310 | integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== 311 | 312 | entities@^3.0.1: 313 | version "3.0.1" 314 | resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" 315 | integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== 316 | 317 | esbuild-android-arm64@0.13.15: 318 | version "0.13.15" 319 | resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz#3fc3ff0bab76fe35dd237476b5d2b32bb20a3d44" 320 | integrity sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg== 321 | 322 | esbuild-darwin-64@0.13.15: 323 | version "0.13.15" 324 | resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz#8e9169c16baf444eacec60d09b24d11b255a8e72" 325 | integrity sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ== 326 | 327 | esbuild-darwin-arm64@0.13.15: 328 | version "0.13.15" 329 | resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz#1b07f893b632114f805e188ddfca41b2b778229a" 330 | integrity sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ== 331 | 332 | esbuild-freebsd-64@0.13.15: 333 | version "0.13.15" 334 | resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz#0b8b7eca1690c8ec94c75680c38c07269c1f4a85" 335 | integrity sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA== 336 | 337 | esbuild-freebsd-arm64@0.13.15: 338 | version "0.13.15" 339 | resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz#2e1a6c696bfdcd20a99578b76350b41db1934e52" 340 | integrity sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ== 341 | 342 | esbuild-linux-32@0.13.15: 343 | version "0.13.15" 344 | resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz#6fd39f36fc66dd45b6b5f515728c7bbebc342a69" 345 | integrity sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g== 346 | 347 | esbuild-linux-64@0.13.15: 348 | version "0.13.15" 349 | resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz#9cb8e4bcd7574e67946e4ee5f1f1e12386bb6dd3" 350 | integrity sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA== 351 | 352 | esbuild-linux-arm64@0.13.15: 353 | version "0.13.15" 354 | resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz#3891aa3704ec579a1b92d2a586122e5b6a2bfba1" 355 | integrity sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA== 356 | 357 | esbuild-linux-arm@0.13.15: 358 | version "0.13.15" 359 | resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz#8a00e99e6a0c6c9a6b7f334841364d8a2b4aecfe" 360 | integrity sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA== 361 | 362 | esbuild-linux-mips64le@0.13.15: 363 | version "0.13.15" 364 | resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz#36b07cc47c3d21e48db3bb1f4d9ef8f46aead4f7" 365 | integrity sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg== 366 | 367 | esbuild-linux-ppc64le@0.13.15: 368 | version "0.13.15" 369 | resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz#f7e6bba40b9a11eb9dcae5b01550ea04670edad2" 370 | integrity sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ== 371 | 372 | esbuild-netbsd-64@0.13.15: 373 | version "0.13.15" 374 | resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz#a2fedc549c2b629d580a732d840712b08d440038" 375 | integrity sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w== 376 | 377 | esbuild-openbsd-64@0.13.15: 378 | version "0.13.15" 379 | resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz#b22c0e5806d3a1fbf0325872037f885306b05cd7" 380 | integrity sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g== 381 | 382 | esbuild-sunos-64@0.13.15: 383 | version "0.13.15" 384 | resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz#d0b6454a88375ee8d3964daeff55c85c91c7cef4" 385 | integrity sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw== 386 | 387 | esbuild-windows-32@0.13.15: 388 | version "0.13.15" 389 | resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz#c96d0b9bbb52f3303322582ef8e4847c5ad375a7" 390 | integrity sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw== 391 | 392 | esbuild-windows-64@0.13.15: 393 | version "0.13.15" 394 | resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz#1f79cb9b1e1bb02fb25cd414cb90d4ea2892c294" 395 | integrity sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ== 396 | 397 | esbuild-windows-arm64@0.13.15: 398 | version "0.13.15" 399 | resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz#482173070810df22a752c686509c370c3be3b3c3" 400 | integrity sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA== 401 | 402 | esbuild@^0.13.12: 403 | version "0.13.15" 404 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.15.tgz#db56a88166ee373f87dbb2d8798ff449e0450cdf" 405 | integrity sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw== 406 | optionalDependencies: 407 | esbuild-android-arm64 "0.13.15" 408 | esbuild-darwin-64 "0.13.15" 409 | esbuild-darwin-arm64 "0.13.15" 410 | esbuild-freebsd-64 "0.13.15" 411 | esbuild-freebsd-arm64 "0.13.15" 412 | esbuild-linux-32 "0.13.15" 413 | esbuild-linux-64 "0.13.15" 414 | esbuild-linux-arm "0.13.15" 415 | esbuild-linux-arm64 "0.13.15" 416 | esbuild-linux-mips64le "0.13.15" 417 | esbuild-linux-ppc64le "0.13.15" 418 | esbuild-netbsd-64 "0.13.15" 419 | esbuild-openbsd-64 "0.13.15" 420 | esbuild-sunos-64 "0.13.15" 421 | esbuild-windows-32 "0.13.15" 422 | esbuild-windows-64 "0.13.15" 423 | esbuild-windows-arm64 "0.13.15" 424 | 425 | estree-walker@^2.0.2: 426 | version "2.0.2" 427 | resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" 428 | integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== 429 | 430 | fsevents@~2.3.2: 431 | version "2.3.2" 432 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 433 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 434 | 435 | function-bind@^1.1.1: 436 | version "1.1.1" 437 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 438 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 439 | 440 | get-intrinsic@^1.0.2: 441 | version "1.1.1" 442 | resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" 443 | integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== 444 | dependencies: 445 | function-bind "^1.1.1" 446 | has "^1.0.3" 447 | has-symbols "^1.0.1" 448 | 449 | has-symbols@^1.0.1, has-symbols@^1.0.2: 450 | version "1.0.2" 451 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" 452 | integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== 453 | 454 | has-tostringtag@^1.0.0: 455 | version "1.0.0" 456 | resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" 457 | integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== 458 | dependencies: 459 | has-symbols "^1.0.2" 460 | 461 | has@^1.0.3: 462 | version "1.0.3" 463 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 464 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 465 | dependencies: 466 | function-bind "^1.1.1" 467 | 468 | htmlparser2@^7.1.2: 469 | version "7.2.0" 470 | resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-7.2.0.tgz#8817cdea38bbc324392a90b1990908e81a65f5a5" 471 | integrity sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog== 472 | dependencies: 473 | domelementtype "^2.0.1" 474 | domhandler "^4.2.2" 475 | domutils "^2.8.0" 476 | entities "^3.0.1" 477 | 478 | is-core-module@^2.8.0: 479 | version "2.8.1" 480 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" 481 | integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== 482 | dependencies: 483 | has "^1.0.3" 484 | 485 | is-expression@^4.0.0: 486 | version "4.0.0" 487 | resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-4.0.0.tgz#c33155962abf21d0afd2552514d67d2ec16fd2ab" 488 | integrity sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A== 489 | dependencies: 490 | acorn "^7.1.1" 491 | object-assign "^4.1.1" 492 | 493 | is-promise@^2.0.0: 494 | version "2.2.2" 495 | resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" 496 | integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== 497 | 498 | is-regex@^1.0.3: 499 | version "1.1.4" 500 | resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" 501 | integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== 502 | dependencies: 503 | call-bind "^1.0.2" 504 | has-tostringtag "^1.0.0" 505 | 506 | js-stringify@^1.0.2: 507 | version "1.0.2" 508 | resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" 509 | integrity sha1-Fzb939lyTyijaCrcYjCufk6Weds= 510 | 511 | jsonc-parser@^2.3.0: 512 | version "2.3.1" 513 | resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.1.tgz#59549150b133f2efacca48fe9ce1ec0659af2342" 514 | integrity sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg== 515 | 516 | jsonc-parser@^3.0.0: 517 | version "3.0.0" 518 | resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" 519 | integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== 520 | 521 | jstransformer@1.0.0: 522 | version "1.0.0" 523 | resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3" 524 | integrity sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM= 525 | dependencies: 526 | is-promise "^2.0.0" 527 | promise "^7.0.1" 528 | 529 | lru-cache@^6.0.0: 530 | version "6.0.0" 531 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" 532 | integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== 533 | dependencies: 534 | yallist "^4.0.0" 535 | 536 | magic-string@^0.25.7: 537 | version "0.25.7" 538 | resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" 539 | integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== 540 | dependencies: 541 | sourcemap-codec "^1.4.4" 542 | 543 | nanoid@^3.1.30: 544 | version "3.2.0" 545 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" 546 | integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== 547 | 548 | object-assign@^4.1.1: 549 | version "4.1.1" 550 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 551 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 552 | 553 | path-parse@^1.0.7: 554 | version "1.0.7" 555 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" 556 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 557 | 558 | picocolors@^1.0.0: 559 | version "1.0.0" 560 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" 561 | integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== 562 | 563 | postcss@^8.1.10, postcss@^8.4.5: 564 | version "8.4.5" 565 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95" 566 | integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg== 567 | dependencies: 568 | nanoid "^3.1.30" 569 | picocolors "^1.0.0" 570 | source-map-js "^1.0.1" 571 | 572 | promise@^7.0.1: 573 | version "7.3.1" 574 | resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" 575 | integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== 576 | dependencies: 577 | asap "~2.0.3" 578 | 579 | pug-attrs@^3.0.0: 580 | version "3.0.0" 581 | resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-3.0.0.tgz#b10451e0348165e31fad1cc23ebddd9dc7347c41" 582 | integrity sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA== 583 | dependencies: 584 | constantinople "^4.0.1" 585 | js-stringify "^1.0.2" 586 | pug-runtime "^3.0.0" 587 | 588 | pug-code-gen@^3.0.2: 589 | version "3.0.2" 590 | resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-3.0.2.tgz#ad190f4943133bf186b60b80de483100e132e2ce" 591 | integrity sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg== 592 | dependencies: 593 | constantinople "^4.0.1" 594 | doctypes "^1.1.0" 595 | js-stringify "^1.0.2" 596 | pug-attrs "^3.0.0" 597 | pug-error "^2.0.0" 598 | pug-runtime "^3.0.0" 599 | void-elements "^3.1.0" 600 | with "^7.0.0" 601 | 602 | pug-error@^2.0.0: 603 | version "2.0.0" 604 | resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-2.0.0.tgz#5c62173cb09c34de2a2ce04f17b8adfec74d8ca5" 605 | integrity sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ== 606 | 607 | pug-filters@^4.0.0: 608 | version "4.0.0" 609 | resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-4.0.0.tgz#d3e49af5ba8472e9b7a66d980e707ce9d2cc9b5e" 610 | integrity sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A== 611 | dependencies: 612 | constantinople "^4.0.1" 613 | jstransformer "1.0.0" 614 | pug-error "^2.0.0" 615 | pug-walk "^2.0.0" 616 | resolve "^1.15.1" 617 | 618 | pug-lexer@^5.0.1: 619 | version "5.0.1" 620 | resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-5.0.1.tgz#ae44628c5bef9b190b665683b288ca9024b8b0d5" 621 | integrity sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w== 622 | dependencies: 623 | character-parser "^2.2.0" 624 | is-expression "^4.0.0" 625 | pug-error "^2.0.0" 626 | 627 | pug-linker@^4.0.0: 628 | version "4.0.0" 629 | resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-4.0.0.tgz#12cbc0594fc5a3e06b9fc59e6f93c146962a7708" 630 | integrity sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw== 631 | dependencies: 632 | pug-error "^2.0.0" 633 | pug-walk "^2.0.0" 634 | 635 | pug-load@^3.0.0: 636 | version "3.0.0" 637 | resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-3.0.0.tgz#9fd9cda52202b08adb11d25681fb9f34bd41b662" 638 | integrity sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ== 639 | dependencies: 640 | object-assign "^4.1.1" 641 | pug-walk "^2.0.0" 642 | 643 | pug-parser@^6.0.0: 644 | version "6.0.0" 645 | resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-6.0.0.tgz#a8fdc035863a95b2c1dc5ebf4ecf80b4e76a1260" 646 | integrity sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw== 647 | dependencies: 648 | pug-error "^2.0.0" 649 | token-stream "1.0.0" 650 | 651 | pug-runtime@^3.0.0, pug-runtime@^3.0.1: 652 | version "3.0.1" 653 | resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-3.0.1.tgz#f636976204723f35a8c5f6fad6acda2a191b83d7" 654 | integrity sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg== 655 | 656 | pug-strip-comments@^2.0.0: 657 | version "2.0.0" 658 | resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz#f94b07fd6b495523330f490a7f554b4ff876303e" 659 | integrity sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ== 660 | dependencies: 661 | pug-error "^2.0.0" 662 | 663 | pug-walk@^2.0.0: 664 | version "2.0.0" 665 | resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-2.0.0.tgz#417aabc29232bb4499b5b5069a2b2d2a24d5f5fe" 666 | integrity sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ== 667 | 668 | pug@^3.0.2: 669 | version "3.0.2" 670 | resolved "https://registry.yarnpkg.com/pug/-/pug-3.0.2.tgz#f35c7107343454e43bc27ae0ff76c731b78ea535" 671 | integrity sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw== 672 | dependencies: 673 | pug-code-gen "^3.0.2" 674 | pug-filters "^4.0.0" 675 | pug-lexer "^5.0.1" 676 | pug-linker "^4.0.0" 677 | pug-load "^3.0.0" 678 | pug-parser "^6.0.0" 679 | pug-runtime "^3.0.1" 680 | pug-strip-comments "^2.0.0" 681 | 682 | request-light@^0.5.4: 683 | version "0.5.7" 684 | resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.5.7.tgz#1c448c22153b55d2cd278eb414df24a5ad6e6d5e" 685 | integrity sha512-i/wKzvcx7Er8tZnvqSxWuNO5ZGggu2UgZAqj/RyZ0si7lBTXL7kZiI/dWxzxnQjaY7s5HEy1qK21Do4Ncr6cVw== 686 | 687 | resolve@^1.15.1, resolve@^1.20.0: 688 | version "1.21.1" 689 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.21.1.tgz#1a88c73f5ca8ab0aabc8b888c4170de26c92c4cc" 690 | integrity sha512-lfEImVbnolPuaSZuLQ52cAxPBHeI77sPwCOWRdy12UG/CNa8an7oBHH1R+Fp1/mUqSJi4c8TIP6FOIPSZAUrEQ== 691 | dependencies: 692 | is-core-module "^2.8.0" 693 | path-parse "^1.0.7" 694 | supports-preserve-symlinks-flag "^1.0.0" 695 | 696 | rollup@^2.59.0: 697 | version "2.66.0" 698 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.66.0.tgz#ee529ea15a20485d579039637fec3050bad03bbb" 699 | integrity sha512-L6mKOkdyP8HK5kKJXaiWG7KZDumPJjuo1P+cfyHOJPNNTK3Moe7zCH5+fy7v8pVmHXtlxorzaBjvkBMB23s98g== 700 | optionalDependencies: 701 | fsevents "~2.3.2" 702 | 703 | semver@^7.3.5: 704 | version "7.3.5" 705 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" 706 | integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== 707 | dependencies: 708 | lru-cache "^6.0.0" 709 | 710 | source-map-js@^1.0.1: 711 | version "1.0.2" 712 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" 713 | integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== 714 | 715 | source-map@^0.6.1: 716 | version "0.6.1" 717 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 718 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 719 | 720 | sourcemap-codec@^1.4.4: 721 | version "1.4.8" 722 | resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" 723 | integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== 724 | 725 | supports-preserve-symlinks-flag@^1.0.0: 726 | version "1.0.0" 727 | resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" 728 | integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 729 | 730 | to-fast-properties@^2.0.0: 731 | version "2.0.0" 732 | resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" 733 | integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= 734 | 735 | token-stream@1.0.0: 736 | version "1.0.0" 737 | resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-1.0.0.tgz#cc200eab2613f4166d27ff9afc7ca56d49df6eb4" 738 | integrity sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ= 739 | 740 | typescript@^4.4.4: 741 | version "4.5.5" 742 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" 743 | integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== 744 | 745 | upath@^2.0.1: 746 | version "2.0.1" 747 | resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" 748 | integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== 749 | 750 | vite@^2.7.2: 751 | version "2.7.13" 752 | resolved "https://registry.yarnpkg.com/vite/-/vite-2.7.13.tgz#99b56e27dfb1e4399e407cf94648f5c7fb9d77f5" 753 | integrity sha512-Mq8et7f3aK0SgSxjDNfOAimZGW9XryfHRa/uV0jseQSilg+KhYDSoNb9h1rknOy6SuMkvNDLKCYAYYUMCE+IgQ== 754 | dependencies: 755 | esbuild "^0.13.12" 756 | postcss "^8.4.5" 757 | resolve "^1.20.0" 758 | rollup "^2.59.0" 759 | optionalDependencies: 760 | fsevents "~2.3.2" 761 | 762 | void-elements@^3.1.0: 763 | version "3.1.0" 764 | resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" 765 | integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk= 766 | 767 | vscode-css-languageservice@^5.1.7: 768 | version "5.1.9" 769 | resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.1.9.tgz#9d473e5c61fa6d4d62719d9b8715ff6c644bf14e" 770 | integrity sha512-/tFOWeZBL3Oc9Zc+2MAi3rEwiXJTSZsvjB+M7nSjWLbGPUIjukUA7YzLgsBoUfR35sPJYnXWUkL56PdfIYM8GA== 771 | dependencies: 772 | vscode-languageserver-textdocument "^1.0.1" 773 | vscode-languageserver-types "^3.16.0" 774 | vscode-nls "^5.0.0" 775 | vscode-uri "^3.0.2" 776 | 777 | vscode-html-languageservice@^4.1.0: 778 | version "4.2.1" 779 | resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-4.2.1.tgz#b95077cffd19bf187e53c7bf79e3e0dd7edbc7cf" 780 | integrity sha512-PgaToZVXJ44nFWEBuSINdDgVV6EnpC3MnXBsysR3O5TKcAfywbYeRGRy+Y4dVR7YeUgDvtb+JkJoSkaYC0mxXQ== 781 | dependencies: 782 | vscode-languageserver-textdocument "^1.0.1" 783 | vscode-languageserver-types "^3.16.0" 784 | vscode-nls "^5.0.0" 785 | vscode-uri "^3.0.2" 786 | 787 | vscode-json-languageservice@^4.1.8: 788 | version "4.1.10" 789 | resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-4.1.10.tgz#5d5729fc4f3e02f41599e0104523a1877c25f0fb" 790 | integrity sha512-IHliMEEYSY0tJjJt0ECb8ESx/nRXpoy9kN42WVQXgaqGyizFAf3jibSiezDQTrrY7f3kywXggCU+kkJEM+OLZQ== 791 | dependencies: 792 | jsonc-parser "^3.0.0" 793 | vscode-languageserver-textdocument "^1.0.1" 794 | vscode-languageserver-types "^3.16.0" 795 | vscode-nls "^5.0.0" 796 | vscode-uri "^3.0.2" 797 | 798 | vscode-jsonrpc@8.0.0-next.5, vscode-jsonrpc@^8.0.0-next.2: 799 | version "8.0.0-next.5" 800 | resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.0-next.5.tgz#44ac984fd8a849b8a351611d5867411226885988" 801 | integrity sha512-owRllqcFTnz5rXxcbmHPFGmpFmLqj9Z1V3Dzrv+s8ejOHLIT62Pyb5Uqzyl2/in2VP22DmzErPgZwrxjLCIKiQ== 802 | 803 | vscode-languageserver-protocol@3.17.0-next.12: 804 | version "3.17.0-next.12" 805 | resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.0-next.12.tgz#8304d9d1f9490b4f1dd9f46b35d22e87d675e704" 806 | integrity sha512-VLRcWKOpCXcx9UrqrS+NSF6pNxV498VGYGW+eyp9a79/F9ElUq3wdG6acXYlEfpWHuIxpm6MXps8FU88wqIgTg== 807 | dependencies: 808 | vscode-jsonrpc "8.0.0-next.5" 809 | vscode-languageserver-types "3.17.0-next.6" 810 | 811 | vscode-languageserver-textdocument@^1.0.1: 812 | version "1.0.3" 813 | resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.3.tgz#879f2649bfa5a6e07bc8b392c23ede2dfbf43eff" 814 | integrity sha512-ynEGytvgTb6HVSUwPJIAZgiHQmPCx8bZ8w5um5Lz+q5DjP0Zj8wTFhQpyg8xaMvefDytw2+HH5yzqS+FhsR28A== 815 | 816 | vscode-languageserver-types@3.17.0-next.6: 817 | version "3.17.0-next.6" 818 | resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.0-next.6.tgz#a62465a6656295e257b33b903bd94ced58464022" 819 | integrity sha512-rHYeCotiabJHgvIYzWjV8g0dHCxyOQtcryTv1Xa1horaQ4jx2V+rjLBstc6zMpCyrnZcjorwEcAvGBDCd6wudw== 820 | 821 | vscode-languageserver-types@^3.15.1, vscode-languageserver-types@^3.16.0: 822 | version "3.16.0" 823 | resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247" 824 | integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== 825 | 826 | vscode-languageserver@^8.0.0-next.2: 827 | version "8.0.0-next.6" 828 | resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-8.0.0-next.6.tgz#c349a1f6e56c7034613161c79818d36587b1bf5f" 829 | integrity sha512-RgGRAsXUksdtCrhtxFUeMXzqE4C/7AHSR6loIQY3GFDNIqrlEIkkQZg2Kkouf/i+eE/Iummn2ZB85VKNTBQgsQ== 830 | dependencies: 831 | vscode-languageserver-protocol "3.17.0-next.12" 832 | 833 | vscode-nls@^5.0.0: 834 | version "5.0.0" 835 | resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" 836 | integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== 837 | 838 | vscode-pug-languageservice@0.29.8: 839 | version "0.29.8" 840 | resolved "https://registry.yarnpkg.com/vscode-pug-languageservice/-/vscode-pug-languageservice-0.29.8.tgz#3d95f974bc273ddd8f5a8fb5d3ac8064b2dc63d1" 841 | integrity sha512-QHYAzDSJLg7GOLxCZ12qsM0dAM0dPeMSS1t4kKfzLsfpErmZpFzkAIXbidVrNMdMffGZMtTuIlcpEyWHbx96Iw== 842 | dependencies: 843 | "@volar/code-gen" "0.29.8" 844 | "@volar/shared" "0.29.8" 845 | "@volar/source-map" "0.29.8" 846 | "@volar/transforms" "0.29.8" 847 | pug-lexer "^5.0.1" 848 | pug-parser "^6.0.0" 849 | vscode-languageserver "^8.0.0-next.2" 850 | 851 | vscode-typescript-languageservice@0.29.8: 852 | version "0.29.8" 853 | resolved "https://registry.yarnpkg.com/vscode-typescript-languageservice/-/vscode-typescript-languageservice-0.29.8.tgz#370572e8c99c8b8190733a4bfc1b45c5f91aa044" 854 | integrity sha512-eecDqHk4WjEvy6VHQ6teHczppQ9yJO2wExCy7yu7WiFj35qbw0h4G6Erv46MvP3ClL8FggFzD7s1qM6vdqJUfw== 855 | dependencies: 856 | "@volar/shared" "0.29.8" 857 | semver "^7.3.5" 858 | upath "^2.0.1" 859 | vscode-languageserver "^8.0.0-next.2" 860 | vscode-languageserver-textdocument "^1.0.1" 861 | 862 | vscode-uri@^2.1.2: 863 | version "2.1.2" 864 | resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.1.2.tgz#c8d40de93eb57af31f3c715dd650e2ca2c096f1c" 865 | integrity sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A== 866 | 867 | vscode-uri@^3.0.2: 868 | version "3.0.3" 869 | resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84" 870 | integrity sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA== 871 | 872 | vscode-vue-languageservice@0.29.8: 873 | version "0.29.8" 874 | resolved "https://registry.yarnpkg.com/vscode-vue-languageservice/-/vscode-vue-languageservice-0.29.8.tgz#6d59aab4fb94215b99b6f7d0e2ab73babd398d05" 875 | integrity sha512-qSJdvW5ttyGUB/8uWDKgo8vnIoFnXYlBP4Z/cn54btsRn6ZMw7IJGJU1381e7p/yGvMTLeGbugD53SghbnSa6g== 876 | dependencies: 877 | "@volar/code-gen" "0.29.8" 878 | "@volar/html2pug" "0.29.8" 879 | "@volar/shared" "0.29.8" 880 | "@volar/source-map" "0.29.8" 881 | "@volar/transforms" "0.29.8" 882 | "@volar/vue-code-gen" "0.29.8" 883 | "@vscode/emmet-helper" "^2.8.0" 884 | "@vue/reactivity" "^3.2.21" 885 | "@vue/shared" "^3.2.21" 886 | request-light "^0.5.4" 887 | upath "^2.0.1" 888 | vscode-css-languageservice "^5.1.7" 889 | vscode-html-languageservice "^4.1.0" 890 | vscode-json-languageservice "^4.1.8" 891 | vscode-languageserver "^8.0.0-next.2" 892 | vscode-languageserver-textdocument "^1.0.1" 893 | vscode-pug-languageservice "0.29.8" 894 | vscode-typescript-languageservice "0.29.8" 895 | 896 | "vue-ari@link:../..": 897 | version "0.0.0" 898 | uid "" 899 | 900 | vue-tsc@^0.29.8: 901 | version "0.29.8" 902 | resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-0.29.8.tgz#f4d8de5dd8756107c878489ccf9178d2d72fff47" 903 | integrity sha512-pT0wLRjvRuSmB+J4WJT6uuV9mO0KtSSXEAtaVXZQzyk5+DJdbLIQTbRce/TXSkfqt1l1WogO78RjtOJFiMCgfQ== 904 | dependencies: 905 | "@volar/shared" "0.29.8" 906 | vscode-vue-languageservice "0.29.8" 907 | 908 | vue@^3.2.25: 909 | version "3.2.28" 910 | resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.28.tgz#840d193bf9713f57a365ef115c4b1286d43e0e5d" 911 | integrity sha512-U+jBwVh3RQ9AgceLFdT7i2FFujoC+kYuGrKo5y8aLluWKZWPS40WgA2pyYHaiSX9ydCbEGr3rc/JzdqskzD95g== 912 | dependencies: 913 | "@vue/compiler-dom" "3.2.28" 914 | "@vue/compiler-sfc" "3.2.28" 915 | "@vue/runtime-dom" "3.2.28" 916 | "@vue/server-renderer" "3.2.28" 917 | "@vue/shared" "3.2.28" 918 | 919 | with@^7.0.0: 920 | version "7.0.2" 921 | resolved "https://registry.yarnpkg.com/with/-/with-7.0.2.tgz#ccee3ad542d25538a7a7a80aad212b9828495bac" 922 | integrity sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w== 923 | dependencies: 924 | "@babel/parser" "^7.9.6" 925 | "@babel/types" "^7.9.6" 926 | assert-never "^1.2.1" 927 | babel-walk "3.0.0-canary-5" 928 | 929 | yallist@^4.0.0: 930 | version "4.0.0" 931 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" 932 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 933 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: ['/test/setup.ts'], 3 | collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!**/node_modules/**'], 4 | testEnvironment: 'jsdom', 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-ari", 3 | "version": "0.0.0-alpha.14", 4 | "license": "MIT", 5 | "repository": "https://github.com/visualjerk/ari", 6 | "main": "lib/index.js", 7 | "module": "lib/index.es.js", 8 | "types": "lib/index.d.ts", 9 | "author": { 10 | "name": "Jörg Bayreuther", 11 | "url": "https://github.com/visualjerk" 12 | }, 13 | "scripts": { 14 | "lint": "eslint . --ext js,ts,tsx,vue --fix", 15 | "test": "jest", 16 | "dev": "yarn build --watch", 17 | "build": "rollup -c", 18 | "build:publish": "BUILD_WITH_TYPES=true yarn build", 19 | "docs:deploy": "cd docs && yarn deploy" 20 | }, 21 | "dependencies": { 22 | "@popperjs/core": "2.11.2", 23 | "body-scroll-lock": "3.0.3" 24 | }, 25 | "peerDependencies": { 26 | "vue": "^3.2.26" 27 | }, 28 | "devDependencies": { 29 | "@ant-design-vue/babel-plugin-jsx": "1.0.0-rc.1", 30 | "@babel/core": "7.16.10", 31 | "@babel/preset-env": "7.16.11", 32 | "@babel/preset-typescript": "7.16.7", 33 | "@rollup/plugin-commonjs": "21.0.1", 34 | "@rollup/plugin-multi-entry": "4.1.0", 35 | "@rollup/plugin-node-resolve": "13.1.3", 36 | "@rollup/plugin-typescript": "8.3.0", 37 | "@testing-library/dom": "8.11.2", 38 | "@testing-library/jest-dom": "5.16.1", 39 | "@testing-library/user-event": "13.5.0", 40 | "@types/jest": "27.4.0", 41 | "@typescript-eslint/eslint-plugin": "5.10.0", 42 | "@typescript-eslint/parser": "5.10.0", 43 | "@vue/compiler-sfc": "3.2.26", 44 | "@vue/test-utils": "2.0.0-rc.18", 45 | "babel-jest": " 27.4.6", 46 | "eslint": "8.7.0", 47 | "eslint-config-prettier": "8.3.0", 48 | "eslint-plugin-vue": "8.3.0", 49 | "husky": "7.0.4", 50 | "jest": "27.4.7", 51 | "lint-staged": "12.2.2", 52 | "prettier": "2.5.1", 53 | "rollup": "2.66.0", 54 | "rollup-plugin-delete": "2.0.0", 55 | "rollup-plugin-dts": "4.1.0", 56 | "tslib": "2.3.1", 57 | "typescript": "4.5.5", 58 | "vue": "3.2.26" 59 | }, 60 | "husky": { 61 | "hooks": { 62 | "pre-commit": "lint-staged" 63 | } 64 | }, 65 | "lint-staged": { 66 | "*.{js,ts,tsx,vue}": [ 67 | "eslint --cache --fix", 68 | "jest" 69 | ], 70 | "*.{css,scss,md}": "prettier --write" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | semi: false, 4 | singleQuote: true, 5 | } 6 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import multi from '@rollup/plugin-multi-entry' 2 | import typescript from '@rollup/plugin-typescript' 3 | import { nodeResolve } from '@rollup/plugin-node-resolve' 4 | import commonjs from '@rollup/plugin-commonjs' 5 | import del from 'rollup-plugin-delete' 6 | import dts from 'rollup-plugin-dts' 7 | 8 | const config = [ 9 | { 10 | input: 'src/**/index.ts', 11 | output: [ 12 | { 13 | file: 'lib/index.js', 14 | format: 'cjs', 15 | }, 16 | { 17 | file: 'lib/index.es.js', 18 | format: 'es', 19 | }, 20 | ], 21 | plugins: [ 22 | del({ targets: 'lib/*' }), 23 | multi(), 24 | typescript({ 25 | tsconfig: './tsconfig.json', 26 | }), 27 | nodeResolve(), 28 | commonjs({ 29 | include: /node_modules/, 30 | }), 31 | ], 32 | external: ['vue'], 33 | }, 34 | ] 35 | 36 | if (process.env.BUILD_WITH_TYPES) { 37 | config.push({ 38 | input: './lib/dts/**/*.d.ts', 39 | output: [{ file: 'lib/index.d.ts', format: 'es' }], 40 | plugins: [multi(), dts(), del({ targets: 'lib/dts', hook: 'buildEnd' })], 41 | }) 42 | } 43 | 44 | export default config 45 | -------------------------------------------------------------------------------- /src/Box/Box.spec.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '.' 2 | import { renderJsx, getByText } from '../../test/utils' 3 | 4 | describe('Box', () => { 5 | it('renders correctly', () => { 6 | renderJsx(foo) 7 | expect(getByText('foo')).toMatchInlineSnapshot(` 8 |
9 | foo 10 |
11 | `) 12 | }) 13 | 14 | it('renders passed attributes', () => { 15 | renderJsx( 16 | 17 | foo 18 | 19 | ) 20 | expect(getByText('foo')).toMatchInlineSnapshot(` 21 |
26 | foo 27 |
28 | `) 29 | }) 30 | 31 | it('can render as other element types', () => { 32 | renderJsx(foo) 33 | expect(getByText('foo')).toMatchInlineSnapshot(` 34 | 35 | foo 36 | 37 | `) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/Box/Box.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent } from '../utils' 2 | import { ComponentOptions, ComponentObjectPropsOptions, ref, Ref } from 'vue' 3 | 4 | export type As = string | ComponentOptions 5 | 6 | export interface BoxProps { 7 | as: As 8 | } 9 | 10 | export const boxProps: ComponentObjectPropsOptions = { 11 | as: { 12 | type: [String, Object], 13 | default: 'div', 14 | }, 15 | } 16 | 17 | export function useBox(): { 18 | ref: Ref 19 | } { 20 | return { 21 | ref: ref(null), 22 | } 23 | } 24 | 25 | export const Box = defineComponent(boxProps, useBox) 26 | -------------------------------------------------------------------------------- /src/Box/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Box' 2 | -------------------------------------------------------------------------------- /src/Button/Button.spec.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '.' 2 | import { renderJsx, getByText } from '../../test/utils' 3 | 4 | describe('Button', () => { 5 | it('renders correctly', async () => { 6 | const { nextTick } = renderJsx() 7 | await nextTick() 8 | expect(getByText('foo')).toMatchInlineSnapshot(` 9 | 14 | `) 15 | }) 16 | 17 | it('can overwrite type', async () => { 18 | const { nextTick } = renderJsx() 19 | await nextTick() 20 | expect(getByText('foo')).toMatchInlineSnapshot(` 21 | 26 | `) 27 | }) 28 | 29 | it('can render as div', async () => { 30 | const { nextTick } = renderJsx() 31 | await nextTick() 32 | expect(getByText('foo')).toMatchInlineSnapshot(` 33 |
37 | foo 38 |
39 | `) 40 | }) 41 | 42 | it('can render as a tag', async () => { 43 | const { nextTick } = renderJsx() 44 | await nextTick() 45 | // type="" seems to be a bug in Vue 46 | // https://github.com/vuejs/vue-next/issues/1701 47 | expect(getByText('foo')).toMatchInlineSnapshot(` 48 | 49 | foo 50 | 51 | `) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/Button/Button.ts: -------------------------------------------------------------------------------- 1 | import { ComponentObjectPropsOptions, ref, computed } from 'vue' 2 | import { defineComponent, useOnElement } from '../utils' 3 | import { useClickable, clickableProps, ClickableProps } from '../Clickable' 4 | 5 | export type ButtonProps = ClickableProps 6 | 7 | export const buttonProps: ComponentObjectPropsOptions = { 8 | ...clickableProps, 9 | as: { 10 | type: [String, Object], 11 | default: 'button', 12 | }, 13 | } 14 | 15 | export function useButton(props: ButtonProps) { 16 | const clickable = useClickable(props) 17 | 18 | const isButton = ref(false) 19 | useOnElement(clickable.ref, (element) => { 20 | isButton.value = element.tagName === 'BUTTON' 21 | }) 22 | const isLink = ref(false) 23 | useOnElement(clickable.ref, (element) => { 24 | isLink.value = element.tagName === 'A' 25 | }) 26 | 27 | return { 28 | ...clickable, 29 | type: computed(() => (isButton.value ? 'button' : null)), 30 | role: computed(() => (isButton.value || isLink.value ? null : 'button')), 31 | } 32 | } 33 | 34 | export const Button = defineComponent(buttonProps, useButton) 35 | -------------------------------------------------------------------------------- /src/Button/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: '' 3 | sidebar: 'docs' 4 | prev: '/docs/' 5 | next: '/docs/modal/' 6 | --- 7 | 8 | # Button 9 | 10 | Accessible `Button` component that enables users to trigger an action or event, such as submitting a form, opening a dialog, canceling an action, or performing a delete operation. It follows the [WAI-ARIA Button Pattern](https://www.w3.org/TR/wai-aria-practices/#button). 11 | 12 | ## Installation 13 | 14 | ```bash 15 | npm i vue-ari 16 | ``` 17 | 18 | or 19 | 20 | ```bash 21 | yarn add vue-ari 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```html 27 | 32 | 33 | 44 | ``` 45 | 46 | ## Styling 47 | 48 | Ari components don't include styling by default. This gives you the ability to add styles however you like. 49 | 50 | ### Example Using Tailwind 51 | 52 | ```html 53 | 66 | 67 | 78 | ``` 79 | -------------------------------------------------------------------------------- /src/Button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Button' 2 | -------------------------------------------------------------------------------- /src/Clickable/Clickable.spec.tsx: -------------------------------------------------------------------------------- 1 | import { Clickable } from '.' 2 | import { renderJsx, getByText, pressSpace, pressEnter } from '../../test/utils' 3 | 4 | describe('Clickable', () => { 5 | it('renders correctly', () => { 6 | renderJsx(foo) 7 | expect(getByText('foo')).toMatchInlineSnapshot(` 8 |
11 | foo 12 |
13 | `) 14 | }) 15 | 16 | it('emits click event on press space', () => { 17 | const clickHandler = jest.fn() 18 | renderJsx(foo) 19 | pressSpace(getByText('foo')) 20 | expect(clickHandler).toBeCalledTimes(1) 21 | }) 22 | 23 | it('emits no click event on keydown space when disabled', () => { 24 | const clickHandler = jest.fn() 25 | renderJsx( 26 | 27 | foo 28 | 29 | ) 30 | pressSpace(getByText('foo')) 31 | expect(clickHandler).toBeCalledTimes(0) 32 | }) 33 | 34 | it('emits only one click event on press space as button', () => { 35 | const clickHandler = jest.fn() 36 | renderJsx( 37 | 38 | foo 39 | 40 | ) 41 | pressSpace(getByText('foo')) 42 | expect(clickHandler).toBeCalledTimes(1) 43 | }) 44 | 45 | it('emits click event on press enter', () => { 46 | const clickHandler = jest.fn() 47 | renderJsx(foo) 48 | pressEnter(getByText('foo')) 49 | expect(clickHandler).toBeCalledTimes(1) 50 | }) 51 | 52 | it('emits no click event on press enter when disabled', () => { 53 | const clickHandler = jest.fn() 54 | renderJsx( 55 | 56 | foo 57 | 58 | ) 59 | pressEnter(getByText('foo')) 60 | expect(clickHandler).toBeCalledTimes(0) 61 | }) 62 | 63 | it('emits only one click event on press enter as button', async () => { 64 | const clickHandler = jest.fn() 65 | renderJsx( 66 | 67 | foo 68 | 69 | ) 70 | pressEnter(getByText('foo')) 71 | expect(clickHandler).toBeCalledTimes(1) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /src/Clickable/Clickable.ts: -------------------------------------------------------------------------------- 1 | import { ComponentObjectPropsOptions } from 'vue' 2 | import { defineComponent } from '../utils' 3 | import { useTabbable, tabbableProps, TabbableProps } from '../Tabbable' 4 | 5 | export type ClickableProps = TabbableProps 6 | 7 | export const clickableProps: ComponentObjectPropsOptions = { 8 | ...tabbableProps, 9 | } 10 | 11 | export function useClickable(props: ClickableProps) { 12 | const tabbable = useTabbable(props) 13 | 14 | function handleKeydown(event: KeyboardEvent & { target: HTMLElement }) { 15 | if ( 16 | event.target?.tagName !== 'BUTTON' && 17 | [' ', 'Enter'].includes(event.key) 18 | ) { 19 | tabbable.onClick(event) 20 | } 21 | } 22 | 23 | return { 24 | ...tabbable, 25 | onKeydown: handleKeydown, 26 | } 27 | } 28 | 29 | export const Clickable = defineComponent(clickableProps, useClickable) 30 | -------------------------------------------------------------------------------- /src/Clickable/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Clickable' 2 | -------------------------------------------------------------------------------- /src/Dialog/Dialog.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComponentObjectPropsOptions, 3 | Ref, 4 | watch, 5 | onMounted, 6 | onBeforeUnmount, 7 | } from 'vue' 8 | import { 9 | getElementFromRef, 10 | getTabbableElements, 11 | getNextTabbable, 12 | elementIsWithin, 13 | focusIsWithin, 14 | defineComponent, 15 | focusFirstFocusable, 16 | forceFocus, 17 | } from '../utils' 18 | import { 19 | useDisclosureContent, 20 | disclosureContentProps, 21 | DisclosureContentProps, 22 | } from '../Disclosure' 23 | 24 | export type DialogProps = DisclosureContentProps 25 | 26 | export const dialogProps: ComponentObjectPropsOptions = { 27 | ...disclosureContentProps, 28 | } 29 | 30 | function useHideOnClickOutside(props: DialogProps, ref: Ref) { 31 | const { hide, baseId } = props 32 | 33 | function hideOnClickOutside(event: MouseEvent) { 34 | const disclosure: HTMLElement = document.querySelector( 35 | `[aria-controls="${baseId}"]` 36 | ) 37 | if ( 38 | props.visible.value && 39 | !elementIsWithin(getElementFromRef(ref), event.target as HTMLElement) && 40 | !elementIsWithin(disclosure, event.target as HTMLElement) 41 | ) { 42 | hide() 43 | } 44 | } 45 | 46 | watch( 47 | () => props.visible.value, 48 | (visible) => { 49 | if (visible) { 50 | document.addEventListener('click', hideOnClickOutside) 51 | } else { 52 | document.removeEventListener('click', hideOnClickOutside) 53 | } 54 | } 55 | ) 56 | 57 | onBeforeUnmount(() => { 58 | document.removeEventListener('click', hideOnClickOutside) 59 | }) 60 | } 61 | 62 | function useHideOnFocusOutside(props: DialogProps, ref: Ref) { 63 | const { hide, baseId } = props 64 | 65 | function hideOnFocusOutside(event: FocusEvent) { 66 | const target = event.target as HTMLElement 67 | const disclosure: HTMLElement = document.querySelector( 68 | `[aria-controls="${baseId}"]` 69 | ) 70 | if ( 71 | props.visible.value && 72 | !elementIsWithin(getElementFromRef(ref), target) && 73 | !elementIsWithin(disclosure, target) 74 | ) { 75 | hide() 76 | } 77 | } 78 | 79 | watch( 80 | () => props.visible.value, 81 | (visible) => { 82 | if (visible) { 83 | document.addEventListener('focusin', hideOnFocusOutside) 84 | } else { 85 | document.removeEventListener('focusin', hideOnFocusOutside) 86 | } 87 | } 88 | ) 89 | 90 | onBeforeUnmount(() => { 91 | document.removeEventListener('focusin', hideOnFocusOutside) 92 | }) 93 | } 94 | 95 | function useHandleToggleFocus(props: DialogProps, ref: Ref) { 96 | let lastFocusedElement: HTMLElement = null 97 | 98 | onMounted(() => { 99 | if (props.visible.value) { 100 | lastFocusedElement = document.activeElement as HTMLElement 101 | } 102 | }) 103 | 104 | watch( 105 | () => props.visible.value, 106 | (visible) => { 107 | if (visible) { 108 | lastFocusedElement = document.activeElement as HTMLElement 109 | focusFirstFocusable(getElementFromRef(ref)) 110 | } 111 | }, 112 | { 113 | flush: 'post', 114 | } 115 | ) 116 | // Needs to be a 'sync' watcher, so we can check 117 | // if focus was within the dialog before it is closed 118 | watch( 119 | () => props.visible.value, 120 | (visible) => { 121 | if (!visible && focusIsWithin(getElementFromRef(ref))) { 122 | forceFocus(lastFocusedElement) 123 | } 124 | }, 125 | { 126 | flush: 'sync', 127 | } 128 | ) 129 | } 130 | 131 | function handleTab( 132 | event: KeyboardEvent, 133 | props: DialogProps, 134 | ref: Ref 135 | ) { 136 | const disclosure: HTMLElement = document.querySelector( 137 | `[aria-controls="${props.baseId}"]` 138 | ) 139 | const dialog = getElementFromRef(ref) 140 | const tabbableElements = getTabbableElements(dialog) 141 | if (!event.shiftKey && reachedLastTabbable(tabbableElements)) { 142 | const nextTabbable = getNextTabbable(disclosure) 143 | if (nextTabbable && nextTabbable !== dialog) { 144 | nextTabbable.focus() 145 | event.preventDefault() 146 | } else { 147 | props.hide() 148 | } 149 | } else if (event.shiftKey && reachedFirstTabbable(tabbableElements)) { 150 | disclosure.focus() 151 | event.preventDefault() 152 | } 153 | 154 | function reachedLastTabbable(tabbableElements) { 155 | return ( 156 | !tabbableElements.length || 157 | tabbableElements[tabbableElements.length - 1] === document.activeElement 158 | ) 159 | } 160 | 161 | function reachedFirstTabbable(tabbableElements) { 162 | return ( 163 | !tabbableElements.length || tabbableElements[0] === document.activeElement 164 | ) 165 | } 166 | } 167 | 168 | export function useDialog(props: DialogProps) { 169 | const disclosureContent = useDisclosureContent(props) 170 | const { ref } = disclosureContent 171 | useHideOnClickOutside(props, ref) 172 | useHideOnFocusOutside(props, ref) 173 | useHandleToggleFocus(props, ref) 174 | 175 | function handleKeydown(event: KeyboardEvent) { 176 | if (event.key === 'Escape') { 177 | props.hide() 178 | } 179 | if (event.key === 'Tab') { 180 | handleTab(event, props, ref) 181 | } 182 | } 183 | 184 | return { 185 | ...disclosureContent, 186 | role: 'dialog', 187 | tabindex: -1, 188 | onKeydown: handleKeydown, 189 | withPortal: true, 190 | } 191 | } 192 | 193 | export const Dialog = defineComponent(dialogProps, useDialog) 194 | -------------------------------------------------------------------------------- /src/Dialog/DialogDisclosure.ts: -------------------------------------------------------------------------------- 1 | import { ComponentObjectPropsOptions } from 'vue' 2 | import { defineComponent, getTabbableElements } from '../utils' 3 | import { useDisclosure, disclosureProps, DisclosureProps } from '../Disclosure' 4 | 5 | export type DialogDisclosureProps = DisclosureProps 6 | 7 | export const dialogDisclosureProps: ComponentObjectPropsOptions = { 8 | ...disclosureProps, 9 | } 10 | 11 | export function useDialogDisclosure(props: DialogDisclosureProps) { 12 | const Disclosure = useDisclosure(props) 13 | 14 | function handleKeydown(event: KeyboardEvent) { 15 | if (event.key === 'Tab' && !event.shiftKey && props.visible.value) { 16 | const dialog = document.getElementById(props.baseId) 17 | const tabbableElements = getTabbableElements(dialog) 18 | if (tabbableElements.length) { 19 | tabbableElements[0].focus() 20 | event.preventDefault() 21 | } 22 | } 23 | } 24 | 25 | return { 26 | ...Disclosure, 27 | 'aria-haspopup': 'dialog', 28 | onKeydown: handleKeydown, 29 | } 30 | } 31 | 32 | export const DialogDisclosure = defineComponent( 33 | dialogDisclosureProps, 34 | useDialogDisclosure 35 | ) 36 | -------------------------------------------------------------------------------- /src/Dialog/DialogState.ts: -------------------------------------------------------------------------------- 1 | import { ComponentObjectPropsOptions } from 'vue' 2 | import { 3 | DisclosureStateReturn, 4 | disclosureStateReturn, 5 | useDisclosureState, 6 | } from '../Disclosure' 7 | 8 | export type DialogStateReturn = DisclosureStateReturn 9 | 10 | export const dialogStateReturn: ComponentObjectPropsOptions = { 11 | ...disclosureStateReturn, 12 | } 13 | 14 | export function useDialogState(): DialogStateReturn { 15 | const disclosure = useDisclosureState() 16 | 17 | return { 18 | ...disclosure, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Dialog/__tests__/Dialog.spec.tsx: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { Dialog } from '..' 3 | import { renderJsx, getByText } from '../../../test/utils' 4 | 5 | describe('Dialog', () => { 6 | it('renders correctly', async () => { 7 | const { nextTick } = renderJsx( 8 | 9 | foo 10 | 11 | ) 12 | await nextTick() 13 | expect(getByText('foo')).toMatchInlineSnapshot(` 14 | 23 | `) 24 | }) 25 | 26 | it('renders in portal', async () => { 27 | const { nextTick } = renderJsx( 28 |
29 | container 30 | 31 | foo 32 | 33 |
34 | ) 35 | await nextTick() 36 | expect(getByText('foo').parentElement).not.toBe(getByText('container')) 37 | }) 38 | 39 | it('renders native attributes', async () => { 40 | const { nextTick } = renderJsx( 41 | 42 | foo 43 | 44 | ) 45 | await nextTick() 46 | expect(getByText('foo')).toHaveAttribute('aria-label', 'bar') 47 | }) 48 | 49 | it('does not warn when receiving native attributes', async () => { 50 | const warn = console.warn 51 | console.warn = jest.fn() 52 | renderJsx( 53 | 54 | foo 55 | 56 | ) 57 | expect(console.warn).toHaveBeenCalledTimes(0) 58 | console.warn = warn 59 | }) 60 | 61 | it('can overwrite default tabindex', async () => { 62 | const { nextTick } = renderJsx( 63 | 64 | foo 65 | 66 | ) 67 | await nextTick() 68 | expect(getByText('foo')).toHaveAttribute('tabindex', '0') 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /src/Dialog/__tests__/DialogDisclosure.spec.tsx: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { DialogDisclosure } from '..' 3 | import { renderJsx, getByText } from '../../../test/utils' 4 | 5 | describe('DialogDisclosure', () => { 6 | it('renders correctly', async () => { 7 | const { nextTick } = renderJsx( 8 | 9 | foo 10 | 11 | ) 12 | await nextTick() 13 | expect(getByText('foo')).toMatchInlineSnapshot(` 14 | 22 | `) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/Dialog/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { DialogDisclosure, Dialog, useDialogState } from '..' 2 | import { 3 | render, 4 | getByText, 5 | click, 6 | pressEnter, 7 | pressSpace, 8 | pressEsc, 9 | tab, 10 | } from '../../../test/utils' 11 | 12 | function createTestSetup({ 13 | template = ` 14 |
15 | foo 16 | bar 17 |
18 | `, 19 | visible = false, 20 | } = {}) { 21 | const { nextTick } = render({ 22 | setup() { 23 | const dialog = useDialogState() 24 | if (visible) { 25 | dialog.show() 26 | } 27 | return { 28 | dialog, 29 | } 30 | }, 31 | components: { 32 | Dialog, 33 | DialogDisclosure, 34 | }, 35 | template, 36 | }) 37 | 38 | return { 39 | nextTick, 40 | content: getByText('bar'), 41 | disclosure: getByText('foo'), 42 | } 43 | } 44 | 45 | describe('Dialog Composition', () => { 46 | it('content is hidden by default', () => { 47 | const { content } = createTestSetup() 48 | expect(content).not.toBeVisible() 49 | }) 50 | 51 | it('disclosure opens content', async () => { 52 | const { content, disclosure, nextTick } = createTestSetup() 53 | expect(content).not.toBeVisible() 54 | click(disclosure) 55 | await nextTick() 56 | expect(content).toBeVisible() 57 | }) 58 | 59 | it('disclosure closes content', async () => { 60 | const { content, disclosure, nextTick } = createTestSetup() 61 | click(disclosure) 62 | await nextTick() 63 | click(disclosure) 64 | await nextTick() 65 | expect(content).not.toBeVisible() 66 | }) 67 | 68 | it('disclosure toggles content with enter', async () => { 69 | const { content, disclosure, nextTick } = createTestSetup() 70 | pressEnter(disclosure) 71 | await nextTick() 72 | expect(content).toBeVisible() 73 | pressEnter(disclosure) 74 | await nextTick() 75 | expect(content).not.toBeVisible() 76 | }) 77 | 78 | it('disclosure toggles content with space', async () => { 79 | const { content, disclosure, nextTick } = createTestSetup() 80 | pressSpace(disclosure) 81 | await nextTick() 82 | expect(content).toBeVisible() 83 | pressSpace(disclosure) 84 | await nextTick() 85 | expect(content).not.toBeVisible() 86 | }) 87 | 88 | it('show focuses dialog', async () => { 89 | const { content, disclosure, nextTick } = createTestSetup() 90 | click(disclosure) 91 | await nextTick() 92 | expect(content).toHaveFocus() 93 | }) 94 | 95 | it('show focuses first focusable element', async () => { 96 | const { disclosure, nextTick } = createTestSetup({ 97 | template: ` 98 |
99 | foo 100 | bar 101 |
102 | `, 103 | }) 104 | click(disclosure) 105 | await nextTick() 106 | expect(getByText('first')).toHaveFocus() 107 | }) 108 | 109 | it('show focuses dialog with tabindex 0', async () => { 110 | const { content, disclosure, nextTick } = createTestSetup({ 111 | template: ` 112 |
113 | foo 114 | bar 115 |
116 | `, 117 | }) 118 | click(disclosure) 119 | await nextTick() 120 | expect(content).toHaveFocus() 121 | }) 122 | 123 | it('esc hides dialog', async () => { 124 | const { content, disclosure, nextTick } = createTestSetup() 125 | click(disclosure) 126 | await nextTick() 127 | pressEsc(content) 128 | await nextTick() 129 | expect(content).not.toBeVisible() 130 | }) 131 | 132 | it('hide focuses disclosure', async () => { 133 | const { content, disclosure, nextTick } = createTestSetup() 134 | click(disclosure) 135 | await nextTick() 136 | pressEsc(content) 137 | await nextTick() 138 | expect(disclosure).toHaveFocus() 139 | }) 140 | 141 | it('hide focuses last active element without real disclosure', async () => { 142 | const { disclosure: outsideButton, nextTick } = createTestSetup({ 143 | template: ` 144 |
145 | 146 | 147 | bar 148 | 149 | 150 |
151 | `, 152 | }) 153 | 154 | click(outsideButton) 155 | await nextTick() 156 | click(getByText('hide')) 157 | await nextTick() 158 | expect(outsideButton).toHaveFocus() 159 | }) 160 | 161 | it('hide focuses document body when visible initially', async () => { 162 | const { nextTick } = createTestSetup({ 163 | template: ` 164 |
165 | 166 | 167 | bar 168 | 169 | 170 |
171 | `, 172 | visible: true, 173 | }) 174 | 175 | click(getByText('hide')) 176 | await nextTick() 177 | expect(document.body).toHaveFocus() 178 | }) 179 | 180 | it('click outside hides dialog', async () => { 181 | const { content, disclosure, nextTick } = createTestSetup({ 182 | template: ` 183 |
184 | foo 185 | bar 186 | 187 |
188 | `, 189 | }) 190 | click(disclosure) 191 | await nextTick() 192 | click(getByText('outside')) 193 | await nextTick() 194 | expect(content).not.toBeVisible() 195 | }) 196 | 197 | it('click inside does not hide dialog', async () => { 198 | const { content, disclosure, nextTick } = createTestSetup({ 199 | template: ` 200 |
201 | foo 202 | bar 203 |
204 | `, 205 | }) 206 | click(disclosure) 207 | await nextTick() 208 | click(getByText('inside')) 209 | await nextTick() 210 | expect(content).toBeVisible() 211 | }) 212 | 213 | it('click on dialog does not hide dialog', async () => { 214 | const { content, disclosure, nextTick } = createTestSetup() 215 | click(disclosure) 216 | await nextTick() 217 | click(content) 218 | await nextTick() 219 | expect(content).toBeVisible() 220 | }) 221 | 222 | it('tab focuses element after disclosure', async () => { 223 | const { disclosure, nextTick } = createTestSetup({ 224 | template: ` 225 |
226 | foo 227 | 228 | bar 229 |
230 | `, 231 | }) 232 | click(disclosure) 233 | await nextTick() 234 | tab() 235 | await nextTick() 236 | expect(getByText('next')).toHaveFocus() 237 | }) 238 | 239 | it('shift tab focuses disclosure', async () => { 240 | const { disclosure, nextTick } = createTestSetup({ 241 | template: ` 242 |
243 | foo 244 | 245 | bar 246 |
247 | `, 248 | }) 249 | click(disclosure) 250 | await nextTick() 251 | tab({ shift: true }) 252 | await nextTick() 253 | expect(disclosure).toHaveFocus() 254 | }) 255 | 256 | it('tab focuses inner tabbable elements', async () => { 257 | const { disclosure, nextTick } = createTestSetup({ 258 | template: ` 259 |
260 | foo 261 | 262 | bar 263 |
264 | `, 265 | }) 266 | click(disclosure) 267 | await nextTick() 268 | tab() 269 | await nextTick() 270 | expect(getByText('nextinner')).toHaveFocus() 271 | }) 272 | 273 | it('shift tab focuses inner tabbable elements', async () => { 274 | const { disclosure, nextTick } = createTestSetup({ 275 | template: ` 276 |
277 | foo 278 | 279 | bar 280 |
281 | `, 282 | }) 283 | click(disclosure) 284 | await nextTick() 285 | tab() 286 | await nextTick() 287 | tab({ shift: true }) 288 | await nextTick() 289 | expect(getByText('inner')).toHaveFocus() 290 | }) 291 | 292 | it('tab on disclosure focuses first tabbable element', async () => { 293 | const { disclosure, nextTick } = createTestSetup({ 294 | template: ` 295 |
296 | foo 297 | 298 | bar 299 |
300 | `, 301 | }) 302 | click(disclosure) 303 | await nextTick() 304 | tab({ shift: true }) 305 | await nextTick() 306 | tab() 307 | await nextTick() 308 | expect(getByText('inner')).toHaveFocus() 309 | }) 310 | 311 | it('tab on disclosure focuses dialog with tabindex 0', async () => { 312 | const { content, disclosure, nextTick } = createTestSetup({ 313 | template: ` 314 |
315 | foo 316 | 317 | bar 318 |
319 | `, 320 | }) 321 | click(disclosure) 322 | await nextTick() 323 | tab({ shift: true }) 324 | await nextTick() 325 | tab() 326 | await nextTick() 327 | expect(content).toHaveFocus() 328 | }) 329 | 330 | it('tab on disclosure focuses next element when dialog is hidden', async () => { 331 | const { disclosure, nextTick } = createTestSetup({ 332 | template: ` 333 |
334 | foo 335 | 336 | bar 337 |
338 | `, 339 | }) 340 | disclosure.focus() 341 | tab() 342 | await nextTick() 343 | expect(getByText('next')).toHaveFocus() 344 | }) 345 | 346 | it('focus out from disclosure hides dialog', async () => { 347 | const { content, disclosure, nextTick } = createTestSetup({ 348 | template: ` 349 |
350 | 351 | foo 352 | bar 353 |
354 | `, 355 | }) 356 | click(disclosure) 357 | await nextTick() 358 | tab({ shift: true }) 359 | await nextTick() 360 | tab({ shift: true }) 361 | await nextTick() 362 | expect(content).not.toBeVisible() 363 | }) 364 | 365 | it('focus out from dialog hides dialog', async () => { 366 | const { content, disclosure, nextTick } = createTestSetup({ 367 | template: ` 368 |
369 | foo 370 | 371 | bar 372 |
373 | `, 374 | }) 375 | click(disclosure) 376 | await nextTick() 377 | tab() 378 | await nextTick() 379 | expect(content).not.toBeVisible() 380 | }) 381 | 382 | it('tab without tabbable element after disclosure hides dialog', async () => { 383 | const { content, disclosure, nextTick } = createTestSetup({ 384 | template: ` 385 |
386 | foo 387 | bar 388 |
389 | `, 390 | }) 391 | click(disclosure) 392 | await nextTick() 393 | tab() 394 | await nextTick() 395 | expect(content).not.toBeVisible() 396 | }) 397 | 398 | it('tab without tabbable element after disclosure and tabindex 0 on dialog hides dialog', async () => { 399 | const { content, disclosure, nextTick } = createTestSetup({ 400 | template: ` 401 |
402 | foo 403 | bar 404 |
405 | `, 406 | }) 407 | click(disclosure) 408 | await nextTick() 409 | tab() 410 | await nextTick() 411 | expect(content).not.toBeVisible() 412 | }) 413 | }) 414 | -------------------------------------------------------------------------------- /src/Dialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Dialog' 2 | export * from './DialogDisclosure' 3 | export * from './DialogState' 4 | -------------------------------------------------------------------------------- /src/Disclosure/Disclosure.ts: -------------------------------------------------------------------------------- 1 | import { ComponentObjectPropsOptions, computed } from 'vue' 2 | import { defineComponent } from '../utils' 3 | import { useButton, buttonProps, ButtonProps } from '../Button' 4 | import { disclosureStateReturn, DisclosureStateReturn } from './DisclosureState' 5 | 6 | export interface DisclosureProps extends ButtonProps, DisclosureStateReturn {} 7 | 8 | export const disclosureProps: ComponentObjectPropsOptions = { 9 | ...buttonProps, 10 | ...disclosureStateReturn, 11 | } 12 | 13 | export function useDisclosure(props: DisclosureProps) { 14 | const button = useButton(props) 15 | 16 | return { 17 | ...button, 18 | 'aria-expanded': computed(() => (props.visible.value ? 'true' : 'false')), 19 | 'aria-controls': props.baseId, 20 | onClick: props.toggle, 21 | } 22 | } 23 | 24 | export const Disclosure = defineComponent(disclosureProps, useDisclosure) 25 | -------------------------------------------------------------------------------- /src/Disclosure/DisclosureContent.ts: -------------------------------------------------------------------------------- 1 | import { ComponentObjectPropsOptions } from 'vue' 2 | import { defineComponent, useVisibilityTransition } from '../utils' 3 | import { useBox, boxProps, BoxProps } from '../Box' 4 | import { disclosureStateReturn, DisclosureStateReturn } from './DisclosureState' 5 | 6 | export interface DisclosureContentProps 7 | extends BoxProps, 8 | DisclosureStateReturn {} 9 | 10 | export const disclosureContentProps: ComponentObjectPropsOptions = 11 | { 12 | ...boxProps, 13 | ...disclosureStateReturn, 14 | } 15 | 16 | export function useDisclosureContent(props: DisclosureContentProps) { 17 | const box = useBox() 18 | 19 | useVisibilityTransition(props.visible, box.ref) 20 | 21 | return { 22 | ...box, 23 | id: props.baseId, 24 | } 25 | } 26 | 27 | export const DisclosureContent = defineComponent( 28 | disclosureContentProps, 29 | useDisclosureContent 30 | ) 31 | -------------------------------------------------------------------------------- /src/Disclosure/DisclosureState.ts: -------------------------------------------------------------------------------- 1 | import { ComponentObjectPropsOptions, ref, Ref, PropType } from 'vue' 2 | 3 | let currentIdCount = 0 4 | 5 | export interface DisclosureStateReturn { 6 | baseId: string 7 | visible: Ref 8 | show: () => void 9 | hide: () => void 10 | toggle: () => void 11 | } 12 | 13 | export const disclosureStateReturn: ComponentObjectPropsOptions = { 14 | baseId: { 15 | type: String, 16 | required: true, 17 | }, 18 | visible: { 19 | required: true, 20 | }, 21 | show: { 22 | type: Function as PropType<() => void>, 23 | }, 24 | hide: { 25 | type: Function as PropType<() => void>, 26 | }, 27 | toggle: { 28 | type: Function as PropType<() => void>, 29 | }, 30 | } 31 | 32 | export function useDisclosureState(): DisclosureStateReturn { 33 | const baseId = `disclosure-${currentIdCount++}` 34 | const visible = ref(false) 35 | 36 | function show() { 37 | visible.value = true 38 | } 39 | 40 | function hide() { 41 | visible.value = false 42 | } 43 | 44 | function toggle() { 45 | visible.value = !visible.value 46 | } 47 | 48 | return { 49 | baseId, 50 | visible, 51 | show, 52 | hide, 53 | toggle, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Disclosure/__tests__/Disclosure.spec.tsx: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { Disclosure } from '..' 3 | import { renderJsx, getByText } from '../../../test/utils' 4 | 5 | describe('Disclosure', () => { 6 | it('renders correctly', async () => { 7 | const { nextTick } = renderJsx( 8 | 9 | foo 10 | 11 | ) 12 | await nextTick() 13 | expect(getByText('foo')).toMatchInlineSnapshot(` 14 | 21 | `) 22 | }) 23 | 24 | it('renders visible state correctly', async () => { 25 | const { nextTick } = renderJsx( 26 | 27 | foo 28 | 29 | ) 30 | await nextTick() 31 | expect(getByText('foo')).toMatchInlineSnapshot(` 32 | 39 | `) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/Disclosure/__tests__/DisclosureContent.spec.tsx: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { DisclosureContent } from '..' 3 | import { renderJsx, getByText } from '../../../test/utils' 4 | 5 | describe('DisclosureContent', () => { 6 | it('renders correctly', async () => { 7 | const { nextTick } = renderJsx( 8 | 9 | foo 10 | 11 | ) 12 | await nextTick() 13 | expect(getByText('foo')).toMatchInlineSnapshot(` 14 | 21 | `) 22 | }) 23 | 24 | it('renders visible state correctly', async () => { 25 | const { nextTick } = renderJsx( 26 | 27 | foo 28 | 29 | ) 30 | await nextTick() 31 | expect(getByText('foo')).toMatchInlineSnapshot(` 32 |
35 | foo 36 |
37 | `) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/Disclosure/__tests__/DisclosureState.spec.tsx: -------------------------------------------------------------------------------- 1 | import { useDisclosureState } from '..' 2 | 3 | describe('DisclosureContent', () => { 4 | it('has correct base id', () => { 5 | const { baseId } = useDisclosureState() 6 | expect(baseId).toBe('disclosure-0') 7 | }) 8 | 9 | it('is hidden by default', () => { 10 | const { visible } = useDisclosureState() 11 | expect(visible.value).toBe(false) 12 | }) 13 | 14 | it('has unique base ids', () => { 15 | const { baseId } = useDisclosureState() 16 | const { baseId: baseId2 } = useDisclosureState() 17 | expect(baseId).not.toBe(baseId2) 18 | }) 19 | 20 | it('can show', () => { 21 | const { visible, show } = useDisclosureState() 22 | show() 23 | expect(visible.value).toBe(true) 24 | }) 25 | 26 | it('can hide', () => { 27 | const { visible, show, hide } = useDisclosureState() 28 | show() 29 | hide() 30 | expect(visible.value).toBe(false) 31 | }) 32 | 33 | it('can toggle', () => { 34 | const { visible, toggle } = useDisclosureState() 35 | toggle() 36 | expect(visible.value).toBe(true) 37 | toggle() 38 | expect(visible.value).toBe(false) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/Disclosure/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { Disclosure, DisclosureContent, useDisclosureState } from '..' 2 | import { 3 | render, 4 | getByText, 5 | click, 6 | pressEnter, 7 | pressSpace, 8 | } from '../../../test/utils' 9 | 10 | describe('Disclosure Composition', () => { 11 | let disclosure, content, nextTick 12 | 13 | beforeEach(() => { 14 | const renderResult = render({ 15 | setup() { 16 | const disclosure = useDisclosureState() 17 | return { 18 | disclosure, 19 | } 20 | }, 21 | components: { 22 | Disclosure, 23 | DisclosureContent, 24 | }, 25 | template: ` 26 |
27 | foo 28 | bar 29 |
30 | `, 31 | }) 32 | 33 | nextTick = renderResult.nextTick 34 | content = getByText('bar') 35 | disclosure = getByText('foo') 36 | }) 37 | 38 | it('content is hidden by default', () => { 39 | expect(content).not.toBeVisible() 40 | }) 41 | 42 | it('disclosure opens content', async () => { 43 | expect(content).not.toBeVisible() 44 | click(disclosure) 45 | await nextTick() 46 | expect(content).toBeVisible() 47 | }) 48 | 49 | it('disclosure closes content', async () => { 50 | click(disclosure) 51 | await nextTick() 52 | click(disclosure) 53 | await nextTick() 54 | expect(content).not.toBeVisible() 55 | }) 56 | 57 | it('disclosure toggles content with enter', async () => { 58 | pressEnter(disclosure) 59 | await nextTick() 60 | expect(content).toBeVisible() 61 | pressEnter(disclosure) 62 | await nextTick() 63 | expect(content).not.toBeVisible() 64 | }) 65 | 66 | it('disclosure toggles content with space', async () => { 67 | pressSpace(disclosure) 68 | await nextTick() 69 | expect(content).toBeVisible() 70 | pressSpace(disclosure) 71 | await nextTick() 72 | expect(content).not.toBeVisible() 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /src/Disclosure/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Disclosure' 2 | export * from './DisclosureContent' 3 | export * from './DisclosureState' 4 | -------------------------------------------------------------------------------- /src/Modal/Modal.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComponentObjectPropsOptions, 3 | Ref, 4 | watch, 5 | onMounted, 6 | onBeforeUnmount, 7 | inject, 8 | } from 'vue' 9 | import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock' 10 | import { 11 | defineComponent, 12 | getElementFromRef, 13 | getTabbableElements, 14 | isFocused, 15 | focusFirstFocusable, 16 | } from '../utils' 17 | import { useDialog, dialogProps, DialogProps } from '../Dialog' 18 | 19 | export type ModalProps = DialogProps 20 | 21 | export const modalProps: ComponentObjectPropsOptions = { 22 | ...dialogProps, 23 | } 24 | 25 | function useFocusTrap(elementRef: Ref) { 26 | function handleKeydownTab(event: KeyboardEvent) { 27 | if (event.key !== 'Tab') { 28 | return 29 | } 30 | const element = getElementFromRef(elementRef) 31 | const tabbableElements = getTabbableElements(element) 32 | const firstTabbableElement = tabbableElements[0] 33 | const lastTabbableElement = tabbableElements[tabbableElements.length - 1] 34 | 35 | if ( 36 | !event.shiftKey && 37 | (isFocused(lastTabbableElement) || isFocused(element)) 38 | ) { 39 | firstTabbableElement?.focus() 40 | event.preventDefault() 41 | } 42 | 43 | if ( 44 | event.shiftKey && 45 | (isFocused(firstTabbableElement) || isFocused(element)) 46 | ) { 47 | lastTabbableElement?.focus() 48 | event.preventDefault() 49 | } 50 | } 51 | return { 52 | handleKeydownTab, 53 | } 54 | } 55 | 56 | function useBodyScrollLock(elementRef: Ref, props: ModalProps) { 57 | watch( 58 | () => props.visible.value, 59 | (visible) => { 60 | const element = getElementFromRef(elementRef) 61 | if (visible) { 62 | disableBodyScroll(element) 63 | } else { 64 | enableBodyScroll(element) 65 | } 66 | } 67 | ) 68 | onBeforeUnmount(() => { 69 | const element = getElementFromRef(elementRef) 70 | enableBodyScroll(element) 71 | }) 72 | } 73 | 74 | function useHandleInitialFocus(elementRef: Ref, props: ModalProps) { 75 | onMounted(() => { 76 | if (props.visible.value) { 77 | focusFirstFocusable(getElementFromRef(elementRef)) 78 | } 79 | }) 80 | } 81 | 82 | export function useModal(props: ModalProps) { 83 | const dialog = useDialog(props) 84 | 85 | useBodyScrollLock(dialog.ref, props) 86 | useHandleInitialFocus(dialog.ref, props) 87 | const { handleKeydownTab } = useFocusTrap(dialog.ref) 88 | function onKeydown(event: KeyboardEvent) { 89 | handleKeydownTab(event) 90 | if (!event.defaultPrevented) { 91 | dialog.onKeydown(event) 92 | } 93 | } 94 | const hasBackdrop = inject('modalBackdrop', false) 95 | 96 | return { 97 | ...dialog, 98 | onKeydown, 99 | 'aria-modal': true, 100 | withPortal: !hasBackdrop, 101 | } 102 | } 103 | 104 | export const Modal = defineComponent(modalProps, useModal) 105 | -------------------------------------------------------------------------------- /src/Modal/ModalBackdrop.ts: -------------------------------------------------------------------------------- 1 | import { ComponentObjectPropsOptions, provide } from 'vue' 2 | import { defineComponent, useVisibilityTransition } from '../utils' 3 | import { modalStateReturn, ModalStateReturn } from './ModalState' 4 | import { useBox, boxProps, BoxProps } from '../Box' 5 | 6 | export type ModalBackdropProps = BoxProps & ModalStateReturn 7 | 8 | export const modalBackdropProps: ComponentObjectPropsOptions = 9 | { 10 | ...boxProps, 11 | ...modalStateReturn, 12 | } 13 | 14 | export function useModalBackdrop(props: ModalBackdropProps) { 15 | const box = useBox() 16 | provide('modalBackdrop', true) 17 | useVisibilityTransition(props.visible, box.ref) 18 | 19 | return { 20 | ...box, 21 | withPortal: true, 22 | } 23 | } 24 | 25 | export const ModalBackdrop = defineComponent( 26 | modalBackdropProps, 27 | useModalBackdrop 28 | ) 29 | -------------------------------------------------------------------------------- /src/Modal/ModalDisclosure.ts: -------------------------------------------------------------------------------- 1 | import { ComponentObjectPropsOptions } from 'vue' 2 | import { defineComponent } from '../utils' 3 | import { 4 | useDialogDisclosure, 5 | dialogDisclosureProps, 6 | DialogDisclosureProps, 7 | } from '../Dialog' 8 | 9 | export type ModalDisclosureProps = DialogDisclosureProps 10 | 11 | export const modalDisclosureProps: ComponentObjectPropsOptions = { 12 | ...dialogDisclosureProps, 13 | } 14 | 15 | export function useModalDisclosure(props: ModalDisclosureProps) { 16 | const disclosure = useDialogDisclosure(props) 17 | 18 | return { 19 | ...disclosure, 20 | } 21 | } 22 | 23 | export const ModalDisclosure = defineComponent( 24 | modalDisclosureProps, 25 | useModalDisclosure 26 | ) 27 | -------------------------------------------------------------------------------- /src/Modal/ModalState.ts: -------------------------------------------------------------------------------- 1 | import { ComponentObjectPropsOptions } from 'vue' 2 | import { DialogStateReturn, dialogStateReturn, useDialogState } from '../Dialog' 3 | 4 | export type ModalStateReturn = DialogStateReturn 5 | 6 | export const modalStateReturn: ComponentObjectPropsOptions = { 7 | ...dialogStateReturn, 8 | } 9 | 10 | export function useModalState(): ModalStateReturn { 11 | const dialog = useDialogState() 12 | 13 | return { 14 | ...dialog, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Modal/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: '' 3 | sidebar: 'docs' 4 | prev: '/docs/button/' 5 | next: '/docs/popover/' 6 | --- 7 | 8 | # Modal 9 | 10 | Accessible `Modal` component that follows the [WAI-ARIA Dialog (Modal) Pattern](https://www.w3.org/TR/wai-aria-practices/#dialog_modal). It is rendered within a Portal. 11 | 12 | ## Installation 13 | 14 | ```bash 15 | npm i vue-ari 16 | ``` 17 | 18 | or 19 | 20 | ```bash 21 | yarn add vue-ari 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```html 27 | 37 | 38 | 55 | ``` 56 | 57 | ## Styling 58 | 59 | Ari components don't include styling by default. This gives you the ability to add styles however you like. 60 | 61 | ### Example Using Tailwind 62 | 63 | ```html 64 | 83 | 84 | 100 | ``` 101 | 102 | ## Reusable Components 103 | 104 | It would get pretty verbose to add the same styling classes wherever you like to use a `Modal`. So the recommended way is wrapping Ari components inside your own base components and use them inside your app. 105 | 106 | Base component for disclosure: 107 | 108 | ```html 109 | 117 | 118 | 129 | ``` 130 | 131 | Base component for modal: 132 | 133 | ```html 134 | 147 | 148 | 161 | ``` 162 | 163 | Inside your app: 164 | 165 | ```html 166 | 174 | 175 | 192 | ``` 193 | 194 | ## Abstracting State 195 | 196 | If you would rather not create a modal state each time, just create a provider component. 197 | 198 | Provider component: 199 | 200 | ```html 201 | 204 | 205 | 216 | ``` 217 | 218 | Base component for disclosure: 219 | 220 | ```html 221 | 229 | 230 | 247 | ``` 248 | 249 | Base component for modal: 250 | 251 | ```html 252 | 265 | 266 | 285 | ``` 286 | 287 | Inside your app: 288 | 289 | ```html 290 | 300 | 301 | 312 | ``` 313 | -------------------------------------------------------------------------------- /src/Modal/__tests__/Modal.spec.tsx: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { Modal } from '..' 3 | import { renderJsx, getByText } from '../../../test/utils' 4 | 5 | describe('Modal', () => { 6 | it('renders correctly', async () => { 7 | const { nextTick } = renderJsx( 8 | 9 | foo 10 | 11 | ) 12 | await nextTick() 13 | expect(getByText('foo')).toMatchInlineSnapshot(` 14 | 24 | `) 25 | }) 26 | 27 | it('renders in portal', async () => { 28 | const { nextTick } = renderJsx( 29 |
30 | container 31 | 32 | foo 33 | 34 |
35 | ) 36 | await nextTick() 37 | expect(getByText('foo').parentElement).not.toBe(getByText('container')) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/Modal/__tests__/ModalBackdrop.spec.tsx: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { ModalBackdrop } from '..' 3 | import { renderJsx, getByText } from '../../../test/utils' 4 | 5 | describe('ModalBackdrop', () => { 6 | it('renders correctly', async () => { 7 | const { nextTick } = renderJsx( 8 | 9 | foo 10 | 11 | ) 12 | await nextTick() 13 | expect(getByText('foo')).toMatchInlineSnapshot(` 14 | 20 | `) 21 | }) 22 | 23 | it('renders correctly when visible', async () => { 24 | const { nextTick } = renderJsx( 25 | 26 | foo 27 | 28 | ) 29 | await nextTick() 30 | expect(getByText('foo')).toMatchInlineSnapshot(` 31 |
32 | foo 33 |
34 | `) 35 | }) 36 | 37 | it('renders in portal', async () => { 38 | const { nextTick } = renderJsx( 39 |
40 | container 41 | 42 | foo 43 | 44 |
45 | ) 46 | await nextTick() 47 | expect(getByText('foo').parentElement).not.toBe(getByText('container')) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /src/Modal/__tests__/ModalDisclosure.spec.tsx: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { ModalDisclosure } from '..' 3 | import { renderJsx, getByText } from '../../../test/utils' 4 | 5 | describe('ModalDisclosure', () => { 6 | it('renders correctly', async () => { 7 | const { nextTick } = renderJsx( 8 | 9 | foo 10 | 11 | ) 12 | await nextTick() 13 | expect(getByText('foo')).toMatchInlineSnapshot(` 14 | 22 | `) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/Modal/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'vue' 2 | import { ModalDisclosure, Modal, ModalBackdrop, useModalState } from '..' 3 | import { render, getByText, click, tab } from '../../../test/utils' 4 | 5 | function createTestSetup({ 6 | template = ` 7 |
8 | foo 9 | bar 10 |
11 | `, 12 | visible = false, 13 | } = {}) { 14 | render({ 15 | setup() { 16 | const modal = useModalState() 17 | if (visible) { 18 | modal.show() 19 | } 20 | return { 21 | modal, 22 | } 23 | }, 24 | components: { 25 | Modal, 26 | ModalBackdrop, 27 | ModalDisclosure, 28 | }, 29 | template, 30 | }) 31 | 32 | return { 33 | content: getByText('bar'), 34 | disclosure: getByText('foo'), 35 | } 36 | } 37 | 38 | describe('Modal Composition', () => { 39 | it('content is hidden by default', () => { 40 | const { content } = createTestSetup() 41 | expect(content).not.toBeVisible() 42 | }) 43 | 44 | it('disclosure opens content', async () => { 45 | const { content, disclosure } = createTestSetup() 46 | expect(content).not.toBeVisible() 47 | click(disclosure) 48 | await nextTick() 49 | expect(content).toBeVisible() 50 | }) 51 | 52 | it('backdrop is hidden by default', () => { 53 | createTestSetup({ 54 | template: ` 55 |
56 | foo 57 | baz 58 | bar 59 |
60 | `, 61 | }) 62 | expect(getByText('baz')).not.toBeVisible() 63 | }) 64 | 65 | it('disclosure opens backdrop', async () => { 66 | const { disclosure } = createTestSetup({ 67 | template: ` 68 |
69 | foo 70 | baz 71 | bar 72 |
73 | `, 74 | }) 75 | const backdrop = getByText('baz') 76 | expect(backdrop).not.toBeVisible() 77 | click(disclosure) 78 | await nextTick() 79 | expect(backdrop).toBeVisible() 80 | }) 81 | 82 | it('modal renders inside backdrop instead of portal', async () => { 83 | const { disclosure, content } = createTestSetup({ 84 | template: ` 85 |
86 | foo 87 | 88 | baz 89 | bar 90 | 91 |
92 | `, 93 | }) 94 | const backdrop = getByText('baz') 95 | click(disclosure) 96 | await nextTick() 97 | expect(content.parentElement).toBe(backdrop) 98 | }) 99 | 100 | it('focus is trapped inside modal', async () => { 101 | const { disclosure, content } = createTestSetup({ 102 | template: ` 103 |
104 | foo 105 | bar 106 | 107 |
108 | `, 109 | }) 110 | click(disclosure) 111 | await nextTick() 112 | tab() 113 | await nextTick() 114 | expect(content).toHaveFocus() 115 | }) 116 | 117 | it('tab focuses elements inside modal', async () => { 118 | const { disclosure } = createTestSetup({ 119 | template: ` 120 |
121 | foo 122 | 123 | bar 124 | 125 | 126 | 127 |
128 | `, 129 | }) 130 | click(disclosure) 131 | await nextTick() 132 | tab() 133 | await nextTick() 134 | expect(getByText('two')).toHaveFocus() 135 | }) 136 | 137 | it('tab on last tabbable element focuses first element', async () => { 138 | const { disclosure } = createTestSetup({ 139 | template: ` 140 |
141 | foo 142 | 143 | bar 144 | 145 | 146 | 147 | 148 |
149 | `, 150 | }) 151 | click(disclosure) 152 | await nextTick() 153 | tab() 154 | await nextTick() 155 | tab() 156 | await nextTick() 157 | expect(getByText('one')).toHaveFocus() 158 | }) 159 | 160 | it('shift tab on first tabbable element focuses last tabbable element', async () => { 161 | const { disclosure } = createTestSetup({ 162 | template: ` 163 |
164 | 165 | foo 166 | 167 | bar 168 | 169 | 170 | 171 |
172 | `, 173 | }) 174 | click(disclosure) 175 | await nextTick() 176 | tab({ shift: true }) 177 | await nextTick() 178 | expect(getByText('two')).toHaveFocus() 179 | }) 180 | 181 | it('shift tab on last tabbable element focuses previous tabbable element', async () => { 182 | const { disclosure } = createTestSetup({ 183 | template: ` 184 |
185 | 186 | foo 187 | 188 | bar 189 | 190 | 191 | 192 | 193 |
194 | `, 195 | }) 196 | click(disclosure) 197 | await nextTick() 198 | tab() 199 | await nextTick() 200 | tab() 201 | await nextTick() 202 | tab({ shift: true }) 203 | await nextTick() 204 | expect(getByText('two')).toHaveFocus() 205 | }) 206 | 207 | it('is focused initially when visible', async () => { 208 | const { content } = createTestSetup({ 209 | visible: true, 210 | }) 211 | expect(content).toHaveFocus() 212 | }) 213 | }) 214 | -------------------------------------------------------------------------------- /src/Modal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ModalDisclosure' 2 | export * from './Modal' 3 | export * from './ModalBackdrop' 4 | export * from './ModalState' 5 | -------------------------------------------------------------------------------- /src/Popover/Popover.ts: -------------------------------------------------------------------------------- 1 | import { ComponentObjectPropsOptions, watch } from 'vue' 2 | import { createPopper } from '@popperjs/core' 3 | import { defineComponent, getElementFromRef } from '../utils' 4 | import { useDialog, dialogProps, DialogProps } from '../Dialog' 5 | 6 | export type PopoverProps = DialogProps 7 | 8 | export const popoverProps: ComponentObjectPropsOptions = { 9 | ...dialogProps, 10 | } 11 | 12 | export function usePopover(props: PopoverProps) { 13 | const dialog = useDialog(props) 14 | const { baseId } = props 15 | const { ref } = dialog 16 | 17 | let popper 18 | watch( 19 | () => props.visible.value, 20 | (visible) => { 21 | const disclosureElement = document.querySelector( 22 | `[aria-controls="${baseId}"]` 23 | ) 24 | const popoverElement = getElementFromRef(ref) 25 | 26 | if (visible) { 27 | popper = createPopper(disclosureElement, popoverElement, { 28 | modifiers: [ 29 | { 30 | name: 'offset', 31 | options: { 32 | offset: [0, 4], 33 | }, 34 | }, 35 | ], 36 | }) 37 | } else { 38 | popper.destroy() 39 | } 40 | } 41 | ) 42 | 43 | return { 44 | ...dialog, 45 | } 46 | } 47 | 48 | export const Popover = defineComponent(popoverProps, usePopover) 49 | -------------------------------------------------------------------------------- /src/Popover/PopoverDisclosure.ts: -------------------------------------------------------------------------------- 1 | import { ComponentObjectPropsOptions } from 'vue' 2 | import { defineComponent } from '../utils' 3 | import { 4 | useDialogDisclosure, 5 | dialogDisclosureProps, 6 | DialogDisclosureProps, 7 | } from '../Dialog' 8 | 9 | export type PopoverDisclosureProps = DialogDisclosureProps 10 | 11 | export const popoverDisclosureProps: ComponentObjectPropsOptions = { 12 | ...dialogDisclosureProps, 13 | } 14 | 15 | export function usePopoverDisclosure(props: PopoverDisclosureProps) { 16 | const disclosure = useDialogDisclosure(props) 17 | 18 | return { 19 | ...disclosure, 20 | } 21 | } 22 | 23 | export const PopoverDisclosure = defineComponent( 24 | popoverDisclosureProps, 25 | usePopoverDisclosure 26 | ) 27 | -------------------------------------------------------------------------------- /src/Popover/PopoverState.ts: -------------------------------------------------------------------------------- 1 | import { ComponentObjectPropsOptions } from 'vue' 2 | import { DialogStateReturn, dialogStateReturn, useDialogState } from '../Dialog' 3 | 4 | export type PopoverStateReturn = DialogStateReturn 5 | 6 | export const popoverStateReturn: ComponentObjectPropsOptions = { 7 | ...dialogStateReturn, 8 | } 9 | 10 | export function usePopoverState(): PopoverStateReturn { 11 | const dialog = useDialogState() 12 | 13 | return { 14 | ...dialog, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Popover/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: '' 3 | sidebar: 'docs' 4 | prev: '/docs/modal/' 5 | --- 6 | 7 | # Popover 8 | 9 | `Popover` is a non-modal dialog that is positioned near its disclosure. It is commonly used for displaying additional related content. 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm i vue-ari 15 | ``` 16 | 17 | or 18 | 19 | ```bash 20 | yarn add vue-ari 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```html 26 | 34 | 35 | 51 | ``` 52 | 53 | ## Styling 54 | 55 | Ari components don't include styling by default. This gives you the ability to add styles however you like. 56 | 57 | ### Example Using Tailwind 58 | 59 | ```html 60 | 74 | 75 | 91 | ``` 92 | 93 | ## Reusable Components 94 | 95 | It would get pretty verbose to add the same styling classes wherever you like to use a `Popover`. So the recommended way is wrapping Ari components inside your own base components and use them inside your app. 96 | 97 | ```html 98 | 106 | 107 | 118 | ``` 119 | -------------------------------------------------------------------------------- /src/Popover/__tests__/Popover.spec.tsx: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { Popover } from '..' 3 | import { renderJsx, getByText } from '../../../test/utils' 4 | 5 | describe('Popover', () => { 6 | it('renders correctly', async () => { 7 | const { nextTick } = renderJsx( 8 | 9 | foo 10 | 11 | ) 12 | await nextTick() 13 | expect(getByText('foo')).toMatchInlineSnapshot(` 14 | 23 | `) 24 | }) 25 | 26 | it('renders in portal', async () => { 27 | const { nextTick } = renderJsx( 28 |
29 | container 30 | 31 | foo 32 | 33 |
34 | ) 35 | await nextTick() 36 | expect(getByText('foo').parentElement).not.toBe(getByText('container')) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/Popover/__tests__/PopoverDisclosure.spec.tsx: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { PopoverDisclosure } from '..' 3 | import { renderJsx, getByText } from '../../../test/utils' 4 | 5 | describe('PopoverDisclosure', () => { 6 | it('renders correctly', async () => { 7 | const { nextTick } = renderJsx( 8 | 9 | foo 10 | 11 | ) 12 | await nextTick() 13 | expect(getByText('foo')).toMatchInlineSnapshot(` 14 | 22 | `) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/Popover/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { PopoverDisclosure, Popover, usePopoverState } from '..' 2 | import { render, getByText, click } from '../../../test/utils' 3 | 4 | function createTestSetup({ 5 | template = ` 6 |
7 | foo 8 | bar 9 |
10 | `, 11 | } = {}) { 12 | const { nextTick } = render({ 13 | setup() { 14 | const popover = usePopoverState() 15 | return { 16 | popover, 17 | } 18 | }, 19 | components: { 20 | Popover, 21 | PopoverDisclosure, 22 | }, 23 | template, 24 | }) 25 | 26 | return { 27 | nextTick, 28 | content: getByText('bar'), 29 | disclosure: getByText('foo'), 30 | } 31 | } 32 | 33 | describe('Popover Composition', () => { 34 | it('content is hidden by default', () => { 35 | const { content } = createTestSetup() 36 | expect(content).not.toBeVisible() 37 | }) 38 | 39 | it('disclosure opens content', async () => { 40 | const { content, disclosure, nextTick } = createTestSetup() 41 | expect(content).not.toBeVisible() 42 | click(disclosure) 43 | await nextTick() 44 | expect(content).toBeVisible() 45 | }) 46 | 47 | it('is positioned with popper when visible', async () => { 48 | const { disclosure, content, nextTick } = createTestSetup() 49 | click(disclosure) 50 | await nextTick() 51 | await nextTick() 52 | expect(content).toHaveStyle('position: absolute') 53 | expect(content).toHaveAttribute('data-popper-placement') 54 | }) 55 | 56 | it('is not positioned with popper when hidden', async () => { 57 | const { disclosure, content, nextTick } = createTestSetup() 58 | click(disclosure) 59 | await nextTick() 60 | await nextTick() 61 | click(disclosure) 62 | await nextTick() 63 | await nextTick() 64 | expect(content).not.toHaveStyle('position: absolute') 65 | expect(content).not.toHaveAttribute('data-popper-placement') 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /src/Popover/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PopoverDisclosure' 2 | export * from './Popover' 3 | export * from './PopoverState' 4 | -------------------------------------------------------------------------------- /src/Portal/Portal.spec.tsx: -------------------------------------------------------------------------------- 1 | import { Portal } from '.' 2 | import { renderJsx, getByText } from '../../test/utils' 3 | 4 | describe('Portal', () => { 5 | it('portal content renders outside its container', async () => { 6 | const { nextTick } = renderJsx( 7 |
8 | container 9 | 10 |
foo
11 |
12 |
13 | ) 14 | await nextTick() 15 | expect(getByText('foo').parentElement).not.toBe(getByText('container')) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/Portal/Portal.ts: -------------------------------------------------------------------------------- 1 | import { h, Teleport, defineComponent } from 'vue' 2 | 3 | let portalTarget 4 | 5 | export const Portal = defineComponent({ 6 | render() { 7 | if (!portalTarget) { 8 | portalTarget = document.createElement('div') 9 | document.body.append(portalTarget) 10 | } 11 | return h(Teleport, { to: portalTarget }, this.$slots.default()) 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /src/Portal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Portal' 2 | -------------------------------------------------------------------------------- /src/Tabbable/Tabbable.spec.tsx: -------------------------------------------------------------------------------- 1 | import { Tabbable } from '.' 2 | import { mount } from '@vue/test-utils' 3 | import { markRaw, h } from 'vue' 4 | import { 5 | render, 6 | renderJsx, 7 | getByText, 8 | click, 9 | mousedown, 10 | mouseover, 11 | } from '../../test/utils' 12 | 13 | describe('Tabbable', () => { 14 | it('renders correctly', () => { 15 | renderJsx(foo) 16 | expect(getByText('foo')).toMatchInlineSnapshot(` 17 |
20 | foo 21 |
22 | `) 23 | }) 24 | 25 | it('can render as other element', () => { 26 | renderJsx(foo) 27 | expect(getByText('foo')).toMatchInlineSnapshot(` 28 | 31 | foo 32 | 33 | `) 34 | }) 35 | 36 | it('renders correctly when disabled', () => { 37 | renderJsx(foo) 38 | expect(getByText('foo')).toMatchInlineSnapshot(` 39 |
42 | foo 43 |
44 | `) 45 | }) 46 | 47 | it('disabled is reactive', async () => { 48 | const wrapper = mount(Tabbable, { 49 | props: { disabled: false }, 50 | slots: { 51 | default: 'foo', 52 | }, 53 | }) 54 | await wrapper.setProps({ disabled: true }) 55 | expect(wrapper.attributes('aria-disabled')).toEqual('true') 56 | }) 57 | 58 | it('renders native focusable elements correctly', async () => { 59 | const { nextTick } = renderJsx(foo) 60 | await nextTick() 61 | expect(getByText('foo')).toMatchInlineSnapshot(` 62 | 65 | `) 66 | }) 67 | 68 | it('renders disabled native focusable elements correctly', async () => { 69 | const { nextTick } = renderJsx( 70 | 71 | foo 72 | 73 | ) 74 | await nextTick() 75 | expect(getByText('foo')).toMatchInlineSnapshot(` 76 | 82 | `) 83 | }) 84 | 85 | it('renders components with native focusable element correctly', async () => { 86 | const testComp = markRaw({ 87 | template: '', 88 | }) 89 | const { nextTick } = render(Tabbable, { 90 | props: { 91 | as: testComp, 92 | }, 93 | }) 94 | await nextTick() 95 | expect(getByText('foo')).toMatchInlineSnapshot(` 96 | 99 | `) 100 | }) 101 | 102 | it('renders components with native focusable element correctly (render function)', async () => { 103 | const testComp = markRaw({ 104 | render() { 105 | return h('button', 'foo') 106 | }, 107 | }) 108 | const { nextTick } = render(Tabbable, { 109 | props: { 110 | as: testComp, 111 | }, 112 | }) 113 | await nextTick() 114 | expect(getByText('foo')).toMatchInlineSnapshot(` 115 | 118 | `) 119 | }) 120 | 121 | it('can be focused', () => { 122 | renderJsx(foo) 123 | const tabbable = getByText('foo') 124 | tabbable.focus() 125 | expect(tabbable).toEqual(document.activeElement) 126 | }) 127 | 128 | it('cannot be focused when disabled', () => { 129 | renderJsx(foo) 130 | const tabbable = getByText('foo') 131 | tabbable.focus() 132 | expect(tabbable).not.toEqual(document.activeElement) 133 | }) 134 | 135 | it('can be focused when focusable', () => { 136 | renderJsx( 137 | 138 | foo 139 | 140 | ) 141 | const tabbable = getByText('foo') 142 | tabbable.focus() 143 | expect(tabbable).toEqual(document.activeElement) 144 | }) 145 | 146 | it('button can be focused when focusable', () => { 147 | renderJsx( 148 | 149 | foo 150 | 151 | ) 152 | const tabbable = getByText('foo') 153 | tabbable.focus() 154 | expect(tabbable).toEqual(document.activeElement) 155 | }) 156 | 157 | it('emits click event', () => { 158 | const clickHandler = jest.fn() 159 | renderJsx(foo) 160 | click(getByText('foo')) 161 | expect(clickHandler).toBeCalledTimes(1) 162 | }) 163 | 164 | it('emits no click event when disabled', () => { 165 | const clickHandler = jest.fn() 166 | renderJsx( 167 | 168 | foo 169 | 170 | ) 171 | click(getByText('foo')) 172 | expect(clickHandler).toBeCalledTimes(0) 173 | }) 174 | 175 | it('emits mousedown event', () => { 176 | const mousedownHandler = jest.fn() 177 | renderJsx(foo) 178 | mousedown(getByText('foo')) 179 | expect(mousedownHandler).toBeCalledTimes(1) 180 | }) 181 | 182 | it('emits no mousedown event when disabled', () => { 183 | const mousedownHandler = jest.fn() 184 | renderJsx( 185 | 186 | foo 187 | 188 | ) 189 | mousedown(getByText('foo')) 190 | expect(mousedownHandler).toBeCalledTimes(0) 191 | }) 192 | 193 | it('emits mouseover event', () => { 194 | const mouseoverHandler = jest.fn() 195 | renderJsx(foo) 196 | mouseover(getByText('foo')) 197 | expect(mouseoverHandler).toBeCalledTimes(1) 198 | }) 199 | 200 | it('emits no mouseover event when disabled', () => { 201 | const mouseoverHandler = jest.fn() 202 | renderJsx( 203 | 204 | foo 205 | 206 | ) 207 | mouseover(getByText('foo')) 208 | expect(mouseoverHandler).toBeCalledTimes(0) 209 | }) 210 | }) 211 | -------------------------------------------------------------------------------- /src/Tabbable/Tabbable.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref, ComponentObjectPropsOptions, PropType } from 'vue' 2 | import { defineComponent, useOnElement } from '../utils' 3 | import { boxProps, BoxProps, useBox } from '../Box' 4 | 5 | function getIsNativeTabbable(element: Element) { 6 | return /^(BUTTON|INPUT|SELECT|TEXTAREA|A|AUDIO|VIDEO)$/.test(element.tagName) 7 | } 8 | 9 | function useIsNativeTabbable(elementRef) { 10 | const isNativeTabbable = ref(false) 11 | useOnElement(elementRef, (element) => { 12 | isNativeTabbable.value = getIsNativeTabbable(element) 13 | }) 14 | return isNativeTabbable 15 | } 16 | 17 | export interface TabbableProps extends BoxProps { 18 | disabled: boolean 19 | focusable: boolean 20 | onClick: () => void 21 | onMousedown: () => void 22 | onMouseover: () => void 23 | } 24 | 25 | export const tabbableProps: ComponentObjectPropsOptions = { 26 | ...boxProps, 27 | disabled: { 28 | type: Boolean, 29 | default: false, 30 | }, 31 | focusable: { 32 | type: Boolean, 33 | default: false, 34 | }, 35 | onClick: { 36 | type: Function as PropType<() => void>, 37 | default: null, 38 | }, 39 | onMousedown: { 40 | type: Function as PropType<() => void>, 41 | default: null, 42 | }, 43 | onMouseover: { 44 | type: Function as PropType<() => void>, 45 | default: null, 46 | }, 47 | } 48 | 49 | export function useTabbable(props: TabbableProps) { 50 | const box = useBox() 51 | const isNativeTabbable = useIsNativeTabbable(box.ref) 52 | const notFocusable = computed(() => props.disabled && !props.focusable) 53 | 54 | const handleNativeEvent = (eventName) => (event: Event) => { 55 | if (props.disabled) { 56 | event.preventDefault() 57 | event.stopPropagation() 58 | return 59 | } 60 | return typeof props[eventName] === 'function' && props[eventName](event) 61 | } 62 | 63 | return { 64 | ...box, 65 | tabindex: computed(() => 66 | notFocusable.value || isNativeTabbable.value ? null : 0 67 | ), 68 | disabled: computed(() => 69 | notFocusable.value && isNativeTabbable.value ? true : null 70 | ), 71 | 'aria-disabled': computed(() => (props.disabled ? true : null)), 72 | onClick: handleNativeEvent('onClick'), 73 | onMousedown: handleNativeEvent('onMousedown'), 74 | onMouseover: handleNativeEvent('onMouseover'), 75 | } 76 | } 77 | 78 | export const Tabbable = defineComponent(tabbableProps, useTabbable) 79 | -------------------------------------------------------------------------------- /src/Tabbable/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Tabbable' 2 | -------------------------------------------------------------------------------- /src/utils/component.ts: -------------------------------------------------------------------------------- 1 | import { h, unref, defineComponent as vueDefineComponent } from 'vue' 2 | import { Portal } from '../Portal' 3 | 4 | function addRefToAttributes(attributes, ref) { 5 | const [key, value] = ref 6 | 7 | // A template ref needs to be kept as is 8 | if (key === 'ref') { 9 | attributes[key] = value 10 | } else { 11 | attributes[key] = unref(value) 12 | } 13 | return attributes 14 | } 15 | 16 | export function refsToAttributes(refs) { 17 | return Object.entries(refs).reduce(addRefToAttributes, {}) 18 | } 19 | 20 | export function defineComponent(componentProps, useAttributeRefs) { 21 | return vueDefineComponent({ 22 | props: componentProps, 23 | inheritAttrs: false, 24 | setup(props, { slots, attrs }) { 25 | const attributeRefs = useAttributeRefs(props) 26 | return () => { 27 | const { withPortal, ...attributes } = refsToAttributes(attributeRefs) 28 | const renderedComp = h(props.as, { ...attributes, ...attrs }, slots) 29 | if (withPortal) { 30 | return h(Portal, null, () => renderedComp) 31 | } 32 | return renderedComp 33 | } 34 | }, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/element.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, Ref } from 'vue' 2 | 3 | type UseOnElementCallback = (element: HTMLElement) => void 4 | 5 | export function getElementFromRef(elementRef: Ref): HTMLElement { 6 | if (!elementRef.value) { 7 | return null 8 | } 9 | return elementRef.value.$el || elementRef.value 10 | } 11 | 12 | export function useOnElement(elementRef: Ref, use: UseOnElementCallback) { 13 | onMounted(() => { 14 | if (!elementRef.value) { 15 | return 16 | } 17 | const element: HTMLElement = getElementFromRef(elementRef) 18 | use(element) 19 | }) 20 | } 21 | 22 | const TABBABLE_SELECTOR = 23 | 'a[href], area[href], input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable], audio[controls], video[controls], summary, [tabindex^="0"], [tabindex^="1"], [tabindex^="2"], [tabindex^="3"], [tabindex^="4"], [tabindex^="5"], [tabindex^="6"], [tabindex^="7"], [tabindex^="8"], [tabindex^="9"]' 24 | 25 | export function isTabbable(element: HTMLElement): boolean { 26 | return element.matches(TABBABLE_SELECTOR) 27 | } 28 | 29 | export function getTabbableElements(element: HTMLElement): HTMLElement[] { 30 | const tabbableElements = Array.from( 31 | element.querySelectorAll(TABBABLE_SELECTOR) as NodeListOf 32 | ) 33 | if (isTabbable(element)) { 34 | tabbableElements.unshift(element) 35 | } 36 | return tabbableElements 37 | } 38 | 39 | export function getNextTabbable(element: HTMLElement): HTMLElement { 40 | const tabbableElements = getTabbableElements(document.body) 41 | const indexOfElement = tabbableElements.indexOf(element) 42 | return tabbableElements[indexOfElement + 1] 43 | } 44 | 45 | export function isFocused(element: HTMLElement): boolean { 46 | return element === document.activeElement 47 | } 48 | 49 | export function focusIsWithin(element: HTMLElement) { 50 | return elementIsWithin(element, document.activeElement as HTMLElement) 51 | } 52 | 53 | export function elementIsWithin(container: HTMLElement, element: HTMLElement) { 54 | if (!container || !element) { 55 | return false 56 | } 57 | return container.contains(element as HTMLElement) 58 | } 59 | 60 | export function focusFirstFocusable(element: HTMLElement) { 61 | const elementToFocus = getTabbableElements(element)[0] || element 62 | elementToFocus.focus() 63 | } 64 | 65 | export function forceFocus(element: HTMLElement) { 66 | const tabIndex = element.getAttribute('tabindex') 67 | if (tabIndex == null) { 68 | element.setAttribute('tabindex', '-1') 69 | } 70 | element.focus() 71 | if (tabIndex == null) { 72 | element.removeAttribute('tabindex') 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './component' 2 | export * from './element' 3 | export * from './visibility' 4 | -------------------------------------------------------------------------------- /src/utils/visibility.ts: -------------------------------------------------------------------------------- 1 | import { 2 | unref, 3 | watch, 4 | onMounted, 5 | getCurrentInstance, 6 | Ref, 7 | RendererNode, 8 | } from 'vue' 9 | import { getElementFromRef } from './element' 10 | 11 | export function useVisibilityTransition( 12 | visibleRef: Ref, 13 | elementRef: Ref 14 | ) { 15 | if (unref(visibleRef) == null) { 16 | return 17 | } 18 | 19 | const currentInstance = getCurrentInstance() 20 | let initialDisplayValue 21 | 22 | function setDisplay(el: RendererNode, value: unknown): void { 23 | el.style.display = value ? initialDisplayValue : 'none' 24 | el.hidden = value ? null : true 25 | } 26 | 27 | watch(visibleRef, (visible) => { 28 | const { transition } = currentInstance.vnode 29 | const el = getElementFromRef(elementRef) 30 | if (!transition) { 31 | setDisplay(el, visible) 32 | return 33 | } 34 | 35 | if (visible) { 36 | transition.beforeEnter(el) 37 | setDisplay(el, true) 38 | transition.enter(el) 39 | return 40 | } 41 | 42 | transition.leave(el, () => { 43 | setDisplay(el, false) 44 | }) 45 | }) 46 | 47 | onMounted(() => { 48 | const visible = unref(visibleRef) 49 | const { transition } = currentInstance.vnode 50 | const el = getElementFromRef(elementRef) 51 | initialDisplayValue = el.style.display === 'none' ? '' : el.style.display 52 | if (transition && visible) { 53 | transition.beforeEnter(el) 54 | transition.enter(el) 55 | return 56 | } 57 | setDisplay(el, visible) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | -------------------------------------------------------------------------------- /test/utils/events.ts: -------------------------------------------------------------------------------- 1 | import { getByText as _getByText, fireEvent } from '@testing-library/dom' 2 | import userEvent from '@testing-library/user-event' 3 | 4 | export const getByText = (text) => _getByText(document.body, text) 5 | export const { click, dblClick, tab } = userEvent 6 | 7 | type TypeOptions = { 8 | delay: number 9 | skipClick?: boolean 10 | skipAutoClose?: boolean 11 | initialSelectionStart?: number 12 | initialSelectionEnd?: number 13 | } 14 | 15 | export const type = ( 16 | element: Element, 17 | text: string, 18 | options: TypeOptions = { 19 | delay: 0, 20 | skipClick: true, 21 | } 22 | ) => { 23 | return userEvent.type(element, text, options) 24 | } 25 | 26 | export const mousedown = (element: Element) => 27 | fireEvent(element, new MouseEvent('mousedown')) 28 | 29 | export const mouseover = (element: Element) => 30 | fireEvent(element, new MouseEvent('mouseover')) 31 | 32 | const createPress = 33 | (key) => async (element: Element & { focus: () => void }) => { 34 | element.focus() 35 | await type(element, key) 36 | } 37 | 38 | export const pressSpace = createPress(' ') 39 | export const pressEnter = createPress('{enter}') 40 | export const pressEsc = createPress('{esc}') 41 | -------------------------------------------------------------------------------- /test/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './render' 2 | export * from './events' 3 | -------------------------------------------------------------------------------- /test/utils/render.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { App } from 'vue' 3 | 4 | const IGNORED_WARNINGS = ['Non-function value encountered for default slot.'] 5 | 6 | const mountedWrappers = new Set() 7 | 8 | export const renderJsx = (template) => { 9 | const component = { 10 | render() { 11 | return template 12 | }, 13 | } 14 | return render(component) 15 | } 16 | 17 | export const render = (component, options = {}) => { 18 | const wrapper = mount(component, { 19 | ...options, 20 | attachTo: document.body, 21 | global: { 22 | plugins: [ignoreWarnings], 23 | }, 24 | }) 25 | mountedWrappers.add(wrapper) 26 | return { 27 | nextTick: wrapper.vm.$nextTick, 28 | } 29 | } 30 | 31 | function cleanupWrapper(wrapper) { 32 | if ( 33 | wrapper.element.parentNode && 34 | wrapper.element.parentNode.parentNode === document.body 35 | ) { 36 | document.body.removeChild(wrapper.element.parentNode) 37 | } 38 | 39 | try { 40 | wrapper.unmount() 41 | } finally { 42 | mountedWrappers.delete(wrapper) 43 | } 44 | } 45 | 46 | function cleanup() { 47 | mountedWrappers.forEach(cleanupWrapper) 48 | } 49 | 50 | function ignoreWarnings(app: App) { 51 | app.config.warnHandler = function (msg, ...args) { 52 | if (IGNORED_WARNINGS.some((warning) => msg.includes(warning))) { 53 | return 54 | } 55 | console.warn(`[Vue warn]: ${msg}`, ...args) 56 | } 57 | } 58 | 59 | afterEach(() => { 60 | cleanup() 61 | }) 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["**/*.spec.tsx", "lib", "examples", "test"], 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "declaration": true, 6 | "declarationDir": "./dts", 7 | "noEmitOnError": false 8 | } 9 | } --------------------------------------------------------------------------------