├── env.d.ts ├── .vscode └── extensions.json ├── vuejs-tour.gif ├── .prettierignore ├── .prettierrc.json ├── .eslintignore ├── tsconfig.node.json ├── .github └── workflows │ ├── release-please.yml │ ├── docs.yml │ ├── npm-publish.yml │ ├── stale.yml │ └── ci.yml ├── src ├── vuejs-tour.ts ├── style │ ├── _variables.scss │ └── style.scss ├── easing.ts ├── Types.ts └── components │ └── VTour.vue ├── docs ├── guide │ ├── component-slots.md │ ├── scroll-to-element.md │ ├── define-the-content.md │ ├── step-scroll-to-element.md │ ├── the-step-type.md │ ├── the-onafter-event.md │ ├── the-onbefore-event.md │ ├── setting-a-target.md │ ├── hiding-the-arrow.md │ ├── tour-margin.md │ ├── highlight-target.md │ ├── step-highlight-target.md │ ├── saving-progress.md │ ├── using-a-backdrop.md │ ├── step-using-a-backdrop.md │ ├── getting-started.md │ ├── using-placement.md │ ├── skipping-a-tour.md │ ├── roadmap.md │ ├── what-is-vuejs-tour.md │ ├── button-labels.md │ ├── create-a-tour.md │ ├── start-options.md │ ├── jump-options.md │ ├── multiple-tours.md │ ├── css-theme.md │ └── accessibility.md ├── index.md └── .vitepress │ └── config.ts ├── .gitignore ├── test ├── helpers │ ├── mountVTour.ts │ └── timers.ts ├── setup │ └── transition.ts ├── setup.ts └── components │ ├── VTour.jumpOptions.spec.ts │ └── VTour.accessibility.spec.ts ├── tsconfig.json ├── vitest.config.ts ├── LICENSE ├── .eslintrc.js ├── vite.config.ts ├── .codacy.yml ├── package.json ├── README.md └── CHANGELOG.md /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /vuejs-tour.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GlobalHive/vuejs-tour/HEAD/vuejs-tour.gif -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | *.local 5 | .DS_Store 6 | .vitepress/cache 7 | .vitepress/dist 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "es5", 6 | "printWidth": 80, 7 | "vueIndentScriptAndStyle": false, 8 | "endOfLine": "lf" 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Build outputs 2 | dist/ 3 | coverage/ 4 | node_modules/ 5 | 6 | # Configuration files 7 | *.config.ts 8 | *.config.js 9 | *.d.ts 10 | *.tsbuildinfo 11 | 12 | # Specific files to exclude from linting 13 | vite.config.ts 14 | vite.config.js 15 | vitest.config.ts 16 | env.d.ts 17 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | pull_request: 4 | branches: 5 | - master 6 | push: 7 | branches: 8 | - master 9 | 10 | name: 1 Release Please 11 | jobs: 12 | release-please: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee 16 | with: 17 | release-type: node -------------------------------------------------------------------------------- /src/vuejs-tour.ts: -------------------------------------------------------------------------------- 1 | import VTour from './components/VTour.vue'; 2 | 3 | export { VTour }; 4 | export type { 5 | ITourStep, 6 | VTourProps, 7 | VTourEvents, 8 | VTourEmits, 9 | VTourData, 10 | VTourExposedMethods, 11 | ButtonLabels, 12 | SaveToLocalStorage, 13 | JumpOptions, 14 | } from './Types'; 15 | 16 | // Re-export NanoPopPosition from nanopop for user convenience 17 | export type { NanoPopPosition } from 'nanopop'; 18 | -------------------------------------------------------------------------------- /docs/guide/component-slots.md: -------------------------------------------------------------------------------- 1 | # Coming Soon 2 | 3 | We're working hard to bring you something amazing. Stay tuned! 4 | 5 | ## Guides 6 | In the meantime, you can check out our [Guides](/guide/what-is-vuejs-tour) to learn more about Vue.js Tour. 7 | 8 | ## Roadmap 9 | Or you can check out our [Roadmap](/guide/roadmap) to see what's coming next. 10 | 11 | ## Other Resources 12 | ![](https://www.icegif.com/wp-content/uploads/2021/11/icegif-530.gif) -------------------------------------------------------------------------------- /docs/guide/scroll-to-element.md: -------------------------------------------------------------------------------- 1 | # Scroll to Element 2 | 3 | To disable scrolling, you can use the `noScroll` prop in the `VTour` component. 4 | 5 | ```vue 6 | 10 | 11 | 16 | ``` 17 | 18 | ::: info 19 | By default, scrolling is enabled. 20 | ::: -------------------------------------------------------------------------------- /docs/guide/define-the-content.md: -------------------------------------------------------------------------------- 1 | # Define the Content 2 | The `content` property is a string that represents the content of the step. The content can be any HTML content. 3 | 4 | ::: code-group 5 | ```vue [Text as Content] 6 | 12 | ``` 13 | ```vue [HTML as Content] 14 | 20 | ``` 21 | ::: -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | .temp 10 | .cache 11 | 12 | node_modules 13 | dist 14 | dist-ssr 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | 28 | # Vitepress 29 | docs/.vitepress/dist 30 | docs/.vitepress/cache 31 | *.tgz 32 | .vitepress/cache/* 33 | coverage/* 34 | 35 | # TypeScript build info and generated config files 36 | *.tsbuildinfo 37 | vite.config.js 38 | vite.config.d.ts 39 | -------------------------------------------------------------------------------- /docs/guide/step-scroll-to-element.md: -------------------------------------------------------------------------------- 1 | # Scroll to Element 2 | 3 | To disable scrolling per step, you can use the `noScroll` option in the `Step`. 4 | 5 | ::: info 6 | This feature will be available in version [`2.4.0`](./roadmap#_(TBA)-🚧). 7 | ::: 8 | 9 | ```vue 10 | 17 | ``` 18 | 19 | ::: info 20 | You can enable the highlight effect globally by setting the `noScroll` prop in the `VTour` component. 21 | 22 | [See Documentation](./scroll-to-element.md) 23 | ::: -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | 4 | name: 3 Create Docs 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | 12 | - name: Setup Node 13 | uses: actions/setup-node@v3 14 | 15 | - name: Build 16 | run : | 17 | npm ci 18 | npm run docs:build 19 | 20 | - name: Deploy 21 | uses: JamesIves/github-pages-deploy-action@6c2d9db40f9296374acc17b90404b6e8864128c8 22 | with: 23 | folder: docs/.vitepress/dist/ 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /docs/guide/the-step-type.md: -------------------------------------------------------------------------------- 1 | # The Step Type 2 | 3 | The Step type is `ITourStep` and it has the following properties: 4 | 5 | ```typescript 6 | export interface ITourStep { 7 | target: string; // The target element to attach the step to 8 | content: string; // The content of the step 9 | placement?: NanoPopPosition; // The placement of the step 10 | onBefore?: () => Promise; // Called before the step is shown 11 | onAfter?: () => Promise; // Called after the step is shown 12 | highlight?: boolean; // Highlight the target element 13 | backdrop?: boolean; // Show a backdrop if set 14 | noScroll?: boolean; // Disable scrolling if set 15 | } 16 | ``` -------------------------------------------------------------------------------- /test/helpers/mountVTour.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import VTour from '../../src/components/VTour.vue'; 3 | 4 | export function mountVTour(overrides: any = {}) { 5 | const base = { 6 | steps: overrides.steps ?? [{ target: 'body', content: 'x' }], 7 | autoStart: overrides.autoStart ?? true, 8 | enableA11y: overrides.enableA11y ?? true, 9 | keyboardNav: overrides.keyboardNav ?? true, 10 | // Zero all delays/durations 11 | startDelay: 0, 12 | teleportDelay: 0, 13 | resizeTimeout: 0, 14 | // Disable scrolling in tests to avoid timing issues with jump.js 15 | noScroll: overrides.noScroll ?? true, 16 | }; 17 | return mount(VTour, { 18 | props: { ...base, ...overrides }, 19 | attachTo: document.body, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | 4 | name: 2 NPM Publish 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | 12 | - name: Setup Node 13 | uses: actions/setup-node@v3 14 | with: 15 | registry-url: https://registry.npmjs.org/ 16 | 17 | - name: Build Project 18 | run : | 19 | npm ci 20 | npm run build-only 21 | 22 | - name: Build Sass 23 | uses: gha-utilities/sass-build@c15aba209f1cfe51f43e319f2724558112cd9336 24 | with: 25 | source: src/style/style.scss 26 | destination: dist/style.css 27 | 28 | - name: Publish 29 | run: npm publish 30 | env: 31 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "noEmit": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | "outDir": "dist", 24 | "declaration": true, 25 | "allowSyntheticDefaultImports": true 26 | }, 27 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], 28 | "exclude": ["src/index.ts"], 29 | "references": [{ "path": "./tsconfig.node.json" }] 30 | } 31 | -------------------------------------------------------------------------------- /docs/guide/the-onafter-event.md: -------------------------------------------------------------------------------- 1 | # The onAfter Event 2 | 3 | The `onAfter` event is triggered after the step is shown. This event is useful when you want to do something after the step is shown. 4 | 5 | ```vue 6 | 15 | ``` 16 | 17 | ::: info 18 | The `onAfter` event is using await, so you can use a promise to delay the step. 19 | ::: 20 | 21 | ```vue 22 | 34 | ``` -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from 'vite'; 4 | import vue from '@vitejs/plugin-vue'; 5 | import { resolve } from 'path'; 6 | 7 | export default defineConfig({ 8 | plugins: [vue()], 9 | test: { 10 | globals: true, 11 | environment: 'happy-dom', 12 | setupFiles: ['./test/setup.ts', './test/setup/transition.ts'], 13 | pool: 'threads', 14 | poolOptions: { 15 | threads: { 16 | singleThread: true, 17 | }, 18 | }, 19 | coverage: { 20 | provider: 'v8', 21 | reporter: ['text', 'html', 'lcov'], 22 | exclude: [ 23 | 'node_modules/', 24 | 'dist/', 25 | 'docs/', 26 | '**/*.d.ts', 27 | 'test/', 28 | 'vite.config.ts', 29 | 'vitest.config.ts', 30 | ], 31 | }, 32 | }, 33 | resolve: { 34 | alias: { 35 | '@': resolve(__dirname, 'src'), 36 | }, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /docs/guide/the-onbefore-event.md: -------------------------------------------------------------------------------- 1 | # The onBefore Event 2 | 3 | The `onBefore` event is triggered before the step is shown. This event is useful when you want to do something before the step is shown. For example, you can use this event to check if the user is allowed to see the step. 4 | 5 | ```vue 6 | 15 | ``` 16 | 17 | ::: info 18 | The `onBefore` event is using await, so you can use a promise to delay the step. 19 | ::: 20 | 21 | ```vue 22 | 34 | ``` -------------------------------------------------------------------------------- /docs/guide/setting-a-target.md: -------------------------------------------------------------------------------- 1 | # Setting a Target 2 | The `target` property is a string that represents the target element to attach the step to. The value of the `target` property is a CSS selector that will be used to find the target element. 3 | 4 | ::: code-group 5 | ```vue [Data as Target] 6 | 12 | 13 | 16 | ``` 17 | ```vue [Class as Target] 18 | 24 | 25 | 28 | ``` 29 | ```vue [ID as Target] 30 | 36 | 37 | 40 | ``` 41 | ::: -------------------------------------------------------------------------------- /src/style/_variables.scss: -------------------------------------------------------------------------------- 1 | $vjt__tooltip_color: #fff !default; 2 | $vjt__tooltip_z_index: 9999 !default; 3 | $vjt__tooltip_font_size: 13px !default; 4 | $vjt__tooltip_arrow_size: 8px !default; 5 | $vjt__tooltip_background: #333 !default; 6 | $vjt__tooltip_border_radius: 4px !default; 7 | $vjt__tooltip_max_width: 300px !default; 8 | $vjt__highlight_offset: 4px !default; 9 | $vjt__highlight_color: #0EA5E9FF !default; 10 | $vjt__highlight_outline_radius: 1px !default; 11 | $vjt__highlight_outline: 2px solid $vjt__highlight_color !default; 12 | $vjt__action_button_color: #fff !default; 13 | $vjt__action_button_font_size: 13px !default; 14 | $vjt__action_button_color_hover: #fff !default; 15 | $vjt__action_button_padding: 4px 16px !default; 16 | $vjt__action_button_border_radius: 4px !default; 17 | $vjt__action_button_background_hover: #000 !default; 18 | $vjt__action_button_border: 1px solid #fff !default; 19 | $vjt__action_button_background: transparent !default; 20 | $vjt__backdrop_z_index: 9998 !default; 21 | $vjt__backdrop_background: rgba(0, 0, 0, 0.5) !default; 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 GlobalHive 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. -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Mark and Close Stale Issues/PRs" 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" # runs daily at midnight 6 | workflow_dispatch: # allows manual triggering 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Run stale action 13 | uses: actions/stale@v9 14 | with: 15 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 16 | stale-issue-message: "This issue has been automatically marked as stale due to inactivity. If you'd like to keep it open, please leave a comment in the next 7 days." 17 | stale-pr-message: "This pull request has been automatically marked as stale due to inactivity. If you’d still like to see it merged, please comment within the next 7 days." 18 | stale-issue-label: "stale" 19 | stale-pr-label: "stale" 20 | days-before-stale: 30 21 | days-before-close: 7 22 | days-before-pr-stale: 30 23 | days-before-pr-close: 7 24 | exempt-issue-labels: "bug,enhancement" 25 | exempt-pr-labels: "WIP" 26 | operations-per-run: 50 27 | delete-branch-on-close: false 28 | env: 29 | TZ: Europe/Zurich 30 | -------------------------------------------------------------------------------- /docs/guide/hiding-the-arrow.md: -------------------------------------------------------------------------------- 1 | # Hiding the Arrow 2 | 3 | To hide the arrow, you can use the `hideArrow` prop in the `VTour` component. 4 | 5 | ```vue 6 | 10 | 11 | 16 | ``` 17 | 18 | 30 | 31 | 38 | 39 | 40 | 41 |
42 |

Target

43 |
44 | 45 | ::: info 46 | By default, the arrow is displayed. 47 | ::: 48 | -------------------------------------------------------------------------------- /docs/guide/tour-margin.md: -------------------------------------------------------------------------------- 1 | # Tour Margin 2 | 3 | You can set the margin of the tour by using the `margin` prop in the `VTour` component. 4 | 5 | ```vue 6 | 10 | 11 | 16 | ``` 17 | 18 | 30 | 31 | 37 | 38 | 39 | 40 |
41 |

Target

42 |
43 | 44 | ::: info 45 | The default value of the `margin` prop is `8`. If the [`highlight`](./highlight-target.md) prop is set, `14` is used as the default value. 46 | ::: -------------------------------------------------------------------------------- /docs/guide/highlight-target.md: -------------------------------------------------------------------------------- 1 | # Highlight Target 2 | The `highlight` property is a boolean that represents whether the target should be highlighted or not. 3 | 4 | ## Using the `highlight` prop 5 | To enable the highlight effect, you can use the `highlight` prop in the `VTour` component. 6 | 7 | ```vue 8 | 12 | 13 | 18 | ``` 19 | 31 | 38 | 39 | 40 |
41 |

Target

42 |
43 | 44 | ::: info 45 | By default, highlighting is disabled. 46 | ::: -------------------------------------------------------------------------------- /docs/guide/step-highlight-target.md: -------------------------------------------------------------------------------- 1 | # Highlight Target Option 2 | The `highlight` option is used to highlight the target element per step. This option is useful when you want to emphasize the target element. 3 | 4 | ## Using the `highlight` option 5 | To enable the highlight effect, you can set the `highlight` option in the `Step`. 6 | 7 | ```vue 8 | 15 | ``` 16 | 28 | 35 | 36 | 37 |
38 |

Target

39 |
40 | 41 | ::: info 42 | You can enable the highlight effect globally by setting the `highlight` prop in the `VTour` component. 43 | 44 | [See Documentation](./highlight-target.md) 45 | ::: -------------------------------------------------------------------------------- /docs/guide/saving-progress.md: -------------------------------------------------------------------------------- 1 | # Saving Progress 2 | 3 | You can save the progress of the tour in the local storage of the browser. This way, the user can continue the tour from where they left off. 4 | 5 | ## Using the `saveToLacalStorage` prop 6 | 7 | To save the progress of the tour, you can use the `saveToLocalStorage` prop in the `VTour` component. This prop accepts a string value of `never`, `step` or `end`. 8 | 9 | ```vue 10 | 14 | 15 | 20 | ``` 21 | 22 | ### `never` 23 | No progress will be saved. Even if the user has already completed the tour, it will start from the beginning. 24 | Which means that you are responsible for managing the progress of the tour. 25 | 26 | ### `step` 27 | The progress of the tour will be saved after each step. So, if the user has completed the first 3 steps and exits, the next time they open the browser, the tour will start from where they left off. 28 | 29 | ### `end` 30 | The progress of the tour will be saved only after the user has completed the tour. If the user exits the tour before completing it, the next time they open the browser, the tour will start from the beginning. 31 | ::: info 32 | This is the default value of the `saveToLocalStorage` prop. 33 | ::: -------------------------------------------------------------------------------- /test/setup/transition.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, nextTick } from 'vue'; 2 | import { config } from '@vue/test-utils'; 3 | 4 | // A Transition stub that fires hooks immediately on the real element 5 | const ImmediateTransition = defineComponent({ 6 | name: 'ImmediateTransition', 7 | setup(_, { slots, attrs }) { 8 | return () => { 9 | const vnode = slots.default?.(); 10 | // Deliver hooks on nextTick when the element is in the DOM 11 | nextTick(() => { 12 | // Try to find the element rendered by this Transition 13 | const el = 14 | document.querySelector('[id$="-tooltip"]') ?? 15 | document.body.querySelector('[data-tour-root]') ?? 16 | document.body.querySelector('[data-test-transition-target]'); 17 | 18 | // Call hooks if present (Vue passes them as props like onBeforeEnter) 19 | // @ts-ignore - attrs carries onBeforeEnter/onEnter/onAfterEnter 20 | attrs.onBeforeEnter?.(el); 21 | // @ts-ignore 22 | if (attrs.onEnter) { 23 | // Vue enter hook receives (el, done) 24 | // @ts-ignore 25 | attrs.onEnter(el, () => {}); 26 | } 27 | // @ts-ignore 28 | attrs.onAfterEnter?.(el); 29 | }); 30 | return vnode as any; 31 | }; 32 | }, 33 | }); 34 | 35 | config.global.stubs = { 36 | Transition: ImmediateTransition, 37 | TransitionGroup: { 38 | render() { 39 | return (this as any).$slots.default?.(); 40 | }, 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | browser: true, 6 | }, 7 | extends: [ 8 | 'plugin:vue/vue3-recommended', 9 | '@vue/eslint-config-typescript/recommended', 10 | '@vue/eslint-config-prettier', 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 2020, 14 | project: ['./tsconfig.json', './tsconfig.node.json'], 15 | }, 16 | rules: { 17 | // Allow unused parameters in TypeScript interfaces and type definitions 18 | '@typescript-eslint/no-unused-vars': [ 19 | 'warn', 20 | { 21 | argsIgnorePattern: '^_', 22 | varsIgnorePattern: '^_', 23 | // Ignore interface and type declaration parameters 24 | args: 'none', 25 | }, 26 | ], 27 | }, 28 | overrides: [ 29 | { 30 | files: ['src/Types.ts'], 31 | rules: { 32 | // Disable unused vars completely for Types.ts since interface parameter names are for documentation 33 | '@typescript-eslint/no-unused-vars': 'off', 34 | }, 35 | }, 36 | { 37 | files: ['*.config.ts', '*.config.js'], 38 | rules: { 39 | // Allow any type assertions in config files 40 | '@typescript-eslint/no-explicit-any': 'off', 41 | '@typescript-eslint/ban-ts-comment': 'off', 42 | '@typescript-eslint/no-unsafe-call': 'off', 43 | '@typescript-eslint/no-unsafe-member-access': 'off', 44 | '@typescript-eslint/no-unsafe-assignment': 'off', 45 | }, 46 | }, 47 | ], 48 | }; 49 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig, type Plugin } from 'vite'; 3 | import dtsPlugin from 'vite-plugin-dts'; 4 | import vue from '@vitejs/plugin-vue'; 5 | 6 | export default defineConfig({ 7 | resolve: { 8 | alias: { 9 | '@': resolve(__dirname, 'src'), 10 | }, 11 | }, 12 | build: { 13 | lib: { 14 | entry: resolve(__dirname, 'src/vuejs-tour.ts'), 15 | name: 'VueJSTour', 16 | formats: ['es', 'umd'], 17 | fileName: 'vuejs-tour', 18 | }, 19 | rollupOptions: { 20 | external: ['vue', 'nanopop', 'jump.js'], 21 | output: { 22 | globals: { 23 | vue: 'Vue', 24 | nanopop: 'nanopop', 25 | 'jump.js': 'jump', 26 | }, 27 | // Rename component CSS to avoid conflict with SCSS output 28 | assetFileNames: (assetInfo) => { 29 | if (assetInfo.name?.endsWith('.css')) { 30 | return 'component-styles.css'; 31 | } 32 | return '[name].[ext]'; 33 | }, 34 | }, 35 | }, 36 | minify: 'esbuild', 37 | sourcemap: true, 38 | target: 'es2020', 39 | }, 40 | plugins: [ 41 | vue(), 42 | dtsPlugin({ 43 | include: ['src/**/*.ts', 'src/**/*.vue'], 44 | exclude: ['src/**/*.spec.ts', 'test/**', 'node_modules/**'], 45 | entryRoot: 'src', 46 | }) as Plugin, 47 | ], 48 | server: { 49 | open: false, // Don't auto-open browser 50 | port: 3000, 51 | }, 52 | preview: { 53 | port: 4173, 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { beforeEach, vi } from 'vitest'; 3 | 4 | // Mock nanopop to avoid real DOM positioning 5 | vi.mock('nanopop', () => ({ 6 | createPopper: vi.fn(() => ({ 7 | update: vi.fn(() => 'right'), 8 | destroy: vi.fn(), 9 | })), 10 | })); 11 | 12 | // Mock jump.js if still used 13 | vi.mock('jump.js', () => ({ 14 | default: vi.fn(() => Promise.resolve()), 15 | })); 16 | 17 | // Global test setup 18 | beforeEach(() => { 19 | // Clear localStorage before each test 20 | localStorage.clear(); 21 | 22 | // Reset all mocks 23 | vi.clearAllMocks(); 24 | 25 | // Clean up DOM 26 | document.body.innerHTML = ''; 27 | document.head.innerHTML = ''; 28 | }); 29 | 30 | // Mock scrollTo for smooth scrolling tests 31 | Object.defineProperty(window, 'scrollTo', { 32 | value: vi.fn(), 33 | writable: true, 34 | }); 35 | 36 | // Mock getBoundingClientRect 37 | Element.prototype.getBoundingClientRect = vi.fn(() => ({ 38 | width: 100, 39 | height: 100, 40 | top: 0, 41 | left: 0, 42 | bottom: 100, 43 | right: 100, 44 | x: 0, 45 | y: 0, 46 | toJSON: vi.fn(), 47 | })); 48 | 49 | // Mock IntersectionObserver 50 | global.IntersectionObserver = class { 51 | observe = vi.fn(); 52 | unobserve = vi.fn(); 53 | disconnect = vi.fn(); 54 | root = null; 55 | rootMargin = '0px'; 56 | thresholds = [0]; 57 | takeRecords = vi.fn(() => []); 58 | }; 59 | 60 | // Mock ResizeObserver 61 | global.ResizeObserver = class { 62 | observe = vi.fn(); 63 | unobserve = vi.fn(); 64 | disconnect = vi.fn(); 65 | }; 66 | -------------------------------------------------------------------------------- /docs/guide/using-a-backdrop.md: -------------------------------------------------------------------------------- 1 | # Using a Backdrop 2 | The `backdrop` property is a boolean that represents whether the backdrop should be displayed or not. The backdrop is a semi-transparent overlay that covers the entire screen, except for the highlighted element. 3 | 4 | ## Using the `backdrop` prop 5 | To enable the backdrop, you can use the `backdrop` prop in the `VTour` component. 6 | 7 | ```vue 8 | 12 | 13 | 18 | ``` 19 | 20 | 32 | 33 | 45 | 46 | 47 | 48 |
49 | 50 |
51 | 52 | ::: info 53 | By default, the backdrop is disabled. 54 | ::: -------------------------------------------------------------------------------- /docs/guide/step-using-a-backdrop.md: -------------------------------------------------------------------------------- 1 | # Using a Backdrop 2 | The `backdrop` option is a boolean that represents whether the backdrop should be displayed or not per step. The backdrop is a semi-transparent overlay that covers the entire screen, except for the highlighted element. 3 | 4 | 5 | ## Using the `backdrop` option 6 | To enable the backdrop, you can use the `backdrop` option in the `Step`. 7 | 8 | ```vue 9 | 16 | ``` 17 | 18 | 30 | 31 | 43 | 44 | 45 | 46 |
47 | 48 |
49 | 50 | ::: info 51 | You can enable the highlight effect globally by setting the `backdrop` prop in the `VTour` component. 52 | 53 | [See Documentation](./using-a-backdrop.md) 54 | ::: -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | Installing VueJS Tour is a straightforward process. This guide will walk you through the installation process and help you set up your first tour. 3 | 4 | ## Try it Online 5 | You can try VueJS Tour directly in your browser on [StackBlitz](https://stackblitz.com/edit/vitejs-vite-vslj9h?file=src%2FApp.vue). 6 | 7 | ## Installation 8 | 9 | ### Prerequisites 10 | - [Node.js](https://nodejs.org/) version 20 or higher. 11 | - [Vue 3.x](https://vuejs.org/) or higher. 12 | - Terminal for installing VueJS Tour. 13 | 14 | VueJS Tour can be installed using npm, pnpm, yarn, or bun. The following command will install VueJS Tour in your project. 15 | ::: code-group sh 16 | ```sh [npm] 17 | npm add @globalhive/vuejs-tour 18 | ``` 19 | ```sh [pnpm] 20 | pnpm add @globalhive/vuejs-tour 21 | ``` 22 | ```sh [yarn] 23 | yarn add @globalhive/vuejs-tour 24 | ``` 25 | ```sh [bun] 26 | bun add @globalhive/vuejs-tour 27 | ``` 28 | ::: 29 | 30 | ## Basic Setup 31 | In the following example, we are importing the `VTour` component from VueJS Tour and using it in the `App.vue` file. 32 | We are also importing the default styles of VueJS Tour. 33 | ```vue 34 | 39 | 40 | 52 | ``` 53 | 54 | Everything is now set up, and you can start [creating your first tour](./create-a-tour). -------------------------------------------------------------------------------- /docs/guide/using-placement.md: -------------------------------------------------------------------------------- 1 | # Using Placement 2 | The `placement` property is a type of `NanoPopPosition` which is a string that represents the placement of the step. 3 | 4 | ```typescript 5 | type Direction = 'top' | 'left' | 'bottom' | 'right'; 6 | type Alignment = 'start' | 'middle' | 'end'; 7 | export type NanoPopPosition = `${Direction}-${Alignment}` | Direction; 8 | ``` 9 | The value of the `placement` property is a string that can be one of the following: 10 | 11 | `top`, `top-start`, `top-middle`, `top-end`, `left`, `left-start`, `left-middle`, `left-end`, `bottom`, `bottom-start`, `bottom-middle`, `bottom-end`, `right`, `right-start`, `right-middle`, `right-end`. 12 | 13 | If not provided, the `placement` property will default to `right-middle`. 14 | 15 | ```vue 16 | 23 | ``` 24 | 25 | 37 | 38 | 47 | 48 | 49 | 50 |
51 |

Target

52 |
53 | 54 | ::: info 55 | `vuejs-tour` will automatically adjust the placement of the step if there is not enough space. 56 | ::: -------------------------------------------------------------------------------- /docs/guide/skipping-a-tour.md: -------------------------------------------------------------------------------- 1 | # Skipping a Tour 2 | 3 | The Skip button is displayed in the tour by default. 4 | You can hide it by customizing the action buttons or by using the `hideSkip` prop in the `VTour` component. 5 | 6 | ## Using the `hideSkip` prop 7 | To hide the Skip button, you can use the `hideSkip` prop in the `VTour` component. 8 | 9 | ```vue 10 | 14 | 15 | 20 | ``` 21 | 22 | 34 | 35 | 41 | 42 | 43 | 44 |
45 |

Target

46 |
47 | 48 | ## Customizing the action buttons 49 | You can also customize the action buttons by using the `actions` slot. 50 | 51 | ```vue 52 | 63 | ``` 64 | 65 | ::: info 66 | The Skip button is displayed by default. 67 | ::: -------------------------------------------------------------------------------- /docs/guide/roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | ✔️ = Done 3 | ❌ = Declined 4 | 🕔 = Postponed 5 | 🚧 = In Progress 6 | 7 | 8 | ## 2.5.0 (2025-11-06) ✔️ 9 | Special thanks to @AdamDrewsTR 10 | 11 | ### Features 12 | 13 | * **VTour:** Adding `jumpOptions` prop ✔️ 14 | * **VTour:** Adding accessibility ✔️ 15 | 16 | ### Documentation 17 | 18 | * **Documentation:** Adding [Jump Options](/guide/jump-options) page ✔️ 19 | * **Documentation:** Adding [Accessibility](/guide/accessibility) page ✔️ 20 | 21 | ## 2.2.0 [2.3.0] (2024-08-13) [Delayed: 23.09.2024] ✔️ 22 | 23 | ### Features 24 | 25 | * **VTour:** Adding `onTourStep` event ✔️ 26 | * **VTour:** Adding `onBeforeStep` event ❌ (Changed to `onTourStep`) ✔️ 27 | * **VTour:** Adding `onAfterStep` event ❌ (Changed to `onTourStep`) ✔️ 28 | * **VTour:** Adding `showProgress` prop ❌ 29 | * **VTour:** Adding `noScroll` prop ✔️ 30 | * **Step:** Adding `arrow️` option ❌ 31 | * **Step:** Adding `noScroll` option ✔️ 32 | * **Step:** Adding `backdrop` option ✔️ 33 | * **Step:** Adding `highlight` option ✔️ 34 | * **Step:** Adding `onBefore` event ✔️ 35 | * **Step:** Adding `onAfter` event ✔️ 36 | 37 | ### Documentation 38 | 39 | * **Documentation:** Adding referece pages 🚧 40 | * **Documentation:** Adding interactive examples ✔️ 41 | 42 | ### Bug Fixes 43 | 44 | Got anything? [Let me know!](https://github.com/GlobalHive/vuejs-tour/issues) 45 | 46 | ## 2.0.1 (2024-07-07) ✔️ 47 | 48 | ### Breaking Changes 49 | 50 | * **VTour:** Removed the plugin approach, switched to component import ✔️ 51 | 52 | ### Features 53 | 54 | * **VTour:** Adding [`margin`](./tour-margin) prop ✔️ 55 | * **VTour:** Adding [`hideSkip`](./skipping-a-tour) prop ✔️ 56 | * **VTour:** Adding [`hideArrow`](./hiding-the-arrow) prop ✔️ 57 | * **VTour:** Adding Typescript ✔️ 58 | * **VTour:** Complete rewrite of the component ✔️ 59 | 60 | ### Bug Fixes 61 | 62 | * **VTour:** Fixing the [`highlight`](./highlight-target) using document space ✔️ 63 | * **VTour:** Fixing wrong [`saveToLocalStorage`](./saving-progress) checks ✔️ 64 | 65 | ### Documentation 66 | 67 | * **Documentation:** Added new examples ✔️ 68 | * **Documentation:** Added new guide ✔️ 69 | * **Documentation:** Added new Reference ✔️ 70 | * **Documentation:** Switched to VitePress ✔️ -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: "VueJS Tour" 6 | tagline: Guide your users through your application quickly and easily. 7 | image: https://vuejs.org/images/logo.png 8 | actions: 9 | - theme: brand 10 | text: What is VueJS Tour? 11 | link: /guide/what-is-vuejs-tour 12 | - theme: alt 13 | text: Quickstart 14 | link: /guide/getting-started 15 | - theme: alt 16 | text: Github 17 | link: https://github.com/GlobalHive/vuejs-tour 18 | 19 | features: 20 | - icon: ✨ 21 | title: Up-to-date 22 | details: VueJS Tour utilizes Vue 3's Composition API and TypeScript. This ensures a more reliable, maintainable, and type-safe codebase. 23 | - icon: 💪 24 | title: Simple and Easy 25 | details: VueJS Tour requires minimal setup, you can quickly create engaging and informative tours that enhance user experience. 26 | - icon: 🎨 27 | title: Themeable 28 | details: VueJS Tour supports theming through SASS/SCSS, allowing you to customize the appearance of your tours. 29 | --- 30 | 31 | 32 | 66 | 67 | -------------------------------------------------------------------------------- /src/easing.ts: -------------------------------------------------------------------------------- 1 | // Robert Penner's easing functions 2 | // Credit: http://robertpenner.com/easing/ 3 | // Ported for ES6 from: https://github.com/jaxgeller/ez.js 4 | 5 | export type EasingFunction = ( 6 | t: number, 7 | b: number, 8 | c: number, 9 | d: number 10 | ) => number; 11 | 12 | // Quadratic 13 | export const easeInQuad: EasingFunction = (t, b, c, d) => { 14 | t /= d; 15 | return c * t * t + b; 16 | }; 17 | 18 | export const easeOutQuad: EasingFunction = (t, b, c, d) => { 19 | t /= d; 20 | return -c * t * (t - 2) + b; 21 | }; 22 | 23 | export const easeInOutQuad: EasingFunction = (t, b, c, d) => { 24 | t /= d / 2; 25 | if (t < 1) return (c / 2) * t * t + b; 26 | t--; 27 | return (-c / 2) * (t * (t - 2) - 1) + b; 28 | }; 29 | 30 | // Cubic 31 | export const easeInCubic: EasingFunction = (t, b, c, d) => { 32 | t /= d; 33 | return c * t * t * t + b; 34 | }; 35 | 36 | export const easeOutCubic: EasingFunction = (t, b, c, d) => { 37 | t /= d; 38 | t--; 39 | return c * (t * t * t + 1) + b; 40 | }; 41 | 42 | export const easeInOutCubic: EasingFunction = (t, b, c, d) => { 43 | t /= d / 2; 44 | if (t < 1) return (c / 2) * t * t * t + b; 45 | t -= 2; 46 | return (c / 2) * (t * t * t + 2) + b; 47 | }; 48 | 49 | // Quartic 50 | export const easeInQuart: EasingFunction = (t, b, c, d) => { 51 | t /= d; 52 | return c * t * t * t * t + b; 53 | }; 54 | 55 | export const easeOutQuart: EasingFunction = (t, b, c, d) => { 56 | t /= d; 57 | t--; 58 | return -c * (t * t * t * t - 1) + b; 59 | }; 60 | 61 | export const easeInOutQuart: EasingFunction = (t, b, c, d) => { 62 | t /= d / 2; 63 | if (t < 1) return (c / 2) * t * t * t * t + b; 64 | t -= 2; 65 | return (-c / 2) * (t * t * t * t - 2) + b; 66 | }; 67 | 68 | // Quintic 69 | export const easeInQuint: EasingFunction = (t, b, c, d) => { 70 | t /= d; 71 | return c * t * t * t * t * t + b; 72 | }; 73 | 74 | export const easeOutQuint: EasingFunction = (t, b, c, d) => { 75 | t /= d; 76 | t--; 77 | return c * (t * t * t * t * t + 1) + b; 78 | }; 79 | 80 | export const easeInOutQuint: EasingFunction = (t, b, c, d) => { 81 | t /= d / 2; 82 | if (t < 1) return (c / 2) * t * t * t * t * t + b; 83 | t -= 2; 84 | return (c / 2) * (t * t * t * t * t + 2) + b; 85 | }; 86 | 87 | // Map of easing function names to implementations 88 | export const easingFunctions: Record = { 89 | easeInQuad, 90 | easeOutQuad, 91 | easeInOutQuad, 92 | easeInCubic, 93 | easeOutCubic, 94 | easeInOutCubic, 95 | easeInQuart, 96 | easeOutQuart, 97 | easeInOutQuart, 98 | easeInQuint, 99 | easeOutQuint, 100 | easeInOutQuint, 101 | }; 102 | -------------------------------------------------------------------------------- /docs/guide/what-is-vuejs-tour.md: -------------------------------------------------------------------------------- 1 | # What is VueJS Tour 2 | VueJS Tour is a customizable Vue.js plugin for creating guided tours, 3 | leveraging Vue 3's Composition API and TypeScript for modern, maintainable tours. 4 | It supports SASS/SCSS theming for seamless design integration. 5 | 6 |
7 | Just want to try it out? Skip to the Quickstart. 8 |
9 | 10 | ## Use Cases 11 | 12 | - **Onboarding New Users** 13 | 14 | Introduce new users to your application by guiding them through the key features and functionalities, ensuring a smooth onboarding experience. 15 | 16 | - **Feature Highlights** 17 | 18 | Use tours to highlight new or underused features to existing users, helping to increase feature adoption and user engagement. 19 | 20 | - **User Support** 21 | 22 | Reduce support requests by providing interactive guides that answer common questions or guide users through complex processes within the application. 23 | 24 | - **Navigation Assistance** 25 | 26 | Help users navigate through the application, especially in complex or feature-rich interfaces, improving overall user experience and satisfaction. 27 | 28 | - **Educational Content** 29 | 30 | Create educational tours that provide users with insights, tips, and best practices related to your application or the services it offers. 31 | 32 | ## Features 33 | - **Multiple tours** 34 | 35 | VueJS Tour supports multiple tours, allowing you to create and manage tours for different sections or features of your application. 36 | - **Modern & Maintainable** 37 | 38 | VueJS Tour leverages Vue 3's Composition API and TypeScript for modern, maintainable tours that are easy to create, update, and maintain. 39 | - **Customizable** 40 | 41 | Customize the look and feel of your tours with SASS/SCSS theming, allowing seamless design integration with your application. 42 | - **Interactive** 43 | 44 | Engage users with interactive tours that allow them to interact with the application, providing a more immersive and engaging experience. 45 | - **Cross-Browser Compatibility** 46 | 47 | VueJS Tour is compatible with all modern browsers, ensuring a consistent experience for all users, regardless of their browser preferences. 48 | - **Lightweight & Performant** 49 | 50 | VueJS Tour is lightweight and performant, ensuring that your tours load quickly and run smoothly, without impacting the overall performance of your application. 51 | - **Developer-Friendly** 52 | 53 | VueJS Tour is developer-friendly, with comprehensive documentation, examples, and support to help you get started and create amazing tours with ease. 54 | - **Open Source** 55 | 56 | VueJS Tour is open source, licensed under the MIT License, allowing you to use, modify, and distribute it freely in your projects. 57 | -------------------------------------------------------------------------------- /.codacy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Codacy configuration for VueJS Tour project 3 | 4 | # CRITICAL: Exclude config files FIRST to prevent any engine from analyzing them 5 | exclude_paths: 6 | - 'vite.config.ts' 7 | - 'vite.config.js' 8 | - 'vitest.config.ts' 9 | - '*.config.ts' 10 | - '*.config.js' 11 | 12 | engines: 13 | # ESLint for JavaScript/TypeScript/Vue linting 14 | eslint: 15 | enabled: true 16 | exclude_paths: 17 | - 'vite.config.*' 18 | - 'vitest.config.*' 19 | - '*.d.ts' 20 | - 'env.d.ts' 21 | - 'docs/.vitepress/**' 22 | configuration: 23 | rules: 24 | "@typescript-eslint/no-unused-vars": "off" 25 | 26 | # TypeScript linting 27 | typescript: 28 | enabled: true 29 | exclude_paths: 30 | - '*.d.ts' 31 | - 'env.d.ts' 32 | - 'vite.config.ts' 33 | - 'vite.config.js' 34 | - 'vite.config.d.ts' 35 | - 'vitest.config.ts' 36 | - 'vitest.config.js' 37 | - 'src/Types.ts' # Public API interface definitions 38 | 39 | # Duplication detection 40 | duplication: 41 | enabled: true 42 | exclude_paths: 43 | - 'test/**' 44 | - 'docs/**' 45 | - 'coverage/**' 46 | 47 | # Code complexity analysis 48 | metrics: 49 | enabled: true 50 | exclude_paths: 51 | - 'test/**' 52 | - 'docs/**' 53 | - 'coverage/**' 54 | - 'vite.config.*' 55 | - 'vitest.config.*' 56 | 57 | # CSS/SCSS linting 58 | csslint: 59 | enabled: true 60 | exclude_paths: 61 | - 'coverage/**' 62 | - 'docs/**' 63 | 64 | # Additional global exclude patterns (config files already excluded at top) 65 | exclude_patterns: 66 | # Dependencies and build artifacts 67 | - 'node_modules/**' 68 | - 'dist/**' 69 | - 'coverage/**' 70 | - '.vitepress/cache/**' 71 | 72 | # Test files (if you want to exclude from analysis) 73 | - 'test/**' 74 | 75 | # Configuration files that don't need deep analysis 76 | - '*.config.*' 77 | - '*.d.ts' 78 | - 'env.d.ts' 79 | - 'tsconfig*.json' 80 | 81 | # Documentation build artifacts 82 | - 'docs/.vitepress/dist/**' 83 | 84 | # Generated files 85 | - '*.tsbuildinfo' 86 | - '*.gif' 87 | - '*.png' 88 | - '*.jpg' 89 | - '*.jpeg' 90 | - '*.svg' 91 | - '*.ico' 92 | 93 | # Package files 94 | - '*.tgz' 95 | - '*.tar.gz' 96 | - 'package-lock.json' 97 | - 'yarn.lock' 98 | - 'pnpm-lock.yaml' 99 | 100 | # IDE and editor files 101 | - '.vscode/**' 102 | - '.idea/**' 103 | - '*.swp' 104 | - '*.swo' 105 | - '*~' 106 | 107 | # OS generated files 108 | - '.DS_Store' 109 | - 'Thumbs.db' 110 | 111 | # File size limits (optional) 112 | file_size_limit: 5000 # Lines 113 | 114 | # Complexity thresholds (optional) 115 | complexity_threshold: 15 116 | -------------------------------------------------------------------------------- /docs/guide/button-labels.md: -------------------------------------------------------------------------------- 1 | # Button Labels 2 | To customize the labels of the buttons, you can use the `buttonLabels` prop in the `VTour` component. 3 | 4 | ## Using the `buttonLabels` prop 5 | 6 | ```vue 7 | 18 | ``` 19 | 20 | 32 | 33 | 39 | 40 | 41 | 42 |
43 |

Target

44 |
45 | 46 | ## Customizing the action buttons 47 | You can also customize the action button labels by using the `actions` slot. 48 | 49 | ```vue 50 | 62 | ``` 63 | 64 | The `VTour` component uses a computed property `getNextLabel` to determine the label of the `Next` button. 65 | ```js 66 | const getNextLabel: ComputedRef = computed(() => { 67 | if(_CurrentStep.currentStep === props.steps.length - 1) return props.buttonLabels?.done || 'Done'; 68 | return props.buttonLabels?.next || 'Next'; 69 | }); 70 | ``` 71 | ::: info 72 | The `nextStep` method will automatically call the `endTour` method when the last step is reached. 73 | ::: -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@globalhive/vuejs-tour", 3 | "version": "2.6.1", 4 | "description": "VueJS Tour is a lightweight, simple and customizable tour plugin. It provides a quick and easy way to guide your users through your application.", 5 | "author": "Global Hive ", 6 | "license": "MIT", 7 | "type": "module", 8 | "main": "dist/vuejs-tour.umd.cjs", 9 | "module": "dist/vuejs-tour.js", 10 | "types": "dist/vuejs-tour.d.ts", 11 | "keywords": [ 12 | "vue", 13 | "vuejs", 14 | "vue.js", 15 | "vuejs-tour", 16 | "tour", 17 | "vuetour", 18 | "vue-tour", 19 | "vue3-tour", 20 | "vue3tour", 21 | "tour" 22 | ], 23 | "homepage": "https://globalhive.github.io/vuejs-tour/", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/globalhive/vuejs-tour.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/globalhive/vuejs-tour/issues" 30 | }, 31 | "exports": { 32 | ".": { 33 | "import": "./dist/vuejs-tour.js", 34 | "require": "./dist/vuejs-tour.umd.cjs" 35 | }, 36 | "./dist/style.css": { 37 | "import": "./dist/style.css", 38 | "require": "./dist/style.css" 39 | }, 40 | "./src/style/style.scss": { 41 | "import": "./src/style/style.scss", 42 | "require": "./src/style/style.scss" 43 | } 44 | }, 45 | "files": [ 46 | "dist", 47 | "src/style" 48 | ], 49 | "scripts": { 50 | "dev": "vite", 51 | "preview": "vite preview", 52 | "build": "npm run build-only && npm run build::sass && npm pack", 53 | "build-only": "vite build", 54 | "type-check": "vue-tsc --build --force", 55 | "build::sass": "sass src/style/style.scss dist/style.css", 56 | "docs:dev": "vitepress dev docs", 57 | "docs:build": "vitepress build docs", 58 | "docs:preview": "vitepress preview docs", 59 | "test": "vitest", 60 | "test:ui": "vitest --ui", 61 | "test:run": "vitest run", 62 | "test:coverage": "vitest run --coverage", 63 | "test:watch": "vitest --watch", 64 | "lint": "vue-tsc --noEmit", 65 | "clean": "rm -rf dist node_modules/.vite", 66 | "format": "prettier --write \"**/*.{js,ts,vue}\" --ignore-path .gitignore" 67 | }, 68 | "dependencies": { 69 | "jump.js": "^1.0.2", 70 | "nanopop": "^2.4.2" 71 | }, 72 | "peerDependencies": { 73 | "vue": "^3.5.22" 74 | }, 75 | "devDependencies": { 76 | "@testing-library/jest-dom": "^6.9.1", 77 | "@testing-library/user-event": "^14.6.1", 78 | "@testing-library/vue": "^8.1.0", 79 | "@tsconfig/node20": "^20.1.6", 80 | "@types/jump.js": "^1.0.6", 81 | "@types/node": "^24.7.2", 82 | "@vitejs/plugin-vue": "^6.0.1", 83 | "@vitest/coverage-v8": "^3.2.4", 84 | "@vitest/ui": "^3.2.4", 85 | "@vue/eslint-config-prettier": "^10.2.0", 86 | "@vue/test-utils": "^2.4.6", 87 | "@vue/tsconfig": "^0.8.1", 88 | "happy-dom": "^20.0.0", 89 | "jsdom": "^27.0.0", 90 | "prettier": "^3.6.2", 91 | "sass": "^1.93.2", 92 | "typescript": "~5.9.3", 93 | "vite": "^7.1.9", 94 | "vite-plugin-dts": "^4.5.4", 95 | "vitepress": "^1.6.4", 96 | "vitest": "^3.2.4", 97 | "vue-tsc": "^3.1.1" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /docs/guide/create-a-tour.md: -------------------------------------------------------------------------------- 1 | # Create a tour 2 | In this guide, you will learn how to create a tour using the `vuejs-tour` package. 3 | 4 | ## Creating Steps 5 | First of all, you need to create an array of steps. Each step should have a `target` and `content` property. The `target` property is a CSS selector that points to the element that the step should target. The `content` property is the text that will be displayed in the tour. 6 | ```vue 7 | 23 | ``` 24 | 25 | After creating the steps, you need to pass them to the `VTour` component as a prop. 26 | ```vue 27 | 43 | ``` 44 | 45 | ## Starting the Tour 46 | To start the tour, you can use the `autoStart` prop. Or you can start the tour manually by calling the `startTour` method on the `VTour` component. 47 | 48 | ### Using `autoStart` prop 49 | ```vue 50 | 63 | ``` 64 | 65 | ### Using `startTour` method 66 | ```vue 67 | 76 | 77 | 90 | ``` 91 | 92 | That's it! You have successfully created a tour using the `vuejs-tour` package. 93 | 94 | ## What's next? 95 | - [Customization](./start-options) 96 | 97 | -------------------------------------------------------------------------------- /docs/guide/start-options.md: -------------------------------------------------------------------------------- 1 | # Start Options 2 | To start a tour, you can use the `autoStart` prop or call the `startTour` method on the `VTour` component. Additionally, you can use the `startDelay` prop to delay the start of the tour. 3 | 4 | ## Using the `autoStart` prop 5 | ```vue 6 | 10 | 11 | 15 | ``` 16 | When using the `autoStart` prop, the tour will start automatically when the component is mounted. 17 | 18 | 41 | 42 | 54 | 55 | 56 | 57 |
58 |

Target

59 |
60 | 61 | ## Using the `startTour` method 62 | ```vue 63 | 69 | 70 | 75 | ``` 76 | When using the `startTour` method, the tour will start when the method is called. This can be useful when you want to start the tour based on user interaction. 77 | 78 |
79 | 80 |
81 | 82 | ::: tip 83 | Every time you use `startTour`, the tour begins from the start. It will do this until finished, unless you change [`saveToLocalStorage`](./saving-progress) to `step`, which saves progress. 84 | ::: 85 | 86 | ## Using the `startDelay` prop 87 | 88 | The `startDelay` prop allows you to delay the start of the tour. This can be useful when you want to give the user some time to get familiar with the page before starting the tour. 89 | 90 | ```vue 91 | 96 | ``` 97 | 98 |
99 | 100 |
101 | 102 | ::: tip 103 | The `startDelay` prop is in milliseconds. 104 | ::: -------------------------------------------------------------------------------- /src/style/style.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | @use "variables" as *; 3 | 4 | [data-hidden="true"] { 5 | visibility: hidden; 6 | pointer-events: none; 7 | } 8 | 9 | [data-hidden="false"] { 10 | visibility: visible; 11 | } 12 | 13 | .vjt-modal-overlay { 14 | position: fixed; 15 | top: 0; 16 | left: 0; 17 | width: 100%; 18 | height: 100%; 19 | pointer-events: none; 20 | z-index: 9999; 21 | } 22 | 23 | // Support multiple tour instances with dynamic IDs 24 | [id$="-backdrop"]{ 25 | position: fixed; 26 | top: 0; 27 | left: 0; 28 | width: 100%; 29 | height: 100%; 30 | background-color: $vjt__backdrop_background; 31 | z-index: $vjt__backdrop_z_index; 32 | 33 | &:not([data-hidden="true"]) { 34 | pointer-events: auto; 35 | } 36 | } 37 | 38 | [id$="-tooltip"] { 39 | background-color: $vjt__tooltip_background; 40 | color: $vjt__tooltip_color; 41 | padding: 0.5rem; 42 | border-radius: $vjt__tooltip_border_radius; 43 | font-size: $vjt__tooltip_font_size; 44 | z-index: $vjt__tooltip_z_index; 45 | max-width: $vjt__tooltip_max_width; 46 | position: absolute; 47 | pointer-events: auto; 48 | } 49 | 50 | 51 | [id$="-tooltip"][data-arrow^='t'] { 52 | [id$="-arrow"] { 53 | bottom: math.div(-$vjt__tooltip_arrow_size, 2); 54 | right: 50%; 55 | } 56 | } 57 | 58 | [id$="-tooltip"][data-arrow^='b'] { 59 | [id$="-arrow"] { 60 | top: math.div(-$vjt__tooltip_arrow_size, 2); 61 | right: 50%; 62 | } 63 | } 64 | 65 | [id$="-tooltip"][data-arrow^='l'] { 66 | [id$="-arrow"] { 67 | right: math.div(-$vjt__tooltip_arrow_size, 2); 68 | top: 50%; 69 | } 70 | } 71 | 72 | [id$="-tooltip"][data-arrow^='r'] { 73 | [id$="-arrow"] { 74 | left: math.div(-$vjt__tooltip_arrow_size, 2); 75 | top: 50%; 76 | } 77 | } 78 | 79 | [id$="-arrow"] { 80 | width: $vjt__tooltip_arrow_size; 81 | height: $vjt__tooltip_arrow_size; 82 | position: absolute; 83 | z-index: -1; 84 | 85 | &::before { 86 | content: ""; 87 | width: $vjt__tooltip_arrow_size; 88 | height: $vjt__tooltip_arrow_size; 89 | background-color: $vjt__tooltip_background; 90 | transform: rotate(45deg); 91 | position: absolute; 92 | } 93 | } 94 | 95 | // Support multiple tour instances with dynamic highlight classes 96 | [class*="vjt-highlight-"] { 97 | outline: $vjt__highlight_outline; 98 | outline-offset: $vjt__highlight_offset; 99 | border-radius: $vjt__highlight_outline_radius; 100 | position: relative; 101 | } 102 | 103 | .vjt-actions { 104 | display: flex; 105 | justify-content: space-between; 106 | align-items: center; 107 | margin-top: 0.5rem; 108 | gap: 0.5rem; 109 | 110 | button { 111 | width: 100%; 112 | padding: 0.25rem 1rem; 113 | border: $vjt__action_button_border; 114 | border-radius: $vjt__action_button_border_radius; 115 | background-color: $vjt__action_button_background; 116 | color: $vjt__action_button_color; 117 | font-size: $vjt__action_button_font_size; 118 | font-weight: 500; 119 | transition: all 0.2s ease-in-out; 120 | cursor: pointer; 121 | 122 | &:hover { 123 | background-color: $vjt__action_button_background_hover; 124 | color: $vjt__action_button_color_hover; 125 | } 126 | } 127 | } 128 | 129 | // Screen-reader-only content (for accessibility announcements) 130 | .vjt-sr-only { 131 | position: absolute; 132 | width: 1px; 133 | height: 1px; 134 | padding: 0; 135 | margin: -1px; 136 | overflow: hidden; 137 | clip: rect(0, 0, 0, 0); 138 | white-space: nowrap; 139 | border-width: 0; 140 | } 141 | -------------------------------------------------------------------------------- /test/helpers/timers.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'vue'; 2 | import { flushPromises } from '@vue/test-utils'; 3 | import { afterEach, beforeEach, vi } from 'vitest'; 4 | 5 | export function useFakeTimersPerTest() { 6 | beforeEach(() => vi.useFakeTimers()); 7 | afterEach(() => vi.useRealTimers()); 8 | } 9 | 10 | export async function flushVue() { 11 | await Promise.resolve(); 12 | await flushPromises(); 13 | await nextTick(); 14 | await nextTick(); 15 | } 16 | 17 | export async function runPending() { 18 | vi.runOnlyPendingTimers(); 19 | await flushVue(); 20 | } 21 | 22 | /** 23 | * Wait for isTransitioning to become false after a step transition. 24 | * Useful after calling nextStep(), lastStep(), or goToStep(). 25 | */ 26 | export async function waitForStepTransition(wrapper: any) { 27 | for (let i = 0; i < 30; i++) { 28 | await runPending(); 29 | const vm = wrapper.vm as any; 30 | const tip = document.querySelector('[id$="-tooltip"]'); 31 | if (!vm.isTransitioning && tip?.getAttribute('data-hidden') === 'false') { 32 | // Extra flushes to ensure Vue re-renders the keyed step content 33 | await flushVue(); 34 | await flushVue(); 35 | return; 36 | } 37 | } 38 | const vm = wrapper.vm as any; 39 | throw new Error( 40 | `Step transition did not complete. isTransitioning=${vm.isTransitioning}, currentStepIndex=${vm.currentStepIndex}` 41 | ); 42 | } 43 | 44 | /** 45 | * Start (if needed) and wait until tooltip exists and data-hidden="false". 46 | * Advances component's timed gates and waits for transition hooks to fire. 47 | */ 48 | export async function startAndWaitReady(wrapper: any) { 49 | if (!wrapper.props('autoStart')) { 50 | await (wrapper.vm as any).startTour(); 51 | } 52 | await flushVue(); 53 | 54 | // Advance the component's timed gates 55 | const startDelay = wrapper.props('startDelay') ?? 0; 56 | const teleportDelay = wrapper.props('teleportDelay') ?? 0; 57 | const transitionMs = 58 | wrapper.props('transitionDuration') ?? wrapper.props('transitionMs') ?? 0; 59 | 60 | // Advance startDelay to trigger the setTimeout callback 61 | if (startDelay > 0) { 62 | vi.advanceTimersByTime(startDelay); 63 | } else { 64 | // Even with 0 delay, need to run the pending timer 65 | vi.runOnlyPendingTimers(); 66 | } 67 | await flushVue(); 68 | 69 | // Now advance teleportDelay (inside the startDelay callback) 70 | if (teleportDelay > 0) { 71 | vi.advanceTimersByTime(teleportDelay); 72 | } else { 73 | vi.runOnlyPendingTimers(); 74 | } 75 | await flushVue(); 76 | 77 | // Advance any transition duration 78 | if (transitionMs > 0) { 79 | vi.advanceTimersByTime(transitionMs); 80 | await flushVue(); 81 | } 82 | 83 | // Drain any remaining 0ms timers/microtasks and allow async operations to complete 84 | for (let i = 0; i < 30; i++) { 85 | await runPending(); 86 | 87 | const vm = wrapper.vm as any; 88 | const tip = document.querySelector('[id$="-tooltip"]'); 89 | 90 | if ( 91 | tip && 92 | vm.tourVisible && 93 | !vm.isTransitioning && 94 | tip.getAttribute('data-hidden') === 'false' 95 | ) { 96 | return tip; 97 | } 98 | } 99 | 100 | const finalTip = document.querySelector('[id$="-tooltip"]'); 101 | const vm = wrapper.vm as any; 102 | throw new Error( 103 | `Tooltip not ready. tourVisible=${vm.tourVisible}, isTransitioning=${vm.isTransitioning}, ` + 104 | `data-hidden="${finalTip?.getAttribute('data-hidden')}", hasContent=${!!finalTip?.querySelector('[id$="-content"]')}` 105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD Pipeline 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | name: Test & Build 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [20, 22] 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: 'npm' 27 | 28 | - name: Install dependencies 29 | run: npm ci 30 | 31 | - name: Run type checking 32 | run: npm run type-check 33 | 34 | - name: Run tests 35 | run: npm run test:run 36 | 37 | - name: Run tests with coverage 38 | run: npm run test:coverage 39 | 40 | - name: Upload coverage to Codecov 41 | if: matrix.node-version == 20 42 | uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 43 | with: 44 | file: ./coverage/lcov.info 45 | flags: unittests 46 | name: codecov-umbrella 47 | fail_ci_if_error: false 48 | 49 | - name: Build library 50 | run: npm run build-only 51 | 52 | - name: Build styles 53 | run: npm run build::sass 54 | 55 | - name: Build documentation 56 | run: npm run docs:build 57 | continue-on-error: true 58 | 59 | - name: Check for build artifacts 60 | run: | 61 | ls -la dist/ 62 | test -f dist/vuejs-tour.js 63 | test -f dist/vuejs-tour.umd.cjs 64 | test -f dist/vuejs-tour.d.ts 65 | test -f dist/style.css 66 | 67 | lint: 68 | name: Lint & Format Check 69 | runs-on: ubuntu-latest 70 | 71 | steps: 72 | - name: Checkout code 73 | uses: actions/checkout@v4 74 | 75 | - name: Setup Node.js 76 | uses: actions/setup-node@v4 77 | with: 78 | node-version: 20 79 | cache: 'npm' 80 | 81 | - name: Install dependencies 82 | run: npm ci 83 | 84 | - name: Run linting 85 | run: npm run lint 86 | 87 | - name: Check code formatting 88 | run: | 89 | npm run format 90 | git diff --exit-code -- ':!*.tsbuildinfo' || (echo "Code is not formatted. Run 'npm run format' to fix." && exit 1) 91 | 92 | security: 93 | name: Security Audit 94 | runs-on: ubuntu-latest 95 | 96 | steps: 97 | - name: Checkout code 98 | uses: actions/checkout@v4 99 | 100 | - name: Setup Node.js 101 | uses: actions/setup-node@v4 102 | with: 103 | node-version: 20 104 | cache: 'npm' 105 | 106 | - name: Install dependencies 107 | run: npm ci 108 | 109 | - name: Run security audit 110 | run: npm audit --audit-level=high 111 | 112 | publish-coverage: 113 | name: Publish Test Coverage 114 | runs-on: ubuntu-latest 115 | needs: [test] 116 | if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' 117 | 118 | steps: 119 | - name: Checkout code 120 | uses: actions/checkout@v4 121 | 122 | - name: Setup Node.js 123 | uses: actions/setup-node@v4 124 | with: 125 | node-version: 20 126 | cache: 'npm' 127 | 128 | - name: Install dependencies 129 | run: npm ci 130 | 131 | - name: Generate coverage report 132 | run: npm run test:coverage 133 | 134 | - name: Generate coverage badge 135 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 136 | with: 137 | script: | 138 | const fs = require('fs'); 139 | const path = 'coverage/coverage-summary.json'; 140 | if (fs.existsSync(path)) { 141 | const coverage = JSON.parse(fs.readFileSync(path, 'utf8')); 142 | const pct = coverage.total.lines.pct; 143 | console.log(`Coverage: ${pct}%`); 144 | } 145 | 146 | - name: Upload coverage reports 147 | uses: actions/upload-artifact@v4 148 | with: 149 | name: coverage-reports 150 | path: | 151 | coverage/ 152 | !coverage/tmp/ 153 | retention-days: 30 154 | -------------------------------------------------------------------------------- /docs/guide/jump-options.md: -------------------------------------------------------------------------------- 1 | # Jump Options 2 | 3 | VueJS Tour now supports customizable scroll animation options via the [jump.js](https://github.com/callmecavs/jump.js) library. 4 | 5 | ## Available Options 6 | 7 | ```typescript 8 | interface JumpOptions { 9 | /** Duration of scroll animation in milliseconds (default: 500) */ 10 | duration?: number; 11 | 12 | /** Vertical offset in pixels from target element (default: -100) */ 13 | offset?: number; 14 | 15 | /** Callback function to execute after scroll completes */ 16 | callback?: () => void; 17 | 18 | /** 19 | * Easing function name (default: 'easeInOutQuad') 20 | * Valid values: 'easeInQuad', 'easeOutQuad', 'easeInOutQuad', 'easeInCubic', 21 | * 'easeOutCubic', 'easeInOutCubic', 'easeInQuart', 'easeOutQuart', 'easeInOutQuart', 22 | * 'easeInQuint', 'easeOutQuint', 'easeInOutQuint' 23 | */ 24 | easing?: string; 25 | 26 | /** Whether to focus the element for accessibility (default: false) */ 27 | a11y?: boolean; 28 | } 29 | ``` 30 | 31 | ## Global Configuration 32 | 33 | Set default jump options for all steps in the tour: 34 | 35 | ```vue 36 | 47 | ``` 48 | 49 | ## Per-Step Configuration 50 | 51 | Override global options for specific steps: 52 | 53 | ```typescript 54 | const steps = [ 55 | { 56 | target: '#step1', 57 | content: 'This step uses global jump options', 58 | }, 59 | { 60 | target: '#step2', 61 | content: 'This step uses custom scroll options', 62 | jumpOptions: { 63 | duration: 300, // Faster scroll 64 | offset: -200, // More space at top 65 | easing: 'easeInCubic', // Different easing 66 | }, 67 | }, 68 | ]; 69 | ``` 70 | 71 | ## Disabling Scroll 72 | 73 | You can disable scrolling globally or per-step using the existing `noScroll` prop: 74 | 75 | ```vue 76 | 77 | 78 | ``` 79 | 80 | ```typescript 81 | // Disable scrolling for specific step 82 | const steps = [ 83 | { 84 | target: '#step1', 85 | content: 'This step will not scroll', 86 | noScroll: true, 87 | }, 88 | ]; 89 | ``` 90 | 91 | ## Priority 92 | 93 | When multiple jump option sources are provided, they are merged with the following priority: 94 | 95 | 1. **Step-specific options** (highest priority) 96 | 2. **Global prop options** 97 | 3. **Default values** (lowest priority) 98 | 99 | Example: 100 | 101 | ```typescript 102 | // Global defaults 103 | const globalJumpOptions = { 104 | duration: 1000, 105 | offset: -100, 106 | a11y: true, 107 | }; 108 | 109 | // Step overrides 110 | const steps = [ 111 | { 112 | target: '#step1', 113 | content: 'Step 1', 114 | jumpOptions: { 115 | duration: 300, // Only override duration 116 | // offset: still -100 from global 117 | // a11y: still true from global 118 | }, 119 | }, 120 | ]; 121 | ``` 122 | 123 | ## Available Easing Functions 124 | 125 | VueJS Tour supports the following built-in easing function names: 126 | 127 | - `'easeInQuad'` - Quadratic acceleration 128 | - `'easeOutQuad'` - Quadratic deceleration 129 | - `'easeInOutQuad'` - Quadratic acceleration and deceleration (default) 130 | - `'easeInCubic'` - Cubic acceleration 131 | - `'easeOutCubic'` - Cubic deceleration 132 | - `'easeInOutCubic'` - Cubic acceleration and deceleration 133 | - `'easeInQuart'` - Quartic acceleration 134 | - `'easeOutQuart'` - Quartic deceleration 135 | - `'easeInOutQuart'` - Quartic acceleration and deceleration 136 | - `'easeInQuint'` - Quintic acceleration 137 | - `'easeOutQuint'` - Quintic deceleration 138 | - `'easeInOutQuint'` - Quintic acceleration and deceleration 139 | 140 | Example usage: 141 | 142 | ```vue 143 | 152 | ``` 153 | 154 | ### Understanding Easing Types 155 | 156 | - **In** (e.g., `easeInQuad`) - Starts slow, ends fast (acceleration) 157 | - **Out** (e.g., `easeOutQuad`) - Starts fast, ends slow (deceleration) 158 | - **InOut** (e.g., `easeInOutQuad`) - Starts slow, speeds up, then slows down (smooth) 159 | - **Quad/Cubic/Quart/Quint** - The strength of the curve (higher = more dramatic) 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VueJS Tour 2 | 3 | [![license](https://img.shields.io/github/license/globalhive/vuejs-tour)](https://github.com/GlobalHive/vuejs-tour/blob/master/LICENSE) 4 | [![docs](https://img.shields.io/github/actions/workflow/status/globalhive/vuejs-tour/docs.yml?branch=master&label=docs)](https://github.com/GlobalHive/vuejs-tour/actions/workflows/docs.yml) 5 | [![build](https://img.shields.io/github/actions/workflow/status/globalhive/vuejs-tour/npm-publish.yml?branch=master)](https://www.npmjs.com/package/@globalhive/vuejs-tour) 6 | [![open issues](https://img.shields.io/github/issues-raw/globalhive/vuejs-tour)](https://github.com/GlobalHive/vuejs-tour/issues) 7 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/f320aadcc4514cc5aaedafd271be2fd7)](https://app.codacy.com/gh/GlobalHive/vuejs-tour/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
8 | [![version](https://img.shields.io/npm/v/@globalhive/vuejs-tour)](https://www.npmjs.com/package/@globalhive/vuejs-tour) 9 | [![downloads](https://img.shields.io/npm/dt/@globalhive/vuejs-tour)](https://www.npmjs.com/package/@globalhive/vuejs-tour) 10 | [![producthunt](https://img.shields.io/badge/view_on-ProductHunt-orange)](https://www.producthunt.com/products/vuejs-tour) 11 | 12 | > VueJS Tour is a lightweight, simple and customizable tour plugin. 13 | > It provides a quick and easy way to guide your users through your application. 14 | 15 | 16 | 17 | ## Table of Contents 18 | 19 | - [Prerequisites](#prerequisites) 20 | - [Installation](#installation) 21 | - [Create a tour](#create-a-tour) 22 | - [Documentation](#documentation) 23 | - [Something Missing?](#something-missing) 24 | 25 | ## Prerequisites 26 | 27 | * [Node.js](https://nodejs.org/) 28 | * [Vue 3 (Composition API)](https://vuejs.org/guide/introduction.html#composition-api) 29 | 30 | ## Installation 31 | 32 | > [!TIP] 33 | > Looking for a nuxt version? 34 | > [Nuxt version (Special thanks to BayBreezy)](https://github.com/BayBreezy/nuxt-tour) 35 | 36 | ## Create a tour 37 | 38 | Add the VueJS Tour component anywhere in your app. It is recommended to add it to `App.vue` 39 | and create the required steps using ` 69 | ``` 70 | 71 | 72 | ## Start the tour 73 | 74 | To start the tour, you can use the `autoStart` prop... 75 | 76 | ```vue 77 | 86 | 87 | 90 | ``` 91 | 92 | ...or call the `startTour()` method on the component instance. 93 | 94 | ```vue 95 | 104 | 105 | 115 | ``` 116 | 117 | The `target` property of the step object can be any valid [CSS selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors). 118 | 119 | ## Documentation 120 | 121 | For more information about the available props and methods, check out the [documentation](https://globalhive.github.io/vuejs-tour/). 122 | 123 | ## Something Missing? 124 | 125 | If you have a feature request or found a bug, [let us know](https://github.com/GlobalHive/vuejs-tour/issues) by submitting an issue. 126 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress'; 2 | 3 | export default defineConfig({ 4 | title: 'VueJS Tour', 5 | description: 'Guide your users through your application quickly and easily.', 6 | lang: 'en-US', 7 | base: '/vuejs-tour/', 8 | vite: { 9 | css: { 10 | preprocessorOptions: { 11 | scss: { 12 | api: 'modern-compiler', 13 | }, 14 | }, 15 | }, 16 | }, 17 | themeConfig: { 18 | logo: 'https://vuejs.org/images/logo.png', 19 | nav: [ 20 | { text: 'Guide', link: '/guide/what-is-vuejs-tour' }, 21 | { 22 | text: '2.5.0', 23 | items: [ 24 | { 25 | text: 'Changelog', 26 | link: 'https://github.com/GlobalHive/vuejs-tour/blob/master/CHANGELOG.md', 27 | }, 28 | { text: 'Roadmap', link: '/guide/roadmap' }, 29 | { 30 | text: 'Issues', 31 | link: 'https://github.com/GlobalHive/vuejs-tour/issues', 32 | }, 33 | ], 34 | }, 35 | ], 36 | sidebar: { 37 | '/guide/': [ 38 | { 39 | text: 'Introduction', 40 | collapsed: false, 41 | items: [ 42 | { text: 'What is VueJS Tour?', link: '/guide/what-is-vuejs-tour' }, 43 | { text: 'Getting Started', link: '/guide/getting-started' }, 44 | { text: 'Create a Tour', link: '/guide/create-a-tour' }, 45 | ], 46 | }, 47 | { 48 | text: 'Customization', 49 | collapsed: false, 50 | items: [ 51 | { 52 | text: 'VTour Component', 53 | items: [ 54 | { text: 'Start Options', link: '/guide/start-options' }, 55 | { text: 'Highlight Target', link: '/guide/highlight-target' }, 56 | { text: 'Using a Backdrop', link: '/guide/using-a-backdrop' }, 57 | { text: 'Hiding the Arrow', link: '/guide/hiding-the-arrow' }, 58 | { text: 'Tour Margin', link: '/guide/tour-margin' }, 59 | { text: 'Saving Progress', link: '/guide/saving-progress' }, 60 | { text: 'Scroll to Element', link: '/guide/scroll-to-element' }, 61 | ], 62 | }, 63 | { 64 | text: 'Step Options', 65 | items: [ 66 | { text: 'The Step Type', link: '/guide/the-step-type' }, 67 | { text: 'Setting a Target', link: '/guide/setting-a-target' }, 68 | { 69 | text: 'Define the Content', 70 | link: '/guide/define-the-content', 71 | }, 72 | { text: 'Using Placement', link: '/guide/using-placement' }, 73 | { 74 | text: 'The onBefore Event', 75 | link: '/guide/the-onbefore-event', 76 | }, 77 | { text: 'The onAfter Event', link: '/guide/the-onafter-event' }, 78 | { 79 | text: 'Highlight Target', 80 | link: '/guide/step-highlight-target', 81 | }, 82 | { 83 | text: 'Using a Backdrop', 84 | link: '/guide/step-using-a-backdrop', 85 | }, 86 | { 87 | text: 'Scroll to Element', 88 | link: '/guide/step-scroll-to-element', 89 | }, 90 | ], 91 | }, 92 | ], 93 | }, 94 | { 95 | text: 'Advanced', 96 | collapsed: false, 97 | items: [ 98 | { text: 'Skipping a Tour', link: '/guide/skipping-a-tour' }, 99 | { text: 'Button Labels', link: '/guide/button-labels' }, 100 | { text: 'Multiple Tours', link: '/guide/multiple-tours' }, 101 | { text: 'Jump Options', link: '/guide/jump-options' }, 102 | { text: 'Accessibility', link: '/guide/accessibility' }, 103 | { 104 | text: 'Styling', 105 | items: [ 106 | { text: 'CSS Theme', link: '/guide/css-theme' }, 107 | { text: 'Component Slots', link: '/guide/component-slots' }, 108 | ], 109 | }, 110 | ], 111 | }, 112 | { 113 | text: "What's next?", 114 | collapsed: false, 115 | items: [ 116 | { text: 'Roadmap', link: '/guide/roadmap' }, 117 | { 118 | text: 'Changelog', 119 | link: 'https://github.com/GlobalHive/vuejs-tour/blob/master/CHANGELOG.md', 120 | }, 121 | ], 122 | }, 123 | ], 124 | '/reference/': [ 125 | { 126 | text: 'Reference', 127 | collapsed: false, 128 | items: [{ text: 'Coming Soon', link: '/reference/coming-soon' }], 129 | }, 130 | ], 131 | }, 132 | socialLinks: [ 133 | { icon: 'github', link: 'https://github.com/GlobalHive/vuejs-tour' }, 134 | ], 135 | footer: { 136 | message: 'Released under the MIT License.', 137 | copyright: 138 | 'Made with ❤️ by Global Hive', 139 | }, 140 | editLink: { 141 | pattern: 142 | 'https://github.com/GlobalHive/vuejs-tour/tree/master/docs/:path', 143 | text: 'Edit this page on GitHub', 144 | }, 145 | }, 146 | }); 147 | -------------------------------------------------------------------------------- /docs/guide/multiple-tours.md: -------------------------------------------------------------------------------- 1 | # Multiple Tours 2 | 3 | To create multiple tours, use the `steps` and `name` props to switch between different tours. 4 | 5 | ### Defining the tours 6 | 7 | First define the steps for each tour. 8 | 9 | Option 1 — recommended when the component has the “prop‑change watcher” enabled (auto‑restart on `name`/`steps` change). No manual start needed except the initial start. 10 | 11 | ```vue{3-8,12} 12 | 24 | 25 | 29 | ``` 30 | 31 | In this case we’re creating two tours, `tour1` and `tour2`, each with their corresponding steps `tourSteps1` and `tourSteps2`. The `tourSteps` and `tourName` refs are reactive and can be changed at runtime. 32 | 33 | ### Switching between tours 34 | 35 | If the watcher is enabled, simply switch the reactive values; the tour will auto‑restart. 36 | 37 | ```vue{14-22} 38 | 59 | 60 | 64 | ``` 65 | 66 | If you do NOT use the watcher, you can still support hot‑switching by calling `startTour()` after changing props: 67 | 68 | ```vue{16} 69 | function switchTour() { 70 | if (tourName.value === 'tour1') { 71 | tourSteps.value = tourSteps2; 72 | tourName.value = 'tour2'; 73 | } else { 74 | tourSteps.value = tourSteps1; 75 | tourName.value = 'tour1'; 76 | } 77 | vTour.value?.startTour(); // manual restart when watcher is disabled 78 | } 79 | ``` 80 | 81 | Notes 82 | 83 | - Prefer replacing the steps array (immutable update) over mutating it in place so the change is detected reliably. 84 | - For multiple, fully independent tours on the same page, give each `VTour` a unique `name`. IDs, highlight classes, and localStorage keys are scoped by `name`, so tours won’t collide. 85 | 86 | # Saving Progress 87 | 88 | You can save a user’s progress in the browser’s localStorage so they can resume later. 89 | 90 | ## Using the `saveToLocalStorage` prop 91 | 92 | Set the `saveToLocalStorage` prop on `VTour` to control if/when progress is saved. Accepted values: `never`, `step`, `end`. The default is `never`. 93 | 94 | ```vue 95 | 99 | 100 | 110 | ``` 111 | 112 | Notes 113 | 114 | - Keys are scoped by the tour’s `name`. The storage key is `vjt-${name}`. If `name` is empty, the key is `vjt-tour`. 115 | - With multiple tours, give each tour a unique `name` so their progress is isolated. 116 | 117 | ### `never` 118 | 119 | No progress is saved. Each start begins from the first step (unless you manually control steps). 120 | 121 | ### `step` 122 | 123 | Saves the current step index after each step. If the user leaves mid‑tour, the next start resumes at the saved step for that tour’s `name`. 124 | 125 | - Key format: `localStorage.setItem('vjt-', '')` 126 | - Works per tour. Switching `name` switches the storage key, so tours don’t affect each other. 127 | 128 | ### `end` 129 | 130 | Saves only when the tour completes. If the user exits before completion, the next start begins at the first step. 131 | 132 | - Completion flag: `localStorage.setItem('vjt-', 'true')` 133 | - When this flag is present, subsequent `startTour()` calls will no‑op unless you reset. 134 | 135 | ### Resetting or clearing progress 136 | 137 | - Programmatic reset (recommended): call `resetTour()` to clear state and, if desired, restart. 138 | - Manual clear: `localStorage.removeItem('vjt-')` 139 | 140 | ### Multiple tours and hot‑switching 141 | 142 | - Multiple independent tours: Use distinct `name` values to keep DOM ids, highlight classes, and storage keys separate. 143 | - Swapping tours at runtime (changing `name` and/or `steps`): 144 | - Each tour resumes from its own saved state (for `step`) or completion flag (for `end`), based on the new `name`. 145 | - If you keep the watcher that restarts on prop changes, switching `name`/`steps` will auto‑restart the visible tour using the correct per‑name key. 146 | 147 | Small copy edit: “everytime” → “every time”. 148 | -------------------------------------------------------------------------------- /test/components/VTour.jumpOptions.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, vi } from 'vitest'; 2 | import type { ITourStep } from '../../src/Types'; 3 | import jump from 'jump.js'; 4 | import { useFakeTimersPerTest, startAndWaitReady } from '../helpers/timers'; 5 | import { mountVTour } from '../helpers/mountVTour'; 6 | 7 | // Mock jump.js 8 | vi.mock('jump.js', () => ({ 9 | default: vi.fn((target, options) => { 10 | // Immediately call the callback 11 | if (options?.callback) { 12 | options.callback(); 13 | } 14 | }), 15 | })); 16 | 17 | describe('VTour Component - Jump Options', () => { 18 | useFakeTimersPerTest(); 19 | 20 | const steps: ITourStep[] = [ 21 | { 22 | target: '#step1', 23 | content: 'Step 1', 24 | }, 25 | { 26 | target: '#step2', 27 | content: 'Step 2', 28 | }, 29 | ]; 30 | 31 | beforeEach(() => { 32 | vi.clearAllMocks(); 33 | document.body.innerHTML = ` 34 |
Target 1
35 |
Target 2
36 | `; 37 | }); 38 | 39 | it('should use default jump options when none provided', async () => { 40 | const wrapper = mountVTour({ steps, noScroll: false }); 41 | 42 | await startAndWaitReady(wrapper); 43 | 44 | expect(jump).toHaveBeenCalled(); 45 | const callArgs = (jump as any).mock.calls[0][1]; 46 | 47 | // Should use default values (a11y follows enableA11y prop which defaults to true via mountVTour) 48 | expect(callArgs.duration).toBe(500); 49 | expect(callArgs.offset).toBe(-100); 50 | expect(typeof callArgs.easing).toBe('function'); // Should be easeInOutQuad function 51 | expect(callArgs.a11y).toBe(true); // mountVTour defaults enableA11y to true 52 | 53 | wrapper.unmount(); 54 | }); 55 | 56 | it('should use global jump options from props', async () => { 57 | const wrapper = mountVTour({ 58 | steps, 59 | noScroll: false, 60 | jumpOptions: { 61 | duration: 1000, 62 | offset: -200, 63 | a11y: true, 64 | }, 65 | }); 66 | 67 | await startAndWaitReady(wrapper); 68 | 69 | expect(jump).toHaveBeenCalled(); 70 | const callArgs = (jump as any).mock.calls[0][1]; 71 | 72 | // Should use global custom values 73 | expect(callArgs.duration).toBe(1000); 74 | expect(callArgs.offset).toBe(-200); 75 | expect(callArgs.a11y).toBe(true); 76 | expect(typeof callArgs.easing).toBe('function'); // Default easeInOutQuad 77 | 78 | wrapper.unmount(); 79 | }); 80 | 81 | it('should use step-specific jump options that override global options', async () => { 82 | const stepsWithOptions: ITourStep[] = [ 83 | { 84 | target: '#step1', 85 | content: 'Step 1', 86 | jumpOptions: { 87 | duration: 300, 88 | offset: -50, 89 | }, 90 | }, 91 | ]; 92 | 93 | const wrapper = mountVTour({ 94 | steps: stepsWithOptions, 95 | noScroll: false, 96 | jumpOptions: { 97 | duration: 1000, 98 | offset: -200, 99 | a11y: true, 100 | }, 101 | }); 102 | 103 | await startAndWaitReady(wrapper); 104 | 105 | expect(jump).toHaveBeenCalled(); 106 | const callArgs = (jump as any).mock.calls[0][1]; 107 | 108 | // Step options should override global options 109 | expect(callArgs.duration).toBe(300); 110 | expect(callArgs.offset).toBe(-50); 111 | expect(callArgs.a11y).toBe(true); // From global 112 | expect(typeof callArgs.easing).toBe('function'); // Default easeInOutQuad 113 | 114 | wrapper.unmount(); 115 | }); 116 | 117 | it('should not scroll when noScroll is enabled', async () => { 118 | const wrapper = mountVTour({ 119 | steps, 120 | noScroll: true, 121 | jumpOptions: { 122 | duration: 1000, 123 | }, 124 | }); 125 | 126 | await startAndWaitReady(wrapper); 127 | 128 | // jump.js should not be called when noScroll is true 129 | expect(jump).not.toHaveBeenCalled(); 130 | 131 | wrapper.unmount(); 132 | }); 133 | 134 | it('should support different easing names', async () => { 135 | const wrapper = mountVTour({ 136 | steps, 137 | noScroll: false, 138 | jumpOptions: { 139 | easing: 'easeInCubic', 140 | }, 141 | }); 142 | 143 | await startAndWaitReady(wrapper); 144 | 145 | expect(jump).toHaveBeenCalled(); 146 | const callArgs = (jump as any).mock.calls[0][1]; 147 | 148 | // Should map string to easing function 149 | expect(typeof callArgs.easing).toBe('function'); 150 | 151 | wrapper.unmount(); 152 | }); 153 | 154 | it('should respect enableA11y prop for jump.js a11y option', async () => { 155 | // Test with enableA11y: false 156 | const wrapper1 = mountVTour({ steps, enableA11y: false, noScroll: false }); 157 | 158 | await startAndWaitReady(wrapper1); 159 | 160 | expect(jump).toHaveBeenCalled(); 161 | const callArgs1 = (jump as any).mock.calls[0][1]; 162 | expect(callArgs1.a11y).toBe(false); 163 | 164 | wrapper1.unmount(); 165 | vi.clearAllMocks(); 166 | 167 | // Test with enableA11y: true (explicit) 168 | const wrapper2 = mountVTour({ steps, enableA11y: true, noScroll: false }); 169 | 170 | await startAndWaitReady(wrapper2); 171 | 172 | expect(jump).toHaveBeenCalled(); 173 | const callArgs2 = (jump as any).mock.calls[0][1]; 174 | expect(callArgs2.a11y).toBe(true); 175 | 176 | wrapper2.unmount(); 177 | }); 178 | 179 | it('should not override default easing with undefined values', async () => { 180 | const wrapper = mountVTour({ 181 | steps, 182 | noScroll: false, 183 | jumpOptions: { 184 | duration: 1000, 185 | // easing is undefined, should use default 'easeInOutQuad' 186 | } as any, 187 | }); 188 | 189 | await startAndWaitReady(wrapper); 190 | 191 | expect(jump).toHaveBeenCalled(); 192 | const callArgs = (jump as any).mock.calls[0][1]; 193 | 194 | // Should still have the default easing function, not undefined 195 | expect(typeof callArgs.easing).toBe('function'); 196 | expect(callArgs.duration).toBe(1000); // Custom duration should be applied 197 | 198 | wrapper.unmount(); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /src/Types.ts: -------------------------------------------------------------------------------- 1 | import type { NanoPopPosition } from 'nanopop'; 2 | 3 | /** 4 | * Scroll animation options for jump.js 5 | * @see https://github.com/callmecavs/jump.js 6 | */ 7 | export interface JumpOptions { 8 | /** Duration of scroll animation in milliseconds (default: 500) */ 9 | readonly duration?: number; 10 | 11 | /** Vertical offset in pixels from target element (default: -100) */ 12 | readonly offset?: number; 13 | 14 | /** Callback function to execute after scroll completes */ 15 | readonly callback?: () => void; 16 | 17 | /** 18 | * Easing function name (default: 'easeInOutQuad') 19 | * Valid values: 'easeInQuad', 'easeOutQuad', 'easeInOutQuad', 'easeInCubic', 20 | * 'easeOutCubic', 'easeInOutCubic', 'easeInQuart', 'easeOutQuart', 'easeInOutQuart', 21 | * 'easeInQuint', 'easeOutQuint', 'easeInOutQuint' 22 | */ 23 | readonly easing?: string; 24 | 25 | /** Whether to focus the element for accessibility (default: false) */ 26 | readonly a11y?: boolean; 27 | } 28 | 29 | /** 30 | * Configuration for a single step in a tour 31 | */ 32 | export interface ITourStep { 33 | /** CSS selector for the target element */ 34 | readonly target: string; 35 | 36 | /** HTML content to display in the tooltip */ 37 | readonly content: string; 38 | 39 | /** Position of the tooltip relative to target (defaults to 'right') */ 40 | readonly placement?: NanoPopPosition; 41 | 42 | /** Callback executed before showing this step */ 43 | readonly onBefore?: () => Promise | void; 44 | 45 | /** Callback executed after showing this step */ 46 | readonly onAfter?: () => Promise | void; 47 | 48 | /** Whether to highlight the target element for this step */ 49 | readonly highlight?: boolean; 50 | 51 | /** Whether to show backdrop for this step */ 52 | readonly backdrop?: boolean; 53 | 54 | /** Whether to disable auto-scrolling for this step */ 55 | readonly noScroll?: boolean; 56 | 57 | /** Custom scroll animation options for this step (overrides global jumpOptions) */ 58 | readonly jumpOptions?: Partial; 59 | 60 | /** Descriptive label for screen readers (e.g., "User Profile Settings") */ 61 | readonly ariaLabel?: string; 62 | } 63 | 64 | /** 65 | * LocalStorage save strategy options 66 | */ 67 | export type SaveToLocalStorage = 'never' | 'step' | 'end'; 68 | 69 | /** 70 | * Button label configuration 71 | */ 72 | export interface ButtonLabels { 73 | readonly next: string; 74 | readonly back: string; 75 | readonly done: string; 76 | readonly skip: string; 77 | } 78 | 79 | /** 80 | * Main VTour component props 81 | */ 82 | export interface VTourProps { 83 | /** Unique name for the tour (used for localStorage keys) */ 84 | readonly name?: string; 85 | 86 | /** Array of tour steps */ 87 | readonly steps: readonly ITourStep[]; 88 | 89 | /** Whether to show backdrop by default */ 90 | readonly backdrop?: boolean; 91 | 92 | /** Whether to automatically start the tour when component is mounted */ 93 | readonly autoStart?: boolean; 94 | 95 | /** Delay in milliseconds before starting the tour */ 96 | readonly startDelay?: number; 97 | 98 | /** Whether to highlight target elements by default */ 99 | readonly highlight?: boolean; 100 | 101 | /** Margin in pixels around the tooltip */ 102 | readonly margin?: number; 103 | 104 | /** Custom button labels */ 105 | readonly buttonLabels?: Partial; 106 | 107 | /** When to save tour progress to localStorage */ 108 | readonly saveToLocalStorage?: SaveToLocalStorage; 109 | 110 | /** Whether to hide the skip button */ 111 | readonly hideSkip?: boolean; 112 | 113 | /** Whether to hide the arrow pointing to target */ 114 | readonly hideArrow?: boolean; 115 | 116 | /** Whether to disable auto-scrolling by default */ 117 | readonly noScroll?: boolean; 118 | 119 | /** Debounce timeout for resize events in milliseconds */ 120 | readonly resizeTimeout?: number; 121 | 122 | /** Default tooltip placement when step doesn't specify one */ 123 | readonly defaultPlacement?: NanoPopPosition; 124 | 125 | /** Default scroll animation options (can be overridden per step) */ 126 | readonly jumpOptions?: Partial; 127 | 128 | /** Enable accessibility features including keyboard navigation and ARIA attributes (default: true) */ 129 | readonly enableA11y?: boolean; 130 | 131 | /** Enable keyboard navigation with Arrow keys, Enter, and Escape (default: true) */ 132 | readonly keyboardNav?: boolean; 133 | 134 | /** Custom ARIA label for the tour dialog (default: "Guided tour") */ 135 | readonly ariaLabel?: string; 136 | 137 | /** Delay in milliseconds to wait for Vue Teleport to render DOM elements (default: 100) */ 138 | readonly teleportDelay?: number; 139 | } 140 | 141 | /** 142 | * Tour events emitted by the component 143 | */ 144 | export interface VTourEvents { 145 | /** Emitted when the tour starts */ 146 | onTourStart: []; 147 | 148 | /** Emitted when the tour ends */ 149 | onTourEnd: []; 150 | 151 | /** Emitted when a step is shown (with step index) */ 152 | onTourStep: [step: number]; 153 | } 154 | 155 | /** 156 | * Tour events for defineEmits (Vue emit constraint format) 157 | */ 158 | export interface VTourEmits { 159 | onTourStart: []; 160 | onTourEnd: []; 161 | onTourStep: [step: number]; 162 | } 163 | 164 | /** 165 | * Internal tour state data structure 166 | */ 167 | export interface VTourData { 168 | currentStep: number; 169 | lastStep: number; 170 | nextStep: number; 171 | getCurrentStep: ITourStep; 172 | getLastStep: ITourStep; 173 | getNextStep: ITourStep; 174 | } 175 | 176 | /** 177 | * Public API methods exposed by the VTour component 178 | */ 179 | export interface VTourExposedMethods { 180 | /** Start the tour */ 181 | startTour: () => Promise; 182 | 183 | /** Move to the next step */ 184 | nextStep: () => Promise; 185 | 186 | /** Move to the previous step */ 187 | lastStep: () => Promise; 188 | 189 | /** End the tour */ 190 | endTour: () => void; 191 | 192 | /** Stop the tour without saving completion */ 193 | stopTour: () => void; 194 | 195 | /** Navigate to a specific step by index */ 196 | goToStep: (stepIndex: number) => Promise; 197 | 198 | /** Reset tour state and optionally restart */ 199 | resetTour: (shouldRestart?: boolean) => void; 200 | 201 | /** Update tooltip position */ 202 | updatePosition: () => Promise; 203 | 204 | /** Update element highlights */ 205 | updateHighlight: () => void; 206 | 207 | /** Update backdrop visibility */ 208 | updateBackdrop: () => void; 209 | } 210 | -------------------------------------------------------------------------------- /docs/guide/css-theme.md: -------------------------------------------------------------------------------- 1 | # CSS Theme 2 | To customize the look and feel of the tour, you can create your own styles and replace the default theme. 3 | 4 | ## Default Theme 5 | 6 | The default theme is defined in the `'@globalhive/vuejs-tour/dist/style.css'` file. 7 | 8 | ```css 9 | [data-hidden] { 10 | display: none; 11 | } 12 | 13 | #vjt-backdrop { 14 | position: fixed; 15 | top: 0; 16 | left: 0; 17 | width: 100%; 18 | height: 100%; 19 | background-color: rgba(0, 0, 0, 0.5); 20 | z-index: 9998; 21 | } 22 | 23 | #vjt-tooltip { 24 | background-color: #333; 25 | color: #fff; 26 | padding: 0.5rem; 27 | border-radius: 4px; 28 | font-size: 13px; 29 | z-index: 9999; 30 | max-width: 300px; 31 | position: absolute; 32 | } 33 | 34 | #vjt-tooltip[data-arrow^=t] #vjt-arrow { 35 | bottom: -4px; 36 | right: 50%; 37 | } 38 | 39 | #vjt-tooltip[data-arrow^=b] #vjt-arrow { 40 | top: -4px; 41 | right: 50%; 42 | } 43 | 44 | #vjt-tooltip[data-arrow^=l] #vjt-arrow { 45 | right: -4px; 46 | top: 50%; 47 | } 48 | 49 | #vjt-tooltip[data-arrow^=r] #vjt-arrow { 50 | left: -4px; 51 | top: 50%; 52 | } 53 | 54 | #vjt-arrow { 55 | width: 8px; 56 | height: 8px; 57 | position: absolute; 58 | z-index: -1; 59 | } 60 | #vjt-arrow::before { 61 | content: ""; 62 | width: 8px; 63 | height: 8px; 64 | background-color: #333; 65 | transform: rotate(45deg); 66 | position: absolute; 67 | } 68 | 69 | .vjt-highlight { 70 | outline: 2px solid #0ea5e9; 71 | outline-offset: 4px; 72 | border-radius: 1px; 73 | position: relative; 74 | z-index: 9999; 75 | } 76 | 77 | .vjt-actions { 78 | display: flex; 79 | justify-content: space-between; 80 | align-items: center; 81 | margin-top: 0.5rem; 82 | gap: 0.5rem; 83 | } 84 | .vjt-actions button { 85 | width: 100%; 86 | padding: 0.25rem 1rem; 87 | border: 1px solid #fff; 88 | border-radius: 4px; 89 | background-color: transparent; 90 | color: #fff; 91 | font-size: 13px; 92 | font-weight: 500; 93 | transition: all 0.2s ease-in-out; 94 | cursor: pointer; 95 | } 96 | .vjt-actions button:hover { 97 | background-color: #000; 98 | color: #fff; 99 | } 100 | ``` 101 | 102 | ## Light Theme 103 | 104 | Here is an example of a light theme for the [New CSS File](#new-css-file) approach: 105 | 106 | ```css 107 | [data-hidden] { 108 | display: none; 109 | } 110 | 111 | #vjt-backdrop { 112 | position: fixed; 113 | top: 0; 114 | left: 0; 115 | width: 100%; 116 | height: 100%; 117 | background-color: rgba(0, 0, 0, 0.5); /* [!code ++] */ 118 | z-index: 9998; 119 | } 120 | 121 | #vjt-tooltip { 122 | background-color: rgb(241 245 249); /* [!code ++:2] */ 123 | color: rgb(15 23 42); 124 | padding: 0.5rem; 125 | border-radius: 4px; 126 | font-size: 13px; 127 | z-index: 9999; 128 | max-width: 300px; 129 | position: absolute; 130 | } 131 | 132 | #vjt-tooltip[data-arrow^=t] #vjt-arrow { 133 | bottom: -4px; 134 | right: 50%; 135 | } 136 | 137 | #vjt-tooltip[data-arrow^=b] #vjt-arrow { 138 | top: -4px; 139 | right: 50%; 140 | } 141 | 142 | #vjt-tooltip[data-arrow^=l] #vjt-arrow { 143 | right: -4px; 144 | top: 50%; 145 | } 146 | 147 | #vjt-tooltip[data-arrow^=r] #vjt-arrow { 148 | left: -4px; 149 | top: 50%; 150 | } 151 | 152 | #vjt-arrow { 153 | width: 8px; 154 | height: 8px; 155 | position: absolute; 156 | z-index: -1; 157 | } 158 | #vjt-arrow::before { 159 | content: ""; 160 | width: 8px; 161 | height: 8px; 162 | background-color: rgb(241 245 249); /* [!code ++] */ 163 | transform: rotate(45deg); 164 | position: absolute; 165 | } 166 | 167 | .vjt-highlight { 168 | outline: 2px solid #0ea5e9; 169 | outline-offset: 4px; 170 | border-radius: 1px; 171 | position: relative; 172 | z-index: 9999; 173 | } 174 | 175 | .vjt-actions { 176 | display: flex; 177 | justify-content: space-between; 178 | align-items: center; 179 | margin-top: 0.5rem; 180 | gap: 0.5rem; 181 | } 182 | .vjt-actions button { 183 | width: 100%; 184 | padding: 0.25rem 1rem; 185 | border: 1px solid rgb(15 23 42); /* [!code ++] */ 186 | border-radius: 4px; 187 | background-color: transparent; 188 | color: rgb(15 23 42); /* [!code ++] */ 189 | font-size: 13px; 190 | font-weight: 500; 191 | transition: all 0.2s ease-in-out; 192 | cursor: pointer; 193 | } 194 | .vjt-actions button:hover { 195 | background-color: rgb(15 23 42); /* [!code ++:2] */ 196 | color: rgb(241 245 249); 197 | } 198 | ``` 199 | 200 | ## Style Block 201 | 202 | Change the theme by adding a style block to your component. 203 | 204 | ```vue 205 | 209 | 210 | 213 | 214 | 231 | ``` 232 | 233 | ::: warning 234 | If using the style block, make sure to not use the `scoped` attribute. 235 | ::: 236 | 237 | ## New CSS File 238 | 239 | Second option is to create a new CSS file and import it in your component. 240 | 241 | ```vue 242 | 247 | ``` 248 | 249 | ::: warning 250 | If using the new CSS file approach, you have to include all the default styles in your custom theme. 251 | It's recommended to copy the default theme and modify it as needed. 252 | ::: 253 | 254 | ## Overriding Variables 255 | 256 | To override the default theme, you can simply override the SCSS variables in your component. 257 | 258 | * Create a new SCSS file `white.scss`. 259 | * Override the variables. 260 | * Import default theme. 261 | * Import your file in the component. 262 | 263 | ::: code-group 264 | 265 | ```scss [white.scss] 266 | $vjt__tooltip_color: rgb(15 23 42); 267 | $vjt__tooltip_background: rgb(241 245 249); 268 | $vjt__action_button_color: rgb(15 23 42); 269 | $vjt__action_button_color_hover: rgb(241 245 249); 270 | $vjt__action_button_background_hover: rgb(15 23 42); 271 | $vjt__action_button_border: 1px solid rgb(15 23 42); 272 | 273 | @import '@globalhive/vuejs-tour/src/style.scss'; 274 | ``` 275 | 276 | ```vue [Component] 277 | 281 | 282 | 285 | ``` 286 | 287 | ::: -------------------------------------------------------------------------------- /docs/guide/accessibility.md: -------------------------------------------------------------------------------- 1 | # Accessibility Guide for VueJS Tour 2 | 3 | ## Current State 4 | 5 | VueJS Tour now includes comprehensive WCAG 2.1 AA accessibility features, including keyboard navigation, ARIA attributes, focus management, and screen reader support. 6 | 7 | **⚠️ Note:** Accessibility features are **disabled by default** (as of v2.5.0) pending further testing and validation. Enable them by setting `enableA11y: true` on the component. 8 | 9 | ## Implemented Accessibility Features 10 | 11 | ### 1. **ARIA Attributes** ✅ 12 | 13 | Implemented: 14 | 15 | - ✅ `aria-label` on tooltip (configurable via `ariaLabel` prop or per-step `ariaLabel`) 16 | - ✅ `aria-describedby` for step content 17 | - ✅ `aria-live="polite"` region for step announcements 18 | - ✅ `aria-modal="true"` when `enableA11y` is enabled 19 | - ✅ `role="dialog"` for proper semantic structure 20 | - ✅ Enhanced `aria-label` attributes on all buttons 21 | 22 | ### 2. **Keyboard Navigation** ✅ 23 | 24 | Implemented: 25 | 26 | - ✅ `Escape` key to close tour 27 | - ✅ `ArrowRight` or `Enter` to go to next step 28 | - ✅ `ArrowLeft` to go to previous step 29 | - ✅ Keyboard navigation can be disabled via `keyboardNav: false` 30 | - ⚠️ Focus trap not yet implemented (planned for future release) 31 | 32 | ### 3. **Focus Management** ✅ 33 | 34 | Implemented: 35 | 36 | - ✅ Tooltip receives focus when opened (when `enableA11y` is true) 37 | - ✅ Previous focus restored when tour ends 38 | - ✅ `tabindex="0"` on tooltip for keyboard accessibility 39 | 40 | ### 4. **Screen Reader Support** ✅ 41 | 42 | Implemented: 43 | 44 | - ✅ Step progress announced ("Step 2 of 5") 45 | - ✅ Descriptive button labels with step context 46 | - ✅ Content changes announced via aria-live region 47 | - ✅ Screen reader only content via `.vjt-sr-only` CSS class 48 | 49 | ### 5. **Semantic Structure** ✅ 50 | 51 | Implemented: 52 | 53 | - ✅ `role="dialog"` on tooltip (changed from `role="tooltip"` for better modal semantics) 54 | - ✅ Step counter announced to screen readers 55 | - ✅ Proper ARIA attributes on all interactive elements 56 | 57 | ## Usage 58 | 59 | ### Enabling Accessibility Features 60 | 61 | Accessibility features are disabled by default. To enable them: 62 | 63 | ```vue 64 | 70 | ``` 71 | 72 | ### Available Props 73 | 74 | ```typescript 75 | interface VTourProps { 76 | // ... other props 77 | 78 | /** Enable accessibility features (default: false) */ 79 | readonly enableA11y?: boolean; 80 | 81 | /** Enable keyboard navigation (default: true, only active when enableA11y is true) */ 82 | readonly keyboardNav?: boolean; 83 | 84 | /** Custom aria-label for the tour (default: "Guided tour") */ 85 | readonly ariaLabel?: string; 86 | } 87 | 88 | interface ITourStep { 89 | // ... other props 90 | 91 | /** Descriptive label for screen readers */ 92 | readonly ariaLabel?: string; 93 | } 94 | ``` 95 | 96 | ### Example: Per-Step Accessibility Labels 97 | 98 | ```vue 99 | 113 | 114 | 117 | ``` 118 | 119 | ## Implementation Details 120 | 121 | ### Features Already Implemented ✅ 122 | 123 | 1. **ARIA Live Region** - Announces step changes to screen readers 124 | 125 | ```vue 126 |
133 | Step {{ currentStepIndex + 1 }} of {{ props.steps.length }} 134 |
135 | ``` 136 | 137 | 2. **Enhanced Tooltip Semantics** - Proper dialog role and ARIA attributes 138 | 139 | ```vue 140 |
148 | ``` 149 | 150 | 3. **Keyboard Navigation** - Arrow keys, Enter, and Escape support 151 | 152 | ```typescript 153 | const onKeydown = (event: KeyboardEvent): void => { 154 | if (!tourVisible.value || !props.enableA11y || !props.keyboardNav) return; 155 | 156 | switch (event.key) { 157 | case 'Escape': 158 | endTour(); 159 | event.preventDefault(); 160 | break; 161 | case 'ArrowRight': 162 | case 'Enter': 163 | nextStep(); 164 | event.preventDefault(); 165 | break; 166 | case 'ArrowLeft': 167 | if (currentStepIndex.value > 0) { 168 | lastStep(); 169 | event.preventDefault(); 170 | } 171 | break; 172 | } 173 | }; 174 | ``` 175 | 176 | 4. **Focus Management** - Stores and restores focus 177 | 178 | ```typescript 179 | let previousFocus: HTMLElement | null = null; 180 | 181 | const startTour = async () => { 182 | if (props.enableA11y && typeof document !== 'undefined') { 183 | previousFocus = document.activeElement as HTMLElement; 184 | } 185 | // ... tour starts 186 | if (props.enableA11y) { 187 | await nextTick(); 188 | _Tooltip.value?.focus(); 189 | } 190 | }; 191 | 192 | const stopTour = () => { 193 | if (props.enableA11y && previousFocus) { 194 | previousFocus.focus(); 195 | previousFocus = null; 196 | } 197 | }; 198 | ``` 199 | 200 | 5. **Enhanced Button Labels** - Descriptive aria-labels for all actions 201 | 202 | ```vue 203 | 216 | ``` 217 | 218 | ### Future Enhancements ⚠️ 219 | 220 | These features are planned for future releases: 221 | 222 | 1. **Focus Trap** - Trap focus within modal when backdrop is active 223 | 2. **Customizable Keyboard Shortcuts** - Allow users to configure key bindings 224 | 3. **Visual Progress Indicators** - Show step progress visually 225 | 4. **Skip to Content** - Quick navigation option 226 | 227 | ## Backward Compatibility 228 | 229 | All accessibility features: 230 | 231 | - ⚠️ Default to **disabled** (`enableA11y: false`) as of v2.4.3 pending further testing 232 | - Are opt-in via `enableA11y: true` prop 233 | - Do not break existing implementations when disabled 234 | - Add ~3KB to bundle size (minimal impact) 235 | 236 | ## Testing Requirements 237 | 238 | 1. **Keyboard-only navigation** 239 | - Can navigate all steps without mouse 240 | - Can dismiss tour with Escape 241 | - Focus visible at all times 242 | 243 | 2. **Screen reader testing** 244 | - NVDA (Windows) 245 | - JAWS (Windows) 246 | - VoiceOver (macOS/iOS) 247 | - TalkBack (Android) 248 | 249 | 3. **Automated testing** 250 | - axe-core integration 251 | - ARIA validity checks 252 | - Keyboard interaction tests 253 | 254 | ## Resources 255 | 256 | - [WAI-ARIA Authoring Practices Guide - Dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) 257 | - [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/) 258 | - [focus-trap library](https://github.com/focus-trap/focus-trap) 259 | - [Vue A11y Best Practices](https://vue-a11y.com/) 260 | 261 | ## Testing Status 262 | 263 | **Implemented (v2.4.3):** ✅ 264 | 265 | - ✅ Keyboard navigation tests (28 test cases) 266 | - ✅ ARIA attributes validation 267 | - ✅ Focus management tests 268 | - ✅ Screen reader announcement tests 269 | 270 | **Pending:** 271 | 272 | 1. **Manual testing with screen readers** 273 | - NVDA (Windows) 274 | - JAWS (Windows) 275 | - VoiceOver (macOS/iOS) 276 | - TalkBack (Android) 277 | 278 | 2. **Real-world validation** 279 | - User testing with keyboard-only users 280 | - Screen reader user feedback 281 | - Production environment testing 282 | 283 | 3. **Automated accessibility testing** 284 | - axe-core integration 285 | - Lighthouse accessibility audits 286 | - pa11y or similar tools 287 | 288 | ## Status 289 | 290 | ✅ **Phase 1 (Critical) features implemented** - All essential WCAG AA compliance features are in place and tested. Features are disabled by default pending further validation. 291 | 292 | **Next steps:** 293 | 294 | 1. Community testing and feedback 295 | 2. Screen reader validation 296 | 3. Enable by default once vetted 297 | 4. Implement Phase 2 features (focus trap, custom shortcuts) 298 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.6.1](https://github.com/GlobalHive/vuejs-tour/compare/v2.6.0...v2.6.1) (2025-12-17) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * move vue to peerDependencies in package-lock.json ([0d8fbab](https://github.com/GlobalHive/vuejs-tour/commit/0d8fbab221a65111cfa3d4a7cd3bd0c574d6f64d)) 9 | * reset clip-path when highlighting is disabled in VTour component ([f0dc1f0](https://github.com/GlobalHive/vuejs-tour/commit/f0dc1f0923a966c0d1305b3b73f73993490ec27c)) 10 | 11 | ## [2.6.0](https://github.com/GlobalHive/vuejs-tour/compare/v2.5.0...v2.6.0) (2025-11-16) 12 | 13 | 14 | ### Features 15 | 16 | * Re-implement hot-switching for tour name and steps in VTour component ([7cbb2e0](https://github.com/GlobalHive/vuejs-tour/commit/7cbb2e0df16bbfbeb090e9655d04c4bed3a4dd80)) 17 | 18 | ## [2.5.0](https://github.com/GlobalHive/vuejs-tour/compare/v2.4.3...v2.5.0) (2025-11-06) 19 | 20 | 21 | ### Features 22 | 23 | * Add comprehensive accessibility tests for VTour component ([28a7fd1](https://github.com/GlobalHive/vuejs-tour/commit/28a7fd16667f3433199894e79817ccf922ea743a)) 24 | * Enhance jump options with customizable easing functions and improve accessibility support ([e295b99](https://github.com/GlobalHive/vuejs-tour/commit/e295b99dbf30f0e6d1a2495212904aab6730c759)) 25 | * enhance VTour component with default tooltip placement and multi-instance support ([f3ef42b](https://github.com/GlobalHive/vuejs-tour/commit/f3ef42b3f3004f959bd065a4caceb279d5415c56)) 26 | 27 | 28 | ### Miscellaneous Chores 29 | 30 | * release 2.5.0 ([ba4c32d](https://github.com/GlobalHive/vuejs-tour/commit/ba4c32d5eda2b0208f5a611d7a8c854e83ecefaf)) 31 | 32 | ## [2.4.3](https://github.com/GlobalHive/vuejs-tour/compare/v2.4.2...v2.4.3) (2025-10-12) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * Fixed wrong button text color ([4693eac](https://github.com/GlobalHive/vuejs-tour/commit/4693eacea8b8b60a2a20ae8fa87be771a930ee08)) 38 | 39 | ## [2.4.2](https://github.com/GlobalHive/vuejs-tour/compare/v2.4.1...v2.4.2) (2025-10-12) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * add scss build to ci (temp) ([aeffefe](https://github.com/GlobalHive/vuejs-tour/commit/aeffefe7922d83ddfa9fcb150c8b7b83c358fcc7)) 45 | * Default scss preprocessor is not modern-compiler ([7964ca2](https://github.com/GlobalHive/vuejs-tour/commit/7964ca224081aca7902ac3f0fc0c16f28c1f9f16)) 46 | * happy-dom RCE ([8db8f68](https://github.com/GlobalHive/vuejs-tour/commit/8db8f6817bef244e61d5fa973a850a9864cf67be)) 47 | * removed unsupported node version 18 ([3be0f49](https://github.com/GlobalHive/vuejs-tour/commit/3be0f49a6ff32425f92e66665f7c08947869563d)) 48 | 49 | ## [2.4.1](https://github.com/GlobalHive/vuejs-tour/compare/v2.4.0...v2.4.1) (2025-10-10) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * Pinning actions to a full length commit SHA ([49f12a0](https://github.com/GlobalHive/vuejs-tour/commit/49f12a04126f12175a9aee51d494f4aff345a4d9)) 55 | 56 | ## [2.4.0](https://github.com/GlobalHive/vuejs-tour/compare/v2.3.8...v2.4.0) (2025-10-10) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * Codacy (Hopefully) ([0f9ad16](https://github.com/GlobalHive/vuejs-tour/commit/0f9ad16a536c42f51143febe490b5a1e01615b28)) 62 | 63 | 64 | ### Miscellaneous Chores 65 | 66 | * release 2.4.0 ([8ef6523](https://github.com/GlobalHive/vuejs-tour/commit/8ef6523d9155d07911eceb4391d614c2d49051ec)) 67 | 68 | ## [2.3.8](https://github.com/GlobalHive/vuejs-tour/compare/v2.3.7...v2.3.8) (2025-09-26) 69 | 70 | 71 | ### Miscellaneous Chores 72 | 73 | * release 2.3.8 ([88433f3](https://github.com/GlobalHive/vuejs-tour/commit/88433f31b1caad5427660a22aa8895a8145406a3)) 74 | 75 | ## [2.3.7](https://github.com/GlobalHive/vuejs-tour/compare/v2.3.6...v2.3.7) (2025-09-03) 76 | 77 | 78 | ### Bug Fixes 79 | 80 | * Refactor localStorage key usage in VTour component ([fd10c04](https://github.com/GlobalHive/vuejs-tour/commit/fd10c043d5344a06984b7bc6db5fd2324c2e2493)) 81 | 82 | ## [2.3.6](https://github.com/GlobalHive/vuejs-tour/compare/v2.3.5...v2.3.6) (2025-07-17) 83 | 84 | 85 | ### Bug Fixes 86 | 87 | * [#89](https://github.com/GlobalHive/vuejs-tour/issues/89) updatePosition in window resize and resizeEnd ([2c5c366](https://github.com/GlobalHive/vuejs-tour/commit/2c5c366cbbab0836dd1e05d39d2c930f525f8616)) 88 | 89 | ## [2.3.5](https://github.com/GlobalHive/vuejs-tour/compare/v2.3.4...v2.3.5) (2025-06-10) 90 | 91 | 92 | ### Bug Fixes 93 | 94 | * [#86](https://github.com/GlobalHive/vuejs-tour/issues/86) lastStep is 0 after refresh ([8e504dc](https://github.com/GlobalHive/vuejs-tour/commit/8e504dc283b8bbd40edc02c2cffb5055eb1690b3)) 95 | * Fixed tsconfig noEmit ([c06ec9f](https://github.com/GlobalHive/vuejs-tour/commit/c06ec9f28862cef7b806f2666378cf71741c7a74)) 96 | 97 | ## [2.3.4](https://github.com/GlobalHive/vuejs-tour/compare/v2.3.3...v2.3.4) (2024-12-11) 98 | 99 | 100 | ### Bug Fixes 101 | 102 | * **VTour:** Fixed highlight target border remains on skip ([baba947](https://github.com/GlobalHive/vuejs-tour/commit/baba947f31ce7c0256c79f74a6afde49e07ca284)) 103 | 104 | ## [2.3.3](https://github.com/GlobalHive/vuejs-tour/compare/v2.3.2...v2.3.3) (2024-11-16) 105 | 106 | 107 | ### Bug Fixes 108 | 109 | * **VTour:** Fixed [#78](https://github.com/GlobalHive/vuejs-tour/issues/78) ([b1cdd63](https://github.com/GlobalHive/vuejs-tour/commit/b1cdd63440deebc0555c39079e02e8e02e34d5cf)) 110 | 111 | ## [2.3.2](https://github.com/GlobalHive/vuejs-tour/compare/v2.3.1...v2.3.2) (2024-10-23) 112 | 113 | 114 | ### Miscellaneous Chores 115 | 116 | * release 2.3.2 ([600d874](https://github.com/GlobalHive/vuejs-tour/commit/600d8742152de2107f2cfdd6805f38933a772ddc)) 117 | 118 | ## [2.3.1](https://github.com/GlobalHive/vuejs-tour/compare/v2.3.0...v2.3.1) (2024-10-20) 119 | 120 | 121 | ### Miscellaneous Chores 122 | 123 | * release 2.3.1 ([8fa9bfc](https://github.com/GlobalHive/vuejs-tour/commit/8fa9bfc4c5c71cc2a1d9d0a238c0610d52c9e06f)) 124 | 125 | ## [2.3.0](https://github.com/GlobalHive/vuejs-tour/compare/v2.2.0...v2.3.0) (2024-09-22) 126 | 127 | 128 | ### Features 129 | 130 | * **Step:** Added backdrop option to step ([8b2824e](https://github.com/GlobalHive/vuejs-tour/commit/8b2824e8a05a7d113d2a46a2137960adf9e0b693)) 131 | * **Step:** Added highlight option to steps ([95ea9f9](https://github.com/GlobalHive/vuejs-tour/commit/95ea9f938313b278f81fae773eb6624d208092fb)) 132 | * **Step:** Added noScroll option to step ([6f60ba6](https://github.com/GlobalHive/vuejs-tour/commit/6f60ba601a28671ae98911a827cfa5eb5c552b5a)) 133 | * **Step:** Added onAfter callback ([09f3fea](https://github.com/GlobalHive/vuejs-tour/commit/09f3fead9b28c9ff0bc6e592d80da96c7380bb35)) 134 | * **Step:** Added onBefore callback to step ([15f0c7a](https://github.com/GlobalHive/vuejs-tour/commit/15f0c7a0b240d61c339cbd560b3e4f3cb004f79d)) 135 | * **VTour:** Added OnTourStep emit ([ce0687c](https://github.com/GlobalHive/vuejs-tour/commit/ce0687cda7739ec7905597a54e9ddf6fc9d06c19)) 136 | 137 | 138 | ### Bug Fixes 139 | 140 | * **VTour:** Fixed stopping tour not removing backdrop & highlight ([408f5af](https://github.com/GlobalHive/vuejs-tour/commit/408f5af03f9b3c082d2c88e24a61b528460c7c3d)) 141 | 142 | ## [2.2.0](https://github.com/GlobalHive/vuejs-tour/compare/v2.1.1...v2.2.0) (2024-09-17) 143 | 144 | 145 | ### Features 146 | 147 | * Added Clip-Path Highlight ([66a0bae](https://github.com/GlobalHive/vuejs-tour/commit/66a0bae3408926a8ccab514ce73c21f37ca4bce1)) 148 | 149 | ## [2.1.1](https://github.com/GlobalHive/vuejs-tour/compare/v2.1.0...v2.1.1) (2024-07-10) 150 | 151 | 152 | ### Bug Fixes 153 | 154 | * **package:** Fixed wrong scripts ([4438349](https://github.com/GlobalHive/vuejs-tour/commit/4438349c749d283557e33bffa43f3cbca7423d03)) 155 | 156 | ## [2.1.0](https://github.com/GlobalHive/vuejs-tour/compare/v2.0.2...v2.1.0) (2024-07-05) 157 | 158 | 159 | ### Features 160 | 161 | * **VTour:** Added noScroll prop ([7ad9141](https://github.com/GlobalHive/vuejs-tour/commit/7ad91418ea21041935b2a64e9ab43c0f836fd1cf)) 162 | 163 | 164 | ### Bug Fixes 165 | 166 | * **VTour:** Fixed tooltip not scrolling with target ([ffbe9d9](https://github.com/GlobalHive/vuejs-tour/commit/ffbe9d9ec451f5098b358bf5f09ea5f360cfd49e)) 167 | * **VTour:** Fixed wrong scrolling call ([b644a86](https://github.com/GlobalHive/vuejs-tour/commit/b644a8674425b2d2c0048d419dddbb9cad4dcd97)) 168 | 169 | ## [2.0.2](https://github.com/GlobalHive/vuejs-tour/compare/v2.0.1...v2.0.2) (2024-07-05) 170 | 171 | 172 | ### Bug Fixes 173 | 174 | * **Step:** Fixed placement not working ([f16ec0f](https://github.com/GlobalHive/vuejs-tour/commit/f16ec0f2ad8276f05256c62379863e976ff43996)) 175 | 176 | ## [2.0.1](https://github.com/GlobalHive/vuejs-tour/compare/v2.0.0...v2.0.1) (2024-07-05) 177 | 178 | 179 | ### Bug Fixes 180 | 181 | * **Package:** Fixed not using npm ([c9c046b](https://github.com/GlobalHive/vuejs-tour/commit/c9c046be28fad2e1f4ee85a50ee635cbed53c194)) 182 | * **Workflow:** Fixed NPM release ([a053465](https://github.com/GlobalHive/vuejs-tour/commit/a0534653b46414198a23efe6271f7e012cfe2f86)) 183 | 184 | ## [2.0.0](https://github.com/GlobalHive/vuejs-tour/compare/v1.7.0...v2.0.0) (2024-07-05) 185 | 186 | 187 | ### ⚠ BREAKING CHANGES 188 | 189 | * **VTour:** Removed the plugin approach, switched to component import 190 | * **Deps:** Switched from popperjs to nanopop 191 | 192 | ### Features 193 | 194 | * **VTour:** Added margin prop 195 | * **VTour:** Added hideSkip prop 196 | * **VTour:** Added hideArrow prop 197 | * **VTour:** Added Typescript 198 | * **VTour:** Complete rewrite of the component 199 | 200 | ### Bug Fixes 201 | 202 | * **VTour:** Fixing the highlight using document space 203 | * **VTour:** Fixing wrong saveToLocalStorage checks 204 | 205 | ## [1.7.0](https://github.com/GlobalHive/vuejs-tour/compare/v1.6.1...v1.7.0) (2024-07-05) 206 | 207 | 208 | ### Features 209 | 210 | * **Docs:** Adding docs using vitepress ([633b017](https://github.com/GlobalHive/vuejs-tour/commit/633b0173b2c0a8bb4a7f03f122c2b1f69f37ffad)) 211 | * **Project:** Added Typescript ([2d80e28](https://github.com/GlobalHive/vuejs-tour/commit/2d80e28315bb708fb539dd13ca631b043de33427)) 212 | 213 | ## [1.6.1](https://github.com/GlobalHive/vuejs-tour/compare/v1.6.0...v1.6.1) (2024-06-03) 214 | 215 | 216 | ### Bug Fixes 217 | 218 | * **VTour:** Fixed not resetting on resetTour() [#54](https://github.com/GlobalHive/vuejs-tour/issues/54) ([fbfc970](https://github.com/GlobalHive/vuejs-tour/commit/fbfc970a83105863f9b614ca006ed9f200142055)) 219 | 220 | ## [1.6.0](https://github.com/GlobalHive/vuejs-tour/compare/v1.5.0...v1.6.0) (2024-03-27) 221 | 222 | 223 | ### Features 224 | 225 | * **VTour:** Added prop 'backdrop' to enable a backdrop / disabling controls [#45](https://github.com/GlobalHive/vuejs-tour/issues/45) ([a5e21f9](https://github.com/GlobalHive/vuejs-tour/commit/a5e21f91a75f2c311919473ec6cb6a4422a8d531)) 226 | 227 | 228 | ### Bug Fixes 229 | 230 | * **npm:** Updated packages ([081b0b7](https://github.com/GlobalHive/vuejs-tour/commit/081b0b7c269a2155a9073da8f6ac4c8acfada8a3)) 231 | * **VTour:** Fixed backdrop not disabling after tour end ([d122180](https://github.com/GlobalHive/vuejs-tour/commit/d122180adeba3f05f689685175522b927c507c51)) 232 | * **VTour:** Fixed tour showing on error (No Target) [#48](https://github.com/GlobalHive/vuejs-tour/issues/48) ([6b3f664](https://github.com/GlobalHive/vuejs-tour/commit/6b3f664e6f2087011e27cd15312662ee0ee8c8bf)) 233 | 234 | ## [1.5.0](https://github.com/GlobalHive/vuejs-tour/compare/v1.4.0...v1.5.0) (2024-01-04) 235 | 236 | 237 | ### Features 238 | 239 | * **VTour:** Added goToStep function ([2f4e8b2](https://github.com/GlobalHive/vuejs-tour/commit/2f4e8b22292bf9baa1b95f7df55e19379d17ab33)) 240 | 241 | 242 | ### Bug Fixes 243 | 244 | * **actions:** Fixed workflow automation ([28d458b](https://github.com/GlobalHive/vuejs-tour/commit/28d458b354d6ba32e93e5488266ee034131a6c18)) 245 | 246 | ## [1.4.0](https://github.com/GlobalHive/vuejs-tour/compare/v1.3.9...v1.4.0) (2023-12-29) 247 | 248 | 249 | ### Features 250 | 251 | * **VTour:** Added saveToLocalStorage prop ([885bc8b](https://github.com/GlobalHive/vuejs-tour/commit/885bc8bf77489de4a139db9cb5a7bc5a5aca7092)) 252 | 253 | ## [1.3.9](https://github.com/GlobalHive/vuejs-tour/compare/v1.3.8...v1.3.9) (2023-12-23) 254 | 255 | 256 | ### Bug Fixes 257 | 258 | * **actions:** Fixed docs action ([959394c](https://github.com/GlobalHive/vuejs-tour/commit/959394cc422fe6343c9e4776375d8e1e72d82b7a)) 259 | * **actions:** Fixed npm action ([ada714f](https://github.com/GlobalHive/vuejs-tour/commit/ada714fab6bac6396c7c8da041ab4963950339e8)) 260 | * **actions:** Fixed release action ([3ca0fbb](https://github.com/GlobalHive/vuejs-tour/commit/3ca0fbba1160dbcaa719cff74d82bb4c521363ba)) 261 | 262 | ## [1.3.8](https://github.com/GlobalHive/vuejs-tour/compare/v1.3.7...v1.3.8) (2023-12-22) 263 | 264 | 265 | ### Bug Fixes 266 | 267 | * **VTour:** Fixed non async code ([aa583c6](https://github.com/GlobalHive/vuejs-tour/commit/aa583c652599bf7794492933113a479ef5647f58)) 268 | 269 | ## [1.3.7](https://github.com/GlobalHive/vuejs-tour/compare/v1.3.6...v1.3.7) (2023-10-16) 270 | 271 | 272 | ### Bug Fixes 273 | 274 | * **version:** Updated version number ([8d3c1b9](https://github.com/GlobalHive/vuejs-tour/commit/8d3c1b9e6450c41607d8b2f08c8e5ee273f2414d)) 275 | 276 | ## [1.3.6](https://github.com/GlobalHive/vuejs-tour/compare/v1.3.5...v1.3.6) (2023-05-22) 277 | 278 | 279 | ### Bug Fixes 280 | 281 | * **component:** Skip text is hardcoded, should be read from component's props ([c7152a5](https://github.com/GlobalHive/vuejs-tour/commit/c7152a5beb8c03df16cd932def561ade4446ade9)) 282 | * **docs:** Document incorrect about buttonLabel property which should be buttonLabels instead [#27](https://github.com/GlobalHive/vuejs-tour/issues/27) ([a30f9dc](https://github.com/GlobalHive/vuejs-tour/commit/a30f9dc0d1e9330c97038c7a1c3a4baa02378d80)) 283 | 284 | ## [1.3.5](https://github.com/GlobalHive/vuejs-tour/compare/v1.3.4...v1.3.5) (2023-04-19) 285 | 286 | 287 | ### Bug Fixes 288 | 289 | * **component:** Hide Previous button on #actions overwrite [#25](https://github.com/GlobalHive/vuejs-tour/issues/25) ([dec6fae](https://github.com/GlobalHive/vuejs-tour/commit/dec6fae969dadc8b617378ff7ce2a4e26cdc6d87)) 290 | 291 | ## [1.3.4](https://github.com/GlobalHive/vuejs-tour/compare/v1.3.3...v1.3.4) (2023-01-26) 292 | 293 | 294 | ### Bug Fixes 295 | 296 | * **component:** Fixed error on first step / no highlight [#20](https://github.com/GlobalHive/vuejs-tour/issues/20) ([968e37a](https://github.com/GlobalHive/vuejs-tour/commit/968e37a75f01629aac71c44f510cd9bdb33d4080)) 297 | 298 | ## [1.3.3](https://github.com/GlobalHive/vuejs-tour/compare/v1.3.2...v1.3.3) (2023-01-09) 299 | 300 | 301 | ### Bug Fixes 302 | 303 | * **component:** Fixed exception when tour finishes [#18](https://github.com/GlobalHive/vuejs-tour/issues/18) ([9d745cf](https://github.com/GlobalHive/vuejs-tour/commit/9d745cfdfd3b545045954a0beac7c124aa72478c)) 304 | * **docs:** Fixed create a tour link ([cc1cbf1](https://github.com/GlobalHive/vuejs-tour/commit/cc1cbf1286617a972d3bd7536f6d19393cca9090)) 305 | 306 | ## [1.3.2](https://github.com/GlobalHive/vuejs-tour/compare/v1.3.1...v1.3.2) (2022-12-18) 307 | 308 | 309 | ### Bug Fixes 310 | 311 | * **readme:** Fixed shields url ([3ce98c4](https://github.com/GlobalHive/vuejs-tour/commit/3ce98c433a5493892eb7723e8893004c2d590257)) 312 | * **vite:** Excluded jump.js from build ([7ef4cff](https://github.com/GlobalHive/vuejs-tour/commit/7ef4cff868d01fb26ac70eef38a4ff2aa61fe86d)) 313 | 314 | ## [1.3.1](https://github.com/GlobalHive/vuejs-tour/compare/v1.3.0...v1.3.1) (2022-12-12) 315 | 316 | 317 | ### Bug Fixes 318 | 319 | * **docs:** Fixed wrong css import ([fa5d356](https://github.com/GlobalHive/vuejs-tour/commit/fa5d356115f5adf8781f25be396c6b7b96db9a60)) 320 | 321 | ## [1.3.0](https://github.com/GlobalHive/vuejs-tour/compare/v1.2.0...v1.3.0) (2022-12-12) 322 | 323 | 324 | ### Features 325 | 326 | * **callbacks:** Added onTourStart and onTourEnd callback ([008f798](https://github.com/GlobalHive/vuejs-tour/commit/008f7985608ba1d9aaa8853b8b7d147011a02902)) 327 | 328 | ## [1.2.0](https://github.com/GlobalHive/vuejs-tour/compare/v1.1.0...v1.2.0) (2022-12-12) 329 | 330 | 331 | ### Features 332 | 333 | * **options:** Added onShow() option - resolves [#10](https://github.com/GlobalHive/vuejs-tour/issues/10) ([b2d88e8](https://github.com/GlobalHive/vuejs-tour/commit/b2d88e8ebe0dfad1fb7ec208b64dc6f107af1e61)) 334 | 335 | 336 | ### Bug Fixes 337 | 338 | * **highlight:** Changed highlight border to outline to not take more space ([87b1cdf](https://github.com/GlobalHive/vuejs-tour/commit/87b1cdf07d9dad9d3a30fbb4cfd1ee3685f35648)) 339 | * **steps:** Fixed steps breaking when targets aren't found - fixes [#9](https://github.com/GlobalHive/vuejs-tour/issues/9) ([9c3d9f8](https://github.com/GlobalHive/vuejs-tour/commit/9c3d9f876ece96890addc9d0c219ebc1b4cc1b8b)) 340 | 341 | ## [1.1.0](https://github.com/GlobalHive/vuejs-tour/compare/v1.0.5...v1.1.0) (2022-12-10) 342 | 343 | 344 | ### Features 345 | 346 | * **docs:** Included tour example ([7796e69](https://github.com/GlobalHive/vuejs-tour/commit/7796e6968aa19bb02b07fd50741d5f34a4f23ebd)) 347 | * **jumpjs:** Added jump.js for smooth scrolling ([8001f5c](https://github.com/GlobalHive/vuejs-tour/commit/8001f5c59fd0d4969fa41808007bf9467cd6e986)) 348 | 349 | 350 | ### Bug Fixes 351 | 352 | * **component:** Button cursor not pointer ([3aea1f3](https://github.com/GlobalHive/vuejs-tour/commit/3aea1f3e5eada4ec07cfe56ed19d45e583d90aa0)) 353 | * **component:** Page not scrolling to top after tour ([f47ad5b](https://github.com/GlobalHive/vuejs-tour/commit/f47ad5b84265c9cfa9ee81af3a2d8a6d958779fd)) 354 | * **component:** Removing highlight ([d35877b](https://github.com/GlobalHive/vuejs-tour/commit/d35877bb00703c45566ad85113791643086a1883)) 355 | * **component:** Target highlighted even without highlight prop ([bcfd70a](https://github.com/GlobalHive/vuejs-tour/commit/bcfd70a46d9e9a10f43ce7be279b81cd88d686aa)) 356 | 357 | ## [1.0.5](https://github.com/GlobalHive/vuejs-tour/compare/v1.0.4...v1.0.5) (2022-12-10) 358 | 359 | 360 | ### Bug Fixes 361 | 362 | * **style:** variables not set to default ([fad38fe](https://github.com/GlobalHive/vuejs-tour/commit/fad38fe840953b52dbd8038c7000781ea64191b6)) 363 | * **workflows:** workflows not running automatically, temporarely set to manual ([36f826f](https://github.com/GlobalHive/vuejs-tour/commit/36f826f5bfd350155c2a55341d97e0be6edee848)) 364 | 365 | ## [1.0.4](https://github.com/GlobalHive/vuejs-tour/compare/v1.0.3...v1.0.4) (2022-12-10) 366 | 367 | 368 | ### Bug Fixes 369 | 370 | * **workflows:** workflows not running automatically ([a02959f](https://github.com/GlobalHive/vuejs-tour/commit/a02959fbce96d5f03f277fc07358c5f4405b5ed4)) 371 | 372 | ## [1.0.3](https://github.com/GlobalHive/vuejs-tour/compare/v1.0.2...v1.0.3) (2022-12-10) 373 | 374 | 375 | ### Bug Fixes 376 | 377 | * **build:** style.css not included by vite ([6bf83bb](https://github.com/GlobalHive/vuejs-tour/commit/6bf83bbd790c800117fada2ea20f45ec6345524a)) 378 | * **workflows:** workflows not running automatically ([9041f03](https://github.com/GlobalHive/vuejs-tour/commit/9041f0355b23faea469123cb52cd54c217bb6a66)) 379 | 380 | ## [1.0.2](https://github.com/GlobalHive/vuejs-tour/compare/v1.0.1...v1.0.2) (2022-12-10) 381 | 382 | 383 | ### Bug Fixes 384 | 385 | * fixed build action ([82c5f1f](https://github.com/GlobalHive/vuejs-tour/commit/82c5f1f5206fbcaca7642d844ed965accf59f8ef)) 386 | 387 | ## [1.0.1](https://github.com/GlobalHive/vuejs-tour/compare/v1.0.0...v1.0.1) (2022-12-09) 388 | 389 | 390 | ### Miscellaneous Chores 391 | 392 | * release 1.0.1 ([6bafa91](https://github.com/GlobalHive/vuejs-tour/commit/6bafa912848ba5a53751c028e8752242a093cad7)) 393 | 394 | ## 1.0.0 (2022-12-09) 395 | 396 | 397 | ### Features 398 | 399 | * **component:** Added onNext and onPrev functions ([7d68a64](https://github.com/GlobalHive/vuejs-tour/commit/7d68a645e95f41f40d716f502aff6a1ea7ceeca2)) 400 | * **component:** Added tour finished to localstorage ([466659f](https://github.com/GlobalHive/vuejs-tour/commit/466659fcbbbdd8d06e9cd80bf0c4f01a7ea0a2ef)) 401 | * **component:** Added tour finished to localstorage ([7e340a7](https://github.com/GlobalHive/vuejs-tour/commit/7e340a7c7bed3d77214050365e03dc12f8d7477f)) 402 | * **docs:** Added documentation ([1ba9859](https://github.com/GlobalHive/vuejs-tour/commit/1ba985944c43a418f44e4680c3e04f17fe1d26f5)) 403 | * **package:** Added npm definitions ([9a1d371](https://github.com/GlobalHive/vuejs-tour/commit/9a1d3712dc1f6860b5c7f2ee30cdfccb61f7569b)) 404 | * **vuejs-tour:** created component ([99a3088](https://github.com/GlobalHive/vuejs-tour/commit/99a30883624d5bb035303824f6223ad55ca2a798)) 405 | 406 | 407 | ### Bug Fixes 408 | 409 | * **docs:** Fixed missing package error ([5994f85](https://github.com/GlobalHive/vuejs-tour/commit/5994f8528a5d95f318d29a99d76d0c4936a5c5dd)) 410 | * **package:** fixed formatting ([50eba1c](https://github.com/GlobalHive/vuejs-tour/commit/50eba1c5ed4c55d1a9bfd470071299bc6e8c6328)) 411 | * **package:** Fixed pop up showing before start ([6d1feaa](https://github.com/GlobalHive/vuejs-tour/commit/6d1feaab5e0ae90bb1b9863cf8c307501387e1fc)) 412 | -------------------------------------------------------------------------------- /test/components/VTour.accessibility.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, vi } from 'vitest'; 2 | import type { ITourStep } from '../../src/Types'; 3 | import { 4 | useFakeTimersPerTest, 5 | startAndWaitReady, 6 | waitForStepTransition, 7 | flushVue, 8 | } from '../helpers/timers'; 9 | import { mountVTour } from '../helpers/mountVTour'; 10 | 11 | describe('VTour Component - Accessibility', () => { 12 | useFakeTimersPerTest(); 13 | 14 | let steps: ITourStep[]; 15 | 16 | beforeEach(() => { 17 | vi.clearAllMocks(); 18 | document.body.innerHTML = ` 19 |
Target 1
20 |
Target 2
21 |
Target 3
22 | `; 23 | 24 | steps = [ 25 | { target: '#step1', content: 'Step 1 content' }, 26 | { target: '#step2', content: 'Step 2 content' }, 27 | { target: '#step3', content: 'Step 3 content' }, 28 | ]; 29 | }); 30 | 31 | describe('ARIA Attributes', () => { 32 | it('should add role="dialog" to tooltip', async () => { 33 | const wrapper = mountVTour({ 34 | steps, 35 | autoStart: true, 36 | }); 37 | 38 | await startAndWaitReady(wrapper); 39 | 40 | const tooltip = document.querySelector('[id$="-tooltip"]'); 41 | expect(tooltip).toBeTruthy(); 42 | expect(tooltip?.getAttribute('role')).toBe('dialog'); 43 | 44 | wrapper.unmount(); 45 | }); 46 | 47 | it('should add aria-modal when accessibility is enabled', async () => { 48 | const wrapper = mountVTour({ 49 | steps, 50 | autoStart: true, 51 | enableA11y: true, 52 | }); 53 | 54 | await startAndWaitReady(wrapper); 55 | 56 | const tooltip = document.querySelector('[id$="-tooltip"]'); 57 | expect(tooltip?.getAttribute('aria-modal')).toBe('true'); 58 | 59 | wrapper.unmount(); 60 | }); 61 | 62 | it('should add default aria-label to tooltip', async () => { 63 | const wrapper = mountVTour({ 64 | steps, 65 | autoStart: true, 66 | }); 67 | 68 | await startAndWaitReady(wrapper); 69 | 70 | const tooltip = document.querySelector('[id$="-tooltip"]'); 71 | expect(tooltip?.getAttribute('aria-label')).toBe('Guided tour'); 72 | 73 | wrapper.unmount(); 74 | }); 75 | 76 | it('should use custom aria-label from props', async () => { 77 | const wrapper = mountVTour({ 78 | steps, 79 | autoStart: true, 80 | ariaLabel: 'Product feature tour', 81 | }); 82 | 83 | await startAndWaitReady(wrapper); 84 | 85 | const tooltip = document.querySelector('[id$="-tooltip"]'); 86 | expect(tooltip?.getAttribute('aria-label')).toBe('Product feature tour'); 87 | 88 | wrapper.unmount(); 89 | }); 90 | 91 | it('should use step-specific aria-label when provided', async () => { 92 | const stepsWithLabels = [ 93 | { target: '#step1', content: 'Step 1', ariaLabel: 'Welcome step' }, 94 | { 95 | target: '#step2', 96 | content: 'Step 2', 97 | ariaLabel: 'Feature introduction', 98 | }, 99 | ]; 100 | 101 | const wrapper = mountVTour({ 102 | steps: stepsWithLabels, 103 | autoStart: true, 104 | }); 105 | 106 | await startAndWaitReady(wrapper); 107 | 108 | const tooltip = document.querySelector('[id$="-tooltip"]'); 109 | expect(tooltip?.getAttribute('aria-label')).toBe('Welcome step'); 110 | 111 | wrapper.unmount(); 112 | }); 113 | 114 | it('should add aria-describedby linking to content', async () => { 115 | const wrapper = mountVTour({ 116 | steps, 117 | autoStart: true, 118 | }); 119 | 120 | await startAndWaitReady(wrapper); 121 | 122 | const tooltip = document.querySelector('[id$="-tooltip"]'); 123 | const ariaDescribedby = tooltip?.getAttribute('aria-describedby'); 124 | expect(ariaDescribedby).toBeTruthy(); 125 | 126 | const contentElement = document.querySelector(`#${ariaDescribedby}`); 127 | expect(contentElement).toBeTruthy(); 128 | 129 | wrapper.unmount(); 130 | }); 131 | 132 | it('should add tabindex="0" to make tooltip focusable when a11y enabled', async () => { 133 | const wrapper = mountVTour({ 134 | steps, 135 | autoStart: true, 136 | enableA11y: true, 137 | }); 138 | 139 | await startAndWaitReady(wrapper); 140 | 141 | const tooltip = document.querySelector('[id$="-tooltip"]'); 142 | expect(tooltip?.getAttribute('tabindex')).toBe('0'); 143 | 144 | wrapper.unmount(); 145 | }); 146 | 147 | it('should not add tabindex when a11y disabled', async () => { 148 | const wrapper = mountVTour({ 149 | steps, 150 | autoStart: true, 151 | enableA11y: false, 152 | }); 153 | 154 | await startAndWaitReady(wrapper); 155 | 156 | const tooltip = document.querySelector('[id$="-tooltip"]'); 157 | expect(tooltip?.getAttribute('tabindex')).toBeNull(); 158 | 159 | wrapper.unmount(); 160 | }); 161 | }); 162 | 163 | describe('ARIA Live Region', () => { 164 | it('should include aria-live region for step announcements', async () => { 165 | const wrapper = mountVTour({ 166 | steps, 167 | autoStart: true, 168 | enableA11y: true, 169 | }); 170 | 171 | await startAndWaitReady(wrapper); 172 | 173 | const liveRegion = document.querySelector( 174 | '[role="status"][aria-live="polite"]' 175 | ); 176 | expect(liveRegion).toBeTruthy(); 177 | expect(liveRegion?.getAttribute('aria-atomic')).toBe('true'); 178 | expect(liveRegion?.textContent?.trim()).toContain('Step 1 of 3'); 179 | 180 | wrapper.unmount(); 181 | }); 182 | 183 | it('should update live region when step changes', async () => { 184 | const wrapper = mountVTour({ 185 | steps, 186 | autoStart: true, 187 | enableA11y: true, 188 | }); 189 | 190 | await startAndWaitReady(wrapper); 191 | 192 | let liveRegion = document.querySelector( 193 | '[role="status"][aria-live="polite"]' 194 | ); 195 | expect(liveRegion?.textContent).toContain('Step 1 of 3'); 196 | 197 | // Navigate to next step 198 | const component = wrapper.vm as any; 199 | await component.nextStep(); 200 | await waitForStepTransition(wrapper); 201 | 202 | // Re-query the live region as it gets recreated with the new :key 203 | liveRegion = document.querySelector( 204 | '[role="status"][aria-live="polite"]' 205 | ); 206 | expect(liveRegion?.textContent).toContain('Step 2 of 3'); 207 | 208 | wrapper.unmount(); 209 | }); 210 | 211 | it('should not include live region when a11y disabled', async () => { 212 | const wrapper = mountVTour({ 213 | steps, 214 | autoStart: true, 215 | enableA11y: false, 216 | }); 217 | 218 | await startAndWaitReady(wrapper); 219 | 220 | const liveRegion = document.querySelector( 221 | '[role="status"][aria-live="polite"]' 222 | ); 223 | expect(liveRegion).toBeNull(); 224 | 225 | wrapper.unmount(); 226 | }); 227 | }); 228 | 229 | describe('Button Labels', () => { 230 | it('should add descriptive aria-labels to back button', async () => { 231 | const wrapper = mountVTour({ 232 | steps, 233 | autoStart: true, 234 | enableA11y: true, 235 | }); 236 | 237 | await startAndWaitReady(wrapper); 238 | 239 | const component = wrapper.vm as any; 240 | await component.nextStep(); 241 | await waitForStepTransition(wrapper); 242 | 243 | const backButton = Array.from(document.querySelectorAll('button')).find( 244 | (btn) => btn.textContent?.includes('Back') 245 | ); 246 | expect(backButton?.getAttribute('aria-label')).toContain( 247 | 'Go to previous step' 248 | ); 249 | // Currently on step 2 (currentStepIndex=1), button shows currentStepIndex not currentStepIndex+1 250 | expect(backButton?.getAttribute('aria-label')).toContain('step 1 of 3'); 251 | 252 | wrapper.unmount(); 253 | }); 254 | 255 | it('should add descriptive aria-labels to skip button', async () => { 256 | const wrapper = mountVTour({ 257 | steps, 258 | autoStart: true, 259 | enableA11y: true, 260 | }); 261 | 262 | await startAndWaitReady(wrapper); 263 | 264 | const skipButton = Array.from(document.querySelectorAll('button')).find( 265 | (btn) => btn.textContent?.includes('Skip') 266 | ); 267 | expect(skipButton?.getAttribute('aria-label')).toBe( 268 | 'Skip tour and close' 269 | ); 270 | 271 | wrapper.unmount(); 272 | }); 273 | 274 | it('should add descriptive aria-labels to next button', async () => { 275 | const wrapper = mountVTour({ 276 | steps, 277 | autoStart: true, 278 | enableA11y: true, 279 | }); 280 | 281 | await startAndWaitReady(wrapper); 282 | 283 | const nextButton = Array.from(document.querySelectorAll('button')).find( 284 | (btn) => btn.textContent?.includes('Next') 285 | ); 286 | expect(nextButton?.getAttribute('aria-label')).toContain( 287 | 'Go to next step' 288 | ); 289 | expect(nextButton?.getAttribute('aria-label')).toContain('step 2 of 3'); 290 | 291 | wrapper.unmount(); 292 | }); 293 | 294 | it('should change next button aria-label to "Finish tour" on last step', async () => { 295 | const wrapper = mountVTour({ 296 | steps, 297 | autoStart: true, 298 | enableA11y: true, 299 | }); 300 | 301 | await startAndWaitReady(wrapper); 302 | 303 | const component = wrapper.vm as any; 304 | 305 | // Navigate to last step 306 | await component.nextStep(); 307 | await waitForStepTransition(wrapper); 308 | await component.nextStep(); 309 | await waitForStepTransition(wrapper); 310 | 311 | const finishButton = Array.from(document.querySelectorAll('button')).find( 312 | (btn) => btn.textContent?.includes('Done') 313 | ); 314 | expect(finishButton?.getAttribute('aria-label')).toBe('Finish tour'); 315 | 316 | wrapper.unmount(); 317 | }); 318 | 319 | it('should not add aria-labels when a11y disabled', async () => { 320 | const wrapper = mountVTour({ 321 | steps, 322 | autoStart: true, 323 | enableA11y: false, 324 | }); 325 | 326 | await startAndWaitReady(wrapper); 327 | 328 | const buttons = Array.from(document.querySelectorAll('button')); 329 | buttons.forEach((btn) => { 330 | expect(btn.getAttribute('aria-label')).toBeNull(); 331 | }); 332 | 333 | wrapper.unmount(); 334 | }); 335 | }); 336 | 337 | describe('Keyboard Navigation', () => { 338 | it('should listen for keyboard events when keyboardNav enabled', async () => { 339 | const addEventListenerSpy = vi.spyOn(window, 'addEventListener'); 340 | 341 | const wrapper = mountVTour({ 342 | steps, 343 | autoStart: true, 344 | keyboardNav: true, 345 | }); 346 | 347 | await startAndWaitReady(wrapper); 348 | 349 | expect(addEventListenerSpy).toHaveBeenCalledWith( 350 | 'keydown', 351 | expect.any(Function) 352 | ); 353 | 354 | wrapper.unmount(); 355 | addEventListenerSpy.mockRestore(); 356 | }); 357 | 358 | it('should not listen for keyboard events when keyboardNav disabled', async () => { 359 | const addEventListenerSpy = vi.spyOn(window, 'addEventListener'); 360 | 361 | const wrapper = mountVTour({ 362 | steps, 363 | autoStart: true, 364 | keyboardNav: false, 365 | }); 366 | 367 | await startAndWaitReady(wrapper); 368 | 369 | const keydownCalls = addEventListenerSpy.mock.calls.filter( 370 | (call) => call[0] === 'keydown' 371 | ); 372 | expect(keydownCalls).toHaveLength(0); 373 | 374 | wrapper.unmount(); 375 | addEventListenerSpy.mockRestore(); 376 | }); 377 | 378 | it('should close tour on Escape key', async () => { 379 | const wrapper = mountVTour({ 380 | steps, 381 | autoStart: true, 382 | keyboardNav: true, 383 | }); 384 | 385 | await startAndWaitReady(wrapper); 386 | 387 | // Tour should be visible 388 | expect(document.querySelector('[id$="-tooltip"]')).toBeTruthy(); 389 | 390 | // Simulate Escape key 391 | const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' }); 392 | window.dispatchEvent(escapeEvent); 393 | 394 | await flushVue(); 395 | 396 | // Tour should be hidden 397 | const tooltip = document.querySelector('[id$="-tooltip"]'); 398 | expect(tooltip?.getAttribute('data-hidden')).toBe('true'); 399 | 400 | wrapper.unmount(); 401 | }); 402 | 403 | it('should go to next step on ArrowRight key', async () => { 404 | const wrapper = mountVTour({ 405 | steps, 406 | autoStart: true, 407 | keyboardNav: true, 408 | }); 409 | 410 | await startAndWaitReady(wrapper); 411 | 412 | let liveRegion = document.querySelector( 413 | '[role="status"][aria-live="polite"]' 414 | ); 415 | expect(liveRegion?.textContent).toContain('Step 1 of 3'); 416 | 417 | // Simulate ArrowRight key 418 | const arrowEvent = new KeyboardEvent('keydown', { key: 'ArrowRight' }); 419 | window.dispatchEvent(arrowEvent); 420 | 421 | await waitForStepTransition(wrapper); 422 | 423 | // Re-query the live region as it gets recreated with the new :key 424 | liveRegion = document.querySelector( 425 | '[role="status"][aria-live="polite"]' 426 | ); 427 | expect(liveRegion?.textContent).toContain('Step 2 of 3'); 428 | 429 | wrapper.unmount(); 430 | }); 431 | 432 | it('should go to previous step on ArrowLeft key', async () => { 433 | const wrapper = mountVTour({ 434 | steps, 435 | autoStart: true, 436 | keyboardNav: true, 437 | }); 438 | 439 | await startAndWaitReady(wrapper); 440 | 441 | const component = wrapper.vm as any; 442 | 443 | // Go to step 2 444 | await component.nextStep(); 445 | await waitForStepTransition(wrapper); 446 | 447 | let liveRegion = document.querySelector( 448 | '[role="status"][aria-live="polite"]' 449 | ); 450 | expect(liveRegion?.textContent).toContain('Step 2 of 3'); 451 | 452 | // Simulate ArrowLeft key 453 | const arrowEvent = new KeyboardEvent('keydown', { key: 'ArrowLeft' }); 454 | window.dispatchEvent(arrowEvent); 455 | 456 | await waitForStepTransition(wrapper); 457 | 458 | // Re-query the live region as it gets recreated with the new :key 459 | liveRegion = document.querySelector( 460 | '[role="status"][aria-live="polite"]' 461 | ); 462 | expect(liveRegion?.textContent).toContain('Step 1 of 3'); 463 | 464 | wrapper.unmount(); 465 | }); 466 | 467 | it('should go to next step on Enter key', async () => { 468 | const wrapper = mountVTour({ 469 | steps, 470 | autoStart: true, 471 | keyboardNav: true, 472 | }); 473 | 474 | await startAndWaitReady(wrapper); 475 | 476 | let liveRegion = document.querySelector( 477 | '[role="status"][aria-live="polite"]' 478 | ); 479 | expect(liveRegion?.textContent).toContain('Step 1 of 3'); 480 | 481 | // Simulate Enter key 482 | const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' }); 483 | window.dispatchEvent(enterEvent); 484 | 485 | await waitForStepTransition(wrapper); 486 | 487 | // Re-query the live region as it gets recreated with the new :key 488 | liveRegion = document.querySelector( 489 | '[role="status"][aria-live="polite"]' 490 | ); 491 | expect(liveRegion?.textContent).toContain('Step 2 of 3'); 492 | 493 | wrapper.unmount(); 494 | }); 495 | 496 | it('should not trigger next step when Enter is pressed on a button', async () => { 497 | const wrapper = mountVTour({ 498 | steps, 499 | autoStart: true, 500 | keyboardNav: true, 501 | }); 502 | 503 | await startAndWaitReady(wrapper); 504 | 505 | const initialStep = wrapper.vm.currentStepIndex; 506 | 507 | // Create a button and simulate Enter key on it 508 | const button = document.createElement('button'); 509 | document.body.appendChild(button); 510 | 511 | const enterEventOnButton = new KeyboardEvent('keydown', { 512 | key: 'Enter', 513 | bubbles: true, 514 | }); 515 | 516 | Object.defineProperty(enterEventOnButton, 'target', { 517 | value: button, 518 | enumerable: true, 519 | }); 520 | 521 | window.dispatchEvent(enterEventOnButton); 522 | await flushVue(); 523 | 524 | // Should still be on the same step 525 | expect(wrapper.vm.currentStepIndex).toBe(initialStep); 526 | 527 | document.body.removeChild(button); 528 | wrapper.unmount(); 529 | }); 530 | 531 | it('should remove keyboard listener on unmount', async () => { 532 | const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener'); 533 | 534 | const wrapper = mountVTour({ 535 | steps, 536 | autoStart: true, 537 | keyboardNav: true, 538 | }); 539 | 540 | await startAndWaitReady(wrapper); 541 | 542 | wrapper.unmount(); 543 | 544 | expect(removeEventListenerSpy).toHaveBeenCalledWith( 545 | 'keydown', 546 | expect.any(Function) 547 | ); 548 | 549 | removeEventListenerSpy.mockRestore(); 550 | }); 551 | }); 552 | 553 | describe('Focus Management', () => { 554 | it('should focus tooltip when tour starts with a11y enabled', async () => { 555 | const wrapper = mountVTour({ 556 | steps, 557 | enableA11y: true, 558 | }); 559 | 560 | const component = wrapper.vm as any; 561 | await component.startTour(); 562 | await startAndWaitReady(wrapper); 563 | 564 | const tooltip = document.querySelector('[id$="-tooltip"]') as HTMLElement; 565 | expect(document.activeElement).toBe(tooltip); 566 | 567 | wrapper.unmount(); 568 | }); 569 | 570 | it('should restore focus to previous element when tour ends', async () => { 571 | // Create a focusable element and focus it 572 | const button = document.createElement('button'); 573 | button.id = 'test-button'; 574 | button.textContent = 'Test'; 575 | document.body.appendChild(button); 576 | button.focus(); 577 | 578 | expect(document.activeElement).toBe(button); 579 | 580 | const wrapper = mountVTour({ 581 | steps, 582 | enableA11y: true, 583 | }); 584 | 585 | const component = wrapper.vm as any; 586 | await component.startTour(); 587 | await startAndWaitReady(wrapper); 588 | 589 | // Focus should move to tooltip 590 | const tooltip = document.querySelector('[id$="-tooltip"]') as HTMLElement; 591 | expect(document.activeElement).toBe(tooltip); 592 | 593 | // End tour 594 | await component.stopTour(); 595 | await flushVue(); 596 | 597 | // Focus should be restored to button 598 | expect(document.activeElement).toBe(button); 599 | 600 | wrapper.unmount(); 601 | button.remove(); 602 | }); 603 | }); 604 | 605 | describe('Accessibility Configuration', () => { 606 | it('should enable all a11y features by default', async () => { 607 | const wrapper = mountVTour({ 608 | steps, 609 | autoStart: true, 610 | }); 611 | 612 | await startAndWaitReady(wrapper); 613 | 614 | const tooltip = document.querySelector('[id$="-tooltip"]'); 615 | expect(tooltip?.getAttribute('aria-modal')).toBe('true'); 616 | expect(tooltip?.getAttribute('tabindex')).toBe('0'); 617 | 618 | const liveRegion = document.querySelector( 619 | '[role="status"][aria-live="polite"]' 620 | ); 621 | expect(liveRegion).toBeTruthy(); 622 | 623 | wrapper.unmount(); 624 | }); 625 | 626 | it('should respect enableA11y: false to disable ARIA features', async () => { 627 | const wrapper = mountVTour({ 628 | steps, 629 | autoStart: true, 630 | enableA11y: false, 631 | }); 632 | 633 | await startAndWaitReady(wrapper); 634 | 635 | const tooltip = document.querySelector('[id$="-tooltip"]'); 636 | expect(tooltip?.getAttribute('aria-modal')).toBeNull(); 637 | expect(tooltip?.getAttribute('tabindex')).toBeNull(); 638 | 639 | const liveRegion = document.querySelector( 640 | '[role="status"][aria-live="polite"]' 641 | ); 642 | expect(liveRegion).toBeNull(); 643 | 644 | wrapper.unmount(); 645 | }); 646 | 647 | it('should respect keyboardNav: false to disable keyboard shortcuts', async () => { 648 | const addEventListenerSpy = vi.spyOn(window, 'addEventListener'); 649 | 650 | const wrapper = mountVTour({ 651 | steps, 652 | autoStart: true, 653 | keyboardNav: false, 654 | }); 655 | 656 | await startAndWaitReady(wrapper); 657 | 658 | const keydownCalls = addEventListenerSpy.mock.calls.filter( 659 | (call) => call[0] === 'keydown' 660 | ); 661 | expect(keydownCalls).toHaveLength(0); 662 | 663 | wrapper.unmount(); 664 | addEventListenerSpy.mockRestore(); 665 | }); 666 | }); 667 | }); 668 | -------------------------------------------------------------------------------- /src/components/VTour.vue: -------------------------------------------------------------------------------- 1 | 639 | 640 | 738 | --------------------------------------------------------------------------------