diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 143753b59..a39044268 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -6,7 +6,7 @@ }, { "path": "./dist/css/bootstrap-grid.min.css", - "maxSize": "6.55 kB" + "maxSize": "6.75 kB" }, { "path": "./dist/css/bootstrap-reboot.css", @@ -18,27 +18,27 @@ }, { "path": "./dist/css/bootstrap-utilities.css", - "maxSize": "9.25 kB" + "maxSize": "9.75 kB" }, { "path": "./dist/css/bootstrap-utilities.min.css", - "maxSize": "8.5 kB" + "maxSize": "9.0 kB" }, { "path": "./dist/css/bootstrap.css", - "maxSize": "28.75 kB" + "maxSize": "30.25 kB" }, { "path": "./dist/css/bootstrap.min.css", - "maxSize": "26.75 kB" + "maxSize": "28 kB" }, { "path": "./dist/js/bootstrap.bundle.js", - "maxSize": "43.25 kB" + "maxSize": "43.0 kB" }, { "path": "./dist/js/bootstrap.bundle.min.js", - "maxSize": "22.75 kB" + "maxSize": "23.0 kB" }, { "path": "./dist/js/bootstrap.esm.js", @@ -46,7 +46,7 @@ }, { "path": "./dist/js/bootstrap.esm.min.js", - "maxSize": "18.5 kB" + "maxSize": "18.25 kB" }, { "path": "./dist/js/bootstrap.js", @@ -54,7 +54,7 @@ }, { "path": "./dist/js/bootstrap.min.js", - "maxSize": "16.25 kB" + "maxSize": "16.0 kB" } ], "ci": { @@ -63,4 +63,4 @@ "v4-dev" ] } -} +} \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index d8e83a8d2..b632124b3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -46,6 +46,7 @@ "error", "after" ], + "prefer-template": "error", "semi": [ "error", "never" diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index c7211e689..446344580 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -18,16 +18,16 @@ the preferred channel for [bug reports](#bug-reports), [features requests](#feat and [submitting pull requests](#pull-requests), but please respect the following restrictions: -* Please **do not** use the issue tracker for personal support requests. Stack Overflow ([`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5) tag), [our GitHub Discussions](https://github.com/twbs/bootstrap/discussions) or [IRC](/README.md#community) are better places to get help. +- Please **do not** use the issue tracker for personal support requests. Stack Overflow ([`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5) tag), [our GitHub Discussions](https://github.com/twbs/bootstrap/discussions) or [IRC](/README.md#community) are better places to get help. -* Please **do not** derail or troll issues. Keep the discussion on topic and +- Please **do not** derail or troll issues. Keep the discussion on topic and respect the opinions of others. -* Please **do not** post comments consisting solely of "+1" or ":thumbsup:". +- Please **do not** post comments consisting solely of "+1" or ":thumbsup:". Use [GitHub's "reactions" feature](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) instead. We reserve the right to delete comments which violate this rule. -* Please **do not** open issues regarding the official themes offered on . +- Please **do not** open issues regarding the official themes offered on . Instead, please email any questions or feedback regarding those themes to `themes AT getbootstrap DOT com`. @@ -101,16 +101,16 @@ Sometimes bugs reported to us are actually caused by bugs in the browser(s) them | Vendor(s) | Browser(s) | Rendering engine | Bug reporting website(s) | Notes | | ------------- | ---------------------------- | ---------------- | ------------------------------------------------------ | -------------------------------------------------------- | -| Mozilla | Firefox | Gecko | https://bugzilla.mozilla.org/enter_bug.cgi | "Core" is normally the right product option to choose. | -| Apple | Safari | WebKit | https://bugs.webkit.org/enter_bug.cgi?product=WebKit | In Apple's bug reporter, choose "Safari" as the product. | -| Google, Opera | Chrome, Chromium, Opera v15+ | Blink | https://bugs.chromium.org/p/chromium/issues/list | Click the "New issue" button. | -| Microsoft | Edge | Blink | https://developer.microsoft.com/en-us/microsoft-edge/ | Go to "Help > Send Feedback" from the browser | +| Mozilla | Firefox | Gecko | | "Core" is normally the right product option to choose. | +| Apple | Safari | WebKit | | In Apple's bug reporter, choose "Safari" as the product. | +| Google, Opera | Chrome, Chromium, Opera v15+ | Blink | | Click the "New issue" button. | +| Microsoft | Edge | Blink | | Go to "Help > Send Feedback" from the browser | ## Feature requests Feature requests are welcome. But take a moment to find out whether your idea -fits with the scope and aims of the project. It's up to *you* to make a strong +fits with the scope and aims of the project. It's up to _you_ to make a strong case to convince the project's developers of the merits of this feature. Please provide as much detail and context as possible. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4675f7007..98e45c55a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -31,7 +31,7 @@ -* https://deploy-preview-{your pr number}--twbs-bootstrap.netlify.app/ +- ### Related issues diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 000000000..957877282 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,3 @@ +name: "CodeQL config" +paths-ignore: + - dist diff --git a/.github/workflows/browserstack.yml b/.github/workflows/browserstack.yml index 425c56684..918e0a152 100644 --- a/.github/workflows/browserstack.yml +++ b/.github/workflows/browserstack.yml @@ -6,7 +6,7 @@ on: env: FORCE_COLOR: 2 - NODE: 16 + NODE: 18 jobs: browserstack: @@ -17,6 +17,8 @@ jobs: steps: - name: Clone repository uses: actions/checkout@v3 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@v3 diff --git a/.github/workflows/bundlewatch.yml b/.github/workflows/bundlewatch.yml index d1a174784..8159ae2da 100644 --- a/.github/workflows/bundlewatch.yml +++ b/.github/workflows/bundlewatch.yml @@ -9,7 +9,7 @@ on: env: FORCE_COLOR: 2 - NODE: 16 + NODE: 18 jobs: bundlewatch: @@ -18,6 +18,8 @@ jobs: steps: - name: Clone repository uses: actions/checkout@v3 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@v3 diff --git a/.github/workflows/calibreapp-image-actions.yml b/.github/workflows/calibreapp-image-actions.yml index e23f5626e..21df1f626 100644 --- a/.github/workflows/calibreapp-image-actions.yml +++ b/.github/workflows/calibreapp-image-actions.yml @@ -17,6 +17,8 @@ jobs: steps: - name: Checkout Repo uses: actions/checkout@v3 + with: + persist-credentials: false - name: Compress Images uses: calibreapp/image-actions@1.1.0 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 70be0563c..98aa891c4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -7,7 +7,6 @@ on: - v4-dev - "!dependabot/**" pull_request: - # The branches below must be a subset of the branches above branches: - main - v4-dev @@ -28,11 +27,20 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v3 + with: + persist-credentials: false - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: + config-file: ./.github/codeql/codeql-config.yml languages: "javascript" + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 + with: + category: "/language:javascript" diff --git a/.github/workflows/cspell.yml b/.github/workflows/cspell.yml index 3751ad339..f79ffdb81 100644 --- a/.github/workflows/cspell.yml +++ b/.github/workflows/cspell.yml @@ -9,7 +9,6 @@ on: env: FORCE_COLOR: 2 - NODE: 16 jobs: cspell: @@ -18,6 +17,8 @@ jobs: steps: - name: Clone repository uses: actions/checkout@v3 + with: + persist-credentials: false - name: Run cspell uses: streetsidesoftware/cspell-action@v2 diff --git a/.github/workflows/css.yml b/.github/workflows/css.yml index 857a5672c..68323a975 100644 --- a/.github/workflows/css.yml +++ b/.github/workflows/css.yml @@ -9,7 +9,7 @@ on: env: FORCE_COLOR: 2 - NODE: 16 + NODE: 18 jobs: css: @@ -18,6 +18,8 @@ jobs: steps: - name: Clone repository uses: actions/checkout@v3 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@v3 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f33413eb4..a47d82fda 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -9,7 +9,7 @@ on: env: FORCE_COLOR: 2 - NODE: 16 + NODE: 18 jobs: docs: @@ -18,6 +18,8 @@ jobs: steps: - name: Clone repository uses: actions/checkout@v3 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@v3 diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml index 82616c574..724f16c62 100644 --- a/.github/workflows/js.yml +++ b/.github/workflows/js.yml @@ -9,7 +9,7 @@ on: env: FORCE_COLOR: 2 - NODE: 16 + NODE: 18 jobs: run: @@ -19,6 +19,8 @@ jobs: steps: - name: Clone repository uses: actions/checkout@v3 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@v3 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 816694ec2..b804462c9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,7 +9,7 @@ on: env: FORCE_COLOR: 2 - NODE: 16 + NODE: 18 jobs: lint: @@ -18,6 +18,8 @@ jobs: steps: - name: Clone repository uses: actions/checkout@v3 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@v3 diff --git a/.github/workflows/node-sass.yml b/.github/workflows/node-sass.yml index 465cee485..b0f9a72de 100644 --- a/.github/workflows/node-sass.yml +++ b/.github/workflows/node-sass.yml @@ -9,7 +9,7 @@ on: env: FORCE_COLOR: 2 - NODE: 16 + NODE: 18 jobs: css: @@ -18,6 +18,8 @@ jobs: steps: - name: Clone repository uses: actions/checkout@v3 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@v3 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 28fd5e8d4..b983cba20 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -17,23 +17,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or advances of +- The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email address, +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities diff --git a/build/build-plugins.js b/build/build-plugins.js index a160209b0..b6ce58df8 100644 --- a/build/build-plugins.js +++ b/build/build-plugins.js @@ -16,7 +16,7 @@ const { babel } = require('@rollup/plugin-babel') const banner = require('./banner.js') const sourcePath = path.resolve(__dirname, '../js/src/').replace(/\\/g, '/') -const jsFiles = globby.sync(sourcePath + '/**/*.js') +const jsFiles = globby.sync(`${sourcePath}/**/*.js`) // Array which holds the resolved plugins const resolvedPlugins = [] @@ -27,7 +27,7 @@ const filenameToEntity = filename => filename.replace('.js', '') for (const file of jsFiles) { resolvedPlugins.push({ - src: file.replace('.js', ''), + src: file, dist: file.replace('src', 'dist'), fileName: path.basename(file), className: filenameToEntity(path.basename(file)) diff --git a/build/rollup.config.js b/build/rollup.config.js index 27f12ac03..f01918ebf 100644 --- a/build/rollup.config.js +++ b/build/rollup.config.js @@ -40,7 +40,7 @@ if (BUNDLE) { const rollupConfig = { input: path.resolve(__dirname, `../js/index.${ESM ? 'esm' : 'umd'}.js`), output: { - banner, + banner: banner(), file: path.resolve(__dirname, `../dist/js/${fileDestination}.js`), format: ESM ? 'esm' : 'umd', globals, diff --git a/js/.eslintrc.json b/js/.eslintrc.json new file mode 100644 index 000000000..97ea9e043 --- /dev/null +++ b/js/.eslintrc.json @@ -0,0 +1,26 @@ +{ + "extends": "../.eslintrc.json", + "env": { + "es2022": true + }, + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "overrides": [ + { + "files": [ + "./*.js", + "./src/**/*.js" + ], + "rules": { + "import/extensions": [ + 2, + { + "js": "always" + } + ] + } + } + ] +} diff --git a/js/index.esm.js b/js/index.esm.js index b83764990..a3c9c84d4 100644 --- a/js/index.esm.js +++ b/js/index.esm.js @@ -5,15 +5,15 @@ * -------------------------------------------------------------------------- */ -export { default as Alert } from './src/alert' -export { default as Button } from './src/button' -export { default as Carousel } from './src/carousel' -export { default as Collapse } from './src/collapse' -export { default as Dropdown } from './src/dropdown' -export { default as Modal } from './src/modal' -export { default as Offcanvas } from './src/offcanvas' -export { default as Popover } from './src/popover' -export { default as ScrollSpy } from './src/scrollspy' -export { default as Tab } from './src/tab' -export { default as Toast } from './src/toast' -export { default as Tooltip } from './src/tooltip' +export { default as Alert } from './src/alert.js' +export { default as Button } from './src/button.js' +export { default as Carousel } from './src/carousel.js' +export { default as Collapse } from './src/collapse.js' +export { default as Dropdown } from './src/dropdown.js' +export { default as Modal } from './src/modal.js' +export { default as Offcanvas } from './src/offcanvas.js' +export { default as Popover } from './src/popover.js' +export { default as ScrollSpy } from './src/scrollspy.js' +export { default as Tab } from './src/tab.js' +export { default as Toast } from './src/toast.js' +export { default as Tooltip } from './src/tooltip.js' diff --git a/js/index.umd.js b/js/index.umd.js index 5abe8db53..a8113f3de 100644 --- a/js/index.umd.js +++ b/js/index.umd.js @@ -5,18 +5,18 @@ * -------------------------------------------------------------------------- */ -import Alert from './src/alert' -import Button from './src/button' -import Carousel from './src/carousel' -import Collapse from './src/collapse' -import Dropdown from './src/dropdown' -import Modal from './src/modal' -import Offcanvas from './src/offcanvas' -import Popover from './src/popover' -import ScrollSpy from './src/scrollspy' -import Tab from './src/tab' -import Toast from './src/toast' -import Tooltip from './src/tooltip' +import Alert from './src/alert.js' +import Button from './src/button.js' +import Carousel from './src/carousel.js' +import Collapse from './src/collapse.js' +import Dropdown from './src/dropdown.js' +import Modal from './src/modal.js' +import Offcanvas from './src/offcanvas.js' +import Popover from './src/popover.js' +import ScrollSpy from './src/scrollspy.js' +import Tab from './src/tab.js' +import Toast from './src/toast.js' +import Tooltip from './src/tooltip.js' export default { Alert, diff --git a/js/src/alert.js b/js/src/alert.js index 59de828e9..a58408b7a 100644 --- a/js/src/alert.js +++ b/js/src/alert.js @@ -5,10 +5,10 @@ * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin } from './util/index' -import EventHandler from './dom/event-handler' -import BaseComponent from './base-component' -import { enableDismissTrigger } from './util/component-functions' +import { defineJQueryPlugin } from './util/index.js' +import EventHandler from './dom/event-handler.js' +import BaseComponent from './base-component.js' +import { enableDismissTrigger } from './util/component-functions.js' /** * Constants diff --git a/js/src/base-component.js b/js/src/base-component.js index 0c1a2592d..ea266cee8 100644 --- a/js/src/base-component.js +++ b/js/src/base-component.js @@ -5,10 +5,10 @@ * -------------------------------------------------------------------------- */ -import Data from './dom/data' -import { executeAfterTransition, getElement } from './util/index' -import EventHandler from './dom/event-handler' -import Config from './util/config' +import Data from './dom/data.js' +import { executeAfterTransition, getElement } from './util/index.js' +import EventHandler from './dom/event-handler.js' +import Config from './util/config.js' /** * Constants diff --git a/js/src/button.js b/js/src/button.js index 03e76041a..f3185cb89 100644 --- a/js/src/button.js +++ b/js/src/button.js @@ -5,9 +5,9 @@ * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin } from './util/index' -import EventHandler from './dom/event-handler' -import BaseComponent from './base-component' +import { defineJQueryPlugin } from './util/index.js' +import EventHandler from './dom/event-handler.js' +import BaseComponent from './base-component.js' /** * Constants diff --git a/js/src/carousel.js b/js/src/carousel.js index 24bbe392a..d190075eb 100644 --- a/js/src/carousel.js +++ b/js/src/carousel.js @@ -7,18 +7,17 @@ import { defineJQueryPlugin, - getElementFromSelector, getNextActiveElement, isRTL, isVisible, reflow, triggerTransitionEnd -} from './util/index' -import EventHandler from './dom/event-handler' -import Manipulator from './dom/manipulator' -import SelectorEngine from './dom/selector-engine' -import Swipe from './util/swipe' -import BaseComponent from './base-component' +} from './util/index.js' +import EventHandler from './dom/event-handler.js' +import Manipulator from './dom/manipulator.js' +import SelectorEngine from './dom/selector-engine.js' +import Swipe from './util/swipe.js' +import BaseComponent from './base-component.js' /** * Constants @@ -431,7 +430,7 @@ class Carousel extends BaseComponent { */ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (event) { - const target = getElementFromSelector(this) + const target = SelectorEngine.getElementFromSelector(this) if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { return diff --git a/js/src/collapse.js b/js/src/collapse.js index 204d18081..16250c870 100644 --- a/js/src/collapse.js +++ b/js/src/collapse.js @@ -8,13 +8,11 @@ import { defineJQueryPlugin, getElement, - getElementFromSelector, - getSelectorFromElement, reflow -} from './util/index' -import EventHandler from './dom/event-handler' -import SelectorEngine from './dom/selector-engine' -import BaseComponent from './base-component' +} from './util/index.js' +import EventHandler from './dom/event-handler.js' +import SelectorEngine from './dom/selector-engine.js' +import BaseComponent from './base-component.js' /** * Constants @@ -68,7 +66,7 @@ class Collapse extends BaseComponent { const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE) for (const elem of toggleList) { - const selector = getSelectorFromElement(elem) + const selector = SelectorEngine.getSelectorFromElement(elem) const filterElement = SelectorEngine.find(selector) .filter(foundElement => foundElement === this._element) @@ -185,7 +183,7 @@ class Collapse extends BaseComponent { this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW) for (const trigger of this._triggerArray) { - const element = getElementFromSelector(trigger) + const element = SelectorEngine.getElementFromSelector(trigger) if (element && !this._isShown(element)) { this._addAriaAndCollapsedClass([trigger], false) @@ -229,7 +227,7 @@ class Collapse extends BaseComponent { const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE) for (const element of children) { - const selected = getElementFromSelector(element) + const selected = SelectorEngine.getElementFromSelector(element) if (selected) { this._addAriaAndCollapsedClass([element], this._isShown(selected)) @@ -285,10 +283,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( event.preventDefault() } - const selector = getSelectorFromElement(this) - const selectorElements = SelectorEngine.find(selector) - - for (const element of selectorElements) { + for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) { Collapse.getOrCreateInstance(element, { toggle: false }).toggle() } }) diff --git a/js/src/dom/event-handler.js b/js/src/dom/event-handler.js index 9876d7733..fde195e7c 100644 --- a/js/src/dom/event-handler.js +++ b/js/src/dom/event-handler.js @@ -5,7 +5,7 @@ * -------------------------------------------------------------------------- */ -import { getjQuery } from '../util/index' +import { getjQuery } from '../util/index.js' /** * Constants @@ -198,9 +198,8 @@ function removeHandler(element, events, typeEvent, handler, delegationSelector) function removeNamespacedHandlers(element, events, typeEvent, namespace) { const storeElementEvent = events[typeEvent] || {} - for (const handlerKey of Object.keys(storeElementEvent)) { + for (const [handlerKey, event] of Object.entries(storeElementEvent)) { if (handlerKey.includes(namespace)) { - const event = storeElementEvent[handlerKey] removeHandler(element, events, typeEvent, event.callable, event.delegationSelector) } } @@ -248,11 +247,10 @@ const EventHandler = { } } - for (const keyHandlers of Object.keys(storeElementEvent)) { + for (const [keyHandlers, event] of Object.entries(storeElementEvent)) { const handlerKey = keyHandlers.replace(stripUidRegex, '') if (!inNamespace || originalTypeEvent.includes(handlerKey)) { - const event = storeElementEvent[keyHandlers] removeHandler(element, events, typeEvent, event.callable, event.delegationSelector) } } @@ -300,8 +298,8 @@ const EventHandler = { } } -function hydrateObj(obj, meta) { - for (const [key, value] of Object.entries(meta || {})) { +function hydrateObj(obj, meta = {}) { + for (const [key, value] of Object.entries(meta)) { try { obj[key] = value } catch { diff --git a/js/src/dom/selector-engine.js b/js/src/dom/selector-engine.js index 1ba104f9e..da93a35be 100644 --- a/js/src/dom/selector-engine.js +++ b/js/src/dom/selector-engine.js @@ -5,11 +5,32 @@ * -------------------------------------------------------------------------- */ -import { isDisabled, isVisible } from '../util/index' +import { isDisabled, isVisible, parseSelector } from '../util/index.js' -/** - * Constants - */ +const getSelector = element => { + let selector = element.getAttribute('data-bs-target') + + if (!selector || selector === '#') { + let hrefAttribute = element.getAttribute('href') + + // The only valid content that could double as a selector are IDs or classes, + // so everything starting with `#` or `.`. If a "real" URL is used as the selector, + // `document.querySelector` will rightfully complain it is invalid. + // See https://github.com/twbs/bootstrap/issues/32273 + if (!hrefAttribute || (!hrefAttribute.includes('#') && !hrefAttribute.startsWith('.'))) { + return null + } + + // Just in case some CMS puts out a full URL with the anchor appended + if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { + hrefAttribute = `#${hrefAttribute.split('#')[1]}` + } + + selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null + } + + return parseSelector(selector) +} const SelectorEngine = { find(selector, element = document.documentElement) { @@ -77,6 +98,28 @@ const SelectorEngine = { ].map(selector => `${selector}:not([tabindex^="-"])`).join(',') return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el)) + }, + + getSelectorFromElement(element) { + const selector = getSelector(element) + + if (selector) { + return SelectorEngine.findOne(selector) ? selector : null + } + + return null + }, + + getElementFromSelector(element) { + const selector = getSelector(element) + + return selector ? SelectorEngine.findOne(selector) : null + }, + + getMultipleElementsFromSelector(element) { + const selector = getSelector(element) + + return selector ? SelectorEngine.find(selector) : [] } } diff --git a/js/src/dropdown.js b/js/src/dropdown.js index 9596baa9a..c699598f7 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -8,6 +8,7 @@ import * as Popper from '@popperjs/core' import { defineJQueryPlugin, + execute, getElement, getNextActiveElement, isDisabled, @@ -15,11 +16,11 @@ import { isRTL, isVisible, noop -} from './util/index' -import EventHandler from './dom/event-handler' -import Manipulator from './dom/manipulator' -import SelectorEngine from './dom/selector-engine' -import BaseComponent from './base-component' +} from './util/index.js' +import EventHandler from './dom/event-handler.js' +import Manipulator from './dom/manipulator.js' +import SelectorEngine from './dom/selector-engine.js' +import BaseComponent from './base-component.js' /** * Constants @@ -319,7 +320,7 @@ class Dropdown extends BaseComponent { return { ...defaultBsPopperConfig, - ...(typeof this._config.popperConfig === 'function' ? this._config.popperConfig(defaultBsPopperConfig) : this._config.popperConfig) + ...execute(this._config.popperConfig, [defaultBsPopperConfig]) } } diff --git a/js/src/modal.js b/js/src/modal.js index 26c7e8c93..da1bcdca3 100644 --- a/js/src/modal.js +++ b/js/src/modal.js @@ -5,14 +5,14 @@ * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin, getElementFromSelector, isRTL, isVisible, reflow } from './util/index' -import EventHandler from './dom/event-handler' -import SelectorEngine from './dom/selector-engine' -import ScrollBarHelper from './util/scrollbar' -import BaseComponent from './base-component' -import Backdrop from './util/backdrop' -import FocusTrap from './util/focustrap' -import { enableDismissTrigger } from './util/component-functions' +import { defineJQueryPlugin, isRTL, isVisible, reflow } from './util/index.js' +import EventHandler from './dom/event-handler.js' +import SelectorEngine from './dom/selector-engine.js' +import ScrollBarHelper from './util/scrollbar.js' +import BaseComponent from './base-component.js' +import Backdrop from './util/backdrop.js' +import FocusTrap from './util/focustrap.js' +import { enableDismissTrigger } from './util/component-functions.js' /** * Constants @@ -336,7 +336,7 @@ class Modal extends BaseComponent { */ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - const target = getElementFromSelector(this) + const target = SelectorEngine.getElementFromSelector(this) if (['A', 'AREA'].includes(this.tagName)) { event.preventDefault() diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 7dd06fd71..6dd1e720f 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -7,17 +7,16 @@ import { defineJQueryPlugin, - getElementFromSelector, isDisabled, isVisible -} from './util/index' -import ScrollBarHelper from './util/scrollbar' -import EventHandler from './dom/event-handler' -import BaseComponent from './base-component' -import SelectorEngine from './dom/selector-engine' -import Backdrop from './util/backdrop' -import FocusTrap from './util/focustrap' -import { enableDismissTrigger } from './util/component-functions' +} from './util/index.js' +import ScrollBarHelper from './util/scrollbar.js' +import EventHandler from './dom/event-handler.js' +import BaseComponent from './base-component.js' +import SelectorEngine from './dom/selector-engine.js' +import Backdrop from './util/backdrop.js' +import FocusTrap from './util/focustrap.js' +import { enableDismissTrigger } from './util/component-functions.js' /** * Constants @@ -231,7 +230,7 @@ class Offcanvas extends BaseComponent { */ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - const target = getElementFromSelector(this) + const target = SelectorEngine.getElementFromSelector(this) if (['A', 'AREA'].includes(this.tagName)) { event.preventDefault() diff --git a/js/src/popover.js b/js/src/popover.js index 1b09dd425..477545672 100644 --- a/js/src/popover.js +++ b/js/src/popover.js @@ -5,8 +5,8 @@ * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin } from './util/index' -import Tooltip from './tooltip' +import { defineJQueryPlugin } from './util/index.js' +import Tooltip from './tooltip.js' /** * Constants diff --git a/js/src/scrollspy.js b/js/src/scrollspy.js index 01aba997d..b2c8555c7 100644 --- a/js/src/scrollspy.js +++ b/js/src/scrollspy.js @@ -5,10 +5,10 @@ * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin, getElement, isDisabled, isVisible } from './util/index' -import EventHandler from './dom/event-handler' -import SelectorEngine from './dom/selector-engine' -import BaseComponent from './base-component' +import { defineJQueryPlugin, getElement, isDisabled, isVisible } from './util/index.js' +import EventHandler from './dom/event-handler.js' +import SelectorEngine from './dom/selector-engine.js' +import BaseComponent from './base-component.js' /** * Constants diff --git a/js/src/tab.js b/js/src/tab.js index 8dc4644c9..efd0398ff 100644 --- a/js/src/tab.js +++ b/js/src/tab.js @@ -5,10 +5,10 @@ * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin, getElementFromSelector, getNextActiveElement, isDisabled } from './util/index' -import EventHandler from './dom/event-handler' -import SelectorEngine from './dom/selector-engine' -import BaseComponent from './base-component' +import { defineJQueryPlugin, getNextActiveElement, isDisabled } from './util/index.js' +import EventHandler from './dom/event-handler.js' +import SelectorEngine from './dom/selector-engine.js' +import BaseComponent from './base-component.js' /** * Constants @@ -106,7 +106,7 @@ class Tab extends BaseComponent { element.classList.add(CLASS_NAME_ACTIVE) - this._activate(getElementFromSelector(element)) // Search and activate/show the proper section + this._activate(SelectorEngine.getElementFromSelector(element)) // Search and activate/show the proper section const complete = () => { if (element.getAttribute('role') !== 'tab') { @@ -133,7 +133,7 @@ class Tab extends BaseComponent { element.classList.remove(CLASS_NAME_ACTIVE) element.blur() - this._deactivate(getElementFromSelector(element)) // Search and deactivate the shown section too + this._deactivate(SelectorEngine.getElementFromSelector(element)) // Search and deactivate the shown section too const complete = () => { if (element.getAttribute('role') !== 'tab') { @@ -203,7 +203,7 @@ class Tab extends BaseComponent { } _setInitialAttributesOnTargetPanel(child) { - const target = getElementFromSelector(child) + const target = SelectorEngine.getElementFromSelector(child) if (!target) { return diff --git a/js/src/toast.js b/js/src/toast.js index a7fe7751d..7c6c7c10b 100644 --- a/js/src/toast.js +++ b/js/src/toast.js @@ -5,10 +5,10 @@ * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin, reflow } from './util/index' -import EventHandler from './dom/event-handler' -import BaseComponent from './base-component' -import { enableDismissTrigger } from './util/component-functions' +import { defineJQueryPlugin, reflow } from './util/index.js' +import EventHandler from './dom/event-handler.js' +import BaseComponent from './base-component.js' +import { enableDismissTrigger } from './util/component-functions.js' /** * Constants diff --git a/js/src/tooltip.js b/js/src/tooltip.js index 748a0e198..85c4a39c8 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -6,12 +6,12 @@ */ import * as Popper from '@popperjs/core' -import { defineJQueryPlugin, findShadowRoot, getElement, getUID, isRTL, noop } from './util/index' -import { DefaultAllowlist } from './util/sanitizer' -import EventHandler from './dom/event-handler' -import Manipulator from './dom/manipulator' -import BaseComponent from './base-component' -import TemplateFactory from './util/template-factory' +import { defineJQueryPlugin, execute, findShadowRoot, getElement, getUID, isRTL, noop } from './util/index.js' +import { DefaultAllowlist } from './util/sanitizer.js' +import EventHandler from './dom/event-handler.js' +import Manipulator from './dom/manipulator.js' +import BaseComponent from './base-component.js' +import TemplateFactory from './util/template-factory.js' /** * Constants @@ -370,9 +370,7 @@ class Tooltip extends BaseComponent { } _createPopper(tip) { - const placement = typeof this._config.placement === 'function' ? - this._config.placement.call(this, tip, this._element) : - this._config.placement + const placement = execute(this._config.placement, [this, tip, this._element]) const attachment = AttachmentMap[placement.toUpperCase()] return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment)) } @@ -392,7 +390,7 @@ class Tooltip extends BaseComponent { } _resolvePossibleFunction(arg) { - return typeof arg === 'function' ? arg.call(this._element) : arg + return execute(arg, [this._element]) } _getPopperConfig(attachment) { @@ -438,7 +436,7 @@ class Tooltip extends BaseComponent { return { ...defaultBsPopperConfig, - ...(typeof this._config.popperConfig === 'function' ? this._config.popperConfig(defaultBsPopperConfig) : this._config.popperConfig) + ...execute(this._config.popperConfig, [defaultBsPopperConfig]) } } @@ -579,9 +577,9 @@ class Tooltip extends BaseComponent { _getDelegateConfig() { const config = {} - for (const key in this._config) { - if (this.constructor.Default[key] !== this._config[key]) { - config[key] = this._config[key] + for (const [key, value] of Object.entries(this._config)) { + if (this.constructor.Default[key] !== value) { + config[key] = value } } diff --git a/js/src/util/backdrop.js b/js/src/util/backdrop.js index 78279e056..ee560b85e 100644 --- a/js/src/util/backdrop.js +++ b/js/src/util/backdrop.js @@ -5,9 +5,9 @@ * -------------------------------------------------------------------------- */ -import EventHandler from '../dom/event-handler' -import { execute, executeAfterTransition, getElement, reflow } from './index' -import Config from './config' +import EventHandler from '../dom/event-handler.js' +import { execute, executeAfterTransition, getElement, reflow } from './index.js' +import Config from './config.js' /** * Constants diff --git a/js/src/util/component-functions.js b/js/src/util/component-functions.js index c2f99cceb..15f2f8c52 100644 --- a/js/src/util/component-functions.js +++ b/js/src/util/component-functions.js @@ -5,8 +5,9 @@ * -------------------------------------------------------------------------- */ -import EventHandler from '../dom/event-handler' -import { getElementFromSelector, isDisabled } from './index' +import EventHandler from '../dom/event-handler.js' +import { isDisabled } from './index.js' +import SelectorEngine from '../dom/selector-engine.js' const enableDismissTrigger = (component, method = 'hide') => { const clickEvent = `click.dismiss${component.EVENT_KEY}` @@ -21,7 +22,7 @@ const enableDismissTrigger = (component, method = 'hide') => { return } - const target = getElementFromSelector(this) || this.closest(`.${name}`) + const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`) const instance = component.getOrCreateInstance(target) // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method diff --git a/js/src/util/config.js b/js/src/util/config.js index 1205905f7..a37fe52a1 100644 --- a/js/src/util/config.js +++ b/js/src/util/config.js @@ -5,8 +5,8 @@ * -------------------------------------------------------------------------- */ -import { isElement, toType } from './index' -import Manipulator from '../dom/manipulator' +import { isElement, toType } from './index.js' +import Manipulator from '../dom/manipulator.js' /** * Class definition @@ -49,8 +49,7 @@ class Config { } _typeCheckConfig(config, configTypes = this.constructor.DefaultType) { - for (const property of Object.keys(configTypes)) { - const expectedTypes = configTypes[property] + for (const [property, expectedTypes] of Object.entries(configTypes)) { const value = config[property] const valueType = isElement(value) ? 'element' : toType(value) diff --git a/js/src/util/focustrap.js b/js/src/util/focustrap.js index ef6916679..1736c4f7c 100644 --- a/js/src/util/focustrap.js +++ b/js/src/util/focustrap.js @@ -5,9 +5,9 @@ * -------------------------------------------------------------------------- */ -import EventHandler from '../dom/event-handler' -import SelectorEngine from '../dom/selector-engine' -import Config from './config' +import EventHandler from '../dom/event-handler.js' +import SelectorEngine from '../dom/selector-engine.js' +import Config from './config.js' /** * Constants diff --git a/js/src/util/index.js b/js/src/util/index.js index 297e57149..77de14d3a 100644 --- a/js/src/util/index.js +++ b/js/src/util/index.js @@ -9,6 +9,20 @@ const MAX_UID = 1_000_000 const MILLISECONDS_MULTIPLIER = 1000 const TRANSITION_END = 'transitionend' +/** + * Properly escape IDs selectors to handle weird IDs + * @param {string} selector + * @returns {string} + */ +const parseSelector = selector => { + if (selector && window.CSS && window.CSS.escape) { + // document.querySelector needs escaping to handle IDs (html5+) containing for instance / + selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`) + } + + return selector +} + // Shout-out Angus Croll (https://goo.gl/pxwQGp) const toType = object => { if (object === null || object === undefined) { @@ -30,47 +44,6 @@ const getUID = prefix => { return prefix } -const getSelector = element => { - let selector = element.getAttribute('data-bs-target') - - if (!selector || selector === '#') { - let hrefAttribute = element.getAttribute('href') - - // The only valid content that could double as a selector are IDs or classes, - // so everything starting with `#` or `.`. If a "real" URL is used as the selector, - // `document.querySelector` will rightfully complain it is invalid. - // See https://github.com/twbs/bootstrap/issues/32273 - if (!hrefAttribute || (!hrefAttribute.includes('#') && !hrefAttribute.startsWith('.'))) { - return null - } - - // Just in case some CMS puts out a full URL with the anchor appended - if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { - hrefAttribute = `#${hrefAttribute.split('#')[1]}` - } - - selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null - } - - return selector -} - -const getSelectorFromElement = element => { - const selector = getSelector(element) - - if (selector) { - return document.querySelector(selector) ? selector : null - } - - return null -} - -const getElementFromSelector = element => { - const selector = getSelector(element) - - return selector ? document.querySelector(selector) : null -} - const getTransitionDurationFromElement = element => { if (!element) { return 0 @@ -117,7 +90,7 @@ const getElement = object => { } if (typeof object === 'string' && object.length > 0) { - return document.querySelector(object) + return document.querySelector(parseSelector(object)) } return null @@ -249,10 +222,8 @@ const defineJQueryPlugin = plugin => { }) } -const execute = callback => { - if (typeof callback === 'function') { - callback() - } +const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => { + return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue } const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => { @@ -318,10 +289,8 @@ export { executeAfterTransition, findShadowRoot, getElement, - getElementFromSelector, getjQuery, getNextActiveElement, - getSelectorFromElement, getTransitionDurationFromElement, getUID, isDisabled, @@ -330,6 +299,7 @@ export { isVisible, noop, onDOMContentLoaded, + parseSelector, reflow, triggerTransitionEnd, toType diff --git a/js/src/util/scrollbar.js b/js/src/util/scrollbar.js index 5cac7b6d1..f6bd9de31 100644 --- a/js/src/util/scrollbar.js +++ b/js/src/util/scrollbar.js @@ -5,9 +5,9 @@ * -------------------------------------------------------------------------- */ -import SelectorEngine from '../dom/selector-engine' -import Manipulator from '../dom/manipulator' -import { isElement } from './index' +import SelectorEngine from '../dom/selector-engine.js' +import Manipulator from '../dom/manipulator.js' +import { isElement } from './index.js' /** * Constants diff --git a/js/src/util/swipe.js b/js/src/util/swipe.js index 7126360ce..fe04a2437 100644 --- a/js/src/util/swipe.js +++ b/js/src/util/swipe.js @@ -5,9 +5,9 @@ * -------------------------------------------------------------------------- */ -import Config from './config' -import EventHandler from '../dom/event-handler' -import { execute } from './index' +import Config from './config.js' +import EventHandler from '../dom/event-handler.js' +import { execute } from './index.js' /** * Constants diff --git a/js/src/util/template-factory.js b/js/src/util/template-factory.js index cf402fa3b..5046f0e31 100644 --- a/js/src/util/template-factory.js +++ b/js/src/util/template-factory.js @@ -5,10 +5,10 @@ * -------------------------------------------------------------------------- */ -import { DefaultAllowlist, sanitizeHtml } from './sanitizer' -import { getElement, isElement } from '../util/index' -import SelectorEngine from '../dom/selector-engine' -import Config from './config' +import { DefaultAllowlist, sanitizeHtml } from './sanitizer.js' +import { execute, getElement, isElement } from './index.js' +import SelectorEngine from '../dom/selector-engine.js' +import Config from './config.js' /** * Constants @@ -143,7 +143,7 @@ class TemplateFactory extends Config { } _resolvePossibleFunction(arg) { - return typeof arg === 'function' ? arg(this) : arg + return execute(arg, [this]) } _putElementInTemplate(element, templateElement) { diff --git a/js/tests/karma.conf.js b/js/tests/karma.conf.js index 6636ff15d..11c6f3045 100644 --- a/js/tests/karma.conf.js +++ b/js/tests/karma.conf.js @@ -105,7 +105,7 @@ if (BROWSERSTACK) { config.browserStack = { username: ENV.BROWSER_STACK_USERNAME, accessKey: ENV.BROWSER_STACK_ACCESS_KEY, - build: `bootstrap-${ENV.GITHUB_SHA ? ENV.GITHUB_SHA.slice(0, 7) + '-' : ''}${new Date().toISOString()}`, + build: `bootstrap-${ENV.GITHUB_SHA ? `${ENV.GITHUB_SHA.slice(0, 7)}-` : ''}${new Date().toISOString()}`, project: 'Bootstrap', retryLimit: 2 } diff --git a/js/tests/unit/collapse.spec.js b/js/tests/unit/collapse.spec.js index 9c8671988..fa50ad361 100644 --- a/js/tests/unit/collapse.spec.js +++ b/js/tests/unit/collapse.spec.js @@ -278,14 +278,14 @@ describe('Collapse', () => { fixtureEl.innerHTML = [ '
', '
', - ' ', + ' ', '
', '
', '
', '
', '
', '
', - ' ', + ' ', '
', '
', '
content
', @@ -293,7 +293,7 @@ describe('Collapse', () => { '
', '
', '
', - ' ', + ' ', '
', '
', '
content
', @@ -887,17 +887,17 @@ describe('Collapse', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', - '', + '', '', '
', - '
' + '
' ].join('') const trigger1 = fixtureEl.querySelector('#trigger1') const trigger2 = fixtureEl.querySelector('#trigger2') const trigger3 = fixtureEl.querySelector('#trigger3') const target1 = fixtureEl.querySelector('#test1') - const target2 = fixtureEl.querySelector('#test2') + const target2 = fixtureEl.querySelector(`#${CSS.escape('0/my/id')}`) const target2Shown = () => { expect(trigger1).not.toHaveClass('collapsed') diff --git a/js/tests/unit/dom/selector-engine.spec.js b/js/tests/unit/dom/selector-engine.spec.js index 0245896c6..905e25bae 100644 --- a/js/tests/unit/dom/selector-engine.spec.js +++ b/js/tests/unit/dom/selector-engine.spec.js @@ -1,5 +1,5 @@ import SelectorEngine from '../../../src/dom/selector-engine' -import { getFixture, clearFixture } from '../../helpers/fixture' +import { clearFixture, getFixture } from '../../helpers/fixture' describe('SelectorEngine', () => { let fixtureEl @@ -232,5 +232,159 @@ describe('SelectorEngine', () => { expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements) }) }) -}) + describe('getSelectorFromElement', () => { + it('should get selector from data-bs-target', () => { + fixtureEl.innerHTML = [ + '
', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('.target') + }) + + it('should get selector from href if no data-bs-target set', () => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('.target') + }) + + it('should get selector from href if data-bs-target equal to #', () => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('.target') + }) + + it('should return null if a selector from a href is a url without an anchor', () => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toBeNull() + }) + + it('should return the anchor if a selector from a href is a url', () => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('#target') + }) + + it('should return null if selector not found', () => { + fixtureEl.innerHTML = '' + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toBeNull() + }) + + it('should return null if no selector', () => { + fixtureEl.innerHTML = '
' + + const testEl = fixtureEl.querySelector('div') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toBeNull() + }) + }) + + describe('getElementFromSelector', () => { + it('should get element from data-bs-target', () => { + fixtureEl.innerHTML = [ + '
', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getElementFromSelector(testEl)).toEqual(fixtureEl.querySelector('.target')) + }) + + it('should get element from href if no data-bs-target set', () => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getElementFromSelector(testEl)).toEqual(fixtureEl.querySelector('.target')) + }) + + it('should return null if element not found', () => { + fixtureEl.innerHTML = '' + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getElementFromSelector(testEl)).toBeNull() + }) + + it('should return null if no selector', () => { + fixtureEl.innerHTML = '
' + + const testEl = fixtureEl.querySelector('div') + + expect(SelectorEngine.getElementFromSelector(testEl)).toBeNull() + }) + }) + + describe('getMultipleElementsFromSelector', () => { + it('should get elements from data-bs-target', () => { + fixtureEl.innerHTML = [ + '
', + '
', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target'))) + }) + + it('should get elements in array, from href if no data-bs-target set', () => { + fixtureEl.innerHTML = [ + '', + '
', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target'))) + }) + + it('should return empty array if elements not found', () => { + fixtureEl.innerHTML = '' + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toHaveSize(0) + }) + + it('should return empty array if no selector', () => { + fixtureEl.innerHTML = '
' + + const testEl = fixtureEl.querySelector('div') + + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toHaveSize(0) + }) + }) +}) diff --git a/js/tests/unit/tab.spec.js b/js/tests/unit/tab.spec.js index e0c7d86a6..1ac5929e1 100644 --- a/js/tests/unit/tab.spec.js +++ b/js/tests/unit/tab.spec.js @@ -177,6 +177,43 @@ describe('Tab', () => { }) }) + it('should work with tab id being an int', done => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
', + '
', + '
', + '
', + ' Working Tab 1 (#tab1) Content Here', + '
', + '
', + ' Working Tab 2 (#2) with numeric ID', + '
', + '
' + ].join('') + const profileTriggerEl = fixtureEl.querySelector('#trigger2') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector(`#${CSS.escape('2')}`)).toHaveClass('active') + done() + }) + + tab.show() + }) + it('should not fire shown when show is prevented', () => { return new Promise((resolve, reject) => { fixtureEl.innerHTML = '' @@ -603,19 +640,19 @@ describe('Tab', () => { '
' ].join('') - const tabEl = fixtureEl.querySelector('#tab1') + const tabEl1 = fixtureEl.querySelector('#tab1') const tabEl2 = fixtureEl.querySelector('#tab2') const tabEl3 = fixtureEl.querySelector('#tab3') const tabEl4 = fixtureEl.querySelector('#tab4') - const tab = new Tab(tabEl) + const tab1 = new Tab(tabEl1) const tab2 = new Tab(tabEl2) const tab3 = new Tab(tabEl3) const tab4 = new Tab(tabEl4) - const spy1 = spyOn(tab, 'show').and.callThrough() + const spy1 = spyOn(tab1, 'show').and.callThrough() const spy2 = spyOn(tab2, 'show').and.callThrough() const spy3 = spyOn(tab3, 'show').and.callThrough() const spy4 = spyOn(tab4, 'show').and.callThrough() - const spyFocus1 = spyOn(tabEl, 'focus').and.callThrough() + const spyFocus1 = spyOn(tabEl1, 'focus').and.callThrough() const spyFocus2 = spyOn(tabEl2, 'focus').and.callThrough() const spyFocus3 = spyOn(tabEl3, 'focus').and.callThrough() const spyFocus4 = spyOn(tabEl4, 'focus').and.callThrough() @@ -623,7 +660,7 @@ describe('Tab', () => { const keydown = createEvent('keydown') keydown.key = 'ArrowRight' - tabEl.dispatchEvent(keydown) + tabEl1.dispatchEvent(keydown) expect(spy1).not.toHaveBeenCalled() expect(spy2).not.toHaveBeenCalled() expect(spy3).not.toHaveBeenCalled() @@ -644,19 +681,19 @@ describe('Tab', () => { '
' ].join('') - const tabEl = fixtureEl.querySelector('#tab1') + const tabEl1 = fixtureEl.querySelector('#tab1') const tabEl2 = fixtureEl.querySelector('#tab2') const tabEl3 = fixtureEl.querySelector('#tab3') const tabEl4 = fixtureEl.querySelector('#tab4') - const tab = new Tab(tabEl) + const tab1 = new Tab(tabEl1) const tab2 = new Tab(tabEl2) const tab3 = new Tab(tabEl3) const tab4 = new Tab(tabEl4) - const spy1 = spyOn(tab, 'show').and.callThrough() + const spy1 = spyOn(tab1, 'show').and.callThrough() const spy2 = spyOn(tab2, 'show').and.callThrough() const spy3 = spyOn(tab3, 'show').and.callThrough() const spy4 = spyOn(tab4, 'show').and.callThrough() - const spyFocus1 = spyOn(tabEl, 'focus').and.callThrough() + const spyFocus1 = spyOn(tabEl1, 'focus').and.callThrough() const spyFocus2 = spyOn(tabEl2, 'focus').and.callThrough() const spyFocus3 = spyOn(tabEl3, 'focus').and.callThrough() const spyFocus4 = spyOn(tabEl4, 'focus').and.callThrough() diff --git a/js/tests/unit/util/config.spec.js b/js/tests/unit/util/config.spec.js index e1693c0c1..0037e09d7 100644 --- a/js/tests/unit/util/config.spec.js +++ b/js/tests/unit/util/config.spec.js @@ -128,7 +128,7 @@ describe('Config', () => { const obj = new DummyConfigClass() expect(() => { obj._typeCheckConfig(config) - }).toThrowError(TypeError, obj.constructor.NAME.toUpperCase() + ': Option "parent" provided type "number" but expected type "(string|element)".') + }).toThrowError(TypeError, `${obj.constructor.NAME.toUpperCase()}: Option "parent" provided type "number" but expected type "(string|element)".`) }) it('should return null stringified when null is passed', () => { diff --git a/js/tests/unit/util/index.spec.js b/js/tests/unit/util/index.spec.js index 9f28ce0aa..202c72061 100644 --- a/js/tests/unit/util/index.spec.js +++ b/js/tests/unit/util/index.spec.js @@ -22,119 +22,6 @@ describe('Util', () => { }) }) - describe('getSelectorFromElement', () => { - it('should get selector from data-bs-target', () => { - fixtureEl.innerHTML = [ - '
', - '
' - ].join('') - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getSelectorFromElement(testEl)).toEqual('.target') - }) - - it('should get selector from href if no data-bs-target set', () => { - fixtureEl.innerHTML = [ - '', - '
' - ].join('') - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getSelectorFromElement(testEl)).toEqual('.target') - }) - - it('should get selector from href if data-bs-target equal to #', () => { - fixtureEl.innerHTML = [ - '', - '
' - ].join('') - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getSelectorFromElement(testEl)).toEqual('.target') - }) - - it('should return null if a selector from a href is a url without an anchor', () => { - fixtureEl.innerHTML = [ - '', - '
' - ].join('') - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getSelectorFromElement(testEl)).toBeNull() - }) - - it('should return the anchor if a selector from a href is a url', () => { - fixtureEl.innerHTML = [ - '', - '
' - ].join('') - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getSelectorFromElement(testEl)).toEqual('#target') - }) - - it('should return null if selector not found', () => { - fixtureEl.innerHTML = '' - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getSelectorFromElement(testEl)).toBeNull() - }) - - it('should return null if no selector', () => { - fixtureEl.innerHTML = '
' - - const testEl = fixtureEl.querySelector('div') - - expect(Util.getSelectorFromElement(testEl)).toBeNull() - }) - }) - - describe('getElementFromSelector', () => { - it('should get element from data-bs-target', () => { - fixtureEl.innerHTML = [ - '
', - '
' - ].join('') - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getElementFromSelector(testEl)).toEqual(fixtureEl.querySelector('.target')) - }) - - it('should get element from href if no data-bs-target set', () => { - fixtureEl.innerHTML = [ - '', - '
' - ].join('') - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getElementFromSelector(testEl)).toEqual(fixtureEl.querySelector('.target')) - }) - - it('should return null if element not found', () => { - fixtureEl.innerHTML = '' - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getElementFromSelector(testEl)).toBeNull() - }) - - it('should return null if no selector', () => { - fixtureEl.innerHTML = '
' - - const testEl = fixtureEl.querySelector('div') - - expect(Util.getElementFromSelector(testEl)).toBeNull() - }) - }) - describe('getTransitionDurationFromElement', () => { it('should get transition from element', () => { fixtureEl.innerHTML = '
' @@ -631,6 +518,25 @@ describe('Util', () => { Util.execute(spy) expect(spy).toHaveBeenCalled() }) + + it('should execute if arg is function & return the result', () => { + const functionFoo = (num1, num2 = 10) => num1 + num2 + const resultFoo = Util.execute(functionFoo, [4, 5]) + expect(resultFoo).toBe(9) + + const resultFoo1 = Util.execute(functionFoo, [4]) + expect(resultFoo1).toBe(14) + + const functionBar = () => 'foo' + const resultBar = Util.execute(functionBar) + expect(resultBar).toBe('foo') + }) + + it('should not execute if arg is not function & return default argument', () => { + const foo = 'bar' + expect(Util.execute(foo)).toBe('bar') + expect(Util.execute(foo, [], 4)).toBe(4) + }) }) describe('executeAfterTransition', () => { diff --git a/js/tests/visual/button.html b/js/tests/visual/button.html index 0c54934f0..47c50889c 100644 --- a/js/tests/visual/button.html +++ b/js/tests/visual/button.html @@ -15,7 +15,7 @@

For checkboxes and radio buttons, ensure that keyboard behavior is functioning correctly.

-

Navigate to the checkboxes with the keyboard (generally, using TAB / SHIFT + TAB), and ensure that SPACE toggles the currently focused checkbox. Click on one of the checkboxes using the mouse, ensure that focus was correctly set on the actual checkbox, and that SPACE toggles the checkbox again.

+

Navigate to the checkboxes with the keyboard (generally, using Tab / Shift + Tab), and ensure that Space toggles the currently focused checkbox. Click on one of the checkboxes using the mouse, ensure that focus was correctly set on the actual checkbox, and that Space toggles the checkbox again.

-

Navigate to the radio button group with the keyboard (generally, using TAB / SHIFT + TAB). If no radio button was initially set to be selected, the first/last radio button should receive focus (depending on whether you navigated "forward" to the group with TAB or "backwards" using SHIFT + TAB). If a radio button was already selected, navigating with the keyboard should set focus to that particular radio button. Only one radio button in a group should receive focus at any given time. Ensure that the selected radio button can be changed by using the and arrow keys. Click on one of the radio buttons with the mouse, ensure that focus was correctly set on the actual radio button, and that and change the selected radio button again.

+

Navigate to the radio button group with the keyboard (generally, using Tab / Shift + Tab). If no radio button was initially set to be selected, the first/last radio button should receive focus (depending on whether you navigated "forward" to the group with Tab or "backwards" using Shift + Tab). If a radio button was already selected, navigating with the keyboard should set focus to that particular radio button. Only one radio button in a group should receive focus at any given time. Ensure that the selected radio button can be changed by using the and arrow keys. Click on one of the radio buttons with the mouse, ensure that focus was correctly set on the actual radio button, and that and change the selected radio button again.