diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 9b9b2edadc52..75841c222ea0 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -19,7 +19,7 @@ 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-4`](https://stackoverflow.com/questions/tagged/bootstrap-4) tag), + Overflow ([`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5) tag), [Slack](https://bootstrap-slack.herokuapp.com/) or [IRC](README.md#community) are better places to get help. * Please **do not** derail or troll issues. Keep the discussion on topic and diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index de3c4b552e9a..c30bed3b4511 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -8,4 +8,4 @@ For general troubleshooting or help getting started: - Join [the official Slack room](https://bootstrap-slack.herokuapp.com/). - Chat with fellow Bootstrappers in IRC. On the `irc.freenode.net` server, in the `##bootstrap` channel. -- Ask and explore Stack Overflow with the [`bootstrap-4`](https://stackoverflow.com/questions/tagged/bootstrap-4) tag. +- Ask and explore Stack Overflow with the [`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5) tag. diff --git a/README.md b/README.md index ddbc6a1c3523..c96d3f04f29f 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Several quick start options are available: - [Download the latest release.](https://github.com/twbs/bootstrap/archive/v5.0.0.zip) - Clone the repo: `git clone https://github.com/twbs/bootstrap.git` - Install with [npm](https://www.npmjs.com/): `npm install bootstrap@next` -- Install with [yarn](https://yarnpkg.com/): `yarn add bootstrap@5.0.0` +- Install with [yarn](https://yarnpkg.com/): `yarn add bootstrap@next` - Install with [Composer](https://getcomposer.org/): `composer require twbs/bootstrap:5.0.0` - Install with [NuGet](https://www.nuget.org/): CSS: `Install-Package bootstrap` Sass: `Install-Package bootstrap.sass` @@ -161,7 +161,7 @@ Get updates on Bootstrap's development and chat with the project maintainers and - Read and subscribe to [The Official Bootstrap Blog](https://blog.getbootstrap.com/). - Join [the official Slack room](https://bootstrap-slack.herokuapp.com/). - Chat with fellow Bootstrappers in IRC. On the `irc.freenode.net` server, in the `##bootstrap` channel. -- Implementation help may be found at Stack Overflow (tagged [`bootstrap-4`](https://stackoverflow.com/questions/tagged/bootstrap-4)). +- Implementation help may be found at Stack Overflow (tagged [`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5)). - Developers should use the keyword `bootstrap` on packages which modify or add to the functionality of Bootstrap when distributing through [npm](https://www.npmjs.com/browse/keyword/bootstrap) or similar delivery mechanisms for maximum discoverability. diff --git a/build/change-version.js b/build/change-version.js index ba6e46a47184..b8a640fa8ee3 100644 --- a/build/change-version.js +++ b/build/change-version.js @@ -95,7 +95,7 @@ function main(args) { 'vendor' ]) const INCLUDED_EXTENSIONS = new Set([ - // This extension whitelist is how we avoid modifying binary files + // This extension allowlist is how we avoid modifying binary files '', '.css', '.html', diff --git a/js/src/dom/event-handler.js b/js/src/dom/event-handler.js index b76677858885..5fea03018288 100644 --- a/js/src/dom/event-handler.js +++ b/js/src/dom/event-handler.js @@ -94,6 +94,7 @@ function getEvent(element) { function bootstrapHandler(element, fn) { return function handler(event) { + event.delegateTarget = element if (handler.oneOff) { EventHandler.off(element, event.type, fn) } @@ -109,6 +110,7 @@ function bootstrapDelegationHandler(element, selector, fn) { for (let { target } = event; target && target !== this; target = target.parentNode) { for (let i = domElements.length; i--;) { if (domElements[i] === target) { + event.delegateTarget = target if (handler.oneOff) { EventHandler.off(element, event.type, fn) } diff --git a/js/src/tooltip.js b/js/src/tooltip.js index d76679a6a09b..33c0f6eec224 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -17,7 +17,7 @@ import { typeCheckConfig } from './util/index' import { - DefaultWhitelist, + DefaultAllowlist, sanitizeHtml } from './util/sanitizer' import Data from './dom/data' @@ -38,7 +38,7 @@ const DATA_KEY = 'bs.tooltip' const EVENT_KEY = `.${DATA_KEY}` const CLASS_PREFIX = 'bs-tooltip' const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g') -const DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn'] +const DISALLOWED_ATTRIBUTES = ['sanitize', 'allowList', 'sanitizeFn'] const DefaultType = { animation: 'boolean', @@ -55,7 +55,7 @@ const DefaultType = { boundary: '(string|element)', sanitize: 'boolean', sanitizeFn: '(null|function)', - whiteList: 'object', + allowList: 'object', popperConfig: '(null|object)' } @@ -84,7 +84,7 @@ const Default = { boundary: 'scrollParent', sanitize: true, sanitizeFn: null, - whiteList: DefaultWhitelist, + allowList: DefaultAllowlist, popperConfig: null } @@ -194,14 +194,14 @@ class Tooltip { if (event) { const dataKey = this.constructor.DATA_KEY - let context = Data.getData(event.target, dataKey) + let context = Data.getData(event.delegateTarget, dataKey) if (!context) { context = new this.constructor( - event.target, + event.delegateTarget, this._getDelegateConfig() ) - Data.setData(event.target, dataKey, context) + Data.setData(event.delegateTarget, dataKey, context) } context._activeTrigger.click = !context._activeTrigger.click @@ -428,7 +428,7 @@ class Tooltip { if (this.config.html) { if (this.config.sanitize) { - content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn) + content = sanitizeHtml(content, this.config.allowList, this.config.sanitizeFn) } element.innerHTML = content @@ -587,14 +587,14 @@ class Tooltip { _enter(event, context) { const dataKey = this.constructor.DATA_KEY - context = context || Data.getData(event.target, dataKey) + context = context || Data.getData(event.delegateTarget, dataKey) if (!context) { context = new this.constructor( - event.target, + event.delegateTarget, this._getDelegateConfig() ) - Data.setData(event.target, dataKey, context) + Data.setData(event.delegateTarget, dataKey, context) } if (event) { @@ -627,14 +627,14 @@ class Tooltip { _leave(event, context) { const dataKey = this.constructor.DATA_KEY - context = context || Data.getData(event.target, dataKey) + context = context || Data.getData(event.delegateTarget, dataKey) if (!context) { context = new this.constructor( - event.target, + event.delegateTarget, this._getDelegateConfig() ) - Data.setData(event.target, dataKey, context) + Data.setData(event.delegateTarget, dataKey, context) } if (event) { @@ -711,7 +711,7 @@ class Tooltip { typeCheckConfig(NAME, config, this.constructor.DefaultType) if (config.sanitize) { - config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn) + config.template = sanitizeHtml(config.template, config.allowList, config.sanitizeFn) } return config diff --git a/js/src/util/sanitizer.js b/js/src/util/sanitizer.js index e1ec36a40051..27bdf6cb1ffd 100644 --- a/js/src/util/sanitizer.js +++ b/js/src/util/sanitizer.js @@ -55,7 +55,7 @@ const allowedAttribute = (attr, allowedAttributeList) => { return false } -export const DefaultWhitelist = { +export const DefaultAllowlist = { // Global attributes allowed on any supplied element below. '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN], a: ['target', 'href', 'title', 'rel'], @@ -89,7 +89,7 @@ export const DefaultWhitelist = { ul: [] } -export function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) { +export function sanitizeHtml(unsafeHtml, allowList, sanitizeFn) { if (!unsafeHtml.length) { return unsafeHtml } @@ -100,24 +100,24 @@ export function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) { const domParser = new window.DOMParser() const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html') - const whitelistKeys = Object.keys(whiteList) + const allowlistKeys = Object.keys(allowList) const elements = [].concat(...createdDocument.body.querySelectorAll('*')) for (let i = 0, len = elements.length; i < len; i++) { const el = elements[i] const elName = el.nodeName.toLowerCase() - if (whitelistKeys.indexOf(elName) === -1) { + if (allowlistKeys.indexOf(elName) === -1) { el.parentNode.removeChild(el) continue } const attributeList = [].concat(...el.attributes) - const whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || []) + const allowedAttributes = [].concat(allowList['*'] || [], allowList[elName] || []) attributeList.forEach(attr => { - if (!allowedAttribute(attr, whitelistedAttributes)) { + if (!allowedAttribute(attr, allowedAttributes)) { el.removeAttribute(attr.nodeName) } }) diff --git a/js/tests/unit/tooltip.spec.js b/js/tests/unit/tooltip.spec.js index e713fe560202..0a98096a405e 100644 --- a/js/tests/unit/tooltip.spec.js +++ b/js/tests/unit/tooltip.spec.js @@ -324,6 +324,28 @@ describe('Tooltip', () => { tooltip.show() }) + it('should show a tooltip when hovering a children element', done => { + fixtureEl.innerHTML = + '' + + '' + + '' + + '' + + '' + + '' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + + spyOn(tooltip, 'show') + + tooltipEl.querySelector('rect').dispatchEvent(createEvent('mouseover', { bubbles: true })) + + setTimeout(() => { + expect(tooltip.show).toHaveBeenCalled() + done() + }, 0) + }) + it('should show a tooltip on mobile', done => { fixtureEl.innerHTML = '' diff --git a/js/tests/unit/util/sanitizer.spec.js b/js/tests/unit/util/sanitizer.spec.js index c4259e7fd66c..dcfad8436f8f 100644 --- a/js/tests/unit/util/sanitizer.spec.js +++ b/js/tests/unit/util/sanitizer.spec.js @@ -1,11 +1,11 @@ -import { DefaultWhitelist, sanitizeHtml } from '../../../src/util/sanitizer' +import { DefaultAllowlist, sanitizeHtml } from '../../../src/util/sanitizer' describe('Sanitizer', () => { describe('sanitizeHtml', () => { it('should return the same on empty string', () => { const empty = '' - const result = sanitizeHtml(empty, DefaultWhitelist, null) + const result = sanitizeHtml(empty, DefaultAllowlist, null) expect(result).toEqual(empty) }) @@ -18,7 +18,7 @@ describe('Sanitizer', () => { '' ].join('') - const result = sanitizeHtml(template, DefaultWhitelist, null) + const result = sanitizeHtml(template, DefaultAllowlist, null) expect(result.indexOf('script') === -1).toEqual(true) }) @@ -30,20 +30,20 @@ describe('Sanitizer', () => { '' ].join('') - const result = sanitizeHtml(template, DefaultWhitelist, null) + const result = sanitizeHtml(template, DefaultAllowlist, null) expect(result.indexOf('aria-pressed') !== -1).toEqual(true) expect(result.indexOf('class="test"') !== -1).toEqual(true) }) - it('should remove not whitelist tags', () => { + it('should remove tags not in allowlist', () => { const template = [ '
', ' ', '
' ].join('') - const result = sanitizeHtml(template, DefaultWhitelist, null) + const result = sanitizeHtml(template, DefaultAllowlist, null) expect(result.indexOf('