From 09d71d290a96ab4574b06a61da99c51a8517e30f Mon Sep 17 00:00:00 2001 From: Markus Sanin Date: Tue, 23 Dec 2025 16:19:03 +0100 Subject: [PATCH 1/5] Add tests to display that it doesn't work right now --- .../components/basic-dropdown-test.gts | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/integration/components/basic-dropdown-test.gts b/tests/integration/components/basic-dropdown-test.gts index d21afa83..548ebd03 100644 --- a/tests/integration/components/basic-dropdown-test.gts +++ b/tests/integration/components/basic-dropdown-test.gts @@ -2078,4 +2078,80 @@ module('Integration | Component | basic-dropdown', function (hooks) { .dom(shadowRoot?.querySelector('#dropdown-is-opened')) .doesNotExist('The dropdown is closed again'); }); + + test('It adds the proper class above to trigger and content when it receives `renderInPlace={{true}}` and @verticalPosition="auto"', async function (assert) { + assert.expect(2); + + await render( + , + ); + + await click( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + ); + + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasClass( + 'ember-basic-dropdown-trigger--above', + 'The proper class has been added', + ); + + assert + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) + .hasClass( + 'ember-basic-dropdown-content--above', + 'The proper class has been added', + ); + }); + + test('It adds the proper class below to trigger and content when it receives `renderInPlace={{true}}` and @verticalPosition="auto"', async function (assert) { + assert.expect(2); + + await render( + , + ); + + await click( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + ); + + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasClass( + 'ember-basic-dropdown-trigger--below', + 'The proper class has been added', + ); + + assert + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) + .hasClass( + 'ember-basic-dropdown-content--below', + 'The proper class has been added', + ); + }); }); From ec06927c1342b3af75601861e10b6a070b655f6b Mon Sep 17 00:00:00 2001 From: Markus Sanin Date: Tue, 23 Dec 2025 16:22:27 +0100 Subject: [PATCH 2/5] Add calculate-position logic from PR #1016 --- src/utils/calculate-position.ts | 89 +++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 9 deletions(-) diff --git a/src/utils/calculate-position.ts b/src/utils/calculate-position.ts index 7a7bc81d..d6cc1f79 100644 --- a/src/utils/calculate-position.ts +++ b/src/utils/calculate-position.ts @@ -34,6 +34,55 @@ export type CalculatePosition = ( options: CalculatePositionOptions, ) => CalculatePositionResult; +type GetViewDataResult = { + scroll: { left: number; top: number }; + triggerLeft: number; + triggerTop: number; + triggerWidth: number; + triggerHeight: number; + dropdownHeight: number; + dropdownWidth: number; + viewportWidth: number; + viewportBottom: number; +}; + +type GetViewData = ( + trigger: Element, + content: HTMLElement, +) => GetViewDataResult; + +const getViewData: GetViewData = (trigger, content) => { + const scroll = { + left: window.scrollX, + top: window.scrollY, + }; + const { + left: triggerLeft, + top: triggerTop, + width: triggerWidth, + height: triggerHeight, + } = trigger.getBoundingClientRect(); + const { height: dropdownHeight, width: dropdownWidth } = + content.getBoundingClientRect(); + const viewportWidth = document.body.clientWidth || window.innerWidth; + const viewportBottom = scroll.top + window.innerHeight; + + return { + scroll, + // The properties top and left of the trigger client rectangle need to be absolute to + // the top left corner of the document as the value it's compared to is also the total + // height and not only the viewport height (window client height + scroll offset). + triggerLeft: triggerLeft + window.scrollX, + triggerTop: triggerTop + window.scrollY, + triggerWidth, + triggerHeight, + dropdownHeight, + dropdownWidth, + viewportWidth, + viewportBottom, + }; +}; + export function calculateWormholedPosition( trigger: HTMLElement, content: HTMLElement, @@ -47,13 +96,16 @@ export function calculateWormholedPosition( }: CalculatePositionOptions, ): CalculatePositionResult { // Collect information about all the involved DOM elements - const scroll = { left: window.pageXOffset, top: window.pageYOffset }; - let { left: triggerLeft, top: triggerTop } = trigger.getBoundingClientRect(); - const { width: triggerWidth, height: triggerHeight } = - trigger.getBoundingClientRect(); - const { height: dropdownHeight } = content.getBoundingClientRect(); - let { width: dropdownWidth } = content.getBoundingClientRect(); - const viewportWidth = document.body.clientWidth || window.innerWidth; + const viewData = getViewData(trigger, content); + const { + scroll, + triggerWidth, + triggerHeight, + dropdownHeight, + viewportWidth, + viewportBottom, + } = viewData; + let { triggerLeft, triggerTop, dropdownWidth } = viewData; const style: CalculatePositionResultStyle = {}; // Apply containers' offset @@ -170,7 +222,6 @@ export function calculateWormholedPosition( } else if (verticalPosition === 'below') { style.top = triggerTopWithScroll + triggerHeight; } else { - const viewportBottom = scroll.top + window.innerHeight; const enoughRoomBelow = triggerTopWithScroll + triggerHeight + dropdownHeight < viewportBottom; const enoughRoomAbove = triggerTop > dropdownHeight; @@ -237,8 +288,28 @@ export function calculateInPlacePosition( positionData.verticalPosition = verticalPosition; dropdownRect = dropdownRect || content.getBoundingClientRect(); positionData.style.top = -dropdownRect.height; - } else { + } else if (verticalPosition === 'below') { positionData.verticalPosition = 'below'; + } else { + // Automatically determine if there is enough space above or below + const { triggerTop, triggerHeight, dropdownHeight, viewportBottom } = + getViewData(trigger, content); + + const enoughRoomBelow = + triggerTop + triggerHeight + dropdownHeight < viewportBottom; + const enoughRoomAbove = triggerTop > dropdownHeight; + + if (enoughRoomBelow) { + verticalPosition = 'below'; + } else if (enoughRoomAbove) { + verticalPosition = 'above'; + dropdownRect = dropdownRect || content.getBoundingClientRect(); + positionData.style.top = -dropdownRect.height; + } else { + // Not enough space above or below + verticalPosition = 'below'; + } + positionData.verticalPosition = verticalPosition; } return positionData; } From acdb0f88ea6988618361684bb16f056889744cdf Mon Sep 17 00:00:00 2001 From: Markus Sanin Date: Tue, 23 Dec 2025 16:22:31 +0100 Subject: [PATCH 3/5] Fix lint --- tests/integration/components/basic-dropdown-test.gts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/integration/components/basic-dropdown-test.gts b/tests/integration/components/basic-dropdown-test.gts index 548ebd03..d3904f35 100644 --- a/tests/integration/components/basic-dropdown-test.gts +++ b/tests/integration/components/basic-dropdown-test.gts @@ -2087,7 +2087,11 @@ module('Integration | Component | basic-dropdown', function (hooks) { {{! template-lint-disable no-inline-styles }}
- + Press me

Content of the dropdown

@@ -2125,7 +2129,11 @@ module('Integration | Component | basic-dropdown', function (hooks) { {{! template-lint-disable no-inline-styles }}
- + Press me

Content of the dropdown

From cf9449e0213889c19cf4d838a076cbf7f56e54b0 Mon Sep 17 00:00:00 2001 From: Markus Sanin Date: Wed, 24 Dec 2025 07:31:41 +0100 Subject: [PATCH 4/5] Fix test --- .../components/basic-dropdown-test.gts | 70 ++++++++++++------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/tests/integration/components/basic-dropdown-test.gts b/tests/integration/components/basic-dropdown-test.gts index d3904f35..18ca7a08 100644 --- a/tests/integration/components/basic-dropdown-test.gts +++ b/tests/integration/components/basic-dropdown-test.gts @@ -2084,18 +2084,29 @@ module('Integration | Component | basic-dropdown', function (hooks) { await render( , ); @@ -2126,18 +2137,29 @@ module('Integration | Component | basic-dropdown', function (hooks) { await render( , ); From ce9d80d337f1496178f7c44c1e2f1617c47ba8b9 Mon Sep 17 00:00:00 2001 From: Markus Sanin Date: Wed, 24 Dec 2025 07:32:51 +0100 Subject: [PATCH 5/5] Fix lint --- tests/integration/components/basic-dropdown-test.gts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/components/basic-dropdown-test.gts b/tests/integration/components/basic-dropdown-test.gts index 18ca7a08..5468b2ce 100644 --- a/tests/integration/components/basic-dropdown-test.gts +++ b/tests/integration/components/basic-dropdown-test.gts @@ -2085,7 +2085,7 @@ module('Integration | Component | basic-dropdown', function (hooks) { await render(