From d7305b365101555452a719cafaa5591d8691da41 Mon Sep 17 00:00:00 2001 From: Markus Sanin Date: Sun, 1 Feb 2026 17:07:09 +0100 Subject: [PATCH 1/5] Refactor disabled getter (remove ember/runloop & fix no-side-effects) --- src/components/basic-dropdown-content.gts | 3 +- src/components/basic-dropdown.gts | 48 +++++++++---------- src/modifiers/basic-dropdown-trigger.ts | 13 +++++ src/types.ts | 1 + tests/integration/components/trigger-test.gts | 1 + 5 files changed, 40 insertions(+), 26 deletions(-) diff --git a/src/components/basic-dropdown-content.gts b/src/components/basic-dropdown-content.gts index aa4269cc..2729bae1 100644 --- a/src/components/basic-dropdown-content.gts +++ b/src/components/basic-dropdown-content.gts @@ -175,7 +175,8 @@ export default class BasicDropdownContent< if (!triggerElement) { triggerElement = document.querySelector(selector) as HTMLElement; } - this.handleRootMouseDown = (e: MouseEvent | TouchEvent) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.handleRootMouseDown = (e: MouseEvent | TouchEvent): any => { const target = (e.composedPath?.()[0] || e.target) as Element; if (target === null) return; if ( diff --git a/src/components/basic-dropdown.gts b/src/components/basic-dropdown.gts index 35e7f917..cfc6915e 100644 --- a/src/components/basic-dropdown.gts +++ b/src/components/basic-dropdown.gts @@ -3,7 +3,6 @@ import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { guidFor } from '@ember/object/internals'; import calculatePosition from '../utils/calculate-position.ts'; -import { schedule } from '@ember/runloop'; import { hash } from '@ember/helper'; import BasicDropdownTrigger from './basic-dropdown-trigger.gts'; import BasicDropdownContent from './basic-dropdown-content.gts'; @@ -27,7 +26,6 @@ import type { TRootEventType, } from '../types.ts'; -const UNINITIALIZED = {}; const IGNORED_STYLES = ['top', 'left', 'right', 'width', 'height']; export interface BasicDropdownDefaultBlock< @@ -122,9 +120,10 @@ export default class BasicDropdown< @tracked width: string | undefined; @tracked height: string | undefined; @tracked otherStyles: Record = {}; - @tracked isOpen = this.args.initiallyOpened || false; @tracked renderInPlace = this.args.renderInPlace !== undefined ? this.args.renderInPlace : false; + @tracked private _isOpen = this.args.initiallyOpened || false; + private previousVerticalPosition?: VerticalPosition | undefined; private previousHorizontalPosition?: HorizontalPosition | undefined; private triggerElement: HTMLElement | null = null; @@ -132,25 +131,33 @@ export default class BasicDropdown< private _uid = guidFor(this); private _dropdownId: string = `ember-basic-dropdown-content-${this._uid}`; - private _previousDisabled = UNINITIALIZED; private _actions: DropdownActions = { open: this.open.bind(this), close: this.close.bind(this), toggle: this.toggle.bind(this), reposition: this.reposition.bind(this), + updatePublicApi: this.updatePublicApi.bind(this), registerTriggerElement: this.registerTriggerElement.bind(this), registerDropdownElement: this.registerDropdownElement.bind(this), getTriggerElement: () => this.triggerElement, }; - private get horizontalPosition() { + private get horizontalPosition(): HorizontalPosition { return this.args.horizontalPosition || 'auto'; // auto-right | right | center | left } - private get verticalPosition() { + private get verticalPosition(): VerticalPosition { return this.args.verticalPosition || 'auto'; // above | below } + get isOpen(): boolean { + return !this.disabled && this._isOpen + } + + set isOpen(v: boolean) { + this._isOpen = v; + } + get destination(): string { return this.args.destination || this._getDestinationId(); } @@ -179,25 +186,7 @@ export default class BasicDropdown< } get disabled(): boolean { - const newVal = this.args.disabled || false; - if ( - this._previousDisabled !== UNINITIALIZED && - this._previousDisabled !== newVal - ) { - // eslint-disable-next-line ember/no-runloop - schedule('actions', () => { - if (newVal && this.publicAPI.isOpen) { - // eslint-disable-next-line ember/no-side-effects - this.isOpen = false; - } - if (this.args.registerAPI) { - this.args.registerAPI(this.publicAPI); - } - }); - } - // eslint-disable-next-line ember/no-side-effects - this._previousDisabled = newVal; - return newVal; + return this.args.disabled || false; } get publicAPI(): Dropdown { @@ -337,6 +326,15 @@ export default class BasicDropdown< return this.applyReposition(triggerElement, dropdownElement, positionData); } + @action + updatePublicApi() { + if (!this.args.registerAPI) { + return; + } + + this.args.registerAPI(this.publicAPI); + } + @action registerTriggerElement(element: HTMLElement): void { this.triggerElement = element; diff --git a/src/modifiers/basic-dropdown-trigger.ts b/src/modifiers/basic-dropdown-trigger.ts index 751b5eae..550335d1 100644 --- a/src/modifiers/basic-dropdown-trigger.ts +++ b/src/modifiers/basic-dropdown-trigger.ts @@ -32,6 +32,8 @@ export default class DropdownTriggerModifier extends Modifier { desiredEventType!: string; stopPropagation: boolean | undefined; + private _previousDisabled: boolean | undefined = undefined; + constructor(owner: Owner, args: ArgsFor) { super(owner, args); registerDestructor(this, cleanup); @@ -51,6 +53,17 @@ export default class DropdownTriggerModifier extends Modifier { this.setup(element); this.didSetup = true; } + + if (this._previousDisabled === undefined) { + this._previousDisabled = this.dropdown.disabled; + } + + if (this.dropdown.disabled !== this._previousDisabled) { + this._previousDisabled = this.dropdown.disabled; + + this.dropdown.actions.updatePublicApi(); + } + this.update(element, positional, named); } diff --git a/src/types.ts b/src/types.ts index 9615fdad..157c286a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,7 @@ export interface DropdownActions { close: (e?: Event, skipFocus?: boolean) => void; open: (e?: Event) => void; reposition: () => undefined | RepositionChanges; + updatePublicApi: () => void; registerTriggerElement: (e: HTMLElement) => void; registerDropdownElement: (e: HTMLElement) => void; getTriggerElement: () => HTMLElement | null; diff --git a/tests/integration/components/trigger-test.gts b/tests/integration/components/trigger-test.gts index 5da08690..6374be11 100644 --- a/tests/integration/components/trigger-test.gts +++ b/tests/integration/components/trigger-test.gts @@ -63,6 +63,7 @@ const dropdownBase: Dropdown = { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { From 4e45840b2bf83c6991509e26fb07e6ba967b47bc Mon Sep 17 00:00:00 2001 From: Markus Sanin Date: Sun, 1 Feb 2026 17:36:24 +0100 Subject: [PATCH 2/5] Fix lint --- src/components/basic-dropdown.gts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/basic-dropdown.gts b/src/components/basic-dropdown.gts index cfc6915e..0b8e2ec8 100644 --- a/src/components/basic-dropdown.gts +++ b/src/components/basic-dropdown.gts @@ -151,7 +151,7 @@ export default class BasicDropdown< } get isOpen(): boolean { - return !this.disabled && this._isOpen + return !this.disabled && this._isOpen; } set isOpen(v: boolean) { From 75187f416f6bd3b7931c51096abdb6558f07e53e Mon Sep 17 00:00:00 2001 From: Markus Sanin Date: Sun, 1 Feb 2026 17:40:09 +0100 Subject: [PATCH 3/5] Fix lint --- tests/integration/components/content-test.gts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/integration/components/content-test.gts b/tests/integration/components/content-test.gts index b5a30575..0b733c5e 100644 --- a/tests/integration/components/content-test.gts +++ b/tests/integration/components/content-test.gts @@ -57,6 +57,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -101,6 +102,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -143,6 +145,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -181,6 +184,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -225,6 +229,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -267,6 +272,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -306,6 +312,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -348,6 +355,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -391,6 +399,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -435,6 +444,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -479,6 +489,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -520,6 +531,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -540,6 +552,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -592,6 +605,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -640,6 +654,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -698,6 +713,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -776,6 +792,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -828,6 +845,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -864,6 +882,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { assert.ok(false, 'Reposition is invoked exactly once'); return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -899,6 +918,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -997,6 +1017,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { repositions++; return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -1041,6 +1062,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { repositions++; return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -1085,6 +1107,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { repositions++; return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -1129,6 +1152,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { repositions++; return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -1175,6 +1199,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { repositions++; return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -1221,6 +1246,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { done(); return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -1279,6 +1305,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { repositions++; return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -1323,6 +1350,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { @@ -1370,6 +1398,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { reposition: () => { return undefined; }, + updatePublicApi: () => {}, registerTriggerElement: () => {}, registerDropdownElement: () => {}, getTriggerElement: () => { From 52f3672eb0805994b6d1dfda520c4c1f9d0103bc Mon Sep 17 00:00:00 2001 From: Markus Sanin Date: Sun, 1 Feb 2026 17:46:06 +0100 Subject: [PATCH 4/5] Add note to migrate --- .../public-pages/docs/migrate-8-0-to-9-0.gts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/app/templates/public-pages/docs/migrate-8-0-to-9-0.gts b/docs/app/templates/public-pages/docs/migrate-8-0-to-9-0.gts index 67f565d0..1612997e 100644 --- a/docs/app/templates/public-pages/docs/migrate-8-0-to-9-0.gts +++ b/docs/app/templates/public-pages/docs/migrate-8-0-to-9-0.gts @@ -77,6 +77,18 @@ import { LinkTo } from '@ember/routing'; a regular HTML attribute instead.

+
  • +

    + If you are using the public API without the trigger modifier and change + @disabled + from + false + to + true, please follow the guidance + in + this PR +

    +
  • If you are using From 36cebdecb8b4618ecf1f61f25f63225d8c98e70e Mon Sep 17 00:00:00 2001 From: Markus Sanin Date: Sun, 1 Feb 2026 17:47:34 +0100 Subject: [PATCH 5/5] Remove unwanted change --- src/components/basic-dropdown-content.gts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/basic-dropdown-content.gts b/src/components/basic-dropdown-content.gts index 2729bae1..4be495c8 100644 --- a/src/components/basic-dropdown-content.gts +++ b/src/components/basic-dropdown-content.gts @@ -175,8 +175,7 @@ export default class BasicDropdownContent< if (!triggerElement) { triggerElement = document.querySelector(selector) as HTMLElement; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.handleRootMouseDown = (e: MouseEvent | TouchEvent): any => { + this.handleRootMouseDown = (e: MouseEvent | TouchEvent): void => { const target = (e.composedPath?.()[0] || e.target) as Element; if (target === null) return; if (