Skip to content

Commit 831fef3

Browse files
authored
Implement @verticalPosition="auto" for @renderInPlace={{true}} (#1045)
* Add tests to display that it doesn't work right now * Add calculate-position logic from PR #1016 * Fix lint * Fix test * Fix lint
1 parent 15ce18e commit 831fef3

File tree

2 files changed

+186
-9
lines changed

2 files changed

+186
-9
lines changed

src/utils/calculate-position.ts

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,55 @@ export type CalculatePosition = (
3434
options: CalculatePositionOptions,
3535
) => CalculatePositionResult;
3636

37+
type GetViewDataResult = {
38+
scroll: { left: number; top: number };
39+
triggerLeft: number;
40+
triggerTop: number;
41+
triggerWidth: number;
42+
triggerHeight: number;
43+
dropdownHeight: number;
44+
dropdownWidth: number;
45+
viewportWidth: number;
46+
viewportBottom: number;
47+
};
48+
49+
type GetViewData = (
50+
trigger: Element,
51+
content: HTMLElement,
52+
) => GetViewDataResult;
53+
54+
const getViewData: GetViewData = (trigger, content) => {
55+
const scroll = {
56+
left: window.scrollX,
57+
top: window.scrollY,
58+
};
59+
const {
60+
left: triggerLeft,
61+
top: triggerTop,
62+
width: triggerWidth,
63+
height: triggerHeight,
64+
} = trigger.getBoundingClientRect();
65+
const { height: dropdownHeight, width: dropdownWidth } =
66+
content.getBoundingClientRect();
67+
const viewportWidth = document.body.clientWidth || window.innerWidth;
68+
const viewportBottom = scroll.top + window.innerHeight;
69+
70+
return {
71+
scroll,
72+
// The properties top and left of the trigger client rectangle need to be absolute to
73+
// the top left corner of the document as the value it's compared to is also the total
74+
// height and not only the viewport height (window client height + scroll offset).
75+
triggerLeft: triggerLeft + window.scrollX,
76+
triggerTop: triggerTop + window.scrollY,
77+
triggerWidth,
78+
triggerHeight,
79+
dropdownHeight,
80+
dropdownWidth,
81+
viewportWidth,
82+
viewportBottom,
83+
};
84+
};
85+
3786
export function calculateWormholedPosition(
3887
trigger: HTMLElement,
3988
content: HTMLElement,
@@ -47,13 +96,16 @@ export function calculateWormholedPosition(
4796
}: CalculatePositionOptions,
4897
): CalculatePositionResult {
4998
// Collect information about all the involved DOM elements
50-
const scroll = { left: window.pageXOffset, top: window.pageYOffset };
51-
let { left: triggerLeft, top: triggerTop } = trigger.getBoundingClientRect();
52-
const { width: triggerWidth, height: triggerHeight } =
53-
trigger.getBoundingClientRect();
54-
const { height: dropdownHeight } = content.getBoundingClientRect();
55-
let { width: dropdownWidth } = content.getBoundingClientRect();
56-
const viewportWidth = document.body.clientWidth || window.innerWidth;
99+
const viewData = getViewData(trigger, content);
100+
const {
101+
scroll,
102+
triggerWidth,
103+
triggerHeight,
104+
dropdownHeight,
105+
viewportWidth,
106+
viewportBottom,
107+
} = viewData;
108+
let { triggerLeft, triggerTop, dropdownWidth } = viewData;
57109
const style: CalculatePositionResultStyle = {};
58110

59111
// Apply containers' offset
@@ -170,7 +222,6 @@ export function calculateWormholedPosition(
170222
} else if (verticalPosition === 'below') {
171223
style.top = triggerTopWithScroll + triggerHeight;
172224
} else {
173-
const viewportBottom = scroll.top + window.innerHeight;
174225
const enoughRoomBelow =
175226
triggerTopWithScroll + triggerHeight + dropdownHeight < viewportBottom;
176227
const enoughRoomAbove = triggerTop > dropdownHeight;
@@ -237,8 +288,28 @@ export function calculateInPlacePosition(
237288
positionData.verticalPosition = verticalPosition;
238289
dropdownRect = dropdownRect || content.getBoundingClientRect();
239290
positionData.style.top = -dropdownRect.height;
240-
} else {
291+
} else if (verticalPosition === 'below') {
241292
positionData.verticalPosition = 'below';
293+
} else {
294+
// Automatically determine if there is enough space above or below
295+
const { triggerTop, triggerHeight, dropdownHeight, viewportBottom } =
296+
getViewData(trigger, content);
297+
298+
const enoughRoomBelow =
299+
triggerTop + triggerHeight + dropdownHeight < viewportBottom;
300+
const enoughRoomAbove = triggerTop > dropdownHeight;
301+
302+
if (enoughRoomBelow) {
303+
verticalPosition = 'below';
304+
} else if (enoughRoomAbove) {
305+
verticalPosition = 'above';
306+
dropdownRect = dropdownRect || content.getBoundingClientRect();
307+
positionData.style.top = -dropdownRect.height;
308+
} else {
309+
// Not enough space above or below
310+
verticalPosition = 'below';
311+
}
312+
positionData.verticalPosition = verticalPosition;
242313
}
243314
return positionData;
244315
}

tests/integration/components/basic-dropdown-test.gts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2078,4 +2078,110 @@ module('Integration | Component | basic-dropdown', function (hooks) {
20782078
.dom(shadowRoot?.querySelector('#dropdown-is-opened'))
20792079
.doesNotExist('The dropdown is closed again');
20802080
});
2081+
2082+
test<ExtendedTestContext>('It adds the proper class above to trigger and content when it receives `renderInPlace={{true}}` and @verticalPosition="auto"', async function (assert) {
2083+
assert.expect(2);
2084+
2085+
await render(
2086+
<template>
2087+
{{! template-lint-disable no-inline-styles no-forbidden-elements }}
2088+
{{! #ember-testing is by default 200% width/height & has a scale. For this test we need to reset it }}
2089+
<style>
2090+
#ember-testing {
2091+
width: 100%;
2092+
height: 100%;
2093+
transform: none;
2094+
overflow: hidden;
2095+
}
2096+
</style>
2097+
<div style="position: relative; width: 100%; height: 100vh">
2098+
<div style="position: absolute; bottom: 0">
2099+
<HostWrapper>
2100+
<BasicDropdown
2101+
@renderInPlace={{true}}
2102+
@verticalPosition="auto"
2103+
as |dropdown|
2104+
>
2105+
<dropdown.Trigger>Press me</dropdown.Trigger>
2106+
<dropdown.Content><h3>Content of the dropdown</h3></dropdown.Content>
2107+
</BasicDropdown>
2108+
</HostWrapper>
2109+
</div>
2110+
</div>
2111+
</template>,
2112+
);
2113+
2114+
await click(
2115+
getRootNode(this.element).querySelector(
2116+
'.ember-basic-dropdown-trigger',
2117+
) as HTMLElement,
2118+
);
2119+
2120+
assert
2121+
.dom('.ember-basic-dropdown-trigger', getRootNode(this.element))
2122+
.hasClass(
2123+
'ember-basic-dropdown-trigger--above',
2124+
'The proper class has been added',
2125+
);
2126+
2127+
assert
2128+
.dom('.ember-basic-dropdown-content', getRootNode(this.element))
2129+
.hasClass(
2130+
'ember-basic-dropdown-content--above',
2131+
'The proper class has been added',
2132+
);
2133+
});
2134+
2135+
test<ExtendedTestContext>('It adds the proper class below to trigger and content when it receives `renderInPlace={{true}}` and @verticalPosition="auto"', async function (assert) {
2136+
assert.expect(2);
2137+
2138+
await render(
2139+
<template>
2140+
{{! template-lint-disable no-inline-styles no-forbidden-elements }}
2141+
{{! #ember-testing is by default width/height 200% & has a scale. For this test we need to reset it }}
2142+
<style>
2143+
#ember-testing {
2144+
width: 100%;
2145+
height: 100%;
2146+
transform: none;
2147+
overflow: hidden;
2148+
}
2149+
</style>
2150+
<div style="position: relative; width: 100%; height: 100vh">
2151+
<div style="position: absolute; top: 0">
2152+
<HostWrapper>
2153+
<BasicDropdown
2154+
@renderInPlace={{true}}
2155+
@verticalPosition="auto"
2156+
as |dropdown|
2157+
>
2158+
<dropdown.Trigger>Press me</dropdown.Trigger>
2159+
<dropdown.Content><h3>Content of the dropdown</h3></dropdown.Content>
2160+
</BasicDropdown>
2161+
</HostWrapper>
2162+
</div>
2163+
</div>
2164+
</template>,
2165+
);
2166+
2167+
await click(
2168+
getRootNode(this.element).querySelector(
2169+
'.ember-basic-dropdown-trigger',
2170+
) as HTMLElement,
2171+
);
2172+
2173+
assert
2174+
.dom('.ember-basic-dropdown-trigger', getRootNode(this.element))
2175+
.hasClass(
2176+
'ember-basic-dropdown-trigger--below',
2177+
'The proper class has been added',
2178+
);
2179+
2180+
assert
2181+
.dom('.ember-basic-dropdown-content', getRootNode(this.element))
2182+
.hasClass(
2183+
'ember-basic-dropdown-content--below',
2184+
'The proper class has been added',
2185+
);
2186+
});
20812187
});

0 commit comments

Comments
 (0)