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 @@
diff --git a/addon/components/bs-dropdown/menu.hbs b/addon/components/bs-dropdown/menu.hbs index 90f40ecd9..16144c8da 100644 --- a/addon/components/bs-dropdown/menu.hbs +++ b/addon/components/bs-dropdown/menu.hbs @@ -8,7 +8,7 @@ {{popper-tooltip this.toggleElement this.popperOptions}} {{did-insert @registerChildElement "menu"}} {{will-destroy @unregisterChildElement "menu"}} - {{create-ref "menuElement"}} + {{this.trackMenuElement}} > {{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 f1c731eac..8959a3365 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 @@ -27,7 +20,7 @@ {{#if this._renderInPlace}} {{yield @@ -42,14 +35,14 @@
{{#if this.shouldShowBackdrop}} - + {{/if}}
{{else}} {{#in-element this.destinationElement insertBefore=null}} {{yield @@ -64,7 +57,7 @@
{{#if this.shouldShowBackdrop}} - + {{/if}}
{{/in-element}} diff --git a/addon/components/bs-modal.js b/addon/components/bs-modal.js index f73983c38..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,14 +313,9 @@ export default class Modal extends Component { * @readonly * @private */ - @ref('backdropElement') backdropElement; + backdropElement; - /** - * @type boolean - * @readonly - * @private - */ - isFastBoot = isFastBoot(this); + 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/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/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/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..a011dc5eb 100644 --- a/package.json +++ b/package.json @@ -86,9 +86,7 @@ "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", "ember-style-modifier": "^0.7.0", "findup-sync": "^4.0.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..76194b3a3 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,54 @@ 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 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/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..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== @@ -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" @@ -5142,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"