Skip to content

Commit e220686

Browse files
authored
fix(combobox, dropdown, input-date-picker, popover, tooltip): fix misplaced floating-ui elements when associated-components are closed (#6709)
**Related Issue:** #6404 ## Summary This removes a utility added in #5484 that would reset position of floating-UI elements using the absolute strategy to avoid scroll bars when hidden. Unfortunately, the util would lead to undesired position resets in some scenarios (e.g., quick toggling between transitions). Moving forward, it is recommended to use `overlay-positioning='fixed'`to escaping clipping containers and prevent hidden floating-ui elements from affecting layout/creating scrollbars.
1 parent 40bb711 commit e220686

File tree

7 files changed

+14
-148
lines changed

7 files changed

+14
-148
lines changed

src/components/combobox/combobox.tsx

+3-8
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ import {
2626
FloatingUIComponent,
2727
LogicalPlacement,
2828
OverlayPositioning,
29-
reposition,
30-
updateAfterClose
29+
reposition
3130
} from "../../utils/floating-ui";
3231
import {
3332
afterConnectDefaultValueSet,
@@ -116,11 +115,7 @@ export class Combobox
116115
@Prop({ reflect: true, mutable: true }) open = false;
117116

118117
@Watch("open")
119-
openHandler(value: boolean): void {
120-
if (!value) {
121-
updateAfterClose(this.floatingEl);
122-
}
123-
118+
openHandler(): void {
124119
if (this.disabled) {
125120
this.open = false;
126121
return;
@@ -398,7 +393,7 @@ export class Combobox
398393
this.setFilteredPlacements();
399394
this.reposition(true);
400395
if (this.open) {
401-
this.openHandler(this.open);
396+
this.openHandler();
402397
}
403398
}
404399

src/components/dropdown/dropdown.tsx

+1-8
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ import {
2929
FloatingUIComponent,
3030
MenuPlacement,
3131
OverlayPositioning,
32-
reposition,
33-
updateAfterClose
32+
reposition
3433
} from "../../utils/floating-ui";
3534
import { guid } from "../../utils/guid";
3635
import { InteractiveComponent, updateHostInteraction } from "../../utils/interactive";
@@ -89,16 +88,10 @@ export class Dropdown
8988
if (!this.disabled) {
9089
if (value) {
9190
this.reposition(true);
92-
} else {
93-
updateAfterClose(this.floatingEl);
9491
}
9592
return;
9693
}
9794

98-
if (!value) {
99-
updateAfterClose(this.floatingEl);
100-
}
101-
10295
this.open = false;
10396
}
10497

src/components/input-date-picker/input-date-picker.tsx

+1-7
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ import {
3232
FloatingUIComponent,
3333
MenuPlacement,
3434
OverlayPositioning,
35-
reposition,
36-
updateAfterClose
35+
reposition
3736
} from "../../utils/floating-ui";
3837
import {
3938
connectForm,
@@ -214,17 +213,12 @@ export class InputDatePicker
214213
@Watch("open")
215214
openHandler(value: boolean): void {
216215
if (this.disabled || this.readOnly) {
217-
if (!value) {
218-
updateAfterClose(this.floatingEl);
219-
}
220216
this.open = false;
221217
return;
222218
}
223219

224220
if (value) {
225221
this.reposition(true);
226-
} else {
227-
updateAfterClose(this.floatingEl);
228222
}
229223
}
230224

src/components/popover/popover.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ import {
2323
LogicalPlacement,
2424
OverlayPositioning,
2525
ReferenceElement,
26-
reposition,
27-
updateAfterClose
26+
reposition
2827
} from "../../utils/floating-ui";
2928
import {
3029
activateFocusTrap,
@@ -197,8 +196,6 @@ export class Popover
197196
openHandler(value: boolean): void {
198197
if (value) {
199198
this.reposition(true);
200-
} else {
201-
updateAfterClose(this.el);
202199
}
203200

204201
this.setExpandedAttr();

src/components/tooltip/tooltip.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ import {
2121
LogicalPlacement,
2222
OverlayPositioning,
2323
ReferenceElement,
24-
reposition,
25-
updateAfterClose
24+
reposition
2625
} from "../../utils/floating-ui";
2726
import { guid } from "../../utils/guid";
2827
import {
@@ -89,8 +88,6 @@ export class Tooltip implements FloatingUIComponent, OpenCloseComponent {
8988
openHandler(value: boolean): void {
9089
if (value) {
9190
this.reposition(true);
92-
} else {
93-
updateAfterClose(this.el);
9491
}
9592
}
9693

src/utils/floating-ui.spec.ts

+1-61
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,12 @@ import {
66
disconnectFloatingUI,
77
effectivePlacements,
88
filterComputedPlacements,
9-
FloatingCSS,
109
FloatingUIComponent,
1110
getEffectivePlacement,
12-
placementDataAttribute,
1311
placements,
1412
positionFloatingUI,
1513
reposition,
16-
repositionDebounceTimeout,
17-
updateAfterClose
14+
repositionDebounceTimeout
1815
} from "./floating-ui";
1916

2017
import * as floatingUIDOM from "@floating-ui/dom";
@@ -158,63 +155,6 @@ describe("repositioning", () => {
158155
expect(floatingEl.style.position).toBe("fixed");
159156
});
160157
});
161-
162-
describe("afterClose helper", () => {
163-
beforeAll(() => {
164-
class TransitionEvent extends globalThis.Event {
165-
readonly elapsedTime: number;
166-
167-
readonly propertyName: string;
168-
169-
readonly pseudoElement: string;
170-
171-
constructor(type: string, transitionEventInitDict: TransitionEventInit = {}) {
172-
super(type, transitionEventInitDict);
173-
174-
this.elapsedTime = transitionEventInitDict.elapsedTime || 0.0;
175-
this.propertyName = transitionEventInitDict.propertyName || "";
176-
this.pseudoElement = transitionEventInitDict.pseudoElement || "";
177-
}
178-
}
179-
180-
// polyfilled as it is not available via JSDOM
181-
globalThis.TransitionEvent = TransitionEvent;
182-
});
183-
184-
describe("resets positioning when closed", () => {
185-
function emitTransitionEnd() {
186-
const closingFloatingUITransitionEvent = new TransitionEvent("transitionend", { propertyName: "opacity" });
187-
floatingEl.dispatchEvent(closingFloatingUITransitionEvent);
188-
}
189-
190-
beforeEach(() => {
191-
floatingEl.setAttribute(placementDataAttribute, "fake-placement");
192-
floatingEl.classList.add(FloatingCSS.animation);
193-
});
194-
195-
it("resets for absolute positioning strategy", async () => {
196-
floatingEl.style.position = "absolute";
197-
198-
updateAfterClose(floatingEl);
199-
emitTransitionEnd();
200-
201-
expect(floatingEl.style.transform).toBe("");
202-
expect(floatingEl.style.top).toBe("0");
203-
expect(floatingEl.style.left).toBe("0");
204-
});
205-
206-
it("does not reset for fixed positioning strategy", async () => {
207-
floatingEl.style.position = "fixed";
208-
209-
updateAfterClose(floatingEl);
210-
emitTransitionEnd();
211-
212-
expect(floatingEl.style.transform).toBe("");
213-
expect(floatingEl.style.top).not.toBe("0");
214-
expect(floatingEl.style.left).not.toBe("0");
215-
});
216-
});
217-
});
218158
});
219159

220160
it("should have correct value for defaultOffsetDistance", () => {

src/utils/floating-ui.ts

+6-56
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
import { Build } from "@stencil/core";
1717
import { debounce } from "lodash-es";
1818
import { config } from "./config";
19-
import { closestElementCrossShadowBoundary, getElementDir } from "./dom";
19+
import { getElementDir } from "./dom";
2020

2121
const floatingUIBrowserCheck = patchFloatingUiForNonChromiumBrowsers();
2222

@@ -451,18 +451,15 @@ export function connectFloatingUI(
451451

452452
disconnectFloatingUI(component, referenceEl, floatingEl);
453453

454-
const position = component.overlayPositioning;
455-
456-
// ensure position matches for initial positioning
457454
Object.assign(floatingEl.style, {
458455
visibility: "hidden",
459456
pointerEvents: "none",
460-
position
461-
});
462457

463-
if (position === "absolute") {
464-
resetPosition(floatingEl);
465-
}
458+
// initial positioning based on https://floating-ui.com/docs/computePosition#initial-layout
459+
position: component.overlayPositioning,
460+
top: "0",
461+
left: "0"
462+
});
466463

467464
const runAutoUpdate = Build.isBrowser
468465
? autoUpdate
@@ -495,8 +492,6 @@ export function disconnectFloatingUI(
495492
return;
496493
}
497494

498-
getTransitionTarget(floatingEl).removeEventListener("transitionend", handleTransitionElTransitionEnd);
499-
500495
const cleanup = cleanupMap.get(component);
501496

502497
if (cleanup) {
@@ -514,48 +509,3 @@ const visiblePointerSize = 4;
514509
* @default 6
515510
*/
516511
export const defaultOffsetDistance = Math.ceil(Math.hypot(visiblePointerSize, visiblePointerSize));
517-
518-
/**
519-
* This utils applies floating element styles to avoid affecting layout when closed.
520-
*
521-
* This should be called when the closing transition will start.
522-
*
523-
* @param floatingEl
524-
*/
525-
export function updateAfterClose(floatingEl: HTMLElement): void {
526-
if (!floatingEl || floatingEl.style.position !== "absolute") {
527-
return;
528-
}
529-
530-
getTransitionTarget(floatingEl).addEventListener("transitionend", handleTransitionElTransitionEnd);
531-
}
532-
533-
function getTransitionTarget(floatingEl: HTMLElement): ShadowRoot | HTMLElement {
534-
// assumes floatingEl w/ shadowRoot is a FloatingUIComponent
535-
return floatingEl.shadowRoot || floatingEl;
536-
}
537-
538-
function handleTransitionElTransitionEnd(event: TransitionEvent): void {
539-
const floatingTransitionEl = event.target as HTMLElement;
540-
541-
if (
542-
// using any prop from floating-ui transition
543-
event.propertyName === "opacity" &&
544-
floatingTransitionEl.classList.contains(FloatingCSS.animation)
545-
) {
546-
const floatingEl = getFloatingElFromTransitionTarget(floatingTransitionEl);
547-
resetPosition(floatingEl);
548-
getTransitionTarget(floatingEl).removeEventListener("transitionend", handleTransitionElTransitionEnd);
549-
}
550-
}
551-
552-
function resetPosition(floatingEl: HTMLElement): void {
553-
// resets position to better match https://floating-ui.com/docs/computePosition#initial-layout
554-
floatingEl.style.transform = "";
555-
floatingEl.style.top = "0";
556-
floatingEl.style.left = "0";
557-
}
558-
559-
function getFloatingElFromTransitionTarget(floatingTransitionEl: HTMLElement): HTMLElement {
560-
return closestElementCrossShadowBoundary(floatingTransitionEl, `[${placementDataAttribute}]`);
561-
}

0 commit comments

Comments
 (0)