diff --git a/build/build-plugins.js b/build/build-plugins.js index 7deda49b12..78f76622a4 100644 --- a/build/build-plugins.js +++ b/build/build-plugins.js @@ -14,6 +14,7 @@ const rollup = require('rollup') const { babel } = require('@rollup/plugin-babel') const banner = require('./banner.js') +const rootPath = path.resolve(__dirname, '../js/dist/') const plugins = [ babel({ // Only transpile our source code @@ -39,7 +40,6 @@ const bsPlugins = { Toast: path.resolve(__dirname, '../js/src/toast.js'), Tooltip: path.resolve(__dirname, '../js/src/tooltip.js') } -const rootPath = path.resolve(__dirname, '../js/dist/') const defaultPluginConfig = { external: [ @@ -87,9 +87,9 @@ const getConfigByPluginKey = pluginKey => { if (pluginKey === 'Dropdown' || pluginKey === 'Tooltip') { const config = Object.assign(defaultPluginConfig) - config.external.push(bsPlugins.Manipulator, 'popper.js') + config.external.push(bsPlugins.Manipulator, '@popperjs/core') config.globals[bsPlugins.Manipulator] = 'Manipulator' - config.globals['popper.js'] = 'Popper' + config.globals['@popperjs/core'] = 'Popper' return config } diff --git a/build/generate-sri.js b/build/generate-sri.js index 0c272ceadd..8a2e59360a 100644 --- a/build/generate-sri.js +++ b/build/generate-sri.js @@ -42,7 +42,7 @@ const files = [ configPropertyName: 'js_bundle_hash' }, { - file: 'node_modules/popper.js/dist/umd/popper.min.js', + file: 'node_modules/@popperjs/core/dist/umd/popper.min.js', configPropertyName: 'popper_hash' } ] diff --git a/build/rollup.config.js b/build/rollup.config.js index 05579d165a..7f9c1c7e11 100644 --- a/build/rollup.config.js +++ b/build/rollup.config.js @@ -3,13 +3,14 @@ const path = require('path') const { babel } = require('@rollup/plugin-babel') const { nodeResolve } = require('@rollup/plugin-node-resolve') +const replace = require('@rollup/plugin-replace') const banner = require('./banner.js') const BUNDLE = process.env.BUNDLE === 'true' const ESM = process.env.ESM === 'true' let fileDest = `bootstrap${ESM ? '.esm' : ''}` -const external = ['popper.js'] +const external = ['@popperjs/core'] const plugins = [ babel({ // Only transpile our source code @@ -19,15 +20,15 @@ const plugins = [ }) ] const globals = { - 'popper.js': 'Popper' + '@popperjs/core': 'Popper' } if (BUNDLE) { fileDest += '.bundle' // Remove last entry in external array to bundle Popper external.pop() - delete globals['popper.js'] - plugins.push(nodeResolve()) + delete globals['@popperjs/core'] + plugins.push(replace({ 'process.env.NODE_ENV': '"production"' }), nodeResolve()) } const rollupConfig = { diff --git a/config.yml b/config.yml index d109da5213..2bdc700bce 100644 --- a/config.yml +++ b/config.yml @@ -75,5 +75,5 @@ params: js_hash: "sha384-supZtwqjyYg6XvvTCi4/w6J6Hm6IKqXaaeoyGhIhonCdkSvA70sSucW7OqXIo4lZ" js_bundle: "https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-alpha3/dist/js/bootstrap.bundle.min.js" js_bundle_hash: "sha384-G/J8d6sz9bTod37AsZzNtTwT77J24FKjJEO1YsU2vW7iPcmYP3/tznu+LcK824t9" - popper: "https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" - popper_hash: "sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" + popper: "https://cdn.jsdelivr.net/npm/@popperjs/core@2.4.2/dist/umd/popper.min.js" + popper_hash: "sha384-a46n7BtEJaPKKs2SeVxZzwKkapYzBUr8c7DyCLEpkRrs4LE03nlh53ZSOPgkJB7U" diff --git a/js/src/dropdown.js b/js/src/dropdown.js index 0ac108ab81..04c299600e 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -5,6 +5,8 @@ * -------------------------------------------------------------------------- */ +import * as Popper from '@popperjs/core' + import { getjQuery, onDOMContentLoaded, @@ -18,7 +20,6 @@ import { import Data from './dom/data' import EventHandler from './dom/event-handler' import Manipulator from './dom/manipulator' -import Popper from 'popper.js' import SelectorEngine from './dom/selector-engine' import BaseComponent from './base-component' @@ -58,7 +59,6 @@ const CLASS_NAME_DROPEND = 'dropend' const CLASS_NAME_DROPSTART = 'dropstart' const CLASS_NAME_MENUEND = 'dropdown-menu-end' const CLASS_NAME_NAVBAR = 'navbar' -const CLASS_NAME_POSITION_STATIC = 'position-static' const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="dropdown"]' const SELECTOR_FORM_CHILD = '.dropdown form' @@ -76,7 +76,7 @@ const PLACEMENT_LEFT = isRTL ? 'right-start' : 'left-start' const Default = { offset: 0, flip: true, - boundary: 'scrollParent', + boundary: 'clippingParents', reference: 'toggle', display: 'dynamic', popperConfig: null @@ -176,14 +176,7 @@ class Dropdown extends BaseComponent { } } - // If boundary is not `scrollParent`, then set position to `static` - // to allow the menu to "escape" the scroll parent's boundaries - // https://github.com/twbs/bootstrap/issues/24251 - if (this._config.boundary !== 'scrollParent') { - parent.classList.add(CLASS_NAME_POSITION_STATIC) - } - - this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig()) + this._popper = Popper.createPopper(referenceElement, this._menu, this._getPopperConfig()) } // If this is a touch-enabled device we add extra @@ -233,6 +226,7 @@ class Dropdown extends BaseComponent { super.dispose() EventHandler.off(this._element, EVENT_KEY) this._menu = null + if (this._popper) { this._popper.destroy() this._popper = null @@ -242,7 +236,7 @@ class Dropdown extends BaseComponent { update() { this._inNavbar = this._detectNavbar() if (this._popper) { - this._popper.scheduleUpdate() + this._popper.update() } } @@ -296,44 +290,24 @@ class Dropdown extends BaseComponent { return Boolean(this._element.closest(`.${CLASS_NAME_NAVBAR}`)) } - _getOffset() { - const offset = {} - - if (typeof this._config.offset === 'function') { - offset.fn = data => { - data.offsets = { - ...data.offsets, - ...(this._config.offset(data.offsets, this._element) || {}) - } - - return data - } - } else { - offset.offset = this._config.offset - } - - return offset - } - _getPopperConfig() { const popperConfig = { placement: this._getPlacement(), - modifiers: { - offset: this._getOffset(), - flip: { - enabled: this._config.flip - }, - preventOverflow: { - boundariesElement: this._config.boundary + modifiers: [{ + name: 'preventOverflow', + options: { + altBoundary: this._config.flip, + rootBoundary: this._config.boundary } - } + }] } // Disable Popper if we have a static display if (this._config.display === 'static') { - popperConfig.modifiers.applyStyle = { + popperConfig.modifiers = [{ + name: 'applyStyles', enabled: false - } + }] } return { diff --git a/js/src/tooltip.js b/js/src/tooltip.js index 17148ed9a6..dc2f83a8a5 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -5,6 +5,8 @@ * -------------------------------------------------------------------------- */ +import * as Popper from '@popperjs/core' + import { getjQuery, onDOMContentLoaded, @@ -25,7 +27,6 @@ import { import Data from './dom/data' import EventHandler from './dom/event-handler' import Manipulator from './dom/manipulator' -import Popper from 'popper.js' import SelectorEngine from './dom/selector-engine' import BaseComponent from './base-component' @@ -51,9 +52,8 @@ const DefaultType = { html: 'boolean', selector: '(string|boolean)', placement: '(string|function)', - offset: '(number|string|function)', container: '(string|element|boolean)', - fallbackPlacement: '(string|array)', + fallbackPlacements: '(null|array)', boundary: '(string|element)', customClass: '(string|function)', sanitize: 'boolean', @@ -82,10 +82,9 @@ const Default = { html: false, selector: false, placement: 'top', - offset: 0, container: false, - fallbackPlacement: 'flip', - boundary: 'scrollParent', + fallbackPlacements: null, + boundary: 'clippingParents', customClass: '', sanitize: true, sanitizeFn: null, @@ -287,7 +286,7 @@ class Tooltip extends BaseComponent { EventHandler.trigger(this._element, this.constructor.Event.INSERTED) - this._popper = new Popper(this._element, tip, this._getPopperConfig(attachment)) + this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment)) tip.classList.add(CLASS_NAME_SHOW) @@ -307,13 +306,9 @@ class Tooltip extends BaseComponent { } const complete = () => { - if (this.config.animation) { - this._fixTransition() - } - const prevHoverState = this._hoverState - this._hoverState = null + this._hoverState = null EventHandler.trigger(this._element, this.constructor.Event.SHOWN) if (prevHoverState === HOVER_STATE_OUT) { @@ -345,7 +340,11 @@ class Tooltip extends BaseComponent { this._cleanTipClass() this._element.removeAttribute('aria-describedby') EventHandler.trigger(this._element, this.constructor.Event.HIDDEN) - this._popper.destroy() + + if (this._popper) { + this._popper.destroy() + this._popper = null + } } const hideEvent = EventHandler.trigger(this._element, this.constructor.Event.HIDE) @@ -380,7 +379,7 @@ class Tooltip extends BaseComponent { update() { if (this._popper !== null) { - this._popper.scheduleUpdate() + this._popper.update() } } @@ -469,26 +468,45 @@ class Tooltip extends BaseComponent { // Private _getPopperConfig(attachment) { + const flipModifier = { + name: 'flip', + options: { + altBoundary: true + } + } + + if (this.config.fallbackPlacements) { + flipModifier.options.fallbackPlacements = this.config.fallbackPlacements + } + const defaultBsConfig = { placement: attachment, - modifiers: { - offset: this._getOffset(), - flip: { - behavior: this.config.fallbackPlacement + modifiers: [ + flipModifier, + { + name: 'preventOverflow', + options: { + rootBoundary: this.config.boundary + } }, - arrow: { - element: `.${this.constructor.NAME}-arrow` + { + name: 'arrow', + options: { + element: `.${this.constructor.NAME}-arrow` + } }, - preventOverflow: { - boundariesElement: this.config.boundary + { + name: 'onChange', + enabled: true, + phase: 'afterWrite', + fn: data => this._handlePopperPlacementChange(data) } - }, - onCreate: data => { - if (data.originalPlacement !== data.placement) { + ], + onFirstUpdate: data => { + if (data.options.placement !== data.placement) { this._handlePopperPlacementChange(data) } - }, - onUpdate: data => this._handlePopperPlacementChange(data) + } } return { @@ -501,25 +519,6 @@ class Tooltip extends BaseComponent { this.getTipElement().classList.add(`${CLASS_PREFIX}-${this.updateAttachment(attachment)}`) } - _getOffset() { - const offset = {} - - if (typeof this.config.offset === 'function') { - offset.fn = data => { - data.offsets = { - ...data.offsets, - ...(this.config.offset(data.offsets, this._element) || {}) - } - - return data - } - } else { - offset.offset = this.config.offset - } - - return offset - } - _getContainer() { if (this.config.container === false) { return document.body @@ -743,23 +742,15 @@ class Tooltip extends BaseComponent { } _handlePopperPlacementChange(popperData) { - this.tip = popperData.instance.popper - this._cleanTipClass() - this._addAttachmentClass(this._getAttachment(popperData.placement)) - } + const { state } = popperData - _fixTransition() { - const tip = this.getTipElement() - const initConfigAnimation = this.config.animation - if (tip.getAttribute('x-placement') !== null) { + if (!state) { return } - tip.classList.remove(CLASS_NAME_FADE) - this.config.animation = false - this.hide() - this.show() - this.config.animation = initConfigAnimation + this.tip = state.elements.popper + this._cleanTipClass() + this._addAttachmentClass(this._getAttachment(state.placement)) } // Static diff --git a/js/tests/integration/bundle-modularity.js b/js/tests/integration/bundle-modularity.js index 003f840210..8546141b19 100644 --- a/js/tests/integration/bundle-modularity.js +++ b/js/tests/integration/bundle-modularity.js @@ -1,5 +1,5 @@ -import 'popper.js' import Tooltip from '../../dist/tooltip' +import '../../dist/carousel' window.addEventListener('load', () => { [].concat(...document.querySelectorAll('[data-bs-toggle="tooltip"]')) diff --git a/js/tests/integration/bundle.js b/js/tests/integration/bundle.js index 75982f76f9..452088a7d8 100644 --- a/js/tests/integration/bundle.js +++ b/js/tests/integration/bundle.js @@ -1,4 +1,3 @@ -import 'popper.js' import { Tooltip } from '../../../dist/js/bootstrap.esm.js' window.addEventListener('load', () => { diff --git a/js/tests/integration/rollup.bundle.js b/js/tests/integration/rollup.bundle.js index 9e2ed26c1c..288f40961d 100644 --- a/js/tests/integration/rollup.bundle.js +++ b/js/tests/integration/rollup.bundle.js @@ -2,6 +2,7 @@ const { babel } = require('@rollup/plugin-babel') const { nodeResolve } = require('@rollup/plugin-node-resolve') +const replace = require('@rollup/plugin-replace') module.exports = { input: 'js/tests/integration/bundle.js', @@ -10,6 +11,9 @@ module.exports = { format: 'iife' }, plugins: [ + replace({ + 'process.env.NODE_ENV': '"production"' + }), nodeResolve(), babel({ exclude: 'node_modules/**', diff --git a/js/tests/karma.conf.js b/js/tests/karma.conf.js index 0728a8cfa4..d0dd8bdd90 100644 --- a/js/tests/karma.conf.js +++ b/js/tests/karma.conf.js @@ -5,6 +5,7 @@ const ip = require('ip') const { babel } = require('@rollup/plugin-babel') const istanbul = require('rollup-plugin-istanbul') const { nodeResolve } = require('@rollup/plugin-node-resolve') +const replace = require('@rollup/plugin-replace') const { browsers, @@ -74,6 +75,9 @@ const conf = { }, rollupPreprocessor: { plugins: [ + replace({ + 'process.env.NODE_ENV': '"dev"' + }), istanbul({ exclude: [ 'node_modules/**', diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js index f6a5feb1b9..d2171f3697 100644 --- a/js/tests/unit/dropdown.spec.js +++ b/js/tests/unit/dropdown.spec.js @@ -1,5 +1,3 @@ -import Popper from 'popper.js' - import Dropdown from '../../src/dropdown' import EventHandler from '../../src/dom/event-handler' @@ -36,50 +34,6 @@ describe('Dropdown', () => { }) describe('constructor', () => { - it('should create offset modifier correctly when offset option is a function', () => { - fixtureEl.innerHTML = [ - '
offset
0
Offset of the dropdown relative to its target.
-When a function is used to determine the offset, it is called with an object containing the offset data as its first argument. The function must return an object with the same structure. The triggering element DOM node is passed as the second argument.
-For more information refer to Popper's offset docs.
-flip
true
boundary
'scrollParent'
'viewport'
, 'window'
, 'scrollParent'
, or an HTMLElement reference (JavaScript only). For more information refer to Popper's preventOverflow docs.'clippingParents'
and can accept an HTMLElement reference (JavaScript only). For more information refer to Popper's preventOverflow docs.reference
'toggle'
'toggle'
, 'parent'
, or an HTMLElement reference. For more information refer to Popper's referenceObject docs.'toggle'
, 'parent'
, or an HTMLElement reference. For more information refer to Popper's constructor docs.display
'hover'
on its own will result in tooltips that cannot be triggered via the keyboard, and should only be used if alternative methods for conveying the same information for keyboard users is present.
offset
0
Offset of the tooltip relative to its target.
-When a function is used to determine the offset, it is called with an object containing the offset data as its first argument. The function must return an object with the same structure. The triggering element DOM node is passed as the second argument.
-For more information refer to Popper's offset docs.
-fallbackPlacement
'flip'
null
boundary
'scrollParent'
'viewport'
, 'window'
, 'scrollParent'
, or an HTMLElement reference (JavaScript only). For more information refer to Popper's preventOverflow docs.'clippingParents'
'clippingParents'
and can accept an HTMLElement reference (JavaScript only). For more information refer to Popper's preventOverflow docs.customClass