Skip to content

Commit

Permalink
fix: special case the possibility of leaving an overlay trigger by en…
Browse files Browse the repository at this point in the history
…tering its overlay content
  • Loading branch information
Westbrook committed Sep 15, 2022
1 parent bedb2d4 commit c32a075
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 1 deletion.
23 changes: 23 additions & 0 deletions packages/overlay/src/OverlayTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export class OverlayTrigger extends SpectrumElement {
private longpressContent?: HTMLElement;
private hoverContent?: HTMLElement;
private targetContent?: HTMLElement;
private overlaidContent?: HTMLElement;

private _longpressId = `longpress-describedby-descriptor`;

Expand Down Expand Up @@ -209,6 +210,7 @@ export class OverlayTrigger extends SpectrumElement {
delete this[name];
(await canClose)();
});
this.overlaidContent = undefined;
}

private manageOpen(): void {
Expand Down Expand Up @@ -237,6 +239,7 @@ export class OverlayTrigger extends SpectrumElement {
},
{ once: true }
);
this.overlaidContent = content;
return OverlayTrigger.openOverlay(
target,
interaction,
Expand Down Expand Up @@ -266,6 +269,26 @@ export class OverlayTrigger extends SpectrumElement {
}

private onTrigger(event: CustomEvent<LongpressEvent>): void {
if (
event.type === 'mouseleave' &&
this.open === 'hover' &&
(event as unknown as MouseEvent).relatedTarget ===
this.overlaidContent
) {
this.overlaidContent.addEventListener(
'mouseleave',
(event: MouseEvent) => {
if (event.relatedTarget === this.targetContent) {
return;
}
this.onTrigger(
event as unknown as CustomEvent<LongpressEvent>
);
},
{ once: true }
);
return;
}
if (this.disabled) return;

switch (event.type) {
Expand Down
93 changes: 93 additions & 0 deletions packages/overlay/test/overlay-trigger-hover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import '@spectrum-web-components/theme/sp-theme.js';
import '@spectrum-web-components/theme/src/themes.js';
import { TemplateResult } from '@spectrum-web-components/base';
import { Theme } from '@spectrum-web-components/theme';
import { Tooltip } from '@spectrum-web-components/tooltip';

async function styledFixture<T extends Element>(
story: TemplateResult
Expand Down Expand Up @@ -89,6 +90,98 @@ describe('Overlay Trigger - Hover', () => {
timeout: 2000,
});
});
describe('"tooltip" mouse interactions', () => {
let el: OverlayTrigger;
let button: ActionButton;
let tooltip: Tooltip;
beforeEach(async () => {
el = await fixture<OverlayTrigger>(
(() => html`
<overlay-trigger placement="right-start">
<sp-action-button slot="trigger">
<sp-icon-magnify slot="icon"></sp-icon-magnify>
</sp-action-button>
<sp-tooltip slot="hover-content" tip>
Magnify
</sp-tooltip>
</overlay-trigger>
`)()
);
await elementUpdated(el);
button = el.querySelector('sp-action-button') as ActionButton;
tooltip = el.querySelector('sp-tooltip') as Tooltip;
});
it('allows pointer to enter the "tooltip" without closing the "tooltip"', async () => {
const opened = oneEvent(button, 'sp-opened');
button.dispatchEvent(
new MouseEvent('mouseenter', {
bubbles: true,
composed: true,
})
);
await nextFrame();
button.dispatchEvent(
new MouseEvent('mouseleave', {
relatedTarget: tooltip,
bubbles: true,
composed: true,
})
);
await nextFrame();
tooltip.dispatchEvent(
new MouseEvent('mouseleave', {
relatedTarget: button,
bubbles: true,
composed: true,
})
);
await opened;

expect(el.open).to.equal('hover');

const closed = oneEvent(button, 'sp-closed');
button.dispatchEvent(
new MouseEvent('mouseleave', {
bubbles: true,
composed: true,
})
);
await closed;

expect(el.open).to.be.null;
});
it('closes the "tooltip" when leaving the "tooltip"', async () => {
const opened = oneEvent(button, 'sp-opened');
button.dispatchEvent(
new MouseEvent('mouseenter', {
bubbles: true,
composed: true,
})
);
await nextFrame();
button.dispatchEvent(
new MouseEvent('mouseleave', {
relatedTarget: tooltip,
bubbles: true,
composed: true,
})
);
await opened;

expect(el.open).to.equal('hover');

const closed = oneEvent(button, 'sp-closed');
tooltip.dispatchEvent(
new MouseEvent('mouseleave', {
bubbles: true,
composed: true,
})
);
await closed;

expect(el.open).to.be.null;
});
});
it('persists hover content', async () => {
const el = await fixture<OverlayTrigger>(
(() => html`
Expand Down
21 changes: 20 additions & 1 deletion packages/tooltip/src/Tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,26 @@ export class Tooltip extends SpectrumElement {
});
};

private closeOverlay = async (): Promise<void> => {
private closeOverlay = async (
event?: PointerEvent | FocusEvent | Event
): Promise<void> => {
if (
event &&
event.type === 'pointerleave' &&
(event as PointerEvent).relatedTarget === this
) {
this.addEventListener(
'pointerleave',
(event: PointerEvent) => {
if (event.relatedTarget === this.parentElement) {
return;
}
this.closeOverlay(event);
},
{ once: true }
);
return;
}
if (this.abortOverlay) this.abortOverlay(true);
if (!this.closeOverlayCallback) return;
(await this.closeOverlayCallback)();
Expand Down
56 changes: 56 additions & 0 deletions packages/tooltip/test/tooltip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,62 @@ describe('Tooltip', () => {

expect(el.open).to.be.false;
});
it('allows pointer to enter the "tooltip" without closing the "tooltip"', async () => {
const button = await fixture<Button>(
html`
<sp-button>
This is a button.
<sp-tooltip self-managed placement="bottom">
Help text.
</sp-tooltip>
</sp-button>
`
);

const el = button.querySelector('sp-tooltip') as Tooltip;

await elementUpdated(el);
await expect(button).to.be.accessible();
let opened = oneEvent(button, 'sp-opened');
button.dispatchEvent(new PointerEvent('pointerenter'));
button.dispatchEvent(
new PointerEvent('pointerleave', {
relatedTarget: el,
})
);
el.dispatchEvent(
new PointerEvent('pointerleave', {
relatedTarget: button,
})
);
await opened;
await elementUpdated(el);

expect(el.open).to.be.true;
await expect(button).to.be.accessible();

let closed = oneEvent(button, 'sp-closed');
button.dispatchEvent(new PointerEvent('pointerleave'));
await closed;
await elementUpdated(el);

expect(el.open).to.be.false;

opened = oneEvent(button, 'sp-opened');
button.dispatchEvent(new PointerEvent('pointerenter'));
button.dispatchEvent(
new PointerEvent('pointerleave', {
relatedTarget: el,
})
);
await opened;
await elementUpdated(el);

closed = oneEvent(button, 'sp-closed');
el.dispatchEvent(new PointerEvent('pointerleave'));
await closed;
await elementUpdated(el);
});
it('cleans up when self manages', async () => {
const button = await fixture<Button>(
html`
Expand Down

0 comments on commit c32a075

Please sign in to comment.