Skip to content

Commit

Permalink
fix(overlay): automatically reposition overlay when the contents resize
Browse files Browse the repository at this point in the history
  • Loading branch information
Westbrook committed Jan 2, 2024
1 parent 0b2fbec commit 83be807
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 29 deletions.
1 change: 0 additions & 1 deletion packages/overlay/src/Overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,6 @@ export class Overlay extends OverlayFeatures {
}

public override manuallyKeepOpen(): void {
super.manuallyKeepOpen();
this.open = true;
this.placementController.allowPlacementUpdate = true;
this.manageOpen(false);
Expand Down
35 changes: 24 additions & 11 deletions packages/overlay/src/PlacementController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,24 @@ export class PlacementController implements ReactiveController {
this.options = options;
if (!target || !options) return;

const cleanup = autoUpdate(
const cleanupAncestorResize = autoUpdate(
options.trigger,
target,
this.updatePlacement,
this.closeForAncestorUpdate,
{
ancestorResize: false,
elementResize: false,
layoutShift: false,
}
);
const cleanupElementResize = autoUpdate(
options.trigger,
target,
this.updatePlacement,
{
ancestorScroll: false,
}
);
this.cleanup = () => {
this.host.elements?.forEach((element) => {
element.addEventListener(
Expand All @@ -126,25 +135,28 @@ export class PlacementController implements ReactiveController {
{ once: true }
);
});
cleanup();
cleanupAncestorResize();
cleanupElementResize();
};
}

allowPlacementUpdate = false;

updatePlacement = (): void => {
closeForAncestorUpdate = (): void => {
if (
!this.allowPlacementUpdate &&
this.options.type !== 'modal' &&
this.cleanup
) {
this.target.dispatchEvent(new Event('close', { bubbles: true }));
return;
}
this.computePlacement();
this.allowPlacementUpdate = false;
};

updatePlacement = (): void => {
this.computePlacement();
};

async computePlacement(): Promise<void> {
const { options, target } = this;

Expand Down Expand Up @@ -196,7 +208,6 @@ export class PlacementController implements ReactiveController {
Object.assign(target.style, {
maxWidth: `${Math.floor(availableWidth)}px`,
maxHeight: appliedHeight,
height: appliedHeight,
});
},
}),
Expand Down Expand Up @@ -228,10 +239,12 @@ export class PlacementController implements ReactiveController {

target.setAttribute('actual-placement', placement);
this.host.elements?.forEach((element) => {
this.originalPlacements.set(
element,
element.getAttribute('placement') as Placement
);
if (!this.originalPlacements.has(element)) {
this.originalPlacements.set(
element,
element.getAttribute('placement') as Placement
);
}
element.setAttribute('placement', placement);
});

Expand Down
7 changes: 6 additions & 1 deletion packages/overlay/stories/overlay.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,12 @@ export const Default = (args: Properties): TemplateResult => template(args);

export const accordion = (): TemplateResult => {
return html`
<overlay-trigger type="modal" placement="right">
<overlay-trigger type="modal" placement="top-start">
<style>
sp-button {
margin-top: 70vh;
}
</style>
<sp-button variant="primary" slot="trigger">
Open overlay w/ accordion
</sp-button>
Expand Down
45 changes: 31 additions & 14 deletions packages/overlay/test/overlay-trigger-longpress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ describe('Overlay Trigger - Longpress', () => {
expect(this.content).to.not.be.null;
expect(this.content.open).to.be.false;

const open = oneEvent(this.el, 'sp-opened');
this.trigger.focus();
await open;
});
it('opens/closes for `Space`', async function () {
const open = oneEvent(this.el, 'sp-opened');
Expand Down Expand Up @@ -134,7 +136,7 @@ describe('Overlay Trigger - Longpress', () => {
});
it('opens/closes for `longpress`', async function () {
expect(this.trigger.holdAffordance).to.be.true;
let open = oneEvent(this.el, 'sp-opened');
const open = oneEvent(this.el, 'sp-opened');
const rect = this.trigger.getBoundingClientRect();
await sendMouse({
steps: [
Expand All @@ -150,12 +152,6 @@ describe('Overlay Trigger - Longpress', () => {
},
],
});
// Hover content opens, first.
await open;
await nextFrame();
await nextFrame();
open = oneEvent(this.el, 'sp-opened');
// Then, the longpress content opens.
await open;
await nextFrame();
await nextFrame();
Expand Down Expand Up @@ -186,18 +182,39 @@ describe('Overlay Trigger - Longpress', () => {
expect(await isOnTopLayer(this.content)).to.be.false;
expect(this.content.open, 'closes for `pointerdown`').to.be.false;
});
});
describe('opens/closes for `longpress`', () => {
beforeEach(async function () {
this.el = await fixture<OverlayTrigger>(longpress());
this.trigger = this.el.querySelector(
'sp-action-button'
) as ActionButton;
this.tooltip = this.el.querySelector(
'[slot="hover-content"]'
) as Tooltip;
this.content = this.el.querySelector(
'[slot="longpress-content"]'
) as Popover;

expect(this.trigger).to.not.be.null;
expect(this.content).to.not.be.null;
expect(this.content.open).to.be.false;
});
it('opens/closes for `longpress` with Button', async function () {
this.tooltip.placement = 'bottom-start';
await elementUpdated(this.tooltip);
const button = document.createElement('sp-button');
button.slot = 'trigger';
this.trigger.remove();
await elementUpdated(this.el);
this.el.append(button);
button.textContent = 'Longpress button';
this.trigger.replaceWith(button);
await elementUpdated(this.el);
// Inject synthetic wait to afford for late replacement of <sp-action-button> with <button>
await nextFrame();
await nextFrame();
await elementUpdated(button);
// Inject synthetic wait to afford for late replacement of <sp-action-button> with <sp-button>
await waitUntil(() => {
const localName = (
this.el as unknown as { targetContent: HTMLElement[] }
).targetContent[0].localName;
return localName === 'sp-button';
});

let open = oneEvent(this.el, 'sp-opened');
const rect = button.getBoundingClientRect();
Expand Down
6 changes: 5 additions & 1 deletion packages/overlay/test/overlay-update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ import { elementUpdated, expect, oneEvent } from '@open-wc/testing';
import { AccordionItem } from '@spectrum-web-components/accordion/src/AccordionItem.js';
import { OverlayTrigger } from '../src/OverlayTrigger.js';
import { accordion } from '../stories/overlay.stories.js';
import { fixture } from '../../../test/testing-helpers.js';
import {
fixture,
ignoreResizeObserverLoopError,
} from '../../../test/testing-helpers.js';

describe('sp-update-overlays event', () => {
ignoreResizeObserverLoopError(before, after);
it('updates overlay height', async () => {
const el = await fixture<OverlayTrigger>(accordion());
const container = el.querySelector('sp-popover') as HTMLElement;
Expand Down
2 changes: 1 addition & 1 deletion test/testing-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export function ignoreResizeObserverLoopError(
globalErrorHandler = window.onerror;
addEventListener('error', (error) => {
console.error('Uncaught global error:', error);
if (error.message?.match?.(/ResizeObserver loop limit exceeded/)) {
if (error.message?.match?.(/ResizeObserver loop/)) {
return;
} else {
globalErrorHandler?.(error);
Expand Down

0 comments on commit 83be807

Please sign in to comment.