From d956a337d1cd178fa26cdf3c995741e75669304b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 28 Jul 2020 23:42:09 +0200 Subject: [PATCH] chore: release v2.16.0 (#5613) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): update all non-major dependencies (#5430) Co-authored-by: Renovate Bot * fix(b-form-checkbox-group): only emit `input` when value loosely changes (#5432) * fix(b-form-checkbox-group, b-form-radio-group): only emit `input` when value loosely changes * Update loose-equal.js * Update form-checkbox-group.spec.js * chore(deps): update all non-major dependencies (#5440) Co-authored-by: Renovate Bot * chore(deps): update devdependency vue-router to ^3.3.0 (#5443) Co-authored-by: Renovate Bot * chore(deps): update all non-major dependencies (#5445) Co-authored-by: Renovate Bot * chore(deps): update devdependency rollup to ^2.11.2 (#5446) Co-authored-by: Renovate Bot * chore: Change Bootstrap v4.3.x to 4.5.x in README (#5447) Since v2.15 Bootstrap-Vue supports Bootstrap v4.5 * chore(deps): update all non-major dependencies (#5451) * chore(deps): update all non-major dependencies * Use `toBeEmptyDomElement()` instead of deprecated `toBeEmpty()` * Correct typo Co-authored-by: Renovate Bot Co-authored-by: Jacob Müller * chore(deps): update devdependency vue-router to ^3.3.2 (#5454) Co-authored-by: Renovate Bot * chore(deps): remove unused `gh-pages` dependency (#5455) * chore(deps): update devdependency gh-pages to v3 * Remove `gh-pages` dependency Co-authored-by: Renovate Bot Co-authored-by: Jacob Müller * chore(deps): update all non-major dependencies (#5458) Co-authored-by: Renovate Bot * Update all bootstrap doc links to latest version (#5450) Co-authored-by: Jacob Müller * chore(deps): update devdependency rollup to ^2.12.1 (#5463) Co-authored-by: Renovate Bot * chore(deps): update all non-major dependencies (#5466) Co-authored-by: Renovate Bot * chore(deps): update devdependency @nuxtjs/sitemap to ^2.3.1 (#5468) Co-authored-by: Renovate Bot * chore(deps): update devdependency lint-staged to ^10.2.9 (#5470) Co-authored-by: Renovate Bot * chore(b-avatar): convert line endings to Unix (#5469) Co-authored-by: Jacob Müller * chore: convert all line endings to unix (#5474) * chore(deps): update all non-major dependencies (#5478) Co-authored-by: Renovate Bot * chore(deps): update all non-major dependencies (#5482) Co-authored-by: Renovate Bot * chore(deps): update devdependency eslint-plugin-import to ^2.21.2 (#5487) Co-authored-by: Renovate Bot * chore(deps): update devdependency @testing-library/jest-dom to ^5.10.0 (#5493) Co-authored-by: Renovate Bot * chore(deps): update all non-major dependencies (#5495) Co-authored-by: Renovate Bot * first attempt (#5462) Co-authored-by: Jacob Müller * chore(deps): update all non-major dependencies (#5499) Co-authored-by: Renovate Bot * chore(deps): update devdependency eslint-plugin-prettier to ^3.1.4 (#5501) Co-authored-by: Renovate Bot * chore(deps): update devdependency @nuxtjs/sitemap to ^2.3.2 (#5503) Co-authored-by: Renovate Bot * chore(deps): update devdependency terser to ^4.8.0 (#5505) Co-authored-by: Renovate Bot * chore(deps): update all non-major dependencies (#5508) Co-authored-by: Renovate Bot * chore(docs): fix typo in sidebar README (#5494) (#5510) * chore: update auto format config (#5526) * chore(deps): update all non-major dependencies (#5511) Co-authored-by: Renovate Bot Co-authored-by: Jacob Müller * chore(deps): update all non-major dependencies (#5531) Co-authored-by: Renovate Bot * fix typo (#5534) * remove mention of `router-tag` from button docs (#5535) Co-authored-by: Jacob Müller * fix(b-table): prevent endless reevaluation when using v-model and object/array literal prop values (#5554) * Update devDependency sass-loader to v9 (#5546) Co-authored-by: Renovate Bot Co-authored-by: Jacob Müller * fix(b-img): Allow empty `alt` prop (fixes #5524) (#5545) * Allow empty `alt` * default to null to avoid check * remove unused import * add avatar support * add test cases * spelling Co-authored-by: Jacob Müller * chore(deps-dev): bump standard-version from 8.0.0 to 8.0.1 (#5576) Bumps [standard-version](https://github.com/conventional-changelog/standard-version) from 8.0.0 to 8.0.1. - [Release notes](https://github.com/conventional-changelog/standard-version/releases) - [Changelog](https://github.com/conventional-changelog/standard-version/blob/master/CHANGELOG.md) - [Commits](https://github.com/conventional-changelog/standard-version/compare/v8.0.0...v8.0.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat(b-form-tags): add `ignoreInputFocusSelector` prop to make input focus behavior configurable (closes #5425) (#5429) * fix(b-form-tags): fix input focus upon clicking on nested element * Update form-tags.js * Add `ignoreInputFocusSelector` prop * Update form-tags.js * Add comment and more selectors to ignoreInputFocusSelector in form-tags.js Co-authored-by: Jacob Müller * chore(deps): update all non-major dependencies (#5533) * chore(deps): update all non-major dependencies * Bump BundleWatch limits for new Bootstrap Icons * Regenerate icon files Co-authored-by: Renovate Bot Co-authored-by: Jacob Müller * chore(docs): add an example to ``'s using icons (#5537) * Adding an example to input-groups using icons * Update README.md Co-authored-by: Jacob Müller * chore(deps): update devdependency @nuxtjs/google-analytics to ^2.4.0 (#5583) Co-authored-by: Renovate Bot * fix(b-icon): use `aria-label` attribute instead of `alt` (#5581) * fix(b-tags): replace spacing utility with static CSS (fixes #5523) (#5544) * remove spacing utility * use mt-auto for better centering * update * add new class to avoid issues with custom rendering Co-authored-by: Jacob Müller * chore(docs): improve icons page (#5579) * feat(docs): improve icons page * Actually use `bootstrapIconsCount` variable * Move icon explorer to the bottom * chore: regenerate `yarn.lock` (#5585) * fix(b-form-tags): unit test (#5586) * chore(deps): update devdependency rollup to ^2.22.0 (#5589) Co-authored-by: Renovate Bot * chore(deps): update all non-major dependencies (#5590) Co-authored-by: Renovate Bot * feat(docs): launch themes page with first BootstrapVue theme (#5549) * docs(footer): uncomment link for themes * docs(header): uncomment link for themes * docs(sidebar): uncomment link for themes * docs(intro/README): uncomment link for themes * docs(theming/README): uncomment link for themes * feature(themes): add first Bootstrap Vue & Creative Tim theme * style(themes): prettify themes files * fix(themes): solve typo * Use `@nuxt/content` for themes * Update index.vue * Update themes.vue * Don't pin `@nuxt/content` * Update themes.vue * Update themes.vue Co-authored-by: Jacob Müller * fix: properly handle special characters in user-provided IDs (closes #4927, #5561) (#5564) * fix(b-form-group): make it work for ids with special characters like "/" Special characters are allowed in HTML5 (https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id) but need to be escaped when used in a selector for usage in e.g. "querySelector" Refs #5561 * Use own `cssEscape()` util + use/test everywhere needed Co-authored-by: Jacob Müller * chore(ci): update `actions/cache` to v2 (#5580) * chore(ci): update `actions/cache` to v2 * Update test.yml * Update test.yml * fix(b-form-tags): unit tests * Revert "fix(b-form-tags): unit tests" This reverts commit 20ebc04c38ca49a0edafcf7b84291cce528de9dc. * Split actions * Run BundleWatch during build * Update build.yml * Revert "Update build.yml" This reverts commit ed4ad3deb59624e8e46f7f46dc9175412b24b8f4. * Update build.yml * chore: replace `packagequality` badge with `codacy` in README (#5596) * chore: replace `packagequality` badge with `codacy` in README * Update README.md * chore(deps): update devdependency rollup to ^2.22.2 (#5597) Co-authored-by: Renovate Bot * chore(deps): update devdependency rollup to ^2.23.0 (#5603) Co-authored-by: Renovate Bot * remove redundant height declaration in .b-sidebar (#5606) * chore(deps): update devdependency eslint-plugin-jest to ^23.18.2 (#5607) Co-authored-by: Renovate Bot Co-authored-by: Jacob Müller * chore(deps): update all non-major dependencies (#5609) Co-authored-by: Renovate Bot * chore(docs): fix Bootstrap browser and devices link * chore(ci): fix BundleWatch token name * chore: add back `packagequality` badge to README * chore: prettify * chore: update contributors * chore(deps): update devdependency eslint-plugin-jest to ^23.19.0 (#5611) Co-authored-by: Renovate Bot * chore: add script to generate release notes (#5612) * chore: bump version to 2.16.0 and update changelog (#5614) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Renovate Bot Co-authored-by: Troy Morehouse Co-authored-by: TitanFighter Co-authored-by: Hiws Co-authored-by: Vitaly Slobodin Co-authored-by: Sergey Skrynnikov Co-authored-by: James George Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Paweł Tatarczuk Co-authored-by: Ivan Gonzalez Co-authored-by: Hiws Co-authored-by: Nazare Emanuel-Ioan Co-authored-by: Dieter Geerts Co-authored-by: michel milano --- .bundlewatch.config.json | 20 +- .github/workflows/build.yml | 55 + .github/workflows/test.yml | 155 +- .gitignore | 1 + CHANGELOG.md | 47 +- README.md | 14 +- docs/assets/vercel.svg | 6 +- docs/components/footer.vue | 2 - docs/components/header.vue | 2 - docs/components/icons-table.vue | 24 +- docs/components/sidebar.vue | 2 - docs/{content/index.js => content.js} | 24 + docs/content/themes/argon-dashboard-pro.yaml | 8 + docs/markdown/intro/README.md | 7 +- .../reference/accessibility/README.md | 2 +- .../reference/starter-templates/README.md | 2 +- docs/markdown/reference/theming/README.md | 14 +- .../reference/utility-classes/README.md | 46 +- docs/markdown/reference/validation/README.md | 4 +- docs/nuxt.config.js | 8 +- docs/pages/docs/icons/index.js | 5 +- docs/pages/docs/index.js | 45 +- docs/pages/index.vue | 38 +- docs/pages/themes.vue | 97 +- package.json | 79 +- scripts/release-notes.js | 173 + src/components/aspect/README.md | 130 +- src/components/aspect/aspect.js | 118 +- src/components/aspect/aspect.spec.js | 242 +- src/components/aspect/index.d.ts | 22 +- src/components/aspect/index.js | 16 +- src/components/aspect/package.json | 42 +- src/components/avatar/README.md | 1134 +- src/components/avatar/_avatar.scss | 228 +- src/components/avatar/avatar.js | 2 +- src/components/avatar/avatar.spec.js | 670 +- src/components/avatar/index.d.ts | 28 +- src/components/avatar/index.js | 18 +- src/components/avatar/index.scss | 2 +- src/components/avatar/package.json | 294 +- src/components/button/README.md | 2 - src/components/card/card-img-lazy.spec.js | 16 + src/components/card/card-img.js | 6 +- src/components/card/card-img.spec.js | 16 + src/components/carousel/carousel.js | 8 +- .../form-checkbox/form-checkbox-group.spec.js | 48 + src/components/form-group/README.md | 2 +- src/components/form-group/form-group.js | 4 +- src/components/form-group/form-group.spec.js | 26 + src/components/form-rating/README.md | 1052 +- src/components/form-rating/_form-rating.scss | 114 +- src/components/form-rating/form-rating.js | 908 +- .../form-rating/form-rating.spec.js | 1110 +- src/components/form-rating/index.d.ts | 28 +- src/components/form-rating/index.js | 16 +- src/components/form-rating/index.scss | 2 +- src/components/form-rating/package.json | 338 +- src/components/form-tags/_form-tags.scss | 10 + src/components/form-tags/form-tag.js | 2 +- src/components/form-tags/form-tags.js | 49 +- src/components/form-tags/form-tags.spec.js | 93 +- src/components/form-tags/package.json | 5 + src/components/form/README.md | 4 +- src/components/image/README.md | 2 +- src/components/image/img.js | 6 +- src/components/image/img.spec.js | 29 + src/components/input-group/README.md | 8 +- src/components/layout/README.md | 2 +- src/components/overlay/index.d.ts | 22 +- src/components/overlay/index.js | 16 +- src/components/overlay/overlay.js | 368 +- src/components/overlay/overlay.spec.js | 514 +- src/components/overlay/package.json | 260 +- src/components/sidebar/README.md | 800 +- src/components/sidebar/_sidebar.scss | 193 +- src/components/sidebar/index.d.ts | 22 +- src/components/sidebar/index.js | 20 +- src/components/sidebar/index.scss | 2 +- src/components/sidebar/package.json | 406 +- src/components/sidebar/sidebar.js | 934 +- src/components/sidebar/sidebar.spec.js | 736 +- src/components/table/helpers/mixin-items.js | 6 +- src/directives/toggle/README.md | 328 +- src/directives/toggle/package.json | 52 +- src/icons/README.md | 25 +- src/icons/helpers/icon-base.js | 4 +- src/icons/helpers/make-icon.js | 9 +- src/icons/icons.d.ts | 718 +- src/icons/icons.js | 2290 +- src/icons/icons.spec.js | 14 +- src/icons/iconstack.spec.js | 6 +- src/icons/package.json | 19788 ++++++++++++++-- src/icons/plugin.js | 1081 +- src/mixins/form-radio-check-group.js | 7 +- src/utils/css-escape.js | 75 + src/utils/css-escape.spec.js | 82 + src/utils/loose-equal.js | 8 +- static/logo-padded.svg | 30 +- static/logo.svg | 28 +- yarn.lock | 4733 ++-- 100 files changed, 31881 insertions(+), 9428 deletions(-) create mode 100644 .github/workflows/build.yml rename docs/{content/index.js => content.js} (70%) create mode 100644 docs/content/themes/argon-dashboard-pro.yaml create mode 100644 scripts/release-notes.js create mode 100644 src/utils/css-escape.js create mode 100644 src/utils/css-escape.spec.js diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index da8935aeeeb..bb6383bcf9a 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -2,27 +2,27 @@ "files": [ { "path": "./dist/bootstrap-vue-icons.js", - "maxSize": "75 kB" + "maxSize": "105 kB" }, { "path": "./dist/bootstrap-vue-icons.min.js", - "maxSize": "70 kB" + "maxSize": "100 kB" }, { "path": "./dist/bootstrap-vue-icons.common.js", - "maxSize": "80 kB" + "maxSize": "110 kB" }, { "path": "./dist/bootstrap-vue-icons.common.min.js", - "maxSize": "75 kB" + "maxSize": "105 kB" }, { "path": "./dist/bootstrap-vue-icons.esm.js", - "maxSize": "80 kB" + "maxSize": "110 kB" }, { "path": "./dist/bootstrap-vue-icons.esm.min.js", - "maxSize": "75 kB" + "maxSize": "105 kB" }, { "path": "./dist/bootstrap-vue-icons.css", @@ -42,19 +42,19 @@ }, { "path": "./dist/bootstrap-vue.common.js", - "maxSize": "275 kB" + "maxSize": "305 kB" }, { "path": "./dist/bootstrap-vue.common.min.js", - "maxSize": "165 kB" + "maxSize": "190 kB" }, { "path": "./dist/bootstrap-vue.esm.js", - "maxSize": "270 kB" + "maxSize": "300 kB" }, { "path": "./dist/bootstrap-vue.esm.min.js", - "maxSize": "160 kB" + "maxSize": "190 kB" }, { "path": "./dist/bootstrap-vue.css", diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000000..0b952659914 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,55 @@ +name: Build + +on: + push: + branches: + - dev + - master + pull_request: + branches: + - dev + - master + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest] + node: [10, 12] + + steps: + - name: Clone repository + uses: actions/checkout@v2 + + - name: Set Node.js version + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node }} + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: Cache node_modules + uses: actions/cache@v2 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ matrix.os }}-node-v${{ matrix.node }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ matrix.os }}-node-v${{ matrix.node }}-yarn- + ${{ matrix.os }}-node-v${{ matrix.node }}- + ${{ matrix.os }}- + + - name: Install dependencies + run: yarn install --check-files --frozen-lockfile --non-interactive + + - name: Build + run: yarn run build + + - name: BundleWatch + run: yarn run bundlewatch + if: matrix.node == '12' + env: + BUNDLEWATCH_GITHUB_TOKEN: "${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f16a470e4a7..b11c217ac57 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,14 +11,13 @@ on: - master jobs: - setup: + lint: runs-on: ${{ matrix.os }} - if: github.event_name == 'push' || !(github.base_ref == 'master' && github.head_ref == 'dev') strategy: matrix: os: [ubuntu-latest] - node: [10, 12] + node: [12] steps: - name: Clone repository @@ -34,53 +33,22 @@ jobs: run: echo "::set-output name=dir::$(yarn cache dir)" - name: Cache node_modules - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-node-v${{ matrix.node }}-${{ hashFiles('yarn.lock') }} + key: ${{ matrix.os }}-node-v${{ matrix.node }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | - ${{ runner.os }}-node-v${{ matrix.node }}-${{ hashFiles('yarn.lock') }} - ${{ runner.OS }}-node-v${{ matrix.node }}- - ${{ runner.OS }}- + ${{ matrix.os }}-node-v${{ matrix.node }}-yarn- + ${{ matrix.os }}-node-v${{ matrix.node }}- + ${{ matrix.os }}- - name: Install dependencies run: yarn install --check-files --frozen-lockfile --non-interactive - - name: Cache workspace - uses: actions/cache@v1 - with: - path: ${{ github.workspace }} - key: ${{ matrix.os }}-node-v${{ matrix.node }}-bootstrap-vue-${{ github.sha }} - - lint: - needs: setup - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest] - node: [12] - - steps: - - name: Clone repository - uses: actions/checkout@v2 - - - name: Set Node.js version - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node }} - - - name: Restore workspace cache - uses: actions/cache@v1 - with: - path: ${{ github.workspace }} - key: ${{ matrix.os }}-node-v${{ matrix.node }}-bootstrap-vue-${{ github.sha }} - - name: Lint run: yarn run test:lint audit: - needs: setup runs-on: ${{ matrix.os }} strategy: @@ -97,17 +65,27 @@ jobs: with: node-version: ${{ matrix.node }} - - name: Restore workspace cache - uses: actions/cache@v1 + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: Cache node_modules + uses: actions/cache@v2 with: - path: ${{ github.workspace }} - key: ${{ matrix.os }}-node-v${{ matrix.node }}-bootstrap-vue-${{ github.sha }} + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ matrix.os }}-node-v${{ matrix.node }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ matrix.os }}-node-v${{ matrix.node }}-yarn- + ${{ matrix.os }}-node-v${{ matrix.node }}- + ${{ matrix.os }}- + + - name: Install dependencies + run: yarn install --check-files --frozen-lockfile --non-interactive - name: Audit run: yarn run audit test-unit: - needs: setup runs-on: ${{ matrix.os }} strategy: @@ -124,86 +102,29 @@ jobs: with: node-version: ${{ matrix.node }} - - name: Restore workspace cache - uses: actions/cache@v1 + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: Cache node_modules + uses: actions/cache@v2 with: - path: ${{ github.workspace }} - key: ${{ matrix.os }}-node-v${{ matrix.node }}-bootstrap-vue-${{ github.sha }} + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ matrix.os }}-node-v${{ matrix.node }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ matrix.os }}-node-v${{ matrix.node }}-yarn- + ${{ matrix.os }}-node-v${{ matrix.node }}- + ${{ matrix.os }}- + + - name: Install dependencies + run: yarn install --check-files --frozen-lockfile --non-interactive - name: Test unit run: yarn run test:unit --coverage --maxWorkers=2 - name: CodeCov uses: codecov/codecov-action@v1 - if: matrix.node == '10' + if: matrix.node == '12' with: token: ${{ secrets.CODECOV_TOKEN }} flags: unittests - - build: - needs: setup - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest] - node: [12] - - steps: - - name: Clone repository - uses: actions/checkout@v2 - - - name: Set Node.js version - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node }} - - - name: Restore workspace cache - uses: actions/cache@v1 - with: - path: ${{ github.workspace }} - key: ${{ matrix.os }}-node-v${{ matrix.node }}-bootstrap-vue-${{ github.sha }} - - - name: Build - run: yarn run build - - - name: Upload build files - uses: actions/upload-artifact@v1 - with: - name: build-files - path: dist - - bundlewatch: - needs: build - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest] - node: [12] - - steps: - - name: Clone repository - uses: actions/checkout@v2 - - - name: Set Node.js version - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node }} - - - name: Restore workspace cache - uses: actions/cache@v1 - with: - path: ${{ github.workspace }} - key: ${{ matrix.os }}-node-v${{ matrix.node }}-bootstrap-vue-${{ github.sha }} - - - name: Download build files - uses: actions/download-artifact@v1 - with: - name: build-files - path: dist - - - name: BundleWatch - run: yarn run bundlewatch - env: - BUNDLEWATCH_GITHUB_TOKEN: "${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}" diff --git a/.gitignore b/.gitignore index 95d1c3b005c..7b3e9532b73 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,6 @@ node_modules/ *.log *.swp .DS_Store +RELEASE-NOTES.md sw.js workbox*.js* diff --git a/CHANGELOG.md b/CHANGELOG.md index a3485b54630..5fbe23e4d0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,49 @@ > All notable changes to this project will be documented in this file. + + +## [2.16.0](https://github.com/bootstrap-vue/bootstrap-vue/compare/v2.15.0...v2.16.0) (2020-07-27) + +Released: 2020-07-27 + +### Features v2.16.0 + +- **b-form-tags:** add `ignoreInputFocusSelector` prop to make input focus behavior configurable + (closes [#5425](https://github.com/bootstrap-vue/bootstrap-vue/issues/5425)) + ([#5429](https://github.com/bootstrap-vue/bootstrap-vue/issues/5429)) + ([26d5953](https://github.com/bootstrap-vue/bootstrap-vue/commit/26d5953f834684d36b0af99da912dba08fd37bd8)) +- **docs:** launch themes page with first BootstrapVue theme + ([#5549](https://github.com/bootstrap-vue/bootstrap-vue/issues/5549)) + ([ec51ef0](https://github.com/bootstrap-vue/bootstrap-vue/commit/ec51ef062f7ed39339cde59b2d9d4cee40347dcc)) + +### Bug Fixes v2.16.0 + +- properly handle special characters in user-provided IDs (closes + [#4927](https://github.com/bootstrap-vue/bootstrap-vue/issues/4927), + [#5561](https://github.com/bootstrap-vue/bootstrap-vue/issues/5561)) + ([#5564](https://github.com/bootstrap-vue/bootstrap-vue/issues/5564)) + ([1fabd68](https://github.com/bootstrap-vue/bootstrap-vue/commit/1fabd68bb44b28a9127810f35bd07e1fdf3d12ec)) +- **b-form-checkbox-group:** only emit `input` when value loosely changes + ([#5432](https://github.com/bootstrap-vue/bootstrap-vue/issues/5432)) + ([e76d408](https://github.com/bootstrap-vue/bootstrap-vue/commit/e76d40874bd2a42126162101e94bb18e9042840b)) +- **b-form-tags:** unit test ([#5586](https://github.com/bootstrap-vue/bootstrap-vue/issues/5586)) + ([f4d509a](https://github.com/bootstrap-vue/bootstrap-vue/commit/f4d509af647eaf87e2b635d08ff9431b25150650)) +- **b-icon:** use `aria-label` attribute instead of `alt` + ([#5581](https://github.com/bootstrap-vue/bootstrap-vue/issues/5581)) + ([72a1363](https://github.com/bootstrap-vue/bootstrap-vue/commit/72a13635b94aedfab1fb6800f2a297fa306f63ef)) +- **b-img:** Allow empty `alt` prop (fixes + [#5524](https://github.com/bootstrap-vue/bootstrap-vue/issues/5524)) + ([#5545](https://github.com/bootstrap-vue/bootstrap-vue/issues/5545)) + ([b22829d](https://github.com/bootstrap-vue/bootstrap-vue/commit/b22829d064b6e3820ef66168ec766a57520f31eb)) +- **b-table:** prevent endless reevaluation when using v-model and object/array literal prop values + ([#5554](https://github.com/bootstrap-vue/bootstrap-vue/issues/5554)) + ([f127d91](https://github.com/bootstrap-vue/bootstrap-vue/commit/f127d916d1ddd3a3da37bcb081150f86b356a7a4)) +- **b-tags:** replace spacing utility with static CSS (fixes + [#5523](https://github.com/bootstrap-vue/bootstrap-vue/issues/5523)) + ([#5544](https://github.com/bootstrap-vue/bootstrap-vue/issues/5544)) + ([e0de687](https://github.com/bootstrap-vue/bootstrap-vue/commit/e0de6871640db405e7b0bfa23f3c33f348894cea)) + ## [v2.15.0](https://github.com/bootstrap-vue/bootstrap-vue/compare/v2.14.0...v2.15.0) @@ -40,7 +83,7 @@ Released: 2020-05-22 ### Bug Fixes v2.15.0 -- **v-b-toggle:** don't check for evt.defaultPrevened (closes +- **v-b-toggle:** don't check for evt.defaultPrevented (closes [#5391](https://github.com/bootstrap-vue/bootstrap-vue/issues/5391)) ([#5396](https://github.com/bootstrap-vue/bootstrap-vue/issues/5396)) ([a1543b2](https://github.com/bootstrap-vue/bootstrap-vue/commit/a1543b297040ea593306ec55d7de5f1e2e776bce)) @@ -333,7 +376,7 @@ Released: 2020-03-25 [#4990](https://github.com/bootstrap-vue/bootstrap-vue/issues/4990)) ([#4991](https://github.com/bootstrap-vue/bootstrap-vue/issues/4991)) ([d1474f2](https://github.com/bootstrap-vue/bootstrap-vue/commit/d1474f28729e4e13ad97b75a87d56f85543d4c96)) -- **b-drodpown-item-button, b-drodpown-item-button:** add `button-class` and `link-class` prop +- **b-dropdown-item-button, b-dropdown-item-button:** add `button-class` and `link-class` prop ([#5014](https://github.com/bootstrap-vue/bootstrap-vue/issues/5014)) ([b39d31c](https://github.com/bootstrap-vue/bootstrap-vue/commit/b39d31cede76b594b5608fa472d53e3dac525e2b)) - **b-form-datepicker, b-form-timepicker:** emit `shown` and `hidden` events diff --git a/README.md b/README.md index 4803aefd2da..ca6ea40e486 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,10 @@

- BootstrapVue, with over 40 available plugins, more than 80 custom components, and over 670 icons, - provides one of the most comprehensive implementations of the Bootstrap v4 component and grid - system for Vue.js, complete with extensive and automated WAI-ARIA accessibility markup. + With more than 85 components, over 45 available plugins, several directives, and 1000+ icons, + BootstrapVue provides one of the most comprehensive implementations of the Bootstrap v4.5 + component and grid system available for Vue.js v2.6, complete with extensive and automated + WAI-ARIA accessibility markup.


@@ -17,7 +18,7 @@ Current version - Bootstrap version + Bootstrap version Vue.js version @@ -35,11 +36,14 @@ Package quality + + Code quality + npm downloads - npm monthly downloads + npm weekly downloads
diff --git a/docs/assets/vercel.svg b/docs/assets/vercel.svg index 021ab2ae538..22f186cb965 100644 --- a/docs/assets/vercel.svg +++ b/docs/assets/vercel.svg @@ -1,3 +1,3 @@ - - - + + + diff --git a/docs/components/footer.vue b/docs/components/footer.vue index 10693c00c4b..bfc59c9020c 100644 --- a/docs/components/footer.vue +++ b/docs/components/footer.vue @@ -19,12 +19,10 @@
  • Reference
  • Playground
  • - diff --git a/docs/components/header.vue b/docs/components/header.vue index b6505dea2bd..aba7808608c 100644 --- a/docs/components/header.vue +++ b/docs/components/header.vue @@ -35,9 +35,7 @@ Directives Icons Reference - Play diff --git a/docs/components/icons-table.vue b/docs/components/icons-table.vue index 65bb9852aa5..46b88375d18 100644 --- a/docs/components/icons-table.vue +++ b/docs/components/icons-table.vue @@ -42,18 +42,18 @@ -
    + -
    - {{ icon.name }} + + {{ icon.name }}
    @@ -121,6 +121,20 @@ .flip-icon-list-leave-active { position: absolute; } + +@media (min-width: 1200px) { + .row-cols-xl-8 > * { + flex: 0 0 12.5%; + max-width: 12.5%; + } +} + +@media (min-width: 1400px) { + .row-cols-xxl-10 > * { + flex: 0 0 10%; + max-width: 10%; + } +} diff --git a/docs/pages/themes.vue b/docs/pages/themes.vue index f674f0ebb43..27de9b877a9 100644 --- a/docs/pages/themes.vue +++ b/docs/pages/themes.vue @@ -4,7 +4,7 @@

    Custom themes and dashboards

    - With the below themes and dashboards built by our partners, you can build eye-catching + With the themes and dashboards built by our partners, you can build eye-catching apps and pages — all using BootstrapVue! The following items have been curated by the BootstrapVue team.

    @@ -26,8 +26,14 @@ > - @@ -75,7 +76,7 @@ site documentation for licensing information.
  • - BootstrapVue does not guarantee that all coustom components provided by a theme are + BootstrapVue does not guarantee that all custom components provided by a theme are WIA-ARIA compliant. Refer to the provider documentation for details.
  • @@ -146,56 +147,22 @@ import BvLogo from '~/components/bv-logo' export default { components: { BvLogo }, - data() { + async asyncData({ $content }) { + // Themes are stored as YAML files in `docs/content/themes` + // The theme preview image should be 800x400px (and 4:3 aspect ratio) + // Data structure: + // title: 'Superduper Dashboard - PRO' + // type: 'dashboard' + // category: 'Admin & Dashboard' + // img: 'https://picsum.photos/800/600/?image=84' + // href: '#' + // description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' + // provider: 'Innovative Ivan' + // price: '$100.00' + const themes = await $content('themes').fetch() + return { - // This could be async data that comes from a JSON file - // Theme image preview should be 800x400px (and 4:3 aspect ratio) - themes: [ - /* - { - title: 'Superduper Dashboard - PRO', - type: 'dashboard', - category: 'Admin & Dashboard', - img: 'https://picsum.photos/800/600/?image=84', - href: '#', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', - provider: 'Innovative Ivan', - price: '$500.00' - }, - { - title: 'Funky dashboard extreme', - type: 'dashboard', - category: 'Admin & Dashboard', - img: 'https://picsum.photos/800/600/?image=82', - href: '#', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', - provider: 'Dashboards-R-Us', - price: 'Free' - }, - { - title: 'Some mystery theme theatre', - img: 'https://picsum.photos/800/600/?image=54', - category: 'Landing & Corporate', - href: '#', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', - provider: 'Cyberdyne Terminators', - price: '$75.00' - }, - { - title: 'Shopper Style Galore', - img: 'https://picsum.photos/800/600/?image=90', - category: 'E-Commerce & Retail', - href: '#', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', - provider: 'Cyberdyne Terminators', - price: '$75.00' - } - */ - ] + themes } }, computed: { diff --git a/package.json b/package.json index e23b70589fe..86a606966f2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bootstrap-vue", - "version": "2.15.0", - "description": "BootstrapVue, with more than 85 custom components, over 45 plugins, several custom directives, and over 670 icons, provides one of the most comprehensive implementations of Bootstrap v4 components and grid system for Vue.js. With extensive and automated WAI-ARIA accessibility markup.", + "version": "2.16.0", + "description": "With more than 85 components, over 45 available plugins, several directives, and 1000+ icons, BootstrapVue provides one of the most comprehensive implementations of the Bootstrap v4 component and grid system available for Vue.js v2.6, complete with extensive and automated WAI-ARIA accessibility markup.", "main": "dist/bootstrap-vue.common.js", "web": "dist/bootstrap-vue.js", "module": "esm/index.js", @@ -31,6 +31,10 @@ "name": "Jacob Müller", "url": "https://github.com/jackmu95" }, + { + "name": "Hiws", + "url": "https://github.com/Hiws" + }, { "name": "Vitaly Mosin", "url": "https://github.com/mosinve" @@ -71,7 +75,8 @@ "lint": "eslint --ext .js,.md,.vue ./", "postinstall": "opencollective || exit 0", "prettify": "prettier --write '**/*.{js,json,md,scss,ts,vue}'", - "release": "yarn run prettify && yarn run test && yarn run build && standard-version", + "release-notes": "node -r esm scripts/release-notes.js", + "release": "yarn run prettify && yarn run test && yarn run build && yarn run release-notes && standard-version", "test": "yarn run test:lint && yarn run test:unit", "test:lint": "yarn run lint", "test:unit": "jest", @@ -84,10 +89,7 @@ }, "lint-staged": { "*{.js,.md,.vue}": "eslint --ext", - "**/*.{js,json,md,scss,ts,vue}": [ - "prettier --write", - "git add" - ] + "**/*.{js,json,md,scss,ts,vue}": "prettier --write" }, "dependencies": { "@nuxt/opencollective": "^0.3.0", @@ -97,68 +99,69 @@ "vue-functional-data-merge": "^3.1.0" }, "devDependencies": { - "@babel/cli": "^7.8.4", - "@babel/core": "^7.9.6", - "@babel/plugin-transform-modules-commonjs": "^7.9.6", - "@babel/plugin-transform-runtime": "^7.9.6", - "@babel/preset-env": "^7.9.6", - "@babel/standalone": "^7.9.6", - "@nuxtjs/google-analytics": "^2.3.0", + "@babel/cli": "^7.10.5", + "@babel/core": "^7.10.5", + "@babel/plugin-transform-modules-commonjs": "^7.10.4", + "@babel/plugin-transform-runtime": "^7.10.5", + "@babel/preset-env": "^7.10.4", + "@babel/standalone": "^7.10.5", + "@nuxt/content": "^1.5.0", + "@nuxtjs/google-analytics": "^2.4.0", "@nuxtjs/pwa": "^3.0.0-beta.20", "@nuxtjs/robots": "^2.4.2", - "@nuxtjs/sitemap": "^2.3.0", - "@testing-library/jest-dom": "^5.8.0", + "@nuxtjs/sitemap": "^2.4.0", + "@testing-library/jest-dom": "^5.11.1", "@vue/test-utils": "^1.0.3", - "autoprefixer": "^9.8.0", + "autoprefixer": "^9.8.5", "babel-core": "^7.0.0-bridge.0", "babel-eslint": "^10.1.0", - "babel-jest": "^26.0.1", + "babel-jest": "^26.1.0", "babel-plugin-istanbul": "^6.0.0", - "bootstrap-icons": "^1.0.0-alpha4", + "bootstrap-icons": "^1.0.0-alpha5", "bundlewatch": "^0.2.7", "clean-css-cli": "^4.3.0", - "codemirror": "^5.54.0", - "codesandbox": "^2.1.14", + "codemirror": "^5.56.0", + "codesandbox": "^2.1.16", "core-js": ">=2.6.5 <3.0.0", "cross-env": "^7.0.2", - "eslint": "^7.0.0", + "eslint": "^7.5.0", "eslint-config-prettier": "^6.11.0", "eslint-config-standard": "^14.1.1", "eslint-config-vue": "^2.0.2", - "eslint-plugin-import": "^2.20.2", - "eslint-plugin-jest": "^23.13.1", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-jest": "^23.19.0", "eslint-plugin-markdown": "^1.0.2", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prettier": "^3.1.3", + "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1", "eslint-plugin-vue": "^6.2.2", "esm": "^3.2.25", - "gh-pages": "^2.2.0", + "execa": "^4.0.3", "highlight.js": "^9.18.1", "html-loader": "^1.1.0", "husky": "^4.2.5", - "improved-yarn-audit": "^2.1.0", - "jest": "^26.0.1", - "lint-staged": "^10.2.6", + "improved-yarn-audit": "^2.2.1", + "jest": "^26.1.0", + "lint-staged": "^10.2.11", "loader-utils": "^2.0.0", - "lodash": "^4.17.15", - "marked": "^1.1.0", + "lodash": "^4.17.19", + "marked": "^1.1.1", "node-sass": "^4.14.1", - "nuxt": "^2.12.2", + "nuxt": "^2.14.0", "postcss-cli": "^7.1.1", "prettier": "1.14.3", "require-context": "^1.1.0", - "rollup": "^2.10.7", + "rollup": "^2.23.0", "rollup-plugin-babel": "^4.4.0", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-node-resolve": "^5.2.0", - "sass-loader": "^8.0.2", - "standard-version": "^8.0.0", - "terser": "^4.7.0", + "sass-loader": "^9.0.2", + "standard-version": "^8.0.2", + "terser": "^4.8.0", "vue": "^2.6.11", - "vue-jest": "^3.0.5", - "vue-router": "^3.2.0", + "vue-jest": "^3.0.6", + "vue-router": "^3.3.4", "vue-server-renderer": "^2.6.11", "vue-template-compiler": "^2.6.11" }, diff --git a/scripts/release-notes.js b/scripts/release-notes.js new file mode 100644 index 00000000000..a8b4b94e9a3 --- /dev/null +++ b/scripts/release-notes.js @@ -0,0 +1,173 @@ +const execa = require('execa') +const fs = require('fs').promises +const groupBy = require('lodash/groupBy') +const path = require('path') +const sortBy = require('lodash/sortBy') +const uniq = require('lodash/uniq') + +const baseDir = path.resolve(__dirname, '..') +const pkg = require(path.resolve(baseDir, 'package.json')) + +// --- Constants --- + +const FILE_NAME = 'RELEASE-NOTES.md' + +const TYPES = { + feat: { title: '🚀 Features' }, + fix: { title: '🐛 Bug Fixes' }, + perf: { title: '🔥 Performance' }, + refactor: { title: '💅 Refactors' }, + chore: { title: '🏡 Chore' }, + test: { title: '👓 Tests' }, + types: { title: '🇹 Types' } +} + +const ALLOWED_TYPES = Object.keys(TYPES) + +const IGNORE_SCOPES = ['deps', 'deps-dev'] + +const KNOWN_AUTHORS = pkg.contributors.map(c => c.name.toLowerCase()) + +// --- Helper methods --- + +const execCommand = (cmd, args) => execa(cmd, args).then(r => r.stdout) + +const isKnownAuthor = name => !!KNOWN_AUTHORS.find(author => name.toLowerCase().includes(author)) + +const getLastGitTag = () => execCommand('git', ['describe', '--tags', '--abbrev=0']) +const getCurrentGitBranch = () => execCommand('git', ['rev-parse', '--abbrev-ref', 'HEAD']) + +// https://git-scm.com/docs/pretty-formats +const getGitDiff = async (from, to) => { + const result = await execCommand('git', [ + '--no-pager', + 'log', + `${from}...${to}`, + '--pretty=%s|%h|%an|%ae' + ]) + return result.split('\n').map(line => { + const [message, commit, authorName, authorEmail] = line.split('|') + return { message, commit, authorName, authorEmail } + }) +} + +const parseCommits = commits => { + const referencesRegex = /#[0-9]+/g + + return commits.filter(c => c.message.includes(':')).map(commit => { + let [type, ...message] = commit.message.split(':') + message = message.join(':') + + // Extract references from message + message = message.replace(/\((closes|fixes) #\d[^)]+\)/g, '') + const references = [] + let referenceMatch + while ((referenceMatch = referencesRegex.exec(message))) { + references.push(referenceMatch[0]) + } + + // Remove references and normalize + message = message + .replace(referencesRegex, '') + .replace(/\(\)/g, '') + .trim() + + // Extract scope from type + let scope = type.match(/\((.*)\)/) + if (scope) { + scope = scope[1] + } + if (!scope) { + scope = 'general' + } + type = type.split('(')[0] + + return { + ...commit, + message, + type, + scope, + references + } + }) +} + +const generateMarkDown = commits => { + const typeGroups = groupBy(commits, 'type') + const emptyLine = '' + const lines = [] + + for (const type of ALLOWED_TYPES) { + // Get commits by type + const commitsForType = typeGroups[type] + + // Early exit when we have no commits for the current group + if (!commitsForType || commitsForType.length === 0) { + continue + } + + // Add type group title + const { title } = TYPES[type] + lines.push(emptyLine, `### ${title}`, emptyLine) + + // Group commits by scope and loop through them + const scopeGroups = groupBy(commitsForType, 'scope') + for (const scopeName in scopeGroups) { + // Add type group title + lines.push(`- \`${scopeName}\``) + + // Add commits + for (const commit of scopeGroups[scopeName]) { + lines.push( + ' - ' + + (commit.references.length > 0 ? commit.references.join(', ') : commit.commit) + + ' ' + + commit.message.replace(/^(.)/, v => v.toUpperCase()) + ) + } + } + + // Add final empty line + lines.push(emptyLine) + } + + // Add authors + const authors = sortBy( + uniq(commits.map(commit => commit.authorName).filter(author => !isKnownAuthor(author))) + ) + if (authors.length > 0) { + lines.push( + emptyLine, + '### 💖 Thanks to', + emptyLine, + ...authors.map(author => `- ${author}`), + emptyLine + ) + } + + return lines.join('\n').trim() +} + +// --- Main command --- + +const main = async () => { + // Get last git tag + const lastGitTag = await getLastGitTag() + + // Get current branch + const currentGitBranch = await getCurrentGitBranch() + + // Get all commits from last release to current branch + let commits = await getGitDiff(currentGitBranch, lastGitTag) + + // Parse commits as conventional commits + commits = parseCommits(commits) + + // Filter commits + commits = commits.filter(c => ALLOWED_TYPES.includes(c.type) && !IGNORE_SCOPES.includes(c.scope)) + + // Write markdown file + await fs.writeFile(FILE_NAME, generateMarkDown(commits), 'utf-8') +} + +main() diff --git a/src/components/aspect/README.md b/src/components/aspect/README.md index c2de9ead36e..e08abd0aece 100644 --- a/src/components/aspect/README.md +++ b/src/components/aspect/README.md @@ -1,65 +1,65 @@ -# Aspect - -> The `` component can be used to maintain a minimum responsive aspect ratio for content. -> When the content is longer than the available height, then the component will expand vertically to -> fit all content. If the content is shorter than the computed aspect height, the component will -> ensure a minimum height is maintained. - -## Overview - -The default [aspect]() ratio is `1:1` (ratio of -`1`), which makes the height always be at least the same as the width. The `aspect` prop can be used -to specify an arbitrary aspect ratio (i.e. `1.5`) or a ratio as a string such as `'16:9'` or -`'4:3'`. - -The width will always be 100% of the available width in the parent element/component. - -```html - - - - - -``` - -## See also - -- [`` component](/docs/components/embed) for responsive embeds (videos, iframes, etc) +# Aspect + +> The `` component can be used to maintain a minimum responsive aspect ratio for content. +> When the content is longer than the available height, then the component will expand vertically to +> fit all content. If the content is shorter than the computed aspect height, the component will +> ensure a minimum height is maintained. + +## Overview + +The default [aspect]() ratio is `1:1` (ratio of +`1`), which makes the height always be at least the same as the width. The `aspect` prop can be used +to specify an arbitrary aspect ratio (i.e. `1.5`) or a ratio as a string such as `'16:9'` or +`'4:3'`. + +The width will always be 100% of the available width in the parent element/component. + +```html + + + + + +``` + +## See also + +- [`` component](/docs/components/embed) for responsive embeds (videos, iframes, etc) diff --git a/src/components/aspect/aspect.js b/src/components/aspect/aspect.js index 21ca225e441..a2e06531e99 100644 --- a/src/components/aspect/aspect.js +++ b/src/components/aspect/aspect.js @@ -1,59 +1,59 @@ -import Vue from '../../utils/vue' -import { mathAbs } from '../../utils/math' -import { toFloat } from '../../utils/number' -import normalizeSlotMixin from '../../mixins/normalize-slot' - -// --- Constants --- -const NAME = 'BAspect' -const CLASS_NAME = 'b-aspect' - -const RX_ASPECT = /^\d+(\.\d*)?[/:]\d+(\.\d*)?$/ -const RX_SEPARATOR = /[/:]/ - -// --- Main Component --- -export const BAspect = /*#__PURE__*/ Vue.extend({ - name: NAME, - mixins: [normalizeSlotMixin], - props: { - aspect: { - // Accepts a number (i.e. `16 / 9`, `1`, `4 / 3`) - // Or a string (i.e. '16/9', '16:9', '4:3' '1:1') - type: [Number, String], - default: '1:1' - }, - tag: { - type: String, - default: 'div' - } - }, - computed: { - padding() { - const aspect = this.aspect - let ratio = 1 - if (RX_ASPECT.test(aspect)) { - // Width and/or Height can be a decimal value below `1`, so - // we only fallback to `1` if the value is `0` or `NaN` - const [width, height] = aspect.split(RX_SEPARATOR).map(v => toFloat(v) || 1) - ratio = width / height - } else { - ratio = toFloat(aspect) || 1 - } - return `${100 / mathAbs(ratio)}%` - } - }, - render(h) { - const $sizer = h('div', { - staticClass: `${CLASS_NAME}-sizer flex-grow-1`, - style: { paddingBottom: this.padding, height: 0 } - }) - const $content = h( - 'div', - { - staticClass: `${CLASS_NAME}-content flex-grow-1 w-100 mw-100`, - style: { marginLeft: '-100%' } - }, - [this.normalizeSlot('default')] - ) - return h(this.tag, { staticClass: `${CLASS_NAME} d-flex` }, [$sizer, $content]) - } -}) +import Vue from '../../utils/vue' +import { mathAbs } from '../../utils/math' +import { toFloat } from '../../utils/number' +import normalizeSlotMixin from '../../mixins/normalize-slot' + +// --- Constants --- +const NAME = 'BAspect' +const CLASS_NAME = 'b-aspect' + +const RX_ASPECT = /^\d+(\.\d*)?[/:]\d+(\.\d*)?$/ +const RX_SEPARATOR = /[/:]/ + +// --- Main Component --- +export const BAspect = /*#__PURE__*/ Vue.extend({ + name: NAME, + mixins: [normalizeSlotMixin], + props: { + aspect: { + // Accepts a number (i.e. `16 / 9`, `1`, `4 / 3`) + // Or a string (i.e. '16/9', '16:9', '4:3' '1:1') + type: [Number, String], + default: '1:1' + }, + tag: { + type: String, + default: 'div' + } + }, + computed: { + padding() { + const aspect = this.aspect + let ratio = 1 + if (RX_ASPECT.test(aspect)) { + // Width and/or Height can be a decimal value below `1`, so + // we only fallback to `1` if the value is `0` or `NaN` + const [width, height] = aspect.split(RX_SEPARATOR).map(v => toFloat(v) || 1) + ratio = width / height + } else { + ratio = toFloat(aspect) || 1 + } + return `${100 / mathAbs(ratio)}%` + } + }, + render(h) { + const $sizer = h('div', { + staticClass: `${CLASS_NAME}-sizer flex-grow-1`, + style: { paddingBottom: this.padding, height: 0 } + }) + const $content = h( + 'div', + { + staticClass: `${CLASS_NAME}-content flex-grow-1 w-100 mw-100`, + style: { marginLeft: '-100%' } + }, + [this.normalizeSlot('default')] + ) + return h(this.tag, { staticClass: `${CLASS_NAME} d-flex` }, [$sizer, $content]) + } +}) diff --git a/src/components/aspect/aspect.spec.js b/src/components/aspect/aspect.spec.js index 60bcbc552e6..6813631451e 100644 --- a/src/components/aspect/aspect.spec.js +++ b/src/components/aspect/aspect.spec.js @@ -1,121 +1,121 @@ -import { mount } from '@vue/test-utils' -import { BAspect } from './aspect' - -describe('aspect', () => { - it('should have expected default structure', async () => { - const wrapper = mount(BAspect) - - expect(wrapper.vm).toBeDefined() - expect(wrapper.element.tagName).toBe('DIV') - expect(wrapper.classes()).toContain('b-aspect') - expect(wrapper.classes()).toContain('d-flex') - expect(wrapper.classes().length).toBe(2) - - const $sizer = wrapper.find('.b-aspect-sizer') - expect($sizer.exists()).toBe(true) - expect($sizer.element.tagName).toBe('DIV') - expect($sizer.classes()).toContain('flex-grow-1') - // Default aspect ratio is 1:1 - expect($sizer.attributes('style')).toContain('padding-bottom: 100%;') - - const $content = wrapper.find('.b-aspect-content') - expect($content.exists()).toBe(true) - expect($content.element.tagName).toBe('DIV') - expect($content.classes()).toContain('flex-grow-1') - expect($content.classes()).toContain('w-100') - expect($content.classes()).toContain('mw-100') - expect($content.attributes('style')).toContain('margin-left: -100%;') - - wrapper.destroy() - }) - - it('should have expected structure when prop `tag` is set', async () => { - const wrapper = mount(BAspect, { - propsData: { - tag: 'section' - } - }) - - expect(wrapper.vm).toBeDefined() - expect(wrapper.element.tagName).toBe('SECTION') - expect(wrapper.classes()).toContain('b-aspect') - expect(wrapper.classes()).toContain('d-flex') - expect(wrapper.classes().length).toBe(2) - - const $sizer = wrapper.find('.b-aspect-sizer') - expect($sizer.exists()).toBe(true) - expect($sizer.element.tagName).toBe('DIV') - expect($sizer.classes()).toContain('flex-grow-1') - // Default aspect ratio is 1:1 - expect($sizer.attributes('style')).toContain('padding-bottom: 100%;') - - const $content = wrapper.find('.b-aspect-content') - expect($content.exists()).toBe(true) - expect($content.element.tagName).toBe('DIV') - expect($content.classes()).toContain('flex-grow-1') - expect($content.classes()).toContain('w-100') - expect($content.classes()).toContain('mw-100') - expect($content.attributes('style')).toContain('margin-left: -100%;') - - wrapper.destroy() - }) - - it('should have expected structure when aspect is set to "4:3"', async () => { - const wrapper = mount(BAspect, { - propsData: { - aspect: '4:3' - } - }) - - expect(wrapper.vm).toBeDefined() - expect(wrapper.element.tagName).toBe('DIV') - expect(wrapper.classes()).toContain('b-aspect') - expect(wrapper.classes()).toContain('d-flex') - expect(wrapper.classes().length).toBe(2) - - const $sizer = wrapper.find('.b-aspect-sizer') - expect($sizer.exists()).toBe(true) - expect($sizer.element.tagName).toBe('DIV') - expect($sizer.classes()).toContain('flex-grow-1') - expect($sizer.attributes('style')).toContain('padding-bottom: 75%;') - - const $content = wrapper.find('.b-aspect-content') - expect($content.exists()).toBe(true) - expect($content.element.tagName).toBe('DIV') - expect($content.classes()).toContain('flex-grow-1') - expect($content.classes()).toContain('w-100') - expect($content.classes()).toContain('mw-100') - expect($content.attributes('style')).toContain('margin-left: -100%;') - - wrapper.destroy() - }) - it('should have expected structure when aspect is set to `16/9`', async () => { - const wrapper = mount(BAspect, { - propsData: { - aspect: 16 / 9 - } - }) - - expect(wrapper.vm).toBeDefined() - expect(wrapper.element.tagName).toBe('DIV') - expect(wrapper.classes()).toContain('b-aspect') - expect(wrapper.classes()).toContain('d-flex') - expect(wrapper.classes().length).toBe(2) - - const $sizer = wrapper.find('.b-aspect-sizer') - expect($sizer.exists()).toBe(true) - expect($sizer.element.tagName).toBe('DIV') - expect($sizer.classes()).toContain('flex-grow-1') - expect($sizer.attributes('style')).toContain('padding-bottom: 56.25%;') - - const $content = wrapper.find('.b-aspect-content') - expect($content.exists()).toBe(true) - expect($content.element.tagName).toBe('DIV') - expect($content.classes()).toContain('flex-grow-1') - expect($content.classes()).toContain('w-100') - expect($content.classes()).toContain('mw-100') - expect($content.attributes('style')).toContain('margin-left: -100%;') - - wrapper.destroy() - }) -}) +import { mount } from '@vue/test-utils' +import { BAspect } from './aspect' + +describe('aspect', () => { + it('should have expected default structure', async () => { + const wrapper = mount(BAspect) + + expect(wrapper.vm).toBeDefined() + expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.classes()).toContain('b-aspect') + expect(wrapper.classes()).toContain('d-flex') + expect(wrapper.classes().length).toBe(2) + + const $sizer = wrapper.find('.b-aspect-sizer') + expect($sizer.exists()).toBe(true) + expect($sizer.element.tagName).toBe('DIV') + expect($sizer.classes()).toContain('flex-grow-1') + // Default aspect ratio is 1:1 + expect($sizer.attributes('style')).toContain('padding-bottom: 100%;') + + const $content = wrapper.find('.b-aspect-content') + expect($content.exists()).toBe(true) + expect($content.element.tagName).toBe('DIV') + expect($content.classes()).toContain('flex-grow-1') + expect($content.classes()).toContain('w-100') + expect($content.classes()).toContain('mw-100') + expect($content.attributes('style')).toContain('margin-left: -100%;') + + wrapper.destroy() + }) + + it('should have expected structure when prop `tag` is set', async () => { + const wrapper = mount(BAspect, { + propsData: { + tag: 'section' + } + }) + + expect(wrapper.vm).toBeDefined() + expect(wrapper.element.tagName).toBe('SECTION') + expect(wrapper.classes()).toContain('b-aspect') + expect(wrapper.classes()).toContain('d-flex') + expect(wrapper.classes().length).toBe(2) + + const $sizer = wrapper.find('.b-aspect-sizer') + expect($sizer.exists()).toBe(true) + expect($sizer.element.tagName).toBe('DIV') + expect($sizer.classes()).toContain('flex-grow-1') + // Default aspect ratio is 1:1 + expect($sizer.attributes('style')).toContain('padding-bottom: 100%;') + + const $content = wrapper.find('.b-aspect-content') + expect($content.exists()).toBe(true) + expect($content.element.tagName).toBe('DIV') + expect($content.classes()).toContain('flex-grow-1') + expect($content.classes()).toContain('w-100') + expect($content.classes()).toContain('mw-100') + expect($content.attributes('style')).toContain('margin-left: -100%;') + + wrapper.destroy() + }) + + it('should have expected structure when aspect is set to "4:3"', async () => { + const wrapper = mount(BAspect, { + propsData: { + aspect: '4:3' + } + }) + + expect(wrapper.vm).toBeDefined() + expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.classes()).toContain('b-aspect') + expect(wrapper.classes()).toContain('d-flex') + expect(wrapper.classes().length).toBe(2) + + const $sizer = wrapper.find('.b-aspect-sizer') + expect($sizer.exists()).toBe(true) + expect($sizer.element.tagName).toBe('DIV') + expect($sizer.classes()).toContain('flex-grow-1') + expect($sizer.attributes('style')).toContain('padding-bottom: 75%;') + + const $content = wrapper.find('.b-aspect-content') + expect($content.exists()).toBe(true) + expect($content.element.tagName).toBe('DIV') + expect($content.classes()).toContain('flex-grow-1') + expect($content.classes()).toContain('w-100') + expect($content.classes()).toContain('mw-100') + expect($content.attributes('style')).toContain('margin-left: -100%;') + + wrapper.destroy() + }) + it('should have expected structure when aspect is set to `16/9`', async () => { + const wrapper = mount(BAspect, { + propsData: { + aspect: 16 / 9 + } + }) + + expect(wrapper.vm).toBeDefined() + expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.classes()).toContain('b-aspect') + expect(wrapper.classes()).toContain('d-flex') + expect(wrapper.classes().length).toBe(2) + + const $sizer = wrapper.find('.b-aspect-sizer') + expect($sizer.exists()).toBe(true) + expect($sizer.element.tagName).toBe('DIV') + expect($sizer.classes()).toContain('flex-grow-1') + expect($sizer.attributes('style')).toContain('padding-bottom: 56.25%;') + + const $content = wrapper.find('.b-aspect-content') + expect($content.exists()).toBe(true) + expect($content.element.tagName).toBe('DIV') + expect($content.classes()).toContain('flex-grow-1') + expect($content.classes()).toContain('w-100') + expect($content.classes()).toContain('mw-100') + expect($content.attributes('style')).toContain('margin-left: -100%;') + + wrapper.destroy() + }) +}) diff --git a/src/components/aspect/index.d.ts b/src/components/aspect/index.d.ts index d2113036ca6..c084f67bdc0 100644 --- a/src/components/aspect/index.d.ts +++ b/src/components/aspect/index.d.ts @@ -1,11 +1,11 @@ -// -// Aspect -// -import Vue from 'vue' -import { BvPlugin, BvComponent } from '../../' - -// Plugin -export declare const AspectPlugin: BvPlugin - -// Component: b-aspect -export declare class BAspect extends BvComponent {} +// +// Aspect +// +import Vue from 'vue' +import { BvPlugin, BvComponent } from '../../' + +// Plugin +export declare const AspectPlugin: BvPlugin + +// Component: b-aspect +export declare class BAspect extends BvComponent {} diff --git a/src/components/aspect/index.js b/src/components/aspect/index.js index 82ca1cf7233..40b22eed368 100644 --- a/src/components/aspect/index.js +++ b/src/components/aspect/index.js @@ -1,8 +1,8 @@ -import { BAspect } from './aspect' -import { pluginFactory } from '../../utils/plugins' - -const AspectPlugin = /*#__PURE__*/ pluginFactory({ - components: { BAspect } -}) - -export { AspectPlugin, BAspect } +import { BAspect } from './aspect' +import { pluginFactory } from '../../utils/plugins' + +const AspectPlugin = /*#__PURE__*/ pluginFactory({ + components: { BAspect } +}) + +export { AspectPlugin, BAspect } diff --git a/src/components/aspect/package.json b/src/components/aspect/package.json index 5a3440f7a3e..cb585bdd58f 100644 --- a/src/components/aspect/package.json +++ b/src/components/aspect/package.json @@ -1,21 +1,21 @@ -{ - "name": "@bootstrap-vue/aspect", - "version": "1.0.0", - "meta": { - "title": "Aspect", - "new": true, - "version": "2.9.0", - "description": "The `` component can be used to maintain a minimum responsive aspect ratio for content.", - "components": [ - { - "component": "BAspect", - "props": [ - { - "prop": "aspect", - "description": "Aspect as a width to height numeric ratio (such as `1.5`) or `width:height` string (such as '16:9')" - } - ] - } - ] - } -} +{ + "name": "@bootstrap-vue/aspect", + "version": "1.0.0", + "meta": { + "title": "Aspect", + "new": true, + "version": "2.9.0", + "description": "The `` component can be used to maintain a minimum responsive aspect ratio for content.", + "components": [ + { + "component": "BAspect", + "props": [ + { + "prop": "aspect", + "description": "Aspect as a width to height numeric ratio (such as `1.5`) or `width:height` string (such as '16:9')" + } + ] + } + ] + } +} diff --git a/src/components/avatar/README.md b/src/components/avatar/README.md index 61f2e26d271..be5f07d967a 100644 --- a/src/components/avatar/README.md +++ b/src/components/avatar/README.md @@ -1,567 +1,567 @@ -# Avatar - -> Avatars are a BootstrapVue custom component, and are typically used to display a user profile as a -> picture, an icon, or short text. `` provides several props for customizing its -> appearance such as color variant and roundness, and optionally supports acting as a button, link -> or [router link](/docs/reference/router-links). - -## Overview - -Avatars are lightweight components, which render inline by default, so that they are vertically -centered beside any adjoining plain text. They also can be used as children of other components. - -```html - - - -``` - -## Avatar types - -The avatar content can be either a an image, an icon, or short text string. Avatar content defaults -to the [`'person-fill'` icon](/docs/icons) when no other content is specified. - -You can also supply custom content via the default slot, although you may need to apply additional -styling on the content. - -### Image content - -Use the `src` prop to specify a URL of an image to use as the avatar content. The image should have -an aspect ratio of `1:1` (meaning the width and height should be equal), otherwise image aspect -distortion will occur. The image will be scaled up or down to fit within the avatar's bounding box. - -```html - - - -``` - -**Notes:** - -- When using a module bundler and project relative image URLs, please refer to the - [Component img src resolving](/docs/reference/images) reference section for additional details. -- The `src` prop takes precedence over the `icon` and `text` props. -- 2.11.0+ If the image fails to load, the avatar will - fallback to the value of the `icon` or `text` props. If neither the `icon` or `text` props are - provided, then the default avatar icon will be shown. Also, when the image fails to load, the - `img-error` event will be emitted. -- [Variant colors](#variants) when using images not normally visible, unless the image fails load. - The variant will affect the focus styling when the image avatar is also an - [actionalble avatar](#actionalble-avatars). - -### Icon content - -Easily use one of [BootstrapVue's icons](/docs/icons) as the avatar content via the `icon` prop. The -prop should be set to a valid icon name. Icons will scale respective to the [`size` prop](#sizing). - -```html - - - -``` - -**Notes:** - -- When providing a BootstrapVue icon name, you _must_ ensure that you have registered the - corresponding icon component (either locally to your component/page, or globally), if not using - the full [`BootstrapVueIcons` plugin](/docs/icons). -- The `icon` prop takes precedence over the `text` prop. -- If the `text`, `src`, or `icon` props are not provided _and_ the [default slot](#custom-content) - has no content, then the `person-fill` icon will be used. - -### Text content - -You can specify a short string as the content of an avatar via the `text` prop. The string should be -short (1 to 3 characters), and will be transformed via CSS to be all uppercase. The font size will -be scaled relative to the [`size` prop setting](#sizing). - -```html - - - -``` - -### Custom content - -Use the `default` slot to render custom content in the avatar, for finer grained control of its -appearance, or if using custom icons or SVGs e.g.: - -```html - -``` - -**Multi-line text example:** - -```html - - - -``` - -**Notes:** - -- The default slot takes precedence over the `text`, `src` and `icon` props. -- The default slot content will be wrapped in a `` element to ensure proper centering. -- You may need additional styling applied to the custom content to compensate for the - [shape of avatar component](#rounding). - -## Styling - -### Variants - -Use the `variant` prop to specify one of Bootstrap theme variant colors. The default variant is -`secondary`. - -```html - - - -``` - -If you have defined additional custom variants via -[SASS theming variables](/docs/reference/theming), the custom variants will also be available to -use. - -### Sizing - -By default, avatars are sized to `2.5em` (which is relative to the current font size). You can -change the size of the avatar by changing the current font size, or use the prop `size` to specify -an explicit size. The sizes `sm`, `md` and `lg` default to `1.5em`, `2.5em` and `3.5em`. Numbers get -converted to pixel values. Any other value _must_ include the units (such as `px`, `em`, or `rem`). - -```html - - - -``` - -**Note:** Avatars are _always_ rendered with an aspect ratio of `1:1`. - -### Square - -Prefer a square avatar? simply set the `square` prop to `true`. - -```html - - - -``` - -### Rounding - -`` renders with a circular border radius. You can change the rounding by setting the prop -`rounded` to one of the values `true`, `'sm'`, `'lg'`, `'top'`, `'left'`, `'right'`, or `'bottom'`. -When set to `true` (or the empty string `''`), it uses the Bootstrap default of medium rounding. - -```html - - - -``` - -**Notes:** - -- The `square` prop takes precedence over the `rounded` prop. -- Alternatively to to the `square` prop, you can set the `rounded` prop to the string `'0'` to - achieve a square avatar. - -### Alignment - -By default `` will be vertically centered with its adjoining content. In some cases you -may want to alter the alignment, such as ensuring that a text-only avatar aligns its text with the -adjoining text. Simply set a [vertical alignment utility](/docs/reference/utility-classes) class on -the component, such as `` or -``, etc. - -## Actionable avatars - -Easily create avatars that respond to clicks, or avatars that change the URL/route when clicked. -Actionable avatars will appear in the document tab sequence, and are accessible for both screen -reader and keyboard-only users. - -Image avatars, when actionalble, employ a basic scale transform on the image when hovered. - -### Button - -Want to trigger the opening of a modal or trigger an action? Set the `button` prop to instruct -`` to render as a `
  • @@ -107,7 +110,8 @@ wrapped in these components for proper styling. Set the `is-text` prop on `` or `` if the content is textual in nature to apply proper styling. Alternatively, place the `` -subcomponent inside of the `` or ``. +subcomponent inside of the `` or ``. This also applies +when you want to use on of [BootstrapVue's icons](/docs/icons). ## Supported form-controls diff --git a/src/components/layout/README.md b/src/components/layout/README.md index 15884cdd79e..86da62914f9 100644 --- a/src/components/layout/README.md +++ b/src/components/layout/README.md @@ -295,7 +295,7 @@ width. Create equal-width columns that span multiple lines by inserting a `.w-100` where you want the columns to break to a new line. Make the breaks responsive by mixing `.w-100` with some -[responsive display utilities](https://getbootstrap.com/docs/4.3/utilities/display/). +[responsive display utilities](https://getbootstrap.com/docs/4.5/utilities/display/). There was a [Safari flexbox bug](https://github.com/philipwalton/flexbugs#flexbug-11) that prevented this from working without an explicit `flex-basis` or `border`. There are workarounds for older diff --git a/src/components/overlay/index.d.ts b/src/components/overlay/index.d.ts index 08e3cd503b4..cda205eb325 100644 --- a/src/components/overlay/index.d.ts +++ b/src/components/overlay/index.d.ts @@ -1,11 +1,11 @@ -// -// Overlay -// -import Vue from 'vue' -import { BvPlugin, BvComponent } from '../../' - -// Plugin -export declare const OverlayPlugin: BvPlugin - -// Component: b-overlay -export declare class BOverlay extends BvComponent {} +// +// Overlay +// +import Vue from 'vue' +import { BvPlugin, BvComponent } from '../../' + +// Plugin +export declare const OverlayPlugin: BvPlugin + +// Component: b-overlay +export declare class BOverlay extends BvComponent {} diff --git a/src/components/overlay/index.js b/src/components/overlay/index.js index dc568b1efcc..33cc0608047 100644 --- a/src/components/overlay/index.js +++ b/src/components/overlay/index.js @@ -1,8 +1,8 @@ -import { BOverlay } from './overlay' -import { pluginFactory } from '../../utils/plugins' - -const OverlayPlugin = /*#__PURE__*/ pluginFactory({ - components: { BOverlay } -}) - -export { OverlayPlugin, BOverlay } +import { BOverlay } from './overlay' +import { pluginFactory } from '../../utils/plugins' + +const OverlayPlugin = /*#__PURE__*/ pluginFactory({ + components: { BOverlay } +}) + +export { OverlayPlugin, BOverlay } diff --git a/src/components/overlay/overlay.js b/src/components/overlay/overlay.js index 1a16a52053c..574b613807b 100644 --- a/src/components/overlay/overlay.js +++ b/src/components/overlay/overlay.js @@ -1,184 +1,184 @@ -import Vue from '../../utils/vue' -import { BVTransition } from '../../utils/bv-transition' -import { toFloat } from '../../utils/number' -import normalizeSlotMixin from '../../mixins/normalize-slot' -import { BSpinner } from '../spinner/spinner' - -const positionCover = { top: 0, left: 0, bottom: 0, right: 0 } - -export const BOverlay = /*#__PURE__*/ Vue.extend({ - name: 'BOverlay', - mixins: [normalizeSlotMixin], - props: { - show: { - type: Boolean, - default: false - }, - variant: { - type: String, - default: 'light' - }, - bgColor: { - // Alternative to variant, allowing a specific - // CSS color to be applied to the overlay - type: String - // default: null - }, - opacity: { - type: [Number, String], - default: 0.85, - validator(value) { - const number = toFloat(value, 0) - return number >= 0 && number <= 1 - } - }, - blur: { - type: String, - default: '2px' - }, - rounded: { - type: [Boolean, String], - default: false - }, - noCenter: { - type: Boolean, - default: false - }, - noFade: { - type: Boolean, - default: false - }, - spinnerType: { - type: String, - default: 'border' - }, - spinnerVariant: { - type: String - // default: null - }, - spinnerSmall: { - type: Boolean, - default: false - }, - overlayTag: { - type: String, - default: 'div' - }, - wrapTag: { - type: String, - default: 'div' - }, - noWrap: { - // If set, does not render the default slot - // and switches to absolute positioning - type: Boolean, - default: false - }, - fixed: { - type: Boolean, - default: false - }, - zIndex: { - type: [Number, String], - default: 10 - } - }, - computed: { - computedRounded() { - const rounded = this.rounded - return rounded === true || rounded === '' ? 'rounded' : !rounded ? '' : `rounded-${rounded}` - }, - computedVariant() { - return this.variant && !this.bgColor ? `bg-${this.variant}` : '' - }, - overlayScope() { - return { - spinnerType: this.spinnerType || null, - spinnerVariant: this.spinnerVariant || null, - spinnerSmall: this.spinnerSmall - } - } - }, - methods: { - defaultOverlayFn({ spinnerType, spinnerVariant, spinnerSmall }) { - return this.$createElement(BSpinner, { - props: { - type: spinnerType, - variant: spinnerVariant, - small: spinnerSmall - } - }) - } - }, - render(h) { - let $overlay = h() - if (this.show) { - const scope = this.overlayScope - // Overlay backdrop - const $background = h('div', { - staticClass: 'position-absolute', - class: [this.computedVariant, this.computedRounded], - style: { - ...positionCover, - opacity: this.opacity, - backgroundColor: this.bgColor || null, - backdropFilter: this.blur ? `blur(${this.blur})` : null - } - }) - // Overlay content - const $content = h( - 'div', - { - staticClass: 'position-absolute', - style: this.noCenter - ? /* istanbul ignore next */ { ...positionCover } - : { top: '50%', left: '50%', transform: 'translateX(-50%) translateY(-50%)' } - }, - [this.normalizeSlot('overlay', scope) || this.defaultOverlayFn(scope)] - ) - // Overlay positioning - $overlay = h( - this.overlayTag, - { - key: 'overlay', - staticClass: 'b-overlay', - class: { - 'position-absolute': !this.noWrap || (this.noWrap && !this.fixed), - 'position-fixed': this.noWrap && this.fixed - }, - style: { ...positionCover, zIndex: this.zIndex || 10 }, - on: { click: evt => this.$emit('click', evt) } - }, - [$background, $content] - ) - } - // Wrap in a fade transition - $overlay = h( - BVTransition, - { - props: { - noFade: this.noFade, - appear: true - }, - on: { - 'after-enter': () => this.$emit('shown'), - 'after-leave': () => this.$emit('hidden') - } - }, - [$overlay] - ) - - if (this.noWrap) { - return $overlay - } - - return h( - this.wrapTag, - { - staticClass: 'b-overlay-wrap position-relative', - attrs: { 'aria-busy': this.show ? 'true' : null } - }, - this.noWrap ? [$overlay] : [this.normalizeSlot('default'), $overlay] - ) - } -}) +import Vue from '../../utils/vue' +import { BVTransition } from '../../utils/bv-transition' +import { toFloat } from '../../utils/number' +import normalizeSlotMixin from '../../mixins/normalize-slot' +import { BSpinner } from '../spinner/spinner' + +const positionCover = { top: 0, left: 0, bottom: 0, right: 0 } + +export const BOverlay = /*#__PURE__*/ Vue.extend({ + name: 'BOverlay', + mixins: [normalizeSlotMixin], + props: { + show: { + type: Boolean, + default: false + }, + variant: { + type: String, + default: 'light' + }, + bgColor: { + // Alternative to variant, allowing a specific + // CSS color to be applied to the overlay + type: String + // default: null + }, + opacity: { + type: [Number, String], + default: 0.85, + validator(value) { + const number = toFloat(value, 0) + return number >= 0 && number <= 1 + } + }, + blur: { + type: String, + default: '2px' + }, + rounded: { + type: [Boolean, String], + default: false + }, + noCenter: { + type: Boolean, + default: false + }, + noFade: { + type: Boolean, + default: false + }, + spinnerType: { + type: String, + default: 'border' + }, + spinnerVariant: { + type: String + // default: null + }, + spinnerSmall: { + type: Boolean, + default: false + }, + overlayTag: { + type: String, + default: 'div' + }, + wrapTag: { + type: String, + default: 'div' + }, + noWrap: { + // If set, does not render the default slot + // and switches to absolute positioning + type: Boolean, + default: false + }, + fixed: { + type: Boolean, + default: false + }, + zIndex: { + type: [Number, String], + default: 10 + } + }, + computed: { + computedRounded() { + const rounded = this.rounded + return rounded === true || rounded === '' ? 'rounded' : !rounded ? '' : `rounded-${rounded}` + }, + computedVariant() { + return this.variant && !this.bgColor ? `bg-${this.variant}` : '' + }, + overlayScope() { + return { + spinnerType: this.spinnerType || null, + spinnerVariant: this.spinnerVariant || null, + spinnerSmall: this.spinnerSmall + } + } + }, + methods: { + defaultOverlayFn({ spinnerType, spinnerVariant, spinnerSmall }) { + return this.$createElement(BSpinner, { + props: { + type: spinnerType, + variant: spinnerVariant, + small: spinnerSmall + } + }) + } + }, + render(h) { + let $overlay = h() + if (this.show) { + const scope = this.overlayScope + // Overlay backdrop + const $background = h('div', { + staticClass: 'position-absolute', + class: [this.computedVariant, this.computedRounded], + style: { + ...positionCover, + opacity: this.opacity, + backgroundColor: this.bgColor || null, + backdropFilter: this.blur ? `blur(${this.blur})` : null + } + }) + // Overlay content + const $content = h( + 'div', + { + staticClass: 'position-absolute', + style: this.noCenter + ? /* istanbul ignore next */ { ...positionCover } + : { top: '50%', left: '50%', transform: 'translateX(-50%) translateY(-50%)' } + }, + [this.normalizeSlot('overlay', scope) || this.defaultOverlayFn(scope)] + ) + // Overlay positioning + $overlay = h( + this.overlayTag, + { + key: 'overlay', + staticClass: 'b-overlay', + class: { + 'position-absolute': !this.noWrap || (this.noWrap && !this.fixed), + 'position-fixed': this.noWrap && this.fixed + }, + style: { ...positionCover, zIndex: this.zIndex || 10 }, + on: { click: evt => this.$emit('click', evt) } + }, + [$background, $content] + ) + } + // Wrap in a fade transition + $overlay = h( + BVTransition, + { + props: { + noFade: this.noFade, + appear: true + }, + on: { + 'after-enter': () => this.$emit('shown'), + 'after-leave': () => this.$emit('hidden') + } + }, + [$overlay] + ) + + if (this.noWrap) { + return $overlay + } + + return h( + this.wrapTag, + { + staticClass: 'b-overlay-wrap position-relative', + attrs: { 'aria-busy': this.show ? 'true' : null } + }, + this.noWrap ? [$overlay] : [this.normalizeSlot('default'), $overlay] + ) + } +}) diff --git a/src/components/overlay/overlay.spec.js b/src/components/overlay/overlay.spec.js index edb2be4e88d..7dfc952a0fb 100644 --- a/src/components/overlay/overlay.spec.js +++ b/src/components/overlay/overlay.spec.js @@ -1,257 +1,257 @@ -import { mount } from '@vue/test-utils' -import { createContainer, waitNT, waitRAF } from '../../../tests/utils' -import { BOverlay } from './overlay' - -describe('overlay', () => { - it('has expected default structure', async () => { - const wrapper = mount(BOverlay, { - slots: { - default: 'foobar' - } - }) - - expect(wrapper.vm).toBeDefined() - await waitNT(wrapper.vm) - await waitRAF() - await waitNT(wrapper.vm) - await waitRAF() - - expect(wrapper.element.tagName).toBe('DIV') - expect(wrapper.classes()).toContain('b-overlay-wrap') - expect(wrapper.classes()).toContain('position-relative') - expect(wrapper.attributes('aria-busy')).not.toBe('true') - expect(wrapper.text()).toContain('foobar') - expect(wrapper.find('.b-overlay').exists()).toBe(false) - expect(wrapper.find('.spinner-border').exists()).toBe(false) - - wrapper.destroy() - }) - - it('has expected default structure when `show` prop is true', async () => { - const wrapper = mount(BOverlay, { - propsData: { - show: true - }, - slots: { - default: 'foobar' - } - }) - - expect(wrapper.vm).toBeDefined() - await waitNT(wrapper.vm) - await waitRAF() - await waitNT(wrapper.vm) - await waitRAF() - - expect(wrapper.element.tagName).toBe('DIV') - expect(wrapper.classes()).toContain('b-overlay-wrap') - expect(wrapper.classes()).toContain('position-relative') - expect(wrapper.attributes('aria-busy')).toBe('true') - expect(wrapper.text()).toContain('foobar') - - const $overlay = wrapper.find('.b-overlay') - expect($overlay.exists()).toBe(true) - expect($overlay.classes()).toContain('position-absolute') - - const $children = $overlay.findAll('div:not(.b-overlay)') - expect($children.length).toBe(2) - - expect($children.at(0).classes()).toContain('position-absolute') - expect($children.at(0).classes()).toContain('bg-light') - expect($children.at(0).text()).toBe('') - - expect($children.at(1).classes()).toContain('position-absolute') - expect($children.at(1).classes()).not.toContain('bg-light') - expect( - $children - .at(1) - .find('.spinner-border') - .exists() - ).toBe(true) - - wrapper.destroy() - }) - - it('responds to changes in the `show` prop', async () => { - const wrapper = mount(BOverlay, { - attachTo: createContainer(), - propsData: { - show: false - }, - slots: { - default: 'foobar' - } - }) - - expect(wrapper.vm).toBeDefined() - await waitNT(wrapper.vm) - await waitRAF() - await waitNT(wrapper.vm) - await waitRAF() - - expect(wrapper.element.tagName).toBe('DIV') - expect(wrapper.classes()).toContain('b-overlay-wrap') - expect(wrapper.classes()).toContain('position-relative') - expect(wrapper.attributes('aria-busy')).not.toBe('true') - expect(wrapper.text()).toContain('foobar') - expect(wrapper.find('.b-overlay').exists()).toBe(false) - expect(wrapper.find('.spinner-border').exists()).toBe(false) - - expect(wrapper.emitted('shown')).toBeUndefined() - expect(wrapper.emitted('hidden')).toBeUndefined() - - await wrapper.setProps({ - show: true - }) - await waitNT(wrapper.vm) - await waitRAF() - await waitNT(wrapper.vm) - await waitRAF() - - expect(wrapper.element.tagName).toBe('DIV') - expect(wrapper.classes()).toContain('b-overlay-wrap') - expect(wrapper.classes()).toContain('position-relative') - expect(wrapper.attributes('aria-busy')).toBe('true') - expect(wrapper.text()).toContain('foobar') - expect(wrapper.find('.b-overlay').exists()).toBe(true) - expect(wrapper.find('.spinner-border').exists()).toBe(true) - - expect(wrapper.emitted('shown')).not.toBeUndefined() - expect(wrapper.emitted('hidden')).toBeUndefined() - expect(wrapper.emitted('shown').length).toBe(1) - - await wrapper.setProps({ - show: false - }) - await waitNT(wrapper.vm) - await waitRAF() - await waitNT(wrapper.vm) - await waitRAF() - - expect(wrapper.element.tagName).toBe('DIV') - expect(wrapper.classes()).toContain('b-overlay-wrap') - expect(wrapper.classes()).toContain('position-relative') - expect(wrapper.attributes('aria-busy')).not.toBe('true') - expect(wrapper.text()).toContain('foobar') - expect(wrapper.find('.b-overlay').exists()).toBe(false) - expect(wrapper.find('.spinner-border').exists()).toBe(false) - - expect(wrapper.emitted('hidden')).not.toBeUndefined() - expect(wrapper.emitted('shown').length).toBe(1) - expect(wrapper.emitted('hidden').length).toBe(1) - - await wrapper.setProps({ - show: true - }) - await waitNT(wrapper.vm) - await waitRAF() - await waitNT(wrapper.vm) - await waitRAF() - - expect(wrapper.emitted('shown').length).toBe(2) - expect(wrapper.emitted('hidden').length).toBe(1) - - await wrapper.setProps({ - show: false - }) - await waitNT(wrapper.vm) - await waitRAF() - await waitNT(wrapper.vm) - await waitRAF() - - expect(wrapper.emitted('shown').length).toBe(2) - expect(wrapper.emitted('hidden').length).toBe(2) - - wrapper.destroy() - }) - - it('emits event when overlay clicked', async () => { - const wrapper = mount(BOverlay, { - propsData: { - show: true - }, - slots: { - default: 'foobar' - } - }) - - expect(wrapper.vm).toBeDefined() - await waitNT(wrapper.vm) - await waitRAF() - await waitNT(wrapper.vm) - await waitRAF() - - expect(wrapper.element.tagName).toBe('DIV') - expect(wrapper.classes()).toContain('b-overlay-wrap') - - const $overlay = wrapper.find('.b-overlay') - expect($overlay.exists()).toBe(true) - - expect(wrapper.emitted('click')).not.toBeDefined() - - await $overlay.trigger('click') - expect(wrapper.emitted('click')).toBeDefined() - expect(wrapper.emitted('click').length).toBe(1) - expect(wrapper.emitted('click')[0][0]).toBeInstanceOf(Event) - expect(wrapper.emitted('click')[0][0].type).toEqual('click') - - wrapper.destroy() - }) - - it('has expected default structure when `no-wrap` is set', async () => { - const wrapper = mount(BOverlay, { - propsData: { - noWrap: true - } - }) - - expect(wrapper.vm).toBeDefined() - await waitNT(wrapper.vm) - await waitRAF() - await waitNT(wrapper.vm) - await waitRAF() - - expect(wrapper.find('div').exists()).toBe(false) - - wrapper.destroy() - }) - - it('has expected default structure when `no-wrap` is set and `show` is true', async () => { - const wrapper = mount(BOverlay, { - propsData: { - noWrap: true, - show: true - } - }) - - expect(wrapper.vm).toBeDefined() - await waitNT(wrapper.vm) - await waitRAF() - await waitNT(wrapper.vm) - await waitRAF() - - expect(wrapper.element.tagName).toBe('DIV') - expect(wrapper.classes()).toContain('b-overlay') - expect(wrapper.classes()).toContain('position-absolute') - expect(wrapper.classes()).not.toContain('b-overlay-wrap') - expect(wrapper.classes()).not.toContain('position-relative') - - const $children = wrapper.findAll('div:not(.b-overlay)') - expect($children.length).toBe(2) - - expect($children.at(0).classes()).toContain('position-absolute') - expect($children.at(0).classes()).toContain('bg-light') - expect($children.at(0).text()).toBe('') - - expect($children.at(1).classes()).toContain('position-absolute') - expect($children.at(1).classes()).not.toContain('bg-light') - expect( - $children - .at(1) - .find('.spinner-border') - .exists() - ).toBe(true) - - wrapper.destroy() - }) -}) +import { mount } from '@vue/test-utils' +import { createContainer, waitNT, waitRAF } from '../../../tests/utils' +import { BOverlay } from './overlay' + +describe('overlay', () => { + it('has expected default structure', async () => { + const wrapper = mount(BOverlay, { + slots: { + default: 'foobar' + } + }) + + expect(wrapper.vm).toBeDefined() + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.classes()).toContain('b-overlay-wrap') + expect(wrapper.classes()).toContain('position-relative') + expect(wrapper.attributes('aria-busy')).not.toBe('true') + expect(wrapper.text()).toContain('foobar') + expect(wrapper.find('.b-overlay').exists()).toBe(false) + expect(wrapper.find('.spinner-border').exists()).toBe(false) + + wrapper.destroy() + }) + + it('has expected default structure when `show` prop is true', async () => { + const wrapper = mount(BOverlay, { + propsData: { + show: true + }, + slots: { + default: 'foobar' + } + }) + + expect(wrapper.vm).toBeDefined() + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.classes()).toContain('b-overlay-wrap') + expect(wrapper.classes()).toContain('position-relative') + expect(wrapper.attributes('aria-busy')).toBe('true') + expect(wrapper.text()).toContain('foobar') + + const $overlay = wrapper.find('.b-overlay') + expect($overlay.exists()).toBe(true) + expect($overlay.classes()).toContain('position-absolute') + + const $children = $overlay.findAll('div:not(.b-overlay)') + expect($children.length).toBe(2) + + expect($children.at(0).classes()).toContain('position-absolute') + expect($children.at(0).classes()).toContain('bg-light') + expect($children.at(0).text()).toBe('') + + expect($children.at(1).classes()).toContain('position-absolute') + expect($children.at(1).classes()).not.toContain('bg-light') + expect( + $children + .at(1) + .find('.spinner-border') + .exists() + ).toBe(true) + + wrapper.destroy() + }) + + it('responds to changes in the `show` prop', async () => { + const wrapper = mount(BOverlay, { + attachTo: createContainer(), + propsData: { + show: false + }, + slots: { + default: 'foobar' + } + }) + + expect(wrapper.vm).toBeDefined() + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.classes()).toContain('b-overlay-wrap') + expect(wrapper.classes()).toContain('position-relative') + expect(wrapper.attributes('aria-busy')).not.toBe('true') + expect(wrapper.text()).toContain('foobar') + expect(wrapper.find('.b-overlay').exists()).toBe(false) + expect(wrapper.find('.spinner-border').exists()).toBe(false) + + expect(wrapper.emitted('shown')).toBeUndefined() + expect(wrapper.emitted('hidden')).toBeUndefined() + + await wrapper.setProps({ + show: true + }) + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.classes()).toContain('b-overlay-wrap') + expect(wrapper.classes()).toContain('position-relative') + expect(wrapper.attributes('aria-busy')).toBe('true') + expect(wrapper.text()).toContain('foobar') + expect(wrapper.find('.b-overlay').exists()).toBe(true) + expect(wrapper.find('.spinner-border').exists()).toBe(true) + + expect(wrapper.emitted('shown')).not.toBeUndefined() + expect(wrapper.emitted('hidden')).toBeUndefined() + expect(wrapper.emitted('shown').length).toBe(1) + + await wrapper.setProps({ + show: false + }) + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.classes()).toContain('b-overlay-wrap') + expect(wrapper.classes()).toContain('position-relative') + expect(wrapper.attributes('aria-busy')).not.toBe('true') + expect(wrapper.text()).toContain('foobar') + expect(wrapper.find('.b-overlay').exists()).toBe(false) + expect(wrapper.find('.spinner-border').exists()).toBe(false) + + expect(wrapper.emitted('hidden')).not.toBeUndefined() + expect(wrapper.emitted('shown').length).toBe(1) + expect(wrapper.emitted('hidden').length).toBe(1) + + await wrapper.setProps({ + show: true + }) + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.emitted('shown').length).toBe(2) + expect(wrapper.emitted('hidden').length).toBe(1) + + await wrapper.setProps({ + show: false + }) + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.emitted('shown').length).toBe(2) + expect(wrapper.emitted('hidden').length).toBe(2) + + wrapper.destroy() + }) + + it('emits event when overlay clicked', async () => { + const wrapper = mount(BOverlay, { + propsData: { + show: true + }, + slots: { + default: 'foobar' + } + }) + + expect(wrapper.vm).toBeDefined() + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.classes()).toContain('b-overlay-wrap') + + const $overlay = wrapper.find('.b-overlay') + expect($overlay.exists()).toBe(true) + + expect(wrapper.emitted('click')).not.toBeDefined() + + await $overlay.trigger('click') + expect(wrapper.emitted('click')).toBeDefined() + expect(wrapper.emitted('click').length).toBe(1) + expect(wrapper.emitted('click')[0][0]).toBeInstanceOf(Event) + expect(wrapper.emitted('click')[0][0].type).toEqual('click') + + wrapper.destroy() + }) + + it('has expected default structure when `no-wrap` is set', async () => { + const wrapper = mount(BOverlay, { + propsData: { + noWrap: true + } + }) + + expect(wrapper.vm).toBeDefined() + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.find('div').exists()).toBe(false) + + wrapper.destroy() + }) + + it('has expected default structure when `no-wrap` is set and `show` is true', async () => { + const wrapper = mount(BOverlay, { + propsData: { + noWrap: true, + show: true + } + }) + + expect(wrapper.vm).toBeDefined() + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.classes()).toContain('b-overlay') + expect(wrapper.classes()).toContain('position-absolute') + expect(wrapper.classes()).not.toContain('b-overlay-wrap') + expect(wrapper.classes()).not.toContain('position-relative') + + const $children = wrapper.findAll('div:not(.b-overlay)') + expect($children.length).toBe(2) + + expect($children.at(0).classes()).toContain('position-absolute') + expect($children.at(0).classes()).toContain('bg-light') + expect($children.at(0).text()).toBe('') + + expect($children.at(1).classes()).toContain('position-absolute') + expect($children.at(1).classes()).not.toContain('bg-light') + expect( + $children + .at(1) + .find('.spinner-border') + .exists() + ).toBe(true) + + wrapper.destroy() + }) +}) diff --git a/src/components/overlay/package.json b/src/components/overlay/package.json index 5d89ff8f081..7c039df08d7 100644 --- a/src/components/overlay/package.json +++ b/src/components/overlay/package.json @@ -1,130 +1,130 @@ -{ - "name": "@bootstrap-vue/overlay", - "version": "1.0.0", - "meta": { - "title": "Overlay", - "version": "2.7.0", - "description": "The b-overlay component is used to visually obscure a particular element or component and its content. It signals to the user of a state change within the element or component and can be used for creating loaders, warnings/alerts and more.", - "components": [ - { - "component": "BOverlay", - "version": "2.7.0", - "props": [ - { - "prop": "show", - "description": "When set, shows the overlay" - }, - { - "prop": "variant", - "description": "Background theme color variant to use for the overlay backdrop" - }, - { - "prop": "bgColor", - "description": "CSS color to use as the opaque overlay backdrop color. If set, overrides the `variant` prop" - }, - { - "prop": "opacity", - "description": "Opacity of the overlay backdrop. Valid range is `0` to `1`" - }, - { - "prop": "blur", - "description": "Value for the CSS blur backdrop-filter. Be sure to include the CSS units. Not supported in IE 11. Set to null or an empty string to disable blurring" - }, - { - "prop": "noFade", - "description": "Disables the fade transition of the overlay" - }, - { - "prop": "rounded", - "description": "Apply rounding to the overlay to match your content routing. Valid values are `true`, `'sm'`, `lg`, `circle`, `pill`, `top`, `right`, `bottom`, or `left`" - }, - { - "prop": "noCenter", - "description": "When set, disables the vertical and horizontal centering of the overlay content" - }, - { - "prop": "overlayTag", - "description": "Element tag to use as for the overlay element" - }, - { - "prop": "noWrap", - "description": "Disabled generating the wrapper element, and ignored the default slot. Requires that `` be placed in an element with position relative set" - }, - { - "prop": "fixed", - "description": "When prop `no-wrap` is set, will use fixed positioning instead of absolute positioning. Handy if you want to obscure the entire application page" - }, - { - "prop": "wrapTag", - "description": "Element tag to use for the overall wrapper element. Has no effect if prop `no-wrap` is set" - }, - { - "prop": "zIndex", - "description": "Z-index value to apply to the overlay. You may need to increase this value to suit your content or placement" - }, - { - "prop": "spinnerType", - "description": "Type of the default spinner to show. Current supported types are 'border' and 'grow'" - }, - { - "prop": "spinnerVariant", - "description": "Applies one of the Bootstrap theme color variants to the default spinner. Default is to use the current font color" - }, - { - "prop": "spinnerSmall", - "description": "When set, renders the default spinner in a smaller size" - } - ], - "events": [ - { - "event": "click", - "version": "2.13.0", - "description": "Emitted when overlay is clicked", - "args": [ - { - "arg": "event", - "type": "MouseEvent", - "description": "Native click event object" - } - ] - }, - { - "event": "shown", - "description": "Emitted when the overlay has been shown" - }, - { - "event": "hidden", - "description": "Emitted when the overlay has been hidden" - } - ], - "slots": [ - { - "name": "overlay", - "description": "Custom content to replace the default overlay spinner", - "scope": [ - { - "prop": "spinnerType", - "type": "String", - "description": "" - }, - { - "prop": "spinnerVariant", - "type": "String", - "description": "" - }, - { - "prop": "spinnerSmall", - "type": "Boolean", - "description": "" - } - ] - }, - { - "name": "default", - "description": "The content to be overlayed. The default slot is ignored if the prop `no-wrap` is set" - } - ] - } - ] - } -} +{ + "name": "@bootstrap-vue/overlay", + "version": "1.0.0", + "meta": { + "title": "Overlay", + "version": "2.7.0", + "description": "The b-overlay component is used to visually obscure a particular element or component and its content. It signals to the user of a state change within the element or component and can be used for creating loaders, warnings/alerts and more.", + "components": [ + { + "component": "BOverlay", + "version": "2.7.0", + "props": [ + { + "prop": "show", + "description": "When set, shows the overlay" + }, + { + "prop": "variant", + "description": "Background theme color variant to use for the overlay backdrop" + }, + { + "prop": "bgColor", + "description": "CSS color to use as the opaque overlay backdrop color. If set, overrides the `variant` prop" + }, + { + "prop": "opacity", + "description": "Opacity of the overlay backdrop. Valid range is `0` to `1`" + }, + { + "prop": "blur", + "description": "Value for the CSS blur backdrop-filter. Be sure to include the CSS units. Not supported in IE 11. Set to null or an empty string to disable blurring" + }, + { + "prop": "noFade", + "description": "Disables the fade transition of the overlay" + }, + { + "prop": "rounded", + "description": "Apply rounding to the overlay to match your content routing. Valid values are `true`, `'sm'`, `lg`, `circle`, `pill`, `top`, `right`, `bottom`, or `left`" + }, + { + "prop": "noCenter", + "description": "When set, disables the vertical and horizontal centering of the overlay content" + }, + { + "prop": "overlayTag", + "description": "Element tag to use as for the overlay element" + }, + { + "prop": "noWrap", + "description": "Disabled generating the wrapper element, and ignored the default slot. Requires that `` be placed in an element with position relative set" + }, + { + "prop": "fixed", + "description": "When prop `no-wrap` is set, will use fixed positioning instead of absolute positioning. Handy if you want to obscure the entire application page" + }, + { + "prop": "wrapTag", + "description": "Element tag to use for the overall wrapper element. Has no effect if prop `no-wrap` is set" + }, + { + "prop": "zIndex", + "description": "Z-index value to apply to the overlay. You may need to increase this value to suit your content or placement" + }, + { + "prop": "spinnerType", + "description": "Type of the default spinner to show. Current supported types are 'border' and 'grow'" + }, + { + "prop": "spinnerVariant", + "description": "Applies one of the Bootstrap theme color variants to the default spinner. Default is to use the current font color" + }, + { + "prop": "spinnerSmall", + "description": "When set, renders the default spinner in a smaller size" + } + ], + "events": [ + { + "event": "click", + "version": "2.13.0", + "description": "Emitted when overlay is clicked", + "args": [ + { + "arg": "event", + "type": "MouseEvent", + "description": "Native click event object" + } + ] + }, + { + "event": "shown", + "description": "Emitted when the overlay has been shown" + }, + { + "event": "hidden", + "description": "Emitted when the overlay has been hidden" + } + ], + "slots": [ + { + "name": "overlay", + "description": "Custom content to replace the default overlay spinner", + "scope": [ + { + "prop": "spinnerType", + "type": "String", + "description": "" + }, + { + "prop": "spinnerVariant", + "type": "String", + "description": "" + }, + { + "prop": "spinnerSmall", + "type": "Boolean", + "description": "" + } + ] + }, + { + "name": "default", + "description": "The content to be overlayed. The default slot is ignored if the prop `no-wrap` is set" + } + ] + } + ] + } +} diff --git a/src/components/sidebar/README.md b/src/components/sidebar/README.md index df61be29cde..42025764ffc 100644 --- a/src/components/sidebar/README.md +++ b/src/components/sidebar/README.md @@ -1,400 +1,400 @@ -# Sidebar - -> Otherwise known as off-canvas or a side drawer, BootstrapVue's custom `` component is a -> fixed-position toggleable slide out box, which can be used for navigation, menus, details, etc. It -> can be positioned on either the left (default) or right of the viewport, with optional backdrop -> support. - -## Overview - -You can place almost any content inside the `` -[optionally scoped default slot](#scoped-default-slot), such as text, buttons, forms, images, or -[vertical navs](/docs/components/nav#vertical-variation). - -The component supports a header and built in close button, of which you can optionally disable and -provide your own header (if needed), and can be easily toggled with our -[`v-b-toggle` directive](/docs/directives/toggle). - -The component has minimal default styling, which provides you with great flexibility in laying out -the content of the sidebar. - -```html - - - -``` - -If the content is taller than the available viewport height, vertical scrolling will automatically -be enabled via CSS on the body of the sidebar. - -## Styling - -Several props are provided for controlling the appearance of the sidebar. - -### Title - -Sidebars should have a title (specifically for accessibility reasons). Easily set the title that -appears in the header either via the `title` prop or the `title` slot. Note the `title` slot takes -precedence over the `title` prop. - -If the [`no-header` prop](#hiding-the-header) is set, then neither the `title` prop or `title` slot -have any effect. - -If you do not provide a title, use either the `aria-label` or `aria-labelledby` props to provide an -accessible title for the sidebar. See the [Accessibility section](#accessibility) below for -additional details. - -### Placement - -By default the sidebar will be placed on the left side fo the viewport. Set the `right` prop to -`true` to have the sidebar appear on the right side of the viewport. - -```html - - - -``` - -### Variants - -Use the props `bg-variant` and `text-variant` to control the theme color variant of the background -and text, respectively. Alternatively, you can apply styles or classes to specify the background and -text colors. - -```html - - - -``` - -The standard Bootstrap theme variants are `'white'`, `'light'`, `'dark'`, `'primary'`, -`'secondary'`, `'success'`, `'danger'`, `'warning'`, and `'info'`. - -The default background variant is `'light'` and the default text variant is `'dark'`. - -### Shadow - -Prefer a sidebar with a backdrop shadow? Set the `shadow` prop to either boolean `true` for a medium -shadow, `'sm'` for a small shadow, or `'lg'` for a larger shadow. Set it to `false` (the default) -for no shadow. - -### Borders - -By default, `` has no borders. Use -[border utility classes](/docs/reference/utility-classes) to add border(s) to `` (via the -`sidebar-class` prop 2.12.0+), or use CSS style -overrides. - -```html - - - -``` - -### Width - -By default the width of `` is set to `320px` (100% on 'xs' screens). Simply provide a -value via the `width` prop (i.e. `'180px'`, `'20em'`, etc) to override this default. The max width -is set to `100%` via CSS. - -### Padding - -The sidebar by default has no padding. You can apply padding utility classes to the component, or -margin/padding utility classes to the content of the sidebar. - -### Disable slide transition - -By default the sidebar will use a sliding transition when showing and hiding. You can disable the -slide transition via the `no-slide` prop. - -**Note:** The BootstrapVue defined transition effect of this component is dependent on the -`prefers-reduced-motion` media query. See the -[reduced motion section of our accessibility documentation](/docs/reference/accessibility) for -additional details. - -When disabling the slid transition, the fade transition of the [optional backdrop](#backdrop) will -also be disabled. - -### Z-index - -The sidebar has a default `z-index` defined in SCSS/CSS. In some situations you may need to use a -different `z-index` to ensure the sidebar appears over or under other content. You can do so either -via CSS styles, or via the `z-index` prop. - -### Scoped default slot - -The `default` slot allows you to provide the body content for your sidebar. It is optionally scoped. -The examples in the following sections demonstrate the use of the default slot scope - -You can apply arbitrary classes to the body section via the `body-class` prop. - -### Header - -By default, `` has a header with optional title and a close button. You can supply a -title via the `title` prop, or via the optionally scoped slot `title`. - -You can apply arbitrary classes to the header section via the `header-class` prop, to override the -default padding, etc. - -#### Hiding the default header - -You can disable the default header (including the close button) via the `no-header` prop. Note that -you will need to provide a method of closing the sidebar. The `default` slot is scoped, which -includes a `hide()` method that can be used to close the sidebar. - -```html - - - -``` - -### Footer - -`` provides a `footer` slot (optionally scoped), to allow you to provide content that -appears at the bottom of the sidebar. The `footer` slot is scoped, which includes a `hide()` method -that can be used to close the sidebar. - -```html - - - -``` - -You can apply arbitrary classes to the footer section via the `footer-class` prop. - -### Lazy rendering - -In some instances, you may not want the content rendered when the sidebar is not visible. Simply set -the `lazy` prop on ``. When `lazy` is `true`, the body and optional footer will _not_ be -rendered (removed from DOM) whenever the sidebar is closed. - -### Backdrop - -2.12.0+ - -Add a basic backdrop when the side bar is open via the `backdrop` prop. When set to `true`, the -sidebar will show an opaque backdrop. Clicking on the backdrop will close the sidebar, unless the -`no-close-on-backdrop` prop is set to `true`. - -Optionally (as of BootstrapVue v2.15.0+) you can use the `backdrop-variant` prop to control the -theme color variant of the backdrop. The default backdrop variant is `dark`. - -```html - - - - - -``` - -Note that when the sidebar is open, it may still be possible to scroll the body (unlike the -behaviour of modals). When the backdrop in enabled, focus constraint will attempt to keep focus -within the sidebar. Note that in rare circumstances it might be possible for users to move focus to -elements outside of the sidebar. - -## Visibility control - -### `v-b-toggle` directive - -Using the [`v-b-toggle` directive](/docs/directive/toggle) is the preferred method for _opening_ the -sidebar, as it automatically handles applying the `aria-controls` and `aria-expanded` accessibility -attributes on the trigger element. - -The majority of examples on this page use the `v-b-toggle` directive. - -### `v-model` - -The `v-model` reflects the current visibility state of the sidebar. While it can be used to control -the visibility state of the sidebar, it is recommended to use the -[`v-b-toggle` directive](#v-b-toggle-directive) to _show_ the sidebar for accessibility reasons. If -you do use the `v-model` to show the sidebar, you should place the `aria-controls="id"` attribute -(where `id` is the ID of the sidebar) on the trigger element, and also set the `aria-expanded` -attribute (also on the trigger element) to either the string `'true'` (if the sidebar is open) or -`'false`' (if the sidebar is closed). - -The `v-model` is internally bound to the `visible` prop, and the `change` event updates the -`v-model`. - -### Closing on $route change - -By default, `` will close itself when the `$route` changes (full path including query and -hash). This can be particularly handy if the sidebar is placed outside of your `` and -is used for navigation. - -You can disable this behaviour by setting the `no-close-on-route-change` prop to `true`. - -## Events - -The sidebar will emit the `shown` event once the sidebar has opened, and the `hidden` event when the -sidebar has closed. - -The `change` event is used to update the `v-model` and is emitted whenever the visibility state of -the sidebar changes. - -## Accessibility - -`` provides several accessibility features. - -When the sidebar is opened, the entire sidebar will receive focus, which is desirable for screen -reader and keyboard-only users. When the sidebar is closed, the element that previously had focus -before the sidebar was opened will be re-focused. - -When the sidebar is open, users can press Esc to close the sidebar. To disable this -feature, set the `no-close-on-esc` prop to `true`. with the backdrop enabled, you can use the prop -`no-close-on-backdrop` to disable the close on backdrop click feature. - -When the `backdrop` prop is `true`, the sidebar will attempt to constrain focus within the sidebar, -and the sidebar will have the attribute `aria-modal="true"` set. - -When you have hidden the header, or do not have a title for the sidebar, set either `aria-label` to -a string that describes the sidebar, or set `aria-labelledby` to an ID of an element that contains -the title. When using the `lazy` prop _and_ you do not have a header, use the `aria-label` prop to -provide an appropriate string to label the sidebar. - -## Implementation notes - -BootstrapVue's custom SCSS/CSS is required for proper styling, and positioning of the sidebar. - -The Bootstrap v4 background (`'bg-*'`) and text (`'text-*'`) utility classes are used for -controlling the background and font color, respectively. - -Some of the default styling for `` can be customized via the use of SASS variables. Refer -to the [theming documentation](/docs/reference/theming) for additional details. - -## See also - -- [`v-b-toggle` directive](/docs/directives/toggle) -- [`` component](/docs/components/collapse) -- [`` component](/docs/components/button#comp-ref-b-button-close) +# Sidebar + +> Otherwise known as off-canvas or a side drawer, BootstrapVue's custom `` component is a +> fixed-position toggleable slide out box, which can be used for navigation, menus, details, etc. It +> can be positioned on either the left (default) or right of the viewport, with optional backdrop +> support. + +## Overview + +You can place almost any content inside the `` +[optionally scoped default slot](#scoped-default-slot), such as text, buttons, forms, images, or +[vertical navs](/docs/components/nav#vertical-variation). + +The component supports a header and built in close button, of which you can optionally disable and +provide your own header (if needed), and can be easily toggled with our +[`v-b-toggle` directive](/docs/directives/toggle). + +The component has minimal default styling, which provides you with great flexibility in laying out +the content of the sidebar. + +```html + + + +``` + +If the content is taller than the available viewport height, vertical scrolling will automatically +be enabled via CSS on the body of the sidebar. + +## Styling + +Several props are provided for controlling the appearance of the sidebar. + +### Title + +Sidebars should have a title (specifically for accessibility reasons). Easily set the title that +appears in the header either via the `title` prop or the `title` slot. Note the `title` slot takes +precedence over the `title` prop. + +If the [`no-header` prop](#hiding-the-header) is set, then neither the `title` prop or `title` slot +have any effect. + +If you do not provide a title, use either the `aria-label` or `aria-labelledby` props to provide an +accessible title for the sidebar. See the [Accessibility section](#accessibility) below for +additional details. + +### Placement + +By default the sidebar will be placed on the left side of the viewport. Set the `right` prop to +`true` to have the sidebar appear on the right side of the viewport. + +```html + + + +``` + +### Variants + +Use the props `bg-variant` and `text-variant` to control the theme color variant of the background +and text, respectively. Alternatively, you can apply styles or classes to specify the background and +text colors. + +```html + + + +``` + +The standard Bootstrap theme variants are `'white'`, `'light'`, `'dark'`, `'primary'`, +`'secondary'`, `'success'`, `'danger'`, `'warning'`, and `'info'`. + +The default background variant is `'light'` and the default text variant is `'dark'`. + +### Shadow + +Prefer a sidebar with a backdrop shadow? Set the `shadow` prop to either boolean `true` for a medium +shadow, `'sm'` for a small shadow, or `'lg'` for a larger shadow. Set it to `false` (the default) +for no shadow. + +### Borders + +By default, `` has no borders. Use +[border utility classes](/docs/reference/utility-classes) to add border(s) to `` (via the +`sidebar-class` prop 2.12.0+), or use CSS style +overrides. + +```html + + + +``` + +### Width + +By default the width of `` is set to `320px` (100% on 'xs' screens). Simply provide a +value via the `width` prop (i.e. `'180px'`, `'20em'`, etc) to override this default. The max width +is set to `100%` via CSS. + +### Padding + +The sidebar by default has no padding. You can apply padding utility classes to the component, or +margin/padding utility classes to the content of the sidebar. + +### Disable slide transition + +By default the sidebar will use a sliding transition when showing and hiding. You can disable the +slide transition via the `no-slide` prop. + +**Note:** The BootstrapVue defined transition effect of this component is dependent on the +`prefers-reduced-motion` media query. See the +[reduced motion section of our accessibility documentation](/docs/reference/accessibility) for +additional details. + +When disabling the slid transition, the fade transition of the [optional backdrop](#backdrop) will +also be disabled. + +### Z-index + +The sidebar has a default `z-index` defined in SCSS/CSS. In some situations you may need to use a +different `z-index` to ensure the sidebar appears over or under other content. You can do so either +via CSS styles, or via the `z-index` prop. + +### Scoped default slot + +The `default` slot allows you to provide the body content for your sidebar. It is optionally scoped. +The examples in the following sections demonstrate the use of the default slot scope + +You can apply arbitrary classes to the body section via the `body-class` prop. + +### Header + +By default, `` has a header with optional title and a close button. You can supply a +title via the `title` prop, or via the optionally scoped slot `title`. + +You can apply arbitrary classes to the header section via the `header-class` prop, to override the +default padding, etc. + +#### Hiding the default header + +You can disable the default header (including the close button) via the `no-header` prop. Note that +you will need to provide a method of closing the sidebar. The `default` slot is scoped, which +includes a `hide()` method that can be used to close the sidebar. + +```html + + + +``` + +### Footer + +`` provides a `footer` slot (optionally scoped), to allow you to provide content that +appears at the bottom of the sidebar. The `footer` slot is scoped, which includes a `hide()` method +that can be used to close the sidebar. + +```html + + + +``` + +You can apply arbitrary classes to the footer section via the `footer-class` prop. + +### Lazy rendering + +In some instances, you may not want the content rendered when the sidebar is not visible. Simply set +the `lazy` prop on ``. When `lazy` is `true`, the body and optional footer will _not_ be +rendered (removed from DOM) whenever the sidebar is closed. + +### Backdrop + +2.12.0+ + +Add a basic backdrop when the side bar is open via the `backdrop` prop. When set to `true`, the +sidebar will show an opaque backdrop. Clicking on the backdrop will close the sidebar, unless the +`no-close-on-backdrop` prop is set to `true`. + +Optionally (as of BootstrapVue v2.15.0+) you can use the `backdrop-variant` prop to control the +theme color variant of the backdrop. The default backdrop variant is `dark`. + +```html + + + + + +``` + +Note that when the sidebar is open, it may still be possible to scroll the body (unlike the +behaviour of modals). When the backdrop in enabled, focus constraint will attempt to keep focus +within the sidebar. Note that in rare circumstances it might be possible for users to move focus to +elements outside of the sidebar. + +## Visibility control + +### `v-b-toggle` directive + +Using the [`v-b-toggle` directive](/docs/directive/toggle) is the preferred method for _opening_ the +sidebar, as it automatically handles applying the `aria-controls` and `aria-expanded` accessibility +attributes on the trigger element. + +The majority of examples on this page use the `v-b-toggle` directive. + +### `v-model` + +The `v-model` reflects the current visibility state of the sidebar. While it can be used to control +the visibility state of the sidebar, it is recommended to use the +[`v-b-toggle` directive](#v-b-toggle-directive) to _show_ the sidebar for accessibility reasons. If +you do use the `v-model` to show the sidebar, you should place the `aria-controls="id"` attribute +(where `id` is the ID of the sidebar) on the trigger element, and also set the `aria-expanded` +attribute (also on the trigger element) to either the string `'true'` (if the sidebar is open) or +`'false`' (if the sidebar is closed). + +The `v-model` is internally bound to the `visible` prop, and the `change` event updates the +`v-model`. + +### Closing on $route change + +By default, `` will close itself when the `$route` changes (full path including query and +hash). This can be particularly handy if the sidebar is placed outside of your `` and +is used for navigation. + +You can disable this behaviour by setting the `no-close-on-route-change` prop to `true`. + +## Events + +The sidebar will emit the `shown` event once the sidebar has opened, and the `hidden` event when the +sidebar has closed. + +The `change` event is used to update the `v-model` and is emitted whenever the visibility state of +the sidebar changes. + +## Accessibility + +`` provides several accessibility features. + +When the sidebar is opened, the entire sidebar will receive focus, which is desirable for screen +reader and keyboard-only users. When the sidebar is closed, the element that previously had focus +before the sidebar was opened will be re-focused. + +When the sidebar is open, users can press Esc to close the sidebar. To disable this +feature, set the `no-close-on-esc` prop to `true`. with the backdrop enabled, you can use the prop +`no-close-on-backdrop` to disable the close on backdrop click feature. + +When the `backdrop` prop is `true`, the sidebar will attempt to constrain focus within the sidebar, +and the sidebar will have the attribute `aria-modal="true"` set. + +When you have hidden the header, or do not have a title for the sidebar, set either `aria-label` to +a string that describes the sidebar, or set `aria-labelledby` to an ID of an element that contains +the title. When using the `lazy` prop _and_ you do not have a header, use the `aria-label` prop to +provide an appropriate string to label the sidebar. + +## Implementation notes + +BootstrapVue's custom SCSS/CSS is required for proper styling, and positioning of the sidebar. + +The Bootstrap v4 background (`'bg-*'`) and text (`'text-*'`) utility classes are used for +controlling the background and font color, respectively. + +Some of the default styling for `` can be customized via the use of SASS variables. Refer +to the [theming documentation](/docs/reference/theming) for additional details. + +## See also + +- [`v-b-toggle` directive](/docs/directives/toggle) +- [`` component](/docs/components/collapse) +- [`` component](/docs/components/button#comp-ref-b-button-close) diff --git a/src/components/sidebar/_sidebar.scss b/src/components/sidebar/_sidebar.scss index 52fe74bb625..6fcccb87ea0 100644 --- a/src/components/sidebar/_sidebar.scss +++ b/src/components/sidebar/_sidebar.scss @@ -1,97 +1,96 @@ -.b-sidebar-outer { - position: fixed !important; - top: 0; - left: 0; - right: 0; - height: 0; - overflow: visible; - z-index: $b-sidebar-zindex; -} - -.b-sidebar-backdrop { - position: fixed !important; - top: 0; - left: 0; - z-index: -1; - width: 100vw; - height: 100vh; - opacity: 0.6; -} - -.b-sidebar { - display: flex; - flex-direction: column; - position: fixed !important; - top: 0; - height: 100vh; - width: $b-sidebar-width; - max-width: 100% !important; - height: 100vh !important; - margin: 0 !important; - outline: 0; - transform: translateX(0); - - &.slide { - transition: transform $b-sidebar-transition-duration ease-in-out; - @media (prefers-reduced-motion: reduce) { - transition: none; - } - } - - &:not(.b-sidebar-right) { - left: 0; - right: auto; - - &.slide:not(.show) { - transform: translateX(-100%); - } - - > .b-sidebar-header .close { - margin-left: auto; - } - } - - &.b-sidebar-right { - left: auto; - right: 0; - - &.slide:not(.show) { - transform: translateX(100%); - } - - > .b-sidebar-header .close { - margin-right: auto; - } - } - - > .b-sidebar-header { - font-size: $b-sidebar-header-font-size; - padding: $b-sidebar-header-padding-y $b-sidebar-header-padding-x; - display: flex; - flex-direction: row; - flex-grow: 0; - align-items: center; - - @at-root { - // Keep the buttons on the correct end when in RTL mode - [dir="rtl"] & { - flex-direction: row-reverse; - } - } - - .close { - float: none; - font-size: $b-sidebar-header-font-size; - } - } - - > .b-sidebar-body { - flex-grow: 1; - height: 100%; - overflow-y: auto; - } - - > .b-sidebar-footer { - flex-grow: 0; - } -} +.b-sidebar-outer { + position: fixed !important; + top: 0; + left: 0; + right: 0; + height: 0; + overflow: visible; + z-index: $b-sidebar-zindex; +} + +.b-sidebar-backdrop { + position: fixed !important; + top: 0; + left: 0; + z-index: -1; + width: 100vw; + height: 100vh; + opacity: 0.6; +} + +.b-sidebar { + display: flex; + flex-direction: column; + position: fixed !important; + top: 0; + height: 100vh; + width: $b-sidebar-width; + max-width: 100% !important; + margin: 0 !important; + outline: 0; + transform: translateX(0); + + &.slide { + transition: transform $b-sidebar-transition-duration ease-in-out; + @media (prefers-reduced-motion: reduce) { + transition: none; + } + } + + &:not(.b-sidebar-right) { + left: 0; + right: auto; + + &.slide:not(.show) { + transform: translateX(-100%); + } + + > .b-sidebar-header .close { + margin-left: auto; + } + } + + &.b-sidebar-right { + left: auto; + right: 0; + + &.slide:not(.show) { + transform: translateX(100%); + } + + > .b-sidebar-header .close { + margin-right: auto; + } + } + + > .b-sidebar-header { + font-size: $b-sidebar-header-font-size; + padding: $b-sidebar-header-padding-y $b-sidebar-header-padding-x; + display: flex; + flex-direction: row; + flex-grow: 0; + align-items: center; + + @at-root { + // Keep the buttons on the correct end when in RTL mode + [dir="rtl"] & { + flex-direction: row-reverse; + } + } + + .close { + float: none; + font-size: $b-sidebar-header-font-size; + } + } + + > .b-sidebar-body { + flex-grow: 1; + height: 100%; + overflow-y: auto; + } + + > .b-sidebar-footer { + flex-grow: 0; + } +} diff --git a/src/components/sidebar/index.d.ts b/src/components/sidebar/index.d.ts index 2b66de90936..2f33fd8484b 100644 --- a/src/components/sidebar/index.d.ts +++ b/src/components/sidebar/index.d.ts @@ -1,11 +1,11 @@ -// -// Sidebar -// -import Vue from 'vue' -import { BvPlugin, BvComponent } from '../../' - -// Plugin -export declare const SidebarPlugin: BvPlugin - -// Component: b-sidebar -export declare class BSidebar extends BvComponent {} +// +// Sidebar +// +import Vue from 'vue' +import { BvPlugin, BvComponent } from '../../' + +// Plugin +export declare const SidebarPlugin: BvPlugin + +// Component: b-sidebar +export declare class BSidebar extends BvComponent {} diff --git a/src/components/sidebar/index.js b/src/components/sidebar/index.js index d777a6a62f2..07664e3a693 100644 --- a/src/components/sidebar/index.js +++ b/src/components/sidebar/index.js @@ -1,10 +1,10 @@ -import { BSidebar } from './sidebar' -import { VBTogglePlugin } from '../../directives/toggle' -import { pluginFactory } from '../../utils/plugins' - -const SidebarPlugin = /*#__PURE__*/ pluginFactory({ - components: { BSidebar }, - plugins: { VBTogglePlugin } -}) - -export { SidebarPlugin, BSidebar } +import { BSidebar } from './sidebar' +import { VBTogglePlugin } from '../../directives/toggle' +import { pluginFactory } from '../../utils/plugins' + +const SidebarPlugin = /*#__PURE__*/ pluginFactory({ + components: { BSidebar }, + plugins: { VBTogglePlugin } +}) + +export { SidebarPlugin, BSidebar } diff --git a/src/components/sidebar/index.scss b/src/components/sidebar/index.scss index 75f97451361..0abffa8bd3b 100644 --- a/src/components/sidebar/index.scss +++ b/src/components/sidebar/index.scss @@ -1 +1 @@ -@import "sidebar"; +@import "sidebar"; diff --git a/src/components/sidebar/package.json b/src/components/sidebar/package.json index 8117d7b1b63..3e67613def3 100644 --- a/src/components/sidebar/package.json +++ b/src/components/sidebar/package.json @@ -1,203 +1,203 @@ -{ - "name": "@bootstrap-vue/sidebar", - "version": "1.0.0", - "meta": { - "title": "Sidebar", - "new": true, - "version": "2.10.0", - "description": "The `` component creates a fixed viewport, left or right, sliding popout drawer.", - "plugins": [ - "VBTogglePlugin" - ], - "components": [ - { - "component": "BSidebar", - "version": "2.10.0", - "props": [ - { - "prop": "title", - "description": "Text content to place in the default header. The `title` slot takes precedence" - }, - { - "prop": "right", - "description": "When `true`, positions the sidebar on the right of the viewport" - }, - { - "prop": "visible", - "description": "When `true`, opens the sidebar. This is the `v-model`" - }, - { - "prop": "bgVariant", - "description": "Theme variant color for the background of the sidebar" - }, - { - "prop": "textVariant", - "description": "Theme variant color for the text of the sidebar" - }, - { - "prop": "noSlide", - "description": "When set, disables the default sliding animation" - }, - { - "prop": "shadow", - "description": "Set to boolean `true` for medium shadow, 'sm' for small shadow, 'lg' for large shadow, or boolean `false` for no shadow. Default is no shadow" - }, - { - "prop": "width", - "description": "CSS width for the sidebar. Defaults to '320px' as defined by SCSS/CSS" - }, - { - "prop": "zIndex", - "description": "Specify an arbitrary z-index value to override the value defined by SCSS/CSS" - }, - { - "prop": "closeLabel", - "description": "`aria-label` to apply to the built-in close button. Defaults to 'Close'" - }, - { - "prop": "sidebarClass", - "version": "2.12.0", - "description": "Class, or classes, to apply to the sidebar content wrapper" - }, - { - "prop": "headerClass", - "description": "Class, or classes, to apply to the built in header. Has no effect if prop `no-header` is set" - }, - { - "prop": "bodyClass", - "description": "Class, or classes, to apply to the body (default slot) of the sidebar" - }, - { - "prop": "footerClass", - "description": "Class, or classes, to apply to the optional `footer` slot" - }, - { - "prop": "backdrop", - "version": "2.12.0", - "description": "When `true`, shows a backdrop when the sidebar is open" - }, - { - "prop": "backdropVariant", - "version": "2.15.0", - "description": "Theme variant color for the backdrop of the sidebar. Defaults to 'dark'" - }, - { - "prop": "lazy", - "description": "When set to `true`, the content of the sidebar will only be rendered while the sidebar is open" - }, - { - "prop": "noHeader", - "description": "When set to `true` disables rendering of the default header (including close button)" - }, - { - "prop": "noHeaderClose", - "description": "When set to `true` disables rendering of the header close button" - }, - { - "prop": "noCloseOnEsc", - "description": "When set to `true`, disables closing the sidebar when the user presses ESC" - }, - { - "prop": "noCloseOnBackdrop", - "version": "2.12.0", - "description": "When set to `true`, disables closing the sidebar when the user clicks on the backdrop. Requires that the prop `backdrop` be set" - }, - { - "prop": "noCloseOnRouteChange", - "description": "When set to `true`, disables closing of the sidebar on route change" - } - ], - "events": [ - { - "event": "change", - "description": "Emitted whenever the visibility of the sidebar changes. Used to update the `v-model`", - "args": [ - { - "arg": "visible", - "type": "Boolean", - "description": "`true` if the sidebar is open, `false` if it is closed (or in the process of closing)" - } - ] - }, - { - "event": "shown", - "description": "Emitted when the sidebar has opened" - }, - { - "event": "hidden", - "description": "Emitted when the sidebar has been hidden" - } - ], - "slots": [ - { - "name": "title", - "description": "Content to place in the title of the built-in header. Takes precedence over the `title` prop", - "scope": [ - { - "prop": "hide", - "type": "Function", - "description": "When called, will close the sidebar" - }, - { - "prop": "visible", - "type": "Boolean", - "description": "`true` if the sidebar is open" - }, - { - "prop": "right", - "type": "Boolean", - "description": "`true` if the sidebar is on the right" - } - ] - }, - { - "name": "header-close", - "description": "Content of the header close button. Defaults to ``" - }, - { - "name": "default", - "description": "Content to place in the body of the sidebar", - "scope": [ - { - "prop": "hide", - "type": "Function", - "description": "When called, will close the sidebar" - }, - { - "prop": "visible", - "type": "Boolean", - "description": "`true` if the sidebar is open" - }, - { - "prop": "right", - "type": "Boolean", - "description": "`true` if the sidebar is on the right" - } - ] - }, - { - "name": "footer", - "description": "Content to place in the optional footer", - "scope": [ - { - "prop": "hide", - "type": "Function", - "description": "When called, will close the sidebar" - }, - { - "prop": "visible", - "type": "Boolean", - "description": "`true` if the sidebar is open" - }, - { - "prop": "right", - "type": "Boolean", - "description": "`true` if the sidebar is on the right" - } - ] - } - ] - } - ] - } -} +{ + "name": "@bootstrap-vue/sidebar", + "version": "1.0.0", + "meta": { + "title": "Sidebar", + "new": true, + "version": "2.10.0", + "description": "The `` component creates a fixed viewport, left or right, sliding popout drawer.", + "plugins": [ + "VBTogglePlugin" + ], + "components": [ + { + "component": "BSidebar", + "version": "2.10.0", + "props": [ + { + "prop": "title", + "description": "Text content to place in the default header. The `title` slot takes precedence" + }, + { + "prop": "right", + "description": "When `true`, positions the sidebar on the right of the viewport" + }, + { + "prop": "visible", + "description": "When `true`, opens the sidebar. This is the `v-model`" + }, + { + "prop": "bgVariant", + "description": "Theme variant color for the background of the sidebar" + }, + { + "prop": "textVariant", + "description": "Theme variant color for the text of the sidebar" + }, + { + "prop": "noSlide", + "description": "When set, disables the default sliding animation" + }, + { + "prop": "shadow", + "description": "Set to boolean `true` for medium shadow, 'sm' for small shadow, 'lg' for large shadow, or boolean `false` for no shadow. Default is no shadow" + }, + { + "prop": "width", + "description": "CSS width for the sidebar. Defaults to '320px' as defined by SCSS/CSS" + }, + { + "prop": "zIndex", + "description": "Specify an arbitrary z-index value to override the value defined by SCSS/CSS" + }, + { + "prop": "closeLabel", + "description": "`aria-label` to apply to the built-in close button. Defaults to 'Close'" + }, + { + "prop": "sidebarClass", + "version": "2.12.0", + "description": "Class, or classes, to apply to the sidebar content wrapper" + }, + { + "prop": "headerClass", + "description": "Class, or classes, to apply to the built in header. Has no effect if prop `no-header` is set" + }, + { + "prop": "bodyClass", + "description": "Class, or classes, to apply to the body (default slot) of the sidebar" + }, + { + "prop": "footerClass", + "description": "Class, or classes, to apply to the optional `footer` slot" + }, + { + "prop": "backdrop", + "version": "2.12.0", + "description": "When `true`, shows a backdrop when the sidebar is open" + }, + { + "prop": "backdropVariant", + "version": "2.15.0", + "description": "Theme variant color for the backdrop of the sidebar. Defaults to 'dark'" + }, + { + "prop": "lazy", + "description": "When set to `true`, the content of the sidebar will only be rendered while the sidebar is open" + }, + { + "prop": "noHeader", + "description": "When set to `true` disables rendering of the default header (including close button)" + }, + { + "prop": "noHeaderClose", + "description": "When set to `true` disables rendering of the header close button" + }, + { + "prop": "noCloseOnEsc", + "description": "When set to `true`, disables closing the sidebar when the user presses ESC" + }, + { + "prop": "noCloseOnBackdrop", + "version": "2.12.0", + "description": "When set to `true`, disables closing the sidebar when the user clicks on the backdrop. Requires that the prop `backdrop` be set" + }, + { + "prop": "noCloseOnRouteChange", + "description": "When set to `true`, disables closing of the sidebar on route change" + } + ], + "events": [ + { + "event": "change", + "description": "Emitted whenever the visibility of the sidebar changes. Used to update the `v-model`", + "args": [ + { + "arg": "visible", + "type": "Boolean", + "description": "`true` if the sidebar is open, `false` if it is closed (or in the process of closing)" + } + ] + }, + { + "event": "shown", + "description": "Emitted when the sidebar has opened" + }, + { + "event": "hidden", + "description": "Emitted when the sidebar has been hidden" + } + ], + "slots": [ + { + "name": "title", + "description": "Content to place in the title of the built-in header. Takes precedence over the `title` prop", + "scope": [ + { + "prop": "hide", + "type": "Function", + "description": "When called, will close the sidebar" + }, + { + "prop": "visible", + "type": "Boolean", + "description": "`true` if the sidebar is open" + }, + { + "prop": "right", + "type": "Boolean", + "description": "`true` if the sidebar is on the right" + } + ] + }, + { + "name": "header-close", + "description": "Content of the header close button. Defaults to ``" + }, + { + "name": "default", + "description": "Content to place in the body of the sidebar", + "scope": [ + { + "prop": "hide", + "type": "Function", + "description": "When called, will close the sidebar" + }, + { + "prop": "visible", + "type": "Boolean", + "description": "`true` if the sidebar is open" + }, + { + "prop": "right", + "type": "Boolean", + "description": "`true` if the sidebar is on the right" + } + ] + }, + { + "name": "footer", + "description": "Content to place in the optional footer", + "scope": [ + { + "prop": "hide", + "type": "Function", + "description": "When called, will close the sidebar" + }, + { + "prop": "visible", + "type": "Boolean", + "description": "`true` if the sidebar is open" + }, + { + "prop": "right", + "type": "Boolean", + "description": "`true` if the sidebar is on the right" + } + ] + } + ] + } + ] + } +} diff --git a/src/components/sidebar/sidebar.js b/src/components/sidebar/sidebar.js index 911089d1729..3e9cadcf2e9 100644 --- a/src/components/sidebar/sidebar.js +++ b/src/components/sidebar/sidebar.js @@ -1,467 +1,467 @@ -import Vue from '../../utils/vue' -import KeyCodes from '../../utils/key-codes' -import BVTransition from '../../utils/bv-transition' -import { attemptFocus, contains, getActiveElement, getTabables } from '../../utils/dom' -import { getComponentConfig } from '../../utils/config' -import { isBrowser } from '../../utils/env' -import { toString } from '../../utils/string' -import attrsMixin from '../../mixins/attrs' -import idMixin from '../../mixins/id' -import listenOnRootMixin from '../../mixins/listen-on-root' -import normalizeSlotMixin from '../../mixins/normalize-slot' -import { - EVENT_TOGGLE, - EVENT_STATE, - EVENT_STATE_REQUEST, - EVENT_STATE_SYNC -} from '../../directives/toggle/toggle' -import { BButtonClose } from '../button/button-close' -import { BIconX } from '../../icons/icons' - -// --- Constants --- - -const NAME = 'BSidebar' -const CLASS_NAME = 'b-sidebar' - -// --- Render methods --- -const renderHeaderTitle = (h, ctx) => { - const title = ctx.normalizeSlot('title', ctx.slotScope) || toString(ctx.title) || null - - // Render a empty `` when to title was provided - if (!title) { - return h('span') - } - - return h('strong', { attrs: { id: ctx.safeId('__title__') } }, [title]) -} - -const renderHeaderClose = (h, ctx) => { - if (ctx.noHeaderClose) { - return h() - } - - const { closeLabel, textVariant, hide } = ctx - - return h( - BButtonClose, - { - ref: 'close-button', - props: { ariaLabel: closeLabel, textVariant }, - on: { click: hide } - }, - [ctx.normalizeSlot('header-close') || h(BIconX)] - ) -} - -const renderHeader = (h, ctx) => { - if (ctx.noHeader) { - return h() - } - - const $title = renderHeaderTitle(h, ctx) - const $close = renderHeaderClose(h, ctx) - - return h( - 'header', - { - key: 'header', - staticClass: `${CLASS_NAME}-header`, - class: ctx.headerClass - }, - ctx.right ? [$close, $title] : [$title, $close] - ) -} - -const renderBody = (h, ctx) => { - return h( - 'div', - { - key: 'body', - staticClass: `${CLASS_NAME}-body`, - class: ctx.bodyClass - }, - [ctx.normalizeSlot('default', ctx.slotScope)] - ) -} - -const renderFooter = (h, ctx) => { - const $footer = ctx.normalizeSlot('footer', ctx.slotScope) - if (!$footer) { - return h() - } - - return h( - 'footer', - { - key: 'footer', - staticClass: `${CLASS_NAME}-footer`, - class: ctx.footerClass - }, - [$footer] - ) -} - -const renderContent = (h, ctx) => { - // We render the header even if `lazy` is enabled as it - // acts as the accessible label for the sidebar - const $header = renderHeader(h, ctx) - if (ctx.lazy && !ctx.isOpen) { - return $header - } - - return [$header, renderBody(h, ctx), renderFooter(h, ctx)] -} - -const renderBackdrop = (h, ctx) => { - if (!ctx.backdrop) { - return h() - } - - const { backdropVariant } = ctx - - return h('div', { - directives: [{ name: 'show', value: ctx.localShow }], - staticClass: 'b-sidebar-backdrop', - class: { [`bg-${backdropVariant}`]: !!backdropVariant }, - on: { click: ctx.onBackdropClick } - }) -} - -// --- Main component --- -// @vue/component -export const BSidebar = /*#__PURE__*/ Vue.extend({ - name: NAME, - // Mixin order is important! - mixins: [attrsMixin, idMixin, listenOnRootMixin, normalizeSlotMixin], - inheritAttrs: false, - model: { - prop: 'visible', - event: 'change' - }, - props: { - title: { - type: String - // default: null - }, - right: { - type: Boolean, - default: false - }, - bgVariant: { - type: String, - default: () => getComponentConfig(NAME, 'bgVariant') - }, - textVariant: { - type: String, - default: () => getComponentConfig(NAME, 'textVariant') - }, - shadow: { - type: [Boolean, String], - default: () => getComponentConfig(NAME, 'shadow') - }, - width: { - type: String, - default: () => getComponentConfig(NAME, 'width') - }, - zIndex: { - type: [Number, String] - // default: null - }, - ariaLabel: { - type: String - // default: null - }, - ariaLabelledby: { - type: String - // default: null - }, - closeLabel: { - // `aria-label` for close button - // Defaults to 'Close' - type: String - // default: undefined - }, - tag: { - type: String, - default: () => getComponentConfig(NAME, 'tag') - }, - sidebarClass: { - type: [String, Array, Object] - // default: null - }, - headerClass: { - type: [String, Array, Object] - // default: null - }, - bodyClass: { - type: [String, Array, Object] - // default: null - }, - footerClass: { - type: [String, Array, Object] - // default: null - }, - backdrop: { - // If `true`, shows a basic backdrop - type: Boolean, - default: false - }, - backdropVariant: { - type: String, - default: () => getComponentConfig(NAME, 'backdropVariant') - }, - noSlide: { - type: Boolean, - default: false - }, - noHeader: { - type: Boolean, - default: false - }, - noHeaderClose: { - type: Boolean, - default: false - }, - noCloseOnEsc: { - type: Boolean, - default: false - }, - noCloseOnBackdrop: { - type: Boolean, - default: false - }, - noCloseOnRouteChange: { - type: Boolean, - default: false - }, - lazy: { - type: Boolean, - default: false - }, - visible: { - type: Boolean, - default: false - } - }, - data() { - return { - // Internal `v-model` state - localShow: !!this.visible, - // For lazy render triggering - isOpen: !!this.visible - } - }, - computed: { - transitionProps() { - return this.noSlide - ? /* istanbul ignore next */ { css: true } - : { - css: true, - enterClass: '', - enterActiveClass: 'slide', - enterToClass: 'show', - leaveClass: 'show', - leaveActiveClass: 'slide', - leaveToClass: '' - } - }, - slotScope() { - return { - visible: this.localShow, - right: this.right, - hide: this.hide - } - }, - computedTile() { - return this.normalizeSlot('title', this.slotScope) || toString(this.title) || null - }, - titleId() { - return this.computedTile ? this.safeId('__title__') : null - }, - computedAttrs() { - return { - ...this.bvAttrs, - id: this.safeId(), - tabindex: '-1', - role: 'dialog', - 'aria-modal': this.backdrop ? 'true' : 'false', - 'aria-hidden': this.localShow ? null : 'true', - 'aria-label': this.ariaLabel || null, - 'aria-labelledby': this.ariaLabelledby || this.titleId || null - } - } - }, - watch: { - visible(newVal, oldVal) { - if (newVal !== oldVal) { - this.localShow = newVal - } - }, - localShow(newVal, oldVal) { - if (newVal !== oldVal) { - this.emitState(newVal) - this.$emit('change', newVal) - } - }, - /* istanbul ignore next */ - $route(newVal = {}, oldVal = {}) /* istanbul ignore next: pain to mock */ { - if (!this.noCloseOnRouteChange && newVal.fullPath !== oldVal.fullPath) { - this.hide() - } - } - }, - created() { - // Define non-reactive properties - this.$_returnFocusEl = null - }, - mounted() { - // Add `$root` listeners - this.listenOnRoot(EVENT_TOGGLE, this.handleToggle) - this.listenOnRoot(EVENT_STATE_REQUEST, this.handleSync) - // Send out a gratuitous state event to ensure toggle button is synced - this.$nextTick(() => { - this.emitState(this.localShow) - }) - }, - /* istanbul ignore next */ - activated() /* istanbul ignore next */ { - this.emitSync() - }, - beforeDestroy() { - this.localShow = false - this.$_returnFocusEl = null - }, - methods: { - hide() { - this.localShow = false - }, - emitState(state = this.localShow) { - this.emitOnRoot(EVENT_STATE, this.safeId(), state) - }, - emitSync(state = this.localShow) { - this.emitOnRoot(EVENT_STATE_SYNC, this.safeId(), state) - }, - handleToggle(id) { - // Note `safeId()` can be null until after mount - if (id && id === this.safeId()) { - this.localShow = !this.localShow - } - }, - handleSync(id) { - // Note `safeId()` can be null until after mount - if (id && id === this.safeId()) { - this.$nextTick(() => { - this.emitSync(this.localShow) - }) - } - }, - onKeydown(evt) { - const { keyCode } = evt - if (!this.noCloseOnEsc && keyCode === KeyCodes.ESC && this.localShow) { - this.hide() - } - }, - onBackdropClick() { - if (this.localShow && !this.noCloseOnBackdrop) { - this.hide() - } - }, - /* istanbul ignore next */ - onTopTrapFocus() /* istanbul ignore next */ { - const tabables = getTabables(this.$refs.content) - attemptFocus(tabables.reverse()[0]) - }, - /* istanbul ignore next */ - onBottomTrapFocus() /* istanbul ignore next */ { - const tabables = getTabables(this.$refs.content) - attemptFocus(tabables[0]) - }, - onBeforeEnter() { - // Returning focus to `document.body` may cause unwanted scrolls, - // so we exclude setting focus on body - this.$_returnFocusEl = getActiveElement(isBrowser ? [document.body] : []) - // Trigger lazy render - this.isOpen = true - }, - onAfterEnter(el) { - if (!contains(el, getActiveElement())) { - attemptFocus(el) - } - this.$emit('shown') - }, - onAfterLeave() { - attemptFocus(this.$_returnFocusEl) - this.$_returnFocusEl = null - // Trigger lazy render - this.isOpen = false - this.$emit('hidden') - } - }, - render(h) { - const localShow = this.localShow - const shadow = this.shadow === '' ? true : this.shadow - - let $sidebar = h( - this.tag, - { - ref: 'content', - directives: [{ name: 'show', value: localShow }], - staticClass: CLASS_NAME, - class: [ - { - shadow: shadow === true, - [`shadow-${shadow}`]: shadow && shadow !== true, - [`${CLASS_NAME}-right`]: this.right, - [`bg-${this.bgVariant}`]: !!this.bgVariant, - [`text-${this.textVariant}`]: !!this.textVariant - }, - this.sidebarClass - ], - attrs: this.computedAttrs, - style: { width: this.width } - }, - [renderContent(h, this)] - ) - - $sidebar = h( - 'transition', - { - props: this.transitionProps, - on: { - beforeEnter: this.onBeforeEnter, - afterEnter: this.onAfterEnter, - afterLeave: this.onAfterLeave - } - }, - [$sidebar] - ) - - const $backdrop = h(BVTransition, { props: { noFade: this.noSlide } }, [ - renderBackdrop(h, this) - ]) - - let $tabTrapTop = h() - let $tabTrapBottom = h() - if (this.backdrop && this.localShow) { - $tabTrapTop = h('div', { - attrs: { tabindex: '0' }, - on: { focus: this.onTopTrapFocus } - }) - $tabTrapBottom = h('div', { - attrs: { tabindex: '0' }, - on: { focus: this.onBottomTrapFocus } - }) - } - - return h( - 'div', - { - staticClass: 'b-sidebar-outer', - style: { zIndex: this.zIndex }, - attrs: { tabindex: '-1' }, - on: { keydown: this.onKeydown } - }, - [$tabTrapTop, $sidebar, $tabTrapBottom, $backdrop] - ) - } -}) +import Vue from '../../utils/vue' +import KeyCodes from '../../utils/key-codes' +import BVTransition from '../../utils/bv-transition' +import { attemptFocus, contains, getActiveElement, getTabables } from '../../utils/dom' +import { getComponentConfig } from '../../utils/config' +import { isBrowser } from '../../utils/env' +import { toString } from '../../utils/string' +import attrsMixin from '../../mixins/attrs' +import idMixin from '../../mixins/id' +import listenOnRootMixin from '../../mixins/listen-on-root' +import normalizeSlotMixin from '../../mixins/normalize-slot' +import { + EVENT_TOGGLE, + EVENT_STATE, + EVENT_STATE_REQUEST, + EVENT_STATE_SYNC +} from '../../directives/toggle/toggle' +import { BButtonClose } from '../button/button-close' +import { BIconX } from '../../icons/icons' + +// --- Constants --- + +const NAME = 'BSidebar' +const CLASS_NAME = 'b-sidebar' + +// --- Render methods --- +const renderHeaderTitle = (h, ctx) => { + const title = ctx.normalizeSlot('title', ctx.slotScope) || toString(ctx.title) || null + + // Render a empty `` when to title was provided + if (!title) { + return h('span') + } + + return h('strong', { attrs: { id: ctx.safeId('__title__') } }, [title]) +} + +const renderHeaderClose = (h, ctx) => { + if (ctx.noHeaderClose) { + return h() + } + + const { closeLabel, textVariant, hide } = ctx + + return h( + BButtonClose, + { + ref: 'close-button', + props: { ariaLabel: closeLabel, textVariant }, + on: { click: hide } + }, + [ctx.normalizeSlot('header-close') || h(BIconX)] + ) +} + +const renderHeader = (h, ctx) => { + if (ctx.noHeader) { + return h() + } + + const $title = renderHeaderTitle(h, ctx) + const $close = renderHeaderClose(h, ctx) + + return h( + 'header', + { + key: 'header', + staticClass: `${CLASS_NAME}-header`, + class: ctx.headerClass + }, + ctx.right ? [$close, $title] : [$title, $close] + ) +} + +const renderBody = (h, ctx) => { + return h( + 'div', + { + key: 'body', + staticClass: `${CLASS_NAME}-body`, + class: ctx.bodyClass + }, + [ctx.normalizeSlot('default', ctx.slotScope)] + ) +} + +const renderFooter = (h, ctx) => { + const $footer = ctx.normalizeSlot('footer', ctx.slotScope) + if (!$footer) { + return h() + } + + return h( + 'footer', + { + key: 'footer', + staticClass: `${CLASS_NAME}-footer`, + class: ctx.footerClass + }, + [$footer] + ) +} + +const renderContent = (h, ctx) => { + // We render the header even if `lazy` is enabled as it + // acts as the accessible label for the sidebar + const $header = renderHeader(h, ctx) + if (ctx.lazy && !ctx.isOpen) { + return $header + } + + return [$header, renderBody(h, ctx), renderFooter(h, ctx)] +} + +const renderBackdrop = (h, ctx) => { + if (!ctx.backdrop) { + return h() + } + + const { backdropVariant } = ctx + + return h('div', { + directives: [{ name: 'show', value: ctx.localShow }], + staticClass: 'b-sidebar-backdrop', + class: { [`bg-${backdropVariant}`]: !!backdropVariant }, + on: { click: ctx.onBackdropClick } + }) +} + +// --- Main component --- +// @vue/component +export const BSidebar = /*#__PURE__*/ Vue.extend({ + name: NAME, + // Mixin order is important! + mixins: [attrsMixin, idMixin, listenOnRootMixin, normalizeSlotMixin], + inheritAttrs: false, + model: { + prop: 'visible', + event: 'change' + }, + props: { + title: { + type: String + // default: null + }, + right: { + type: Boolean, + default: false + }, + bgVariant: { + type: String, + default: () => getComponentConfig(NAME, 'bgVariant') + }, + textVariant: { + type: String, + default: () => getComponentConfig(NAME, 'textVariant') + }, + shadow: { + type: [Boolean, String], + default: () => getComponentConfig(NAME, 'shadow') + }, + width: { + type: String, + default: () => getComponentConfig(NAME, 'width') + }, + zIndex: { + type: [Number, String] + // default: null + }, + ariaLabel: { + type: String + // default: null + }, + ariaLabelledby: { + type: String + // default: null + }, + closeLabel: { + // `aria-label` for close button + // Defaults to 'Close' + type: String + // default: undefined + }, + tag: { + type: String, + default: () => getComponentConfig(NAME, 'tag') + }, + sidebarClass: { + type: [String, Array, Object] + // default: null + }, + headerClass: { + type: [String, Array, Object] + // default: null + }, + bodyClass: { + type: [String, Array, Object] + // default: null + }, + footerClass: { + type: [String, Array, Object] + // default: null + }, + backdrop: { + // If `true`, shows a basic backdrop + type: Boolean, + default: false + }, + backdropVariant: { + type: String, + default: () => getComponentConfig(NAME, 'backdropVariant') + }, + noSlide: { + type: Boolean, + default: false + }, + noHeader: { + type: Boolean, + default: false + }, + noHeaderClose: { + type: Boolean, + default: false + }, + noCloseOnEsc: { + type: Boolean, + default: false + }, + noCloseOnBackdrop: { + type: Boolean, + default: false + }, + noCloseOnRouteChange: { + type: Boolean, + default: false + }, + lazy: { + type: Boolean, + default: false + }, + visible: { + type: Boolean, + default: false + } + }, + data() { + return { + // Internal `v-model` state + localShow: !!this.visible, + // For lazy render triggering + isOpen: !!this.visible + } + }, + computed: { + transitionProps() { + return this.noSlide + ? /* istanbul ignore next */ { css: true } + : { + css: true, + enterClass: '', + enterActiveClass: 'slide', + enterToClass: 'show', + leaveClass: 'show', + leaveActiveClass: 'slide', + leaveToClass: '' + } + }, + slotScope() { + return { + visible: this.localShow, + right: this.right, + hide: this.hide + } + }, + computedTile() { + return this.normalizeSlot('title', this.slotScope) || toString(this.title) || null + }, + titleId() { + return this.computedTile ? this.safeId('__title__') : null + }, + computedAttrs() { + return { + ...this.bvAttrs, + id: this.safeId(), + tabindex: '-1', + role: 'dialog', + 'aria-modal': this.backdrop ? 'true' : 'false', + 'aria-hidden': this.localShow ? null : 'true', + 'aria-label': this.ariaLabel || null, + 'aria-labelledby': this.ariaLabelledby || this.titleId || null + } + } + }, + watch: { + visible(newVal, oldVal) { + if (newVal !== oldVal) { + this.localShow = newVal + } + }, + localShow(newVal, oldVal) { + if (newVal !== oldVal) { + this.emitState(newVal) + this.$emit('change', newVal) + } + }, + /* istanbul ignore next */ + $route(newVal = {}, oldVal = {}) /* istanbul ignore next: pain to mock */ { + if (!this.noCloseOnRouteChange && newVal.fullPath !== oldVal.fullPath) { + this.hide() + } + } + }, + created() { + // Define non-reactive properties + this.$_returnFocusEl = null + }, + mounted() { + // Add `$root` listeners + this.listenOnRoot(EVENT_TOGGLE, this.handleToggle) + this.listenOnRoot(EVENT_STATE_REQUEST, this.handleSync) + // Send out a gratuitous state event to ensure toggle button is synced + this.$nextTick(() => { + this.emitState(this.localShow) + }) + }, + /* istanbul ignore next */ + activated() /* istanbul ignore next */ { + this.emitSync() + }, + beforeDestroy() { + this.localShow = false + this.$_returnFocusEl = null + }, + methods: { + hide() { + this.localShow = false + }, + emitState(state = this.localShow) { + this.emitOnRoot(EVENT_STATE, this.safeId(), state) + }, + emitSync(state = this.localShow) { + this.emitOnRoot(EVENT_STATE_SYNC, this.safeId(), state) + }, + handleToggle(id) { + // Note `safeId()` can be null until after mount + if (id && id === this.safeId()) { + this.localShow = !this.localShow + } + }, + handleSync(id) { + // Note `safeId()` can be null until after mount + if (id && id === this.safeId()) { + this.$nextTick(() => { + this.emitSync(this.localShow) + }) + } + }, + onKeydown(evt) { + const { keyCode } = evt + if (!this.noCloseOnEsc && keyCode === KeyCodes.ESC && this.localShow) { + this.hide() + } + }, + onBackdropClick() { + if (this.localShow && !this.noCloseOnBackdrop) { + this.hide() + } + }, + /* istanbul ignore next */ + onTopTrapFocus() /* istanbul ignore next */ { + const tabables = getTabables(this.$refs.content) + attemptFocus(tabables.reverse()[0]) + }, + /* istanbul ignore next */ + onBottomTrapFocus() /* istanbul ignore next */ { + const tabables = getTabables(this.$refs.content) + attemptFocus(tabables[0]) + }, + onBeforeEnter() { + // Returning focus to `document.body` may cause unwanted scrolls, + // so we exclude setting focus on body + this.$_returnFocusEl = getActiveElement(isBrowser ? [document.body] : []) + // Trigger lazy render + this.isOpen = true + }, + onAfterEnter(el) { + if (!contains(el, getActiveElement())) { + attemptFocus(el) + } + this.$emit('shown') + }, + onAfterLeave() { + attemptFocus(this.$_returnFocusEl) + this.$_returnFocusEl = null + // Trigger lazy render + this.isOpen = false + this.$emit('hidden') + } + }, + render(h) { + const localShow = this.localShow + const shadow = this.shadow === '' ? true : this.shadow + + let $sidebar = h( + this.tag, + { + ref: 'content', + directives: [{ name: 'show', value: localShow }], + staticClass: CLASS_NAME, + class: [ + { + shadow: shadow === true, + [`shadow-${shadow}`]: shadow && shadow !== true, + [`${CLASS_NAME}-right`]: this.right, + [`bg-${this.bgVariant}`]: !!this.bgVariant, + [`text-${this.textVariant}`]: !!this.textVariant + }, + this.sidebarClass + ], + attrs: this.computedAttrs, + style: { width: this.width } + }, + [renderContent(h, this)] + ) + + $sidebar = h( + 'transition', + { + props: this.transitionProps, + on: { + beforeEnter: this.onBeforeEnter, + afterEnter: this.onAfterEnter, + afterLeave: this.onAfterLeave + } + }, + [$sidebar] + ) + + const $backdrop = h(BVTransition, { props: { noFade: this.noSlide } }, [ + renderBackdrop(h, this) + ]) + + let $tabTrapTop = h() + let $tabTrapBottom = h() + if (this.backdrop && this.localShow) { + $tabTrapTop = h('div', { + attrs: { tabindex: '0' }, + on: { focus: this.onTopTrapFocus } + }) + $tabTrapBottom = h('div', { + attrs: { tabindex: '0' }, + on: { focus: this.onBottomTrapFocus } + }) + } + + return h( + 'div', + { + staticClass: 'b-sidebar-outer', + style: { zIndex: this.zIndex }, + attrs: { tabindex: '-1' }, + on: { keydown: this.onKeydown } + }, + [$tabTrapTop, $sidebar, $tabTrapBottom, $backdrop] + ) + } +}) diff --git a/src/components/sidebar/sidebar.spec.js b/src/components/sidebar/sidebar.spec.js index fb2ef04409f..405db38ec01 100644 --- a/src/components/sidebar/sidebar.spec.js +++ b/src/components/sidebar/sidebar.spec.js @@ -1,368 +1,368 @@ -import { createWrapper, mount } from '@vue/test-utils' -import { createContainer, waitNT, waitRAF } from '../../../tests/utils' -import { BSidebar } from './sidebar' - -const EVENT_TOGGLE = 'bv::toggle::collapse' -const EVENT_STATE = 'bv::collapse::state' -const EVENT_STATE_SYNC = 'bv::collapse::sync::state' -const EVENT_STATE_REQUEST = 'bv::request::collapse::state' - -describe('sidebar', () => { - it('should have expected default structure', async () => { - const wrapper = mount(BSidebar, { - attachTo: createContainer(), - propsData: { - id: 'test-1', - visible: true - } - }) - - expect(wrapper.vm).toBeDefined() - - const $sidebar = wrapper.find('.b-sidebar') - expect($sidebar.exists()).toBe(true) - - const $backdrop = wrapper.find('.b-sidebar-backdrop') - expect($backdrop.exists()).toBe(false) - - expect($sidebar.element.tagName).toBe('DIV') - expect($sidebar.attributes('id')).toBeDefined() - expect($sidebar.attributes('id')).toEqual('test-1') - expect($sidebar.classes()).toContain('b-sidebar') - expect($sidebar.classes()).not.toContain('b-sidebar-right') - // `show` and `slide` class only added during transition - expect($sidebar.classes()).not.toContain('show') - expect($sidebar.classes()).not.toContain('slide') - expect($sidebar.text()).toEqual('') - // Check for no presence of `display: none' from `v-show` directive - expect($sidebar.element).toBeVisible() - - expect($sidebar.find('.b-sidebar-header').exists()).toBe(true) - expect($sidebar.find('.b-sidebar-body').exists()).toBe(true) - expect($sidebar.find('.b-sidebar-footer').exists()).toBe(false) - - await wrapper.setProps({ visible: false }) - await waitRAF() - await waitRAF() - expect(wrapper.vm).toBeDefined() - expect(wrapper.element.tagName).toBe('DIV') - // Check for no presence of `display: none' from `v-show` directive - expect($sidebar.element).not.toBeVisible() - - await wrapper.setProps({ visible: true }) - await waitRAF() - await waitRAF() - expect(wrapper.element.tagName).toBe('DIV') - // Check for no presence of `display: none' from `v-show` directive - expect($sidebar.element).toBeVisible() - - wrapper.destroy() - }) - - it('shows backdrop when prop `backdrop` is `true`', async () => { - const wrapper = mount(BSidebar, { - attachTo: createContainer(), - propsData: { - id: 'test-backdrop', - noCloseOnBackdrop: true, - visible: true, - backdrop: true - } - }) - - expect(wrapper.vm).toBeDefined() - - const $sidebar = wrapper.find('.b-sidebar') - expect($sidebar.exists()).toBe(true) - - const $backdrop = wrapper.find('.b-sidebar-backdrop') - expect($backdrop.exists()).toBe(true) - expect($backdrop.classes()).toContain('bg-dark') - - await $backdrop.trigger('click') - await waitRAF() - await waitRAF() - expect($sidebar.element).toBeVisible() - expect($backdrop.element).toBeVisible() - - await wrapper.setProps({ noCloseOnBackdrop: false }) - await waitRAF() - await waitRAF() - expect($sidebar.element).toBeVisible() - expect($backdrop.element).toBeVisible() - - await $backdrop.trigger('click') - await waitRAF() - await waitRAF() - expect($sidebar.element).not.toBeVisible() - expect($backdrop.element).not.toBeVisible() - - wrapper.destroy() - }) - - it('applies "bg-*" class to backdrop based on `backdrop-variant` prop', async () => { - const wrapper = mount(BSidebar, { - attachTo: createContainer(), - propsData: { - id: 'test-backdrop', - noCloseOnBackdrop: true, - visible: true, - backdrop: true, - backdropVariant: 'transparent' - } - }) - - expect(wrapper.vm).toBeDefined() - - const $sidebar = wrapper.find('.b-sidebar') - expect($sidebar.exists()).toBe(true) - - const $backdrop = wrapper.find('.b-sidebar-backdrop') - expect($backdrop.exists()).toBe(true) - expect($backdrop.classes()).toContain('bg-transparent') - - await $backdrop.trigger('click') - await waitRAF() - await waitRAF() - expect($sidebar.element).toBeVisible() - expect($backdrop.element).toBeVisible() - - await wrapper.setProps({ noCloseOnBackdrop: false }) - await waitRAF() - await waitRAF() - expect($sidebar.element).toBeVisible() - expect($backdrop.element).toBeVisible() - - await $backdrop.trigger('click') - await waitRAF() - await waitRAF() - expect($sidebar.element).not.toBeVisible() - expect($backdrop.element).not.toBeVisible() - - wrapper.destroy() - }) - - it('shows and hides in response to v-b-toggle events', async () => { - const wrapper = mount(BSidebar, { - attachTo: createContainer(), - propsData: { - id: 'test-toggle' - } - }) - - expect(wrapper.vm).toBeDefined() - - const $sidebar = wrapper.find('.b-sidebar') - expect($sidebar.exists()).toBe(true) - expect($sidebar.element.tagName).toBe('DIV') - expect($sidebar.element).not.toBeVisible() - - wrapper.vm.$root.$emit(EVENT_TOGGLE, 'test-toggle') - await waitNT(wrapper.vm) - await waitRAF() - await waitRAF() - expect($sidebar.element.tagName).toBe('DIV') - expect($sidebar.element).toBeVisible() - - wrapper.vm.$root.$emit(EVENT_TOGGLE, 'test-toggle') - await waitNT(wrapper.vm) - await waitRAF() - await waitRAF() - expect($sidebar.element.tagName).toBe('DIV') - expect($sidebar.element).not.toBeVisible() - - wrapper.vm.$root.$emit(EVENT_TOGGLE, 'foobar') - await waitNT(wrapper.vm) - await waitRAF() - await waitRAF() - expect($sidebar.element.tagName).toBe('DIV') - expect($sidebar.element).not.toBeVisible() - - wrapper.destroy() - }) - - it('closes when ESC key is pressed', async () => { - const wrapper = mount(BSidebar, { - attachTo: createContainer(), - propsData: { - id: 'test-esc' - } - }) - - expect(wrapper.vm).toBeDefined() - - const $sidebar = wrapper.find('.b-sidebar') - expect($sidebar.exists()).toBe(true) - expect($sidebar.element.tagName).toBe('DIV') - expect($sidebar.element).not.toBeVisible() - - wrapper.vm.$root.$emit(EVENT_TOGGLE, 'test-esc') - await waitNT(wrapper.vm) - await waitRAF() - await waitRAF() - expect($sidebar.element.tagName).toBe('DIV') - expect($sidebar.element).toBeVisible() - - await wrapper.trigger('keydown.esc') - await waitRAF() - await waitRAF() - expect($sidebar.element.tagName).toBe('DIV') - expect($sidebar.element).not.toBeVisible() - - await wrapper.setProps({ noCloseOnEsc: true }) - wrapper.vm.$root.$emit(EVENT_TOGGLE, 'test-esc') - await waitRAF() - await waitRAF() - expect($sidebar.element.tagName).toBe('DIV') - expect($sidebar.element).toBeVisible() - - await wrapper.trigger('keydown.esc') - await waitRAF() - await waitRAF() - expect($sidebar.element.tagName).toBe('DIV') - expect($sidebar.element).toBeVisible() - - wrapper.destroy() - }) - - it('handles state sync requests', async () => { - const wrapper = mount(BSidebar, { - attachTo: createContainer(), - propsData: { - id: 'test-sync', - visible: true - } - }) - - expect(wrapper.vm).toBeDefined() - - const rootWrapper = createWrapper(wrapper.vm.$root) - await waitNT(wrapper.vm) - await waitRAF() - await waitRAF() - expect(rootWrapper.emitted(EVENT_STATE)).toBeDefined() - expect(rootWrapper.emitted(EVENT_STATE).length).toBe(1) - expect(rootWrapper.emitted(EVENT_STATE)[0][0]).toBe('test-sync') // ID - expect(rootWrapper.emitted(EVENT_STATE)[0][1]).toBe(true) // Visible state - expect(rootWrapper.emitted(EVENT_STATE_SYNC)).not.toBeDefined() - - rootWrapper.vm.$root.$emit(EVENT_STATE_REQUEST, 'test-sync') - await waitNT(wrapper.vm) - await waitRAF() - await waitRAF() - expect(rootWrapper.emitted(EVENT_STATE_SYNC)).toBeDefined() - expect(rootWrapper.emitted(EVENT_STATE_SYNC).length).toBe(1) - expect(rootWrapper.emitted(EVENT_STATE_SYNC)[0][0]).toBe('test-sync') // ID - expect(rootWrapper.emitted(EVENT_STATE_SYNC)[0][1]).toBe(true) // Visible state - - wrapper.destroy() - }) - - it('should have expected structure when `no-header` is set', async () => { - const wrapper = mount(BSidebar, { - attachTo: createContainer(), - propsData: { - id: 'test-2', - visible: true, - noHeader: true - } - }) - - expect(wrapper.vm).toBeDefined() - expect(wrapper.element.tagName).toBe('DIV') - expect(wrapper.find('.b-sidebar-header').exists()).toBe(false) - expect(wrapper.find('.b-sidebar-body').exists()).toBe(true) - expect(wrapper.find('.b-sidebar-footer').exists()).toBe(false) - - wrapper.destroy() - }) - - it('should have expected structure when `no-header-close` is set', async () => { - const wrapper = mount(BSidebar, { - attachTo: createContainer(), - propsData: { - id: 'test-3', - visible: true, - noHeaderClose: true - } - }) - - expect(wrapper.vm).toBeDefined() - expect(wrapper.element.tagName).toBe('DIV') - expect(wrapper.find('.b-sidebar-header').exists()).toBe(true) - expect(wrapper.find('.b-sidebar-header .close').exists()).toBe(false) - expect(wrapper.find('.b-sidebar-body').exists()).toBe(true) - expect(wrapper.find('.b-sidebar-footer').exists()).toBe(false) - - wrapper.destroy() - }) - - it('should have expected structure when `lazy` is set', async () => { - const wrapper = mount(BSidebar, { - attachTo: createContainer(), - propsData: { - id: 'test-4', - visible: false, - lazy: true - } - }) - - expect(wrapper.vm).toBeDefined() - expect(wrapper.element.tagName).toBe('DIV') - expect(wrapper.find('.b-sidebar-header').exists()).toBe(true) - expect(wrapper.find('.b-sidebar-body').exists()).toBe(false) - expect(wrapper.find('.b-sidebar-footer').exists()).toBe(false) - - await wrapper.setProps({ visible: true }) - await waitRAF() - await waitRAF() - expect(wrapper.element.tagName).toBe('DIV') - expect(wrapper.find('.b-sidebar-header').exists()).toBe(true) - expect(wrapper.find('.b-sidebar-body').exists()).toBe(true) - expect(wrapper.find('.b-sidebar-footer').exists()).toBe(false) - - wrapper.destroy() - }) - - it('should have expected structure when `footer` slot provided', async () => { - const wrapper = mount(BSidebar, { - attachTo: createContainer(), - propsData: { - id: 'test-5', - visible: true - }, - slots: { - footer: 'FOOTER' - } - }) - - expect(wrapper.vm).toBeDefined() - expect(wrapper.element.tagName).toBe('DIV') - expect(wrapper.find('.b-sidebar-header').exists()).toBe(true) - expect(wrapper.find('.b-sidebar-body').exists()).toBe(true) - expect(wrapper.find('.b-sidebar-footer').exists()).toBe(true) - expect(wrapper.find('.b-sidebar-footer').text()).toEqual('FOOTER') - - wrapper.destroy() - }) - - it('should have expected structure when `title` prop provided', async () => { - const wrapper = mount(BSidebar, { - attachTo: createContainer(), - propsData: { - id: 'test-title', - visible: true, - title: 'TITLE' - } - }) - - expect(wrapper.vm).toBeDefined() - expect(wrapper.element.tagName).toBe('DIV') - expect(wrapper.find('.b-sidebar-header').exists()).toBe(true) - expect(wrapper.find('.b-sidebar-header > strong').text()).toEqual('TITLE') - expect(wrapper.find('.b-sidebar-body').exists()).toBe(true) - expect(wrapper.find('.b-sidebar-footer').exists()).toBe(false) - - wrapper.destroy() - }) -}) +import { createWrapper, mount } from '@vue/test-utils' +import { createContainer, waitNT, waitRAF } from '../../../tests/utils' +import { BSidebar } from './sidebar' + +const EVENT_TOGGLE = 'bv::toggle::collapse' +const EVENT_STATE = 'bv::collapse::state' +const EVENT_STATE_SYNC = 'bv::collapse::sync::state' +const EVENT_STATE_REQUEST = 'bv::request::collapse::state' + +describe('sidebar', () => { + it('should have expected default structure', async () => { + const wrapper = mount(BSidebar, { + attachTo: createContainer(), + propsData: { + id: 'test-1', + visible: true + } + }) + + expect(wrapper.vm).toBeDefined() + + const $sidebar = wrapper.find('.b-sidebar') + expect($sidebar.exists()).toBe(true) + + const $backdrop = wrapper.find('.b-sidebar-backdrop') + expect($backdrop.exists()).toBe(false) + + expect($sidebar.element.tagName).toBe('DIV') + expect($sidebar.attributes('id')).toBeDefined() + expect($sidebar.attributes('id')).toEqual('test-1') + expect($sidebar.classes()).toContain('b-sidebar') + expect($sidebar.classes()).not.toContain('b-sidebar-right') + // `show` and `slide` class only added during transition + expect($sidebar.classes()).not.toContain('show') + expect($sidebar.classes()).not.toContain('slide') + expect($sidebar.text()).toEqual('') + // Check for no presence of `display: none' from `v-show` directive + expect($sidebar.element).toBeVisible() + + expect($sidebar.find('.b-sidebar-header').exists()).toBe(true) + expect($sidebar.find('.b-sidebar-body').exists()).toBe(true) + expect($sidebar.find('.b-sidebar-footer').exists()).toBe(false) + + await wrapper.setProps({ visible: false }) + await waitRAF() + await waitRAF() + expect(wrapper.vm).toBeDefined() + expect(wrapper.element.tagName).toBe('DIV') + // Check for no presence of `display: none' from `v-show` directive + expect($sidebar.element).not.toBeVisible() + + await wrapper.setProps({ visible: true }) + await waitRAF() + await waitRAF() + expect(wrapper.element.tagName).toBe('DIV') + // Check for no presence of `display: none' from `v-show` directive + expect($sidebar.element).toBeVisible() + + wrapper.destroy() + }) + + it('shows backdrop when prop `backdrop` is `true`', async () => { + const wrapper = mount(BSidebar, { + attachTo: createContainer(), + propsData: { + id: 'test-backdrop', + noCloseOnBackdrop: true, + visible: true, + backdrop: true + } + }) + + expect(wrapper.vm).toBeDefined() + + const $sidebar = wrapper.find('.b-sidebar') + expect($sidebar.exists()).toBe(true) + + const $backdrop = wrapper.find('.b-sidebar-backdrop') + expect($backdrop.exists()).toBe(true) + expect($backdrop.classes()).toContain('bg-dark') + + await $backdrop.trigger('click') + await waitRAF() + await waitRAF() + expect($sidebar.element).toBeVisible() + expect($backdrop.element).toBeVisible() + + await wrapper.setProps({ noCloseOnBackdrop: false }) + await waitRAF() + await waitRAF() + expect($sidebar.element).toBeVisible() + expect($backdrop.element).toBeVisible() + + await $backdrop.trigger('click') + await waitRAF() + await waitRAF() + expect($sidebar.element).not.toBeVisible() + expect($backdrop.element).not.toBeVisible() + + wrapper.destroy() + }) + + it('applies "bg-*" class to backdrop based on `backdrop-variant` prop', async () => { + const wrapper = mount(BSidebar, { + attachTo: createContainer(), + propsData: { + id: 'test-backdrop', + noCloseOnBackdrop: true, + visible: true, + backdrop: true, + backdropVariant: 'transparent' + } + }) + + expect(wrapper.vm).toBeDefined() + + const $sidebar = wrapper.find('.b-sidebar') + expect($sidebar.exists()).toBe(true) + + const $backdrop = wrapper.find('.b-sidebar-backdrop') + expect($backdrop.exists()).toBe(true) + expect($backdrop.classes()).toContain('bg-transparent') + + await $backdrop.trigger('click') + await waitRAF() + await waitRAF() + expect($sidebar.element).toBeVisible() + expect($backdrop.element).toBeVisible() + + await wrapper.setProps({ noCloseOnBackdrop: false }) + await waitRAF() + await waitRAF() + expect($sidebar.element).toBeVisible() + expect($backdrop.element).toBeVisible() + + await $backdrop.trigger('click') + await waitRAF() + await waitRAF() + expect($sidebar.element).not.toBeVisible() + expect($backdrop.element).not.toBeVisible() + + wrapper.destroy() + }) + + it('shows and hides in response to v-b-toggle events', async () => { + const wrapper = mount(BSidebar, { + attachTo: createContainer(), + propsData: { + id: 'test-toggle' + } + }) + + expect(wrapper.vm).toBeDefined() + + const $sidebar = wrapper.find('.b-sidebar') + expect($sidebar.exists()).toBe(true) + expect($sidebar.element.tagName).toBe('DIV') + expect($sidebar.element).not.toBeVisible() + + wrapper.vm.$root.$emit(EVENT_TOGGLE, 'test-toggle') + await waitNT(wrapper.vm) + await waitRAF() + await waitRAF() + expect($sidebar.element.tagName).toBe('DIV') + expect($sidebar.element).toBeVisible() + + wrapper.vm.$root.$emit(EVENT_TOGGLE, 'test-toggle') + await waitNT(wrapper.vm) + await waitRAF() + await waitRAF() + expect($sidebar.element.tagName).toBe('DIV') + expect($sidebar.element).not.toBeVisible() + + wrapper.vm.$root.$emit(EVENT_TOGGLE, 'foobar') + await waitNT(wrapper.vm) + await waitRAF() + await waitRAF() + expect($sidebar.element.tagName).toBe('DIV') + expect($sidebar.element).not.toBeVisible() + + wrapper.destroy() + }) + + it('closes when ESC key is pressed', async () => { + const wrapper = mount(BSidebar, { + attachTo: createContainer(), + propsData: { + id: 'test-esc' + } + }) + + expect(wrapper.vm).toBeDefined() + + const $sidebar = wrapper.find('.b-sidebar') + expect($sidebar.exists()).toBe(true) + expect($sidebar.element.tagName).toBe('DIV') + expect($sidebar.element).not.toBeVisible() + + wrapper.vm.$root.$emit(EVENT_TOGGLE, 'test-esc') + await waitNT(wrapper.vm) + await waitRAF() + await waitRAF() + expect($sidebar.element.tagName).toBe('DIV') + expect($sidebar.element).toBeVisible() + + await wrapper.trigger('keydown.esc') + await waitRAF() + await waitRAF() + expect($sidebar.element.tagName).toBe('DIV') + expect($sidebar.element).not.toBeVisible() + + await wrapper.setProps({ noCloseOnEsc: true }) + wrapper.vm.$root.$emit(EVENT_TOGGLE, 'test-esc') + await waitRAF() + await waitRAF() + expect($sidebar.element.tagName).toBe('DIV') + expect($sidebar.element).toBeVisible() + + await wrapper.trigger('keydown.esc') + await waitRAF() + await waitRAF() + expect($sidebar.element.tagName).toBe('DIV') + expect($sidebar.element).toBeVisible() + + wrapper.destroy() + }) + + it('handles state sync requests', async () => { + const wrapper = mount(BSidebar, { + attachTo: createContainer(), + propsData: { + id: 'test-sync', + visible: true + } + }) + + expect(wrapper.vm).toBeDefined() + + const rootWrapper = createWrapper(wrapper.vm.$root) + await waitNT(wrapper.vm) + await waitRAF() + await waitRAF() + expect(rootWrapper.emitted(EVENT_STATE)).toBeDefined() + expect(rootWrapper.emitted(EVENT_STATE).length).toBe(1) + expect(rootWrapper.emitted(EVENT_STATE)[0][0]).toBe('test-sync') // ID + expect(rootWrapper.emitted(EVENT_STATE)[0][1]).toBe(true) // Visible state + expect(rootWrapper.emitted(EVENT_STATE_SYNC)).not.toBeDefined() + + rootWrapper.vm.$root.$emit(EVENT_STATE_REQUEST, 'test-sync') + await waitNT(wrapper.vm) + await waitRAF() + await waitRAF() + expect(rootWrapper.emitted(EVENT_STATE_SYNC)).toBeDefined() + expect(rootWrapper.emitted(EVENT_STATE_SYNC).length).toBe(1) + expect(rootWrapper.emitted(EVENT_STATE_SYNC)[0][0]).toBe('test-sync') // ID + expect(rootWrapper.emitted(EVENT_STATE_SYNC)[0][1]).toBe(true) // Visible state + + wrapper.destroy() + }) + + it('should have expected structure when `no-header` is set', async () => { + const wrapper = mount(BSidebar, { + attachTo: createContainer(), + propsData: { + id: 'test-2', + visible: true, + noHeader: true + } + }) + + expect(wrapper.vm).toBeDefined() + expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.find('.b-sidebar-header').exists()).toBe(false) + expect(wrapper.find('.b-sidebar-body').exists()).toBe(true) + expect(wrapper.find('.b-sidebar-footer').exists()).toBe(false) + + wrapper.destroy() + }) + + it('should have expected structure when `no-header-close` is set', async () => { + const wrapper = mount(BSidebar, { + attachTo: createContainer(), + propsData: { + id: 'test-3', + visible: true, + noHeaderClose: true + } + }) + + expect(wrapper.vm).toBeDefined() + expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.find('.b-sidebar-header').exists()).toBe(true) + expect(wrapper.find('.b-sidebar-header .close').exists()).toBe(false) + expect(wrapper.find('.b-sidebar-body').exists()).toBe(true) + expect(wrapper.find('.b-sidebar-footer').exists()).toBe(false) + + wrapper.destroy() + }) + + it('should have expected structure when `lazy` is set', async () => { + const wrapper = mount(BSidebar, { + attachTo: createContainer(), + propsData: { + id: 'test-4', + visible: false, + lazy: true + } + }) + + expect(wrapper.vm).toBeDefined() + expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.find('.b-sidebar-header').exists()).toBe(true) + expect(wrapper.find('.b-sidebar-body').exists()).toBe(false) + expect(wrapper.find('.b-sidebar-footer').exists()).toBe(false) + + await wrapper.setProps({ visible: true }) + await waitRAF() + await waitRAF() + expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.find('.b-sidebar-header').exists()).toBe(true) + expect(wrapper.find('.b-sidebar-body').exists()).toBe(true) + expect(wrapper.find('.b-sidebar-footer').exists()).toBe(false) + + wrapper.destroy() + }) + + it('should have expected structure when `footer` slot provided', async () => { + const wrapper = mount(BSidebar, { + attachTo: createContainer(), + propsData: { + id: 'test-5', + visible: true + }, + slots: { + footer: 'FOOTER' + } + }) + + expect(wrapper.vm).toBeDefined() + expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.find('.b-sidebar-header').exists()).toBe(true) + expect(wrapper.find('.b-sidebar-body').exists()).toBe(true) + expect(wrapper.find('.b-sidebar-footer').exists()).toBe(true) + expect(wrapper.find('.b-sidebar-footer').text()).toEqual('FOOTER') + + wrapper.destroy() + }) + + it('should have expected structure when `title` prop provided', async () => { + const wrapper = mount(BSidebar, { + attachTo: createContainer(), + propsData: { + id: 'test-title', + visible: true, + title: 'TITLE' + } + }) + + expect(wrapper.vm).toBeDefined() + expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.find('.b-sidebar-header').exists()).toBe(true) + expect(wrapper.find('.b-sidebar-header > strong').text()).toEqual('TITLE') + expect(wrapper.find('.b-sidebar-body').exists()).toBe(true) + expect(wrapper.find('.b-sidebar-footer').exists()).toBe(false) + + wrapper.destroy() + }) +}) diff --git a/src/components/table/helpers/mixin-items.js b/src/components/table/helpers/mixin-items.js index a226a17a780..d18258a2483 100644 --- a/src/components/table/helpers/mixin-items.js +++ b/src/components/table/helpers/mixin-items.js @@ -105,8 +105,10 @@ export default { } }, // Watch for changes on `computedItems` and update the `v-model` - computedItems(newVal) { - this.$emit('input', newVal) + computedItems(newVal, oldVal) { + if (!looseEqual(newVal, oldVal)) { + this.$emit('input', newVal) + } }, // Watch for context changes context(newVal, oldVal) { diff --git a/src/directives/toggle/README.md b/src/directives/toggle/README.md index 92aa0c87eac..049a031e556 100644 --- a/src/directives/toggle/README.md +++ b/src/directives/toggle/README.md @@ -1,164 +1,164 @@ -# Toggle - -> `v-b-toggle` is a light-weight directive for toggling the visibility of collapses and sidebars, -> and includes automated [WAI-ARIA accessibility](/docs/reference/accessibility) attribute handling. - -## Overview - -The `v-b-toggle` directive can be used on interactive elements, such as buttons, to toggle the -visibility state of the [``](/docs/components/collapse) and -[``](/docs/components/sidebar) components. - -Besides toggling the visibility of the target component, the directive automatically updates ARIA -accessibility attributes on the element it is applied to so that they reflect the visibility state -of the target component. Refer to the [Accessibility section](#accessibility) below for additional -details and caveats. - -## Directive syntax and usage - -The directive is applied to the element or component that triggers the visibility of the target. The -target component can be specified (via its ID) as either a directive modifier(s), the directive -argument, or as a string/array passed to as the directive value: - -- `v-b-toggle.my-collapse` - the directive modifier (multiple targets allowed, each modifier is a - target ID) -- `v-b-toggle:my-collapse` - the directive argument - ([Vue dynamic argument](https://vuejs.org/v2/guide/syntax.html#Dynamic-Arguments) is supported) - v2.14.0+ -- `v-b-toggle="'my-collapse'"` - the directive value as a string ID -- `v-b-toggle="'my-collapse1 my-collapse2'"` - the directive value as a space separated string of - IDs - v2.14.0+ -- `v-b-toggle="['my-collapse1', 'my-collapse2']"` - the directive value as an array of string IDs - v2.14.0+ - -Modifiers, argument, and the value can be used at the same time when targeting multiple components. - -**Example usage:** - -```html - - - -``` - -## Usage on links - -2.15.0+ - -If placing the directive on a link (or a component that renders a link), the target ID can -alternatively be specified via the `href` attribute. - -Note that the browser URL will change and the page may scroll the target into view. To prevent the -URL from changing and the page from scrolling, add `@click.prevent` to the link. - -**Example usage:** - -```html - - - -``` - -## Hiding and showing content in the toggle trigger element - -When using the `v-b-toggle` directive, the class `collapsed` will automatically be placed on the -trigger element when the target component is closed, and removed when open. As of BootstrapVue -`2.14.0`, the class `not-collapsed` will be applied when the target is _not_ closed. - -**Example HTML markup:** - -```html -
    - - CloseOpen My Collapse - - - - -
    -``` - -**Example Custom CSS:** - -```css -.collapsed > .when-open, -.not-collapsed > .when-closed { - display: none; -} -``` - -## Preventing the target from opening or closing - -To prevent the trigger element from toggling the target, set the `disabled` prop on `