From b4f43a5cb329469a7370a7a20d5b3a8a8f201000 Mon Sep 17 00:00:00 2001 From: Gaurav Munjal Date: Tue, 3 Nov 2026 08:08:10 -0500 Subject: [PATCH 1/6] SCF-2415: run CI on push only for scf_master and tags With push on every branch and pull_request on every base branch, each push to a branch with an open PR ran the full matrix twice. PRs are still covered by the pull_request trigger, which uses '**' so stacked PRs on gaurav/chore/... bases run (b057803b). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 348976a2c..f4a825a48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: branches: - - '*' + - scf_master tags: - '*' pull_request: From 4b70cb2f773e770454ca775f13252b8017e3e85c Mon Sep 17 00:00:00 2001 From: Gaurav Munjal Date: Tue, 3 Nov 2026 08:07:58 -0500 Subject: [PATCH 2/6] SCF-2415: make the modal footer's submit button a submit button via the type attribute Since f005cd0c the default submit button set its type with @buttonType="submit", which only BsButton understands. A custom @buttonComponent that renders - + {{else}} {{/if}} diff --git a/tests/integration/components/bs-modal/footer-test.js b/tests/integration/components/bs-modal/footer-test.js index 4e8e9b74e..b22ce9c72 100644 --- a/tests/integration/components/bs-modal/footer-test.js +++ b/tests/integration/components/bs-modal/footer-test.js @@ -1,3 +1,5 @@ +import { setComponentTemplate } from '@ember/component'; +import templateOnly from '@ember/component/template-only'; import { module } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { click, render } from '@ember/test-helpers'; @@ -64,6 +66,38 @@ module('Integration | Component | bs-modal/footer', function (hooks) { assert.true(defaultPrevented, 'Native form submission is prevented.'); }); + module('native form submission', function (hooks) { + hooks.beforeEach(function () { + this.defaultPrevented = undefined; + this.listener = (event) => { + this.defaultPrevented = event.defaultPrevented; + // never let the test runner page navigate + event.preventDefault(); + }; + window.addEventListener('submit', this.listener); + }); + + hooks.afterEach(function () { + window.removeEventListener('submit', this.listener); + }); + + test('a custom buttonComponent submits the footer', async function (assert) { + let submitCount = 0; + this.set('submit', () => submitCount++); + this.CustomButton = setComponentTemplate( + hbs``, + templateOnly() + ); + await render( + hbs`` + ); + await click('.modal-footer button:last-child'); + + assert.strictEqual(submitCount, 1, 'onSubmit is called once.'); + assert.true(this.defaultPrevented, 'Native form submission is prevented.'); + }); + }); + test('Footer can have a custom submitButtonType', async function (assert) { await render(hbs``); assert.dom('.modal-footer button:last-child').hasClass('btn-danger', 'Submit button is a danger button.'); From 4614a42a8ee1ab5bbcc3b5401ec9a959fb5d5ff2 Mon Sep 17 00:00:00 2001 From: Gaurav Munjal Date: Tue, 3 Nov 2026 08:07:58 -0500 Subject: [PATCH 3/6] SCF-2415: keep the modal footer's native submission unless onSubmit handles it 6c23b4c9 made the footer
always call preventDefault() on submit, to stop the page reload when its submit button calls @onSubmit. That also blocked native submission where it used to happen: a standalone footer without @onSubmit, and a footer given an action attribute (e.g. ). Only prevent the default when @onSubmit is given and the form has no action attribute. That still covers the reload in BsModal, where the footer always gets onSubmit and no action. Co-Authored-By: Claude Opus 5.5 --- addon/components/bs-modal/footer.js | 12 +++++++++--- .../components/bs-modal/footer-test.js | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/addon/components/bs-modal/footer.js b/addon/components/bs-modal/footer.js index 729eb115f..db033fd66 100644 --- a/addon/components/bs-modal/footer.js +++ b/addon/components/bs-modal/footer.js @@ -70,8 +70,14 @@ import { action } from '@ember/object'; export default class ModalFooter extends Component { @action handleSubmit(event) { - // the footer is a , so prevent the browser's native submission (and the page reload that comes with it) - event.preventDefault(); - this.args.onSubmit?.(); + if (!this.args.onSubmit) { + return; + } + // the footer is a , so prevent the browser's native submission (and the page reload that comes with it), + // unless an `action` attribute asks for it + if (!event.target.hasAttribute('action')) { + event.preventDefault(); + } + this.args.onSubmit(); } } diff --git a/tests/integration/components/bs-modal/footer-test.js b/tests/integration/components/bs-modal/footer-test.js index b22ce9c72..af7aceb89 100644 --- a/tests/integration/components/bs-modal/footer-test.js +++ b/tests/integration/components/bs-modal/footer-test.js @@ -81,6 +81,25 @@ module('Integration | Component | bs-modal/footer', function (hooks) { window.removeEventListener('submit', this.listener); }); + test('it is not prevented without onSubmit', async function (assert) { + await render(hbs``); + await click('.modal-footer button:last-child'); + + assert.false(this.defaultPrevented, 'Native form submission is not prevented.'); + }); + + test('it is not prevented when the footer has an action attribute', async function (assert) { + let submitCount = 0; + this.set('submit', () => submitCount++); + await render( + hbs`` + ); + await click('.modal-footer button:last-child'); + + assert.strictEqual(submitCount, 1, 'onSubmit is called once.'); + assert.false(this.defaultPrevented, 'Native form submission is not prevented.'); + }); + test('a custom buttonComponent submits the footer', async function (assert) { let submitCount = 0; this.set('submit', () => submitCount++); From 52be6925ad57865705296c34440e7f2a04db2bb4 Mon Sep 17 00:00:00 2001 From: Gaurav Munjal Date: Tue, 3 Nov 2026 08:08:10 -0500 Subject: [PATCH 4/6] SCF-2415: hide BsAlert for @visible={{null}} again 8945d07c (upstream #2027) replaced `@arg visible = true` with `this._visible ?? true`, so @visible={{null}} (e.g. an unset model attribute) now shows the alert. @arg only falls back to its default for undefined, so null used to hide it. Fall back for undefined only. Co-Authored-By: Claude Opus 5.5 --- addon/components/bs-alert.js | 2 +- tests/integration/components/bs-alert-test.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/addon/components/bs-alert.js b/addon/components/bs-alert.js index e483c607e..411d5f521 100644 --- a/addon/components/bs-alert.js +++ b/addon/components/bs-alert.js @@ -86,7 +86,7 @@ export default class Alert extends Component { * @public */ get visible() { - return this._visible ?? true; + return this._visible !== undefined ? this._visible : true; } /** diff --git a/tests/integration/components/bs-alert-test.js b/tests/integration/components/bs-alert-test.js index f250917a1..d9de0242b 100644 --- a/tests/integration/components/bs-alert-test.js +++ b/tests/integration/components/bs-alert-test.js @@ -141,6 +141,22 @@ module('Integration | Component | bs-alert', function (hooks) { assert.dom('*').hasText('', 'alert has no content'); }); + test('alert is initially hidden when visible=null', async function (assert) { + await render(hbs`Test`); + + assert.dom('.alert').doesNotExist('alert has no alert class'); + assert.dom('*').hasText('', 'alert has no content'); + }); + + test('alert can be hidden by setting visible=null', async function (assert) { + this.set('visible', true); + await render(hbs`Test`); + this.set('visible', null); + await settled(); + + assert.dom('.alert').doesNotExist('alert has no alert class'); + }); + test('alert can be made visible when setting visible=true', async function (assert) { this.set('visible', false); await render(hbs`Test`); From ad6dd684bc38abcaf1f7612ba16677dfbbc70409 Mon Sep 17 00:00:00 2001 From: Gaurav Munjal Date: Tue, 3 Nov 2026 08:08:10 -0500 Subject: [PATCH 5/6] SCF-2415: follow BsAlert's @visible in the same render 8945d07c copied @visible into the tracked _visible from a {{did-update this.updateVisibility @visible}} modifier. Writing it during modifier commit schedules a second render, and only that render runs {{did-update this.showOrHide this._visible}}, so each @visible change took an extra render pass before the fade started. Resolve visibility in the visible getter instead. A dismissal records how often @visible had changed at the time (_dismissedAt), and holds until @visible changes again, which the getter counts in untracked fields. That keeps #2027's behavior (dismiss doesn't touch @visible, and setting @visible to false and back to true shows the alert again) without writing tracked state while rendering, which is what broke @localCopy on Ember canary. showOrHide's did-update now reads @visible itself, so without a fade onDismissed is called from the afterRender queue instead of from the modifier: a handler that sets @visible (as in #2027's test) would otherwise update a value that render has already used. With a fade it was already called later, from a timer. Co-Authored-By: Claude Opus 5.5 --- addon/components/bs-alert.hbs | 3 +-- addon/components/bs-alert.js | 40 +++++++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/addon/components/bs-alert.hbs b/addon/components/bs-alert.hbs index e192d07fb..ed8b27fbb 100644 --- a/addon/components/bs-alert.hbs +++ b/addon/components/bs-alert.hbs @@ -1,8 +1,7 @@
{{#unless this.hidden}} {{#if this.dismissible}} diff --git a/addon/components/bs-alert.js b/addon/components/bs-alert.js index 411d5f521..bbad72857 100644 --- a/addon/components/bs-alert.js +++ b/addon/components/bs-alert.js @@ -1,7 +1,7 @@ import { action } from '@ember/object'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; -import { later } from '@ember/runloop'; +import { later, schedule } from '@ember/runloop'; import usesTransition from 'ember-bootstrap/utils/decorators/uses-transition'; import deprecateSubclassing from 'ember-bootstrap/utils/deprecate-subclassing'; import arg from 'ember-bootstrap/utils/decorators/arg'; @@ -86,15 +86,33 @@ export default class Alert extends Component { * @public */ get visible() { - return this._visible !== undefined ? this._visible : true; + let visible = this.args.visible; + if (visible !== this._visibleArg) { + // untracked bookkeeping, so the alert can be shown again after being dismissed once `@visible` changes + // eslint-disable-next-line ember/no-side-effects + this._visibleArg = visible; + this._visibleArgChanges++; + } + if (this._dismissedAt === this._visibleArgChanges) { + return false; + } + return visible !== undefined ? visible : true; } /** - * @property _visible + * `@visible` as last seen by `visible`, and how often it has changed. These are untracked, as `visible` updates them + * while rendering. + * @private + */ + _visibleArg = this.args.visible; + _visibleArgChanges = 0; + + /** + * The value of `_visibleArgChanges` when the alert was dismissed, so the alert stays hidden until `@visible` changes. * @private */ @tracked - _visible = this.args.visible; + _dismissedAt = -1; /** * Set to false to disable the fade out animation when hiding the alert. @@ -163,7 +181,7 @@ export default class Alert extends Component { @action dismiss() { if (this.args.onDismiss?.() !== false) { - this._visible = false; + this._dismissedAt = this._visibleArgChanges; } } @@ -202,7 +220,12 @@ export default class Alert extends Component { ); } else { this.hidden = true; - this.args.onDismissed?.(); + // hide() runs while rendering, and onDismissed may set @visible, which that render has already read + schedule('afterRender', this, function () { + if (!this.isDestroyed) { + this.args.onDismissed?.(); + } + }); } } @@ -214,9 +237,4 @@ export default class Alert extends Component { this.hide(); } } - - @action - updateVisibility() { - this._visible = this.args.visible; - } } From ee88a0fc06e331d2428fb742720921bae8fad4ba Mon Sep 17 00:00:00 2001 From: Gaurav Munjal Date: Tue, 3 Nov 2026 08:08:10 -0500 Subject: [PATCH 6/6] SCF-2415: skip the "does not catch errors thrown by onSubmit" test only under Embroider 7b53aa7a skipped it everywhere for an Embroider-only failure (kaliber5/ember-bootstrap#1682), so classic builds stopped checking that BsForm doesn't swallow errors thrown by @onSubmit. Skip it only when @embroider/core is installed, the check @embroider/test-setup's maybeEmbroider uses to build with Embroider. Co-Authored-By: Claude Opus 5.5 --- tests/integration/components/bs-form-test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/integration/components/bs-form-test.js b/tests/integration/components/bs-form-test.js index 0e06ce531..dc912aad5 100644 --- a/tests/integration/components/bs-form-test.js +++ b/tests/integration/components/bs-form-test.js @@ -33,6 +33,10 @@ import setupNoDeprecations from '../../helpers/setup-no-deprecations'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import sinon from 'sinon'; import Ember from 'ember'; +import { dependencySatisfies, macroCondition } from '@embroider/macros'; + +// see https://github.com/kaliber5/ember-bootstrap/issues/1682 +const testUnlessEmbroider = macroCondition(dependencySatisfies('@embroider/core', '*')) ? skip : test; const nextRunloop = function () { return new Promise((resolve) => { @@ -241,7 +245,7 @@ module('Integration | Component | bs-form', function (hooks) { assert.dom(`.${formFeedbackClass()}`).hasText('There is an error'); }); - skip('it does not catch errors thrown by onSubmit action', async function (assert) { + testUnlessEmbroider('it does not catch errors thrown by onSubmit action', async function (assert) { let onErrorStub = sinon.stub(); let expectedError = new Error();