From 4deaa2a0c5384fdb8be0d74d8bf3165fa13c91cc Mon Sep 17 00:00:00 2001 From: Gaurav Munjal Date: Fri, 30 Oct 2026 11:05:57 -0400 Subject: [PATCH 1/2] SCF-2416: replace ember-on-helper with bs-on-document and bs-on-window ember-on-helper 0.1.0 depends on ember-cli-babel 7, and has had no release since 2019 and no commits since January 2020, so it can't be updated. Upstream ember-bootstrap still depends on it. The new bs-on-document and bs-on-window helpers share one classic Helper base class (utils/global-event-listener-helper). It adds the listener in compute, removes the previous one with the same options when the arguments change, and removes it in willDestroy. Named arguments such as capture=true are passed as listener options. ember-on-helper threw in FastBoot, which is why BsModal only rendered on-window outside of it. The base class skips targets without addEventListener (FastBoot's window has none), so that guard and the modal's isFastBoot field are gone. The FastBoot modal test covers it. Templates: bs-dropdown's three on-document calls and bs-modal's on-window call use the bs- helpers. Both are re-exported from app/, like bs-default. New tests: - both helpers: the callback fires, the listener is removed on teardown, and changing the event name or callback swaps it. capture=true catches events that stop propagation, and the capturing listener is removed with the same options. - BsModal: a window resize adjusts the dialog padding (adjustDialog had no coverage). - BsDropdown: once closed, outside clicks, Escape and Tab no longer call onHide. Co-Authored-By: Claude Opus 5.5 --- addon/components/bs-dropdown.hbs | 6 +- addon/components/bs-modal.hbs | 9 +-- addon/components/bs-modal.js | 7 -- addon/helpers/bs-on-document.js | 14 ++++ addon/helpers/bs-on-window.js | 14 ++++ addon/utils/global-event-listener-helper.js | 53 ++++++++++++ app/helpers/bs-on-document.js | 1 + app/helpers/bs-on-window.js | 1 + package.json | 1 - .../components/bs-dropdown-test.js | 18 +++++ tests/integration/components/bs-modal-test.js | 24 ++++++ .../helpers/bs-on-document-test.js | 81 +++++++++++++++++++ .../integration/helpers/bs-on-window-test.js | 32 ++++++++ yarn.lock | 7 -- 14 files changed, 242 insertions(+), 26 deletions(-) create mode 100644 addon/helpers/bs-on-document.js create mode 100644 addon/helpers/bs-on-window.js create mode 100644 addon/utils/global-event-listener-helper.js create mode 100644 app/helpers/bs-on-document.js create mode 100644 app/helpers/bs-on-window.js create mode 100644 tests/integration/helpers/bs-on-document-test.js create mode 100644 tests/integration/helpers/bs-on-window-test.js diff --git a/addon/components/bs-dropdown.hbs b/addon/components/bs-dropdown.hbs index 71bf7bd52..cbbb9e9c9 100644 --- a/addon/components/bs-dropdown.hbs +++ b/addon/components/bs-dropdown.hbs @@ -31,9 +31,9 @@ ) }} {{#if this.isOpen}} - {{on-document "keydown" this.handleKeyEvent}} - {{on-document "click" this.closeHandler capture=true}} - {{on-document "keyup" this.closeHandler}} + {{bs-on-document "keydown" this.handleKeyEvent}} + {{bs-on-document "click" this.closeHandler capture=true}} + {{bs-on-document "keyup" this.closeHandler}} {{/if}} diff --git a/addon/components/bs-modal.hbs b/addon/components/bs-modal.hbs index f1c731eac..0a3db7a9a 100644 --- a/addon/components/bs-modal.hbs +++ b/addon/components/bs-modal.hbs @@ -2,14 +2,7 @@ {{did-update this.handleVisibilityChanges @open}} {{#if this.inDom}} - {{! - ember-on-helper do not support FastBoot yet. This guard can be removed as - soon as ember-on-helper guards for FastBoot itself. It is tracked in this - GitHub issue: https://github.com/buschtoens/ember-on-helper/issues/3 - }} - {{#unless this.isFastBoot}} - {{on-window "resize" this.adjustDialog}} - {{/unless}} + {{bs-on-window "resize" this.adjustDialog}} {{#let (component (ensure-safe-component (bs-default @dialogComponent (component "bs-modal/dialog"))) onClose=this.close diff --git a/addon/components/bs-modal.js b/addon/components/bs-modal.js index f73983c38..0ee04e7fd 100644 --- a/addon/components/bs-modal.js +++ b/addon/components/bs-modal.js @@ -313,13 +313,6 @@ export default class Modal extends Component { */ @ref('backdropElement') backdropElement; - /** - * @type boolean - * @readonly - * @private - */ - isFastBoot = isFastBoot(this); - /** * The action to be sent when the modal footer's submit button (if present) is pressed. * Note that if your modal body contains a form (e.g. [Components.Form](Components.Form.html)) this action will diff --git a/addon/helpers/bs-on-document.js b/addon/helpers/bs-on-document.js new file mode 100644 index 000000000..655302a81 --- /dev/null +++ b/addon/helpers/bs-on-document.js @@ -0,0 +1,14 @@ +import GlobalEventListenerHelper from 'ember-bootstrap/utils/global-event-listener-helper'; + +/** + * Adds an event listener to `document` for as long as the helper is rendered: + * `{{bs-on-document "click" this.handler capture=true}}` + * + * @class BsOnDocumentHelper + * @private + */ +export default class BsOnDocumentHelper extends GlobalEventListenerHelper { + get eventTarget() { + return typeof document !== 'undefined' ? document : undefined; + } +} diff --git a/addon/helpers/bs-on-window.js b/addon/helpers/bs-on-window.js new file mode 100644 index 000000000..c18ba5c46 --- /dev/null +++ b/addon/helpers/bs-on-window.js @@ -0,0 +1,14 @@ +import GlobalEventListenerHelper from 'ember-bootstrap/utils/global-event-listener-helper'; + +/** + * Adds an event listener to `window` for as long as the helper is rendered: + * `{{bs-on-window "resize" this.handler}}` + * + * @class BsOnWindowHelper + * @private + */ +export default class BsOnWindowHelper extends GlobalEventListenerHelper { + get eventTarget() { + return typeof window !== 'undefined' ? window : undefined; + } +} diff --git a/addon/utils/global-event-listener-helper.js b/addon/utils/global-event-listener-helper.js new file mode 100644 index 000000000..c935a98dc --- /dev/null +++ b/addon/utils/global-event-listener-helper.js @@ -0,0 +1,53 @@ +import Helper from '@ember/component/helper'; +import { assert } from '@ember/debug'; + +/** + * Base class for the `bs-on-document` and `bs-on-window` helpers. It adds an event listener to the global event + * target returned by `eventTarget`, replaces it when the arguments change, and removes it when the helper is + * destroyed. Named arguments (e.g. `capture=true`) are passed as listener options. + * + * When the global is missing or is not an event target (FastBoot defines a `window` without `addEventListener`), it + * does nothing. + * + * @class GlobalEventListenerHelper + * @private + */ +export default class GlobalEventListenerHelper extends Helper { + /** + * @property eventTarget + * @type EventTarget|undefined + * @private + */ + get eventTarget() { + return undefined; + } + + listener = null; + + compute([eventName, callback], options) { + assert(`The event name passed to ${this.constructor.name} must be a string`, typeof eventName === 'string'); + assert(`The callback passed to ${this.constructor.name} must be a function`, typeof callback === 'function'); + + this.removeListener(); + + let target = this.eventTarget; + if (typeof target?.addEventListener === 'function') { + options = { ...options }; + target.addEventListener(eventName, callback, options); + this.listener = { target, eventName, callback, options }; + } + } + + removeListener() { + if (this.listener) { + let { target, eventName, callback, options } = this.listener; + target.removeEventListener(eventName, callback, options); + this.listener = null; + } + } + + willDestroy() { + super.willDestroy(...arguments); + this.removeListener(); + } +} diff --git a/app/helpers/bs-on-document.js b/app/helpers/bs-on-document.js new file mode 100644 index 000000000..bba1244cc --- /dev/null +++ b/app/helpers/bs-on-document.js @@ -0,0 +1 @@ +export { default } from 'ember-bootstrap/helpers/bs-on-document'; diff --git a/app/helpers/bs-on-window.js b/app/helpers/bs-on-window.js new file mode 100644 index 000000000..951dc86c1 --- /dev/null +++ b/app/helpers/bs-on-window.js @@ -0,0 +1 @@ +export { default } from 'ember-bootstrap/helpers/bs-on-window'; diff --git a/package.json b/package.json index d5bec29d8..e7449aa1c 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,6 @@ "ember-decorators": "^6.1.0", "ember-element-helper": "^0.8.7", "ember-focus-trap": "^1.0.0", - "ember-on-helper": "^0.1.0", "ember-popper-modifier": "^3.0.0", "ember-ref-bucket": "^4.1.0", "ember-render-helpers": "0.2.0", diff --git a/tests/integration/components/bs-dropdown-test.js b/tests/integration/components/bs-dropdown-test.js index 2ba59e521..0fe839bcd 100644 --- a/tests/integration/components/bs-dropdown-test.js +++ b/tests/integration/components/bs-dropdown-test.js @@ -260,6 +260,24 @@ module('Integration | Component | bs-dropdown', function (hooks) { assert.ok(action.calledOnce); }); + test('closed dropdown does not listen to document events anymore', async function (assert) { + let action = sinon.spy(); + this.actions.hide = action; + await render( + hbs`Dropdown
  • Something
  • ` + ); + + await click('a.dropdown-toggle'); + await click('*'); + assert.dom('.dropdown-menu').doesNotExist('Dropdown is closed'); + assert.ok(action.calledOnce, 'onHide was called on close'); + + await click('*'); + await triggerKeyEvent(document, 'keydown', 27); // escape + await triggerKeyEvent(document, 'keyup', 9); // tab + assert.ok(action.calledOnce, 'onHide was not called again'); + }); + test('dropdown yields the openDropdown action', async function (assert) { let action = sinon.spy(); this.actions.show = action; diff --git a/tests/integration/components/bs-modal-test.js b/tests/integration/components/bs-modal-test.js index bcb6bf23e..b46333be7 100644 --- a/tests/integration/components/bs-modal-test.js +++ b/tests/integration/components/bs-modal-test.js @@ -18,6 +18,10 @@ module('Integration | Component | bs-modal', function (hooks) { this.send = (actionName, ...args) => this.actions[actionName].apply(this, args); }); + hooks.afterEach(function () { + sinon.restore(); + }); + test('Modal yields header, footer and body components', async function (assert) { await render(hbs` @@ -271,6 +275,26 @@ module('Integration | Component | bs-modal', function (hooks) { assert.dom('.modal-backdrop').hasClass(visibilityClass(), 'Modal backdrop has visibility class'); }); + test('it adjusts the dialog padding when the window is resized', async function (assert) { + // make the body not overflow, so an overflowing modal gets a left padding + sinon.stub(document.body, 'clientWidth').get(() => window.innerWidth); + this.set('tall', false); + await render(hbs` + + {{#if this.tall}}{{/if}} + + `); + + assert.strictEqual(find('.modal').style.paddingLeft, '', 'no padding while the modal fits'); + + this.set('tall', true); + await settled(); + assert.strictEqual(find('.modal').style.paddingLeft, '', 'padding is not adjusted before a resize'); + + await triggerEvent(window, 'resize'); + assert.notStrictEqual(find('.modal').style.paddingLeft, '', 'padding is adjusted on resize'); + }); + test('it can reopen after closing by clicking the backdrop', async function (assert) { this.set('open', false); this.set('close', () => this.set('open', false)); diff --git a/tests/integration/helpers/bs-on-document-test.js b/tests/integration/helpers/bs-on-document-test.js new file mode 100644 index 000000000..6ac00fa08 --- /dev/null +++ b/tests/integration/helpers/bs-on-document-test.js @@ -0,0 +1,81 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { click, render, settled } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import setupNoDeprecations from '../../helpers/setup-no-deprecations'; +import sinon from 'sinon'; + +module('Integration | Helper | bs-on-document', function (hooks) { + setupRenderingTest(hooks); + setupNoDeprecations(hooks); + + test('it calls the callback for events on document', async function (assert) { + this.handler = sinon.spy(); + await render(hbs`{{bs-on-document "click" this.handler}}`); + + document.dispatchEvent(new Event('click')); + assert.ok(this.handler.calledOnce, 'callback was called'); + assert.strictEqual(this.handler.firstCall.args[0].type, 'click', 'callback receives the event'); + }); + + test('it removes the listener when it is destroyed', async function (assert) { + this.handler = sinon.spy(); + this.set('show', true); + await render(hbs`{{#if this.show}}{{bs-on-document "click" this.handler}}{{/if}}`); + + this.set('show', false); + await settled(); + + document.dispatchEvent(new Event('click')); + assert.ok(this.handler.notCalled, 'callback was not called after teardown'); + }); + + test('it replaces the listener when the event name changes', async function (assert) { + this.handler = sinon.spy(); + this.set('eventName', 'click'); + await render(hbs`{{bs-on-document this.eventName this.handler}}`); + + this.set('eventName', 'keydown'); + await settled(); + + document.dispatchEvent(new Event('click')); + assert.ok(this.handler.notCalled, 'old event is not listened to anymore'); + + document.dispatchEvent(new Event('keydown')); + assert.ok(this.handler.calledOnce, 'new event is listened to'); + }); + + test('it replaces the listener when the callback changes', async function (assert) { + let first = sinon.spy(); + let second = sinon.spy(); + this.set('handler', first); + await render(hbs`{{bs-on-document "click" this.handler}}`); + + this.set('handler', second); + await settled(); + + document.dispatchEvent(new Event('click')); + assert.ok(first.notCalled, 'old callback is not called anymore'); + assert.ok(second.calledOnce, 'new callback is called'); + }); + + test('it supports capture=true and removes the capturing listener', async function (assert) { + this.handler = sinon.spy(); + this.stopPropagation = (e) => e.stopPropagation(); + this.set('show', true); + await render(hbs` + + {{#if this.show}}{{bs-on-document "click" this.handler capture=true}}{{/if}} + `); + + await click('button'); + assert.ok(this.handler.calledOnce, 'capturing listener is called although the event does not bubble up'); + + this.set('show', false); + await settled(); + + await click('button'); + document.dispatchEvent(new Event('click')); + assert.ok(this.handler.calledOnce, 'capturing listener was removed'); + }); +}); diff --git a/tests/integration/helpers/bs-on-window-test.js b/tests/integration/helpers/bs-on-window-test.js new file mode 100644 index 000000000..d1f403309 --- /dev/null +++ b/tests/integration/helpers/bs-on-window-test.js @@ -0,0 +1,32 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, settled } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import setupNoDeprecations from '../../helpers/setup-no-deprecations'; +import sinon from 'sinon'; + +module('Integration | Helper | bs-on-window', function (hooks) { + setupRenderingTest(hooks); + setupNoDeprecations(hooks); + + test('it calls the callback for events on window', async function (assert) { + this.handler = sinon.spy(); + await render(hbs`{{bs-on-window "resize" this.handler}}`); + + window.dispatchEvent(new Event('resize')); + assert.ok(this.handler.calledOnce, 'callback was called'); + assert.strictEqual(this.handler.firstCall.args[0].type, 'resize', 'callback receives the event'); + }); + + test('it removes the listener when it is destroyed', async function (assert) { + this.handler = sinon.spy(); + this.set('show', true); + await render(hbs`{{#if this.show}}{{bs-on-window "resize" this.handler}}{{/if}}`); + + this.set('show', false); + await settled(); + + window.dispatchEvent(new Event('resize')); + assert.ok(this.handler.notCalled, 'callback was not called after teardown'); + }); +}); diff --git a/yarn.lock b/yarn.lock index 7b0f554d4..e41887645 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5101,13 +5101,6 @@ ember-modifier@^3.0.0, "ember-modifier@^3.2.0 || ^4.0.0", ember-modifier@^3.2.7: ember-cli-typescript "^5.0.0" ember-compatibility-helpers "^1.2.5" -ember-on-helper@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/ember-on-helper/-/ember-on-helper-0.1.0.tgz#c8b1fef9173fc8546c4933b57ecd7ffbcebad99e" - integrity sha512-jjafBnWfoA4VSSje476ft5G+urlvvuSDddwAJjKDCjKY9mbe3hAEsJiMBAaPObJRMm1FOglCuKjQZfwDDls6MQ== - dependencies: - ember-cli-babel "^7.7.3" - ember-page-title@^8.0.0: version "8.2.4" resolved "https://registry.yarnpkg.com/ember-page-title/-/ember-page-title-8.2.4.tgz#f046c5d276e55e83e756cc948fede5998209f50d" From 535fbf5f0a9205657306ea00b26f1c80bd54e3ef Mon Sep 17 00:00:00 2001 From: Gaurav Munjal Date: Fri, 30 Oct 2026 12:50:14 -0400 Subject: [PATCH 2/2] SCF-2416: replace ember-ref-bucket with per-component modifiers ember-ref-bucket 4.1.0 depends on ember-cli-babel 7. This ports upstream ember-bootstrap #2270 ("replace ember-ref-bucket with custom modifiers", by Sander Knauff, not yet released upstream) by hand, since upstream is now TypeScript in a monorepo. Each component that used @ref now has a plain field and a function-based modifier that sets it, built by the new addon/utils/track-element.js helper, e.g. trackMainNode = trackElement(this, '_element'); and its templates use {{this.trackMainNode}} instead of {{create-ref "mainNode"}} (19 sites). The names match upstream: trackMainNode, trackMenuElement, trackModalElement, trackBackdropElement, trackOverlayElement and trackPopperElement. Differences from upstream: - The modifiers come from one shared helper rather than being written out in each component. - trackElement passes { eager: false } to modifier(). ember-modifier 3.2.7 (the version partners uses) deprecates function-based modifiers without it, and setupNoDeprecations turns that into a test failure. 4.x ignores the option. - BsModal's modalElement is @tracked. It is a dependency of the @computed scrollbarWidth, so a plain assignment on reopen would trip Ember's DEBUG mandatory-setter assertion. ref-bucket's untracked getter meant scrollbarWidth was never invalidated; now it is re-measured against the new element. - bs-form has no create-ref in this fork, so there is no trackFormElement. popperElement stays tracked (was @trackedRef), as upstream. The other fields were untracked getters before and are plain fields now; the classic components only read them from JS, so no set() is needed. The refs on child components (BsModal's dialog, the contextual-help overlay) still work because the modifier reaches the child's element through ...attributes. ember-ref-bucket's {{create-ref}} modifier leaked into apps as an implicit app/ export; it is gone now (release notes). New tests: - BsModal: after closing and reopening, the padding is measured against the new .modal element, on show and on resize. - BsPopover and BsTooltip, rendered in place with click triggers: clicking their own content doesn't close them (overlayElement via ...attributes), clicking the trigger does. All three fail with modalElement untracked or overlayElement unset. Co-Authored-By: Claude Opus 5.5 --- addon/components/bs-carousel/slide.hbs | 2 +- addon/components/bs-carousel/slide.js | 6 ++-- addon/components/bs-collapse.hbs | 2 +- addon/components/bs-collapse.js | 6 ++-- addon/components/bs-contextual-help.js | 6 ++-- .../components/bs-contextual-help/element.js | 6 ++-- addon/components/bs-dropdown/menu.hbs | 8 +++--- addon/components/bs-dropdown/menu.js | 6 ++-- addon/components/bs-form/element.hbs | 2 +- addon/components/bs-form/element.js | 6 ++-- addon/components/bs-modal.hbs | 8 +++--- addon/components/bs-modal.js | 10 +++++-- addon/components/bs-modal/dialog.hbs | 2 +- addon/components/bs-modal/dialog.js | 6 ++-- addon/components/bs-popover.hbs | 2 +- addon/components/bs-popover/element.hbs | 4 +-- addon/components/bs-tab/pane.hbs | 2 +- addon/components/bs-tab/pane.js | 8 ++++-- addon/components/bs-tooltip.hbs | 2 +- addon/components/bs-tooltip/element.hbs | 4 +-- addon/utils/track-element.js | 16 +++++++++++ package.json | 1 - tests/integration/components/bs-modal-test.js | 28 +++++++++++++++++++ .../integration/components/bs-popover-test.js | 17 +++++++++++ .../integration/components/bs-tooltip-test.js | 17 +++++++++++ yarn.lock | 11 +------- 26 files changed, 138 insertions(+), 50 deletions(-) create mode 100644 addon/utils/track-element.js diff --git a/addon/components/bs-carousel/slide.hbs b/addon/components/bs-carousel/slide.hbs index ff7681870..762178d94 100644 --- a/addon/components/bs-carousel/slide.hbs +++ b/addon/components/bs-carousel/slide.hbs @@ -1,7 +1,7 @@
    {{yield}}
    \ No newline at end of file diff --git a/addon/components/bs-carousel/slide.js b/addon/components/bs-carousel/slide.js index f35a4bf4f..39181e279 100644 --- a/addon/components/bs-carousel/slide.js +++ b/addon/components/bs-carousel/slide.js @@ -6,7 +6,7 @@ import { next } from '@ember/runloop'; import overrideableCP from 'ember-bootstrap/utils/cp/overrideable'; import { addObserver } from '@ember/object/observers'; import deprecateSubclassing from 'ember-bootstrap/utils/deprecate-subclassing'; -import { ref } from 'ember-ref-bucket'; +import trackElement from 'ember-bootstrap/utils/track-element'; /** A visible user-defined slide. @@ -26,7 +26,9 @@ export default class CarouselSlide extends Component.extend(ComponentChild) { * @type null | HTMLElement * @private */ - @ref('mainNode') _element = null; + _element = null; + + trackMainNode = trackElement(this, '_element'); /** * Defines slide visibility. diff --git a/addon/components/bs-collapse.hbs b/addon/components/bs-collapse.hbs index b7e851384..ee5168a9c 100644 --- a/addon/components/bs-collapse.hbs +++ b/addon/components/bs-collapse.hbs @@ -1,7 +1,7 @@
    {{yield (hash @@ -28,7 +28,7 @@ {{popper-tooltip this.toggleElement this.popperOptions}} {{did-insert @registerChildElement "menu"}} {{will-destroy @unregisterChildElement "menu"}} - {{create-ref "menuElement"}} + {{this.trackMenuElement}} > {{yield (hash @@ -52,7 +52,7 @@ {{popper-tooltip this.toggleElement this.popperOptions}} {{did-insert @registerChildElement "menu"}} {{will-destroy @unregisterChildElement "menu"}} - {{create-ref "menuElement"}} + {{this.trackMenuElement}} > {{yield (hash @@ -72,7 +72,7 @@ {{popper-tooltip this.toggleElement this.popperOptions}} {{did-insert @registerChildElement "menu"}} {{will-destroy @unregisterChildElement "menu"}} - {{create-ref "menuElement"}} + {{this.trackMenuElement}} > {{yield (hash diff --git a/addon/components/bs-dropdown/menu.js b/addon/components/bs-dropdown/menu.js index a9269640c..f3426fa81 100644 --- a/addon/components/bs-dropdown/menu.js +++ b/addon/components/bs-dropdown/menu.js @@ -5,7 +5,7 @@ import { next } from '@ember/runloop'; import { getDestinationElement } from 'ember-bootstrap/utils/dom'; import defaultValue from 'ember-bootstrap/utils/default-decorator'; import deprecateSubclassing from 'ember-bootstrap/utils/deprecate-subclassing'; -import { ref } from 'ember-ref-bucket'; +import trackElement from 'ember-bootstrap/utils/track-element'; /** Component for the dropdown menu. @@ -25,7 +25,9 @@ export default class DropdownMenu extends Component { * @type null | HTMLElement * @private */ - @ref('menuElement') menuElement = null; + menuElement = null; + + trackMenuElement = trackElement(this, 'menuElement'); /** * @property ariaRole diff --git a/addon/components/bs-form/element.hbs b/addon/components/bs-form/element.hbs index 43b8286d2..724b9a511 100644 --- a/addon/components/bs-form/element.hbs +++ b/addon/components/bs-form/element.hbs @@ -2,7 +2,7 @@
    ` tag. diff --git a/addon/components/bs-modal.hbs b/addon/components/bs-modal.hbs index 0a3db7a9a..8959a3365 100644 --- a/addon/components/bs-modal.hbs +++ b/addon/components/bs-modal.hbs @@ -20,7 +20,7 @@ {{#if this._renderInPlace}} {{yield @@ -35,14 +35,14 @@
    {{#if this.shouldShowBackdrop}} - + {{/if}}
    {{else}} {{#in-element this.destinationElement insertBefore=null}} {{yield @@ -57,7 +57,7 @@
    {{#if this.shouldShowBackdrop}} - + {{/if}}
    {{/in-element}} diff --git a/addon/components/bs-modal.js b/addon/components/bs-modal.js index 0ee04e7fd..7d8b8fb2a 100644 --- a/addon/components/bs-modal.js +++ b/addon/components/bs-modal.js @@ -10,7 +10,7 @@ import isFastBoot from 'ember-bootstrap/utils/is-fastboot'; import deprecateSubclassing from 'ember-bootstrap/utils/deprecate-subclassing'; import arg from '../utils/decorators/arg'; import { tracked } from '@glimmer/tracking'; -import { ref } from 'ember-ref-bucket'; +import trackElement from 'ember-bootstrap/utils/track-element'; function nextRunloop() { return new Promise((resolve) => next(resolve)); @@ -301,7 +301,9 @@ export default class Modal extends Component { * @readonly * @private */ - @ref('modalElement') modalElement; + @tracked modalElement; + + trackModalElement = trackElement(this, 'modalElement'); /** * The DOM element of the backdrop element. @@ -311,7 +313,9 @@ export default class Modal extends Component { * @readonly * @private */ - @ref('backdropElement') backdropElement; + backdropElement; + + trackBackdropElement = trackElement(this, 'backdropElement'); /** * The action to be sent when the modal footer's submit button (if present) is pressed. diff --git a/addon/components/bs-modal/dialog.hbs b/addon/components/bs-modal/dialog.hbs index 1aaf4aab8..45262831c 100644 --- a/addon/components/bs-modal/dialog.hbs +++ b/addon/components/bs-modal/dialog.hbs @@ -9,7 +9,7 @@ {{on "mouseup" this.handleMouseUp}} {{on "click" this.handleClick}} {{style paddingLeft=(concat @paddingLeft "px") paddingRight=(concat @paddingRight "px") display=(if @inDom "block" "")}} - {{create-ref "mainNode"}} + {{this.trackMainNode}} {{did-insert this.getOrSetTitleId}} {{did-insert this.setInitialFocus}} > diff --git a/addon/components/bs-modal/dialog.js b/addon/components/bs-modal/dialog.js index 750d6e82f..049e3615c 100644 --- a/addon/components/bs-modal/dialog.js +++ b/addon/components/bs-modal/dialog.js @@ -3,7 +3,7 @@ import { isBlank } from '@ember/utils'; import Component from '@glimmer/component'; import { next } from '@ember/runloop'; import deprecateSubclassing from 'ember-bootstrap/utils/deprecate-subclassing'; -import { ref } from 'ember-ref-bucket'; +import trackElement from 'ember-bootstrap/utils/track-element'; import { tracked } from '@glimmer/tracking'; /** @@ -20,7 +20,9 @@ export default class ModalDialog extends Component { * @property id * @type null | HTMLElement */ - @ref('mainNode') _element = null; + _element = null; + + trackMainNode = trackElement(this, '_element'); /** * Name of the size class diff --git a/addon/components/bs-popover.hbs b/addon/components/bs-popover.hbs index c0089a325..668ac4523 100644 --- a/addon/components/bs-popover.hbs +++ b/addon/components/bs-popover.hbs @@ -15,7 +15,7 @@ @viewportPadding={{this.viewportPadding}} @class={{@class}} - {{create-ref "overlayElement"}} + {{this.trackOverlayElement}} ...attributes > {{yield diff --git a/addon/components/bs-popover/element.hbs b/addon/components/bs-popover/element.hbs index b182114da..173d4e288 100644 --- a/addon/components/bs-popover/element.hbs +++ b/addon/components/bs-popover/element.hbs @@ -9,7 +9,7 @@ role="tooltip" ...attributes {{popper-tooltip @popperTarget this.popperOptions}} - {{create-ref "popperElement"}} + {{this.trackPopperElement}} >
    {{#if @title}} @@ -29,7 +29,7 @@ role="tooltip" ...attributes {{popper-tooltip @popperTarget this.popperOptions}} - {{create-ref "popperElement"}} + {{this.trackPopperElement}} >
    {{#if @title}} diff --git a/addon/components/bs-tab/pane.hbs b/addon/components/bs-tab/pane.hbs index cfb7346b7..1ae9d8b3b 100644 --- a/addon/components/bs-tab/pane.hbs +++ b/addon/components/bs-tab/pane.hbs @@ -2,7 +2,7 @@ class="tab-pane {{if (macroCondition (macroGetOwnConfig "isNotBS3")) (if this.showContent "show")}} {{if (macroCondition (macroGetOwnConfig "isBS3")) (if this.showContent "in")}} {{if this.active "active"}} {{if this.usesTransition "fade"}}" role="tabpanel" ...attributes - {{create-ref "mainNode"}} + {{this.trackMainNode}} > {{yield}}
    \ No newline at end of file diff --git a/addon/components/bs-tab/pane.js b/addon/components/bs-tab/pane.js index bc3c02978..42cb4ea5b 100644 --- a/addon/components/bs-tab/pane.js +++ b/addon/components/bs-tab/pane.js @@ -9,7 +9,7 @@ import usesTransition from 'ember-bootstrap/utils/cp/uses-transition'; import defaultValue from 'ember-bootstrap/utils/default-decorator'; import { guidFor } from '@ember/object/internals'; import deprecateSubclassing from 'ember-bootstrap/utils/deprecate-subclassing'; -import { ref } from 'ember-ref-bucket'; +import trackElement from 'ember-bootstrap/utils/track-element'; /** The tab pane of a tab component. @@ -28,7 +28,9 @@ export default class TabPane extends Component.extend(ComponentChild) { * @property id * @type null | HTMLElement */ - @ref('mainNode') _element = null; + _element = null; + + trackMainNode = trackElement(this, '_element'); /** * @property id * @type string @@ -145,7 +147,7 @@ export default class TabPane extends Component.extend(ComponentChild) { show() { if (this.usesTransition) { if (!this._element) { - // _element is initially set by `{{create-ref}}` which happens in next run loop, so can be undefined here. + // _element is initially set by `{{this.trackMainNode}}`, which runs after render, so can be undefined here. this.setProperties({ active: true, showContent: true, diff --git a/addon/components/bs-tooltip.hbs b/addon/components/bs-tooltip.hbs index 98a619517..572546001 100644 --- a/addon/components/bs-tooltip.hbs +++ b/addon/components/bs-tooltip.hbs @@ -14,7 +14,7 @@ @viewportPadding={{this.viewportPadding}} @class={{@class}} - {{create-ref "overlayElement"}} + {{this.trackOverlayElement}} ...attributes > {{#if (has-block)}} diff --git a/addon/components/bs-tooltip/element.hbs b/addon/components/bs-tooltip/element.hbs index 7a7a664b7..3e222563a 100644 --- a/addon/components/bs-tooltip/element.hbs +++ b/addon/components/bs-tooltip/element.hbs @@ -9,7 +9,7 @@ role="tooltip" ...attributes {{popper-tooltip @popperTarget this.popperOptions}} - {{create-ref "popperElement"}} + {{this.trackPopperElement}} >
    @@ -28,7 +28,7 @@ role="tooltip" ...attributes {{popper-tooltip @popperTarget this.popperOptions}} - {{create-ref "popperElement"}} + {{this.trackPopperElement}} >
    diff --git a/addon/utils/track-element.js b/addon/utils/track-element.js new file mode 100644 index 000000000..e6415359b --- /dev/null +++ b/addon/utils/track-element.js @@ -0,0 +1,16 @@ +import { modifier } from 'ember-modifier'; + +/** + * Returns a modifier that stores the element it is installed on as `context[property]`. + * + * @private + */ +export default function trackElement(context, property) { + return modifier( + (element) => { + context[property] = element; + }, + // ember-modifier 3 deprecates function-based modifiers without this, 4 ignores it + { eager: false } + ); +} diff --git a/package.json b/package.json index e7449aa1c..a011dc5eb 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,6 @@ "ember-element-helper": "^0.8.7", "ember-focus-trap": "^1.0.0", "ember-popper-modifier": "^3.0.0", - "ember-ref-bucket": "^4.1.0", "ember-render-helpers": "0.2.0", "ember-style-modifier": "^0.7.0", "findup-sync": "^4.0.0", diff --git a/tests/integration/components/bs-modal-test.js b/tests/integration/components/bs-modal-test.js index b46333be7..76194b3a3 100644 --- a/tests/integration/components/bs-modal-test.js +++ b/tests/integration/components/bs-modal-test.js @@ -295,6 +295,34 @@ module('Integration | Component | bs-modal', function (hooks) { assert.notStrictEqual(find('.modal').style.paddingLeft, '', 'padding is adjusted on resize'); }); + test('it measures against the new modal element after reopening', async function (assert) { + // make the body not overflow, so an overflowing modal gets a left padding + sinon.stub(document.body, 'clientWidth').get(() => window.innerWidth); + this.set('open', true); + await render(hbs` + + + + `); + + let firstModal = find('.modal'); + assert.notStrictEqual(firstModal.style.paddingLeft, '', 'padding is adjusted on the first open'); + + this.set('open', false); + await settled(); + assert.dom('.modal').doesNotExist('Modal is hidden'); + + this.set('open', true); + await settled(); + let secondModal = find('.modal'); + assert.notStrictEqual(secondModal, firstModal, 'the modal element is replaced'); + assert.notStrictEqual(secondModal.style.paddingLeft, '', 'padding is adjusted on reopen'); + + secondModal.style.paddingLeft = ''; + await triggerEvent(window, 'resize'); + assert.notStrictEqual(secondModal.style.paddingLeft, '', 'padding is adjusted on resize after reopen'); + }); + test('it can reopen after closing by clicking the backdrop', async function (assert) { this.set('open', false); this.set('close', () => this.set('open', false)); diff --git a/tests/integration/components/bs-popover-test.js b/tests/integration/components/bs-popover-test.js index 5a749d5d5..81c0946be 100644 --- a/tests/integration/components/bs-popover-test.js +++ b/tests/integration/components/bs-popover-test.js @@ -137,6 +137,23 @@ module('Integration | Component | bs-popover', function (hooks) { assert.dom('.popover').exists('popover is still visible'); }); + test('it stays open when its own content is clicked [renderInPlace=true]', async function (assert) { + await render(hbs` +
    + + Content + +
    + `); + + await click('#target'); + assert.dom('.popover').exists('popover is visible'); + await click('#content'); + assert.dom('.popover').exists('popover is still visible'); + await click('#target'); + assert.dom('.popover').doesNotExist('popover is hidden by clicking the trigger'); + }); + test('it yields close action', async function (assert) { let hideAction = sinon.spy(); this.set('hide', hideAction); diff --git a/tests/integration/components/bs-tooltip-test.js b/tests/integration/components/bs-tooltip-test.js index 3cb0cb2b1..d3926c968 100644 --- a/tests/integration/components/bs-tooltip-test.js +++ b/tests/integration/components/bs-tooltip-test.js @@ -464,6 +464,23 @@ module('Integration | Component | bs-tooltip', function (hooks) { assertPositioning(assert); }); + test('it stays open when its own content is clicked [renderInPlace=true]', async function (assert) { + await render(hbs` +
    + + Content + +
    + `); + + await click('#target'); + assert.dom('.tooltip').exists('tooltip is visible'); + await click('#content'); + assert.dom('.tooltip').exists('tooltip is still visible'); + await click('#target'); + assert.dom('.tooltip').doesNotExist('tooltip is hidden by clicking the trigger'); + }); + test('it yields close action', async function (assert) { let hideAction = sinon.spy(); this.set('hide', hideAction); diff --git a/yarn.lock b/yarn.lock index e41887645..9cd3992d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4705,7 +4705,7 @@ ember-cli-htmlbars@^5.7.1: strip-bom "^4.0.0" walk-sync "^2.2.0" -ember-cli-htmlbars@^6.0.1, ember-cli-htmlbars@^6.1.1, ember-cli-htmlbars@^6.3.0: +ember-cli-htmlbars@^6.1.1, ember-cli-htmlbars@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-6.3.0.tgz#ac85f2bbd09788992ab7f9ca832cd044fb8e5798" integrity sha512-N9Y80oZfcfWLsqickMfRd9YByVcTGyhYRnYQ2XVPVrp6jyUyOeRWmEAPh7ERSXpp8Ws4hr/JB9QVQrn/yZa+Ag== @@ -5135,15 +5135,6 @@ ember-qunit@^6.2.0: silent-error "^1.1.1" validate-peer-dependencies "^2.2.0" -ember-ref-bucket@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ember-ref-bucket/-/ember-ref-bucket-4.1.0.tgz#2a52e72a395a14033d034c834fab648f26d74baa" - integrity sha512-oEUU2mDtuYuMM039U9YEqrrOCVHH6rQfvbFOmh3WxOVEgubmLVyKEpGgU4P/6j0B/JxTqqTwM3ULTQyDto8dKg== - dependencies: - ember-cli-babel "^7.26.11" - ember-cli-htmlbars "^6.0.1" - ember-modifier "^3.2.7" - ember-render-helpers@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/ember-render-helpers/-/ember-render-helpers-0.2.0.tgz#5f7af8ee74ae29f85e0d156b2775edff23f6de21"