Skip to content

Commit

Permalink
fix(overlay): track "modalRoots" for expanded overlay management
Browse files Browse the repository at this point in the history
  • Loading branch information
Westbrook committed Mar 3, 2021
1 parent d9bcd6f commit dceccb1
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 18 deletions.
33 changes: 30 additions & 3 deletions packages/overlay/src/ActiveOverlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,17 +198,44 @@ export class ActiveOverlay extends SpectrumElement {
);
}

private _modalRoot?: ActiveOverlay;

public get hasModalRoot(): boolean {
return !!this._modalRoot;
}

public feature(): void {
this.tabIndex = 0;
if (this.interaction === 'modal') {
const parentOverlay = this.trigger.closest('active-overlay');
const parentIsModal = parentOverlay && parentOverlay.slot === 'open';
// If an overlay it triggered from within a "modal" overlay, it needs to continue
// to act like one to get treated correctly in regards to tab trapping.
if (this.interaction === 'modal' || parentIsModal || this._modalRoot) {
this.slot = 'open';
// If this isn't a modal root, walk up the overlays to the next modal root
// and "feature" each on of the intervening overlays.
if (this._modalRoot) {
parentOverlay?.feature();
}
}
}

public obscure(): void {
if (this.interaction === 'modal') {
public obscure(
nextOverlayInteraction: TriggerInteractions
): ActiveOverlay | undefined {
if (this.slot && nextOverlayInteraction === 'modal') {
this.removeAttribute('slot');
// Obscure upto and including the next modal root.
if (this.interaction !== 'modal') {
const parentOverlay = this.trigger.closest('active-overlay');
this._modalRoot = parentOverlay?.obscure(
nextOverlayInteraction
);
return this._modalRoot;
}
return this;
}
return undefined;
}

public firstUpdated(changedProperties: PropertyValues): void {
Expand Down
4 changes: 2 additions & 2 deletions packages/overlay/src/OverlayTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ import overlayTriggerStyles from './overlay-trigger.css.js';
* @slot click-content - The content that will be displayed on click
* @slot longpress-content - The content that will be displayed on click
*
* @fires sp-open - Announces that the overlay has been opened
* @fires sp-close - Announces that the overlay has been closed
* @fires sp-opened - Announces that the overlay has been opened
* @fires sp-closed - Announces that the overlay has been closed
*/
export class OverlayTrigger extends LitElement {
private closeClickOverlay?: () => void;
Expand Down
132 changes: 119 additions & 13 deletions packages/overlay/test/overlay-trigger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ import {
} from '@open-wc/testing';

import '../overlay-trigger.js';
import { OverlayTrigger, ActiveOverlay, TriggerInteractions, OverlayOpenCloseDetail } from '../';
import {
OverlayTrigger,
ActiveOverlay,
TriggerInteractions,
OverlayOpenCloseDetail,
} from '../';
import '@spectrum-web-components/button/sp-button.js';
import { Button } from '@spectrum-web-components/button';
import '@spectrum-web-components/popover/sp-popover.js';
Expand Down Expand Up @@ -709,8 +714,8 @@ describe('Overlay Trigger', () => {
) as HTMLElement;
const outerPopover = testDiv.querySelector('#outer-popover') as Popover;

el.addEventListener('sp-opened',openedSpy);
el.addEventListener('sp-closed',closedSpy);
el.addEventListener('sp-opened', openedSpy);
el.addEventListener('sp-closed', closedSpy);

outerButton.click();

Expand All @@ -719,15 +724,13 @@ describe('Overlay Trigger', () => {
'outer content stolen and reparented'
);

await waitUntil(
() => openedSpy.calledOnce,
'opened event sent'
);
await waitUntil(() => openedSpy.calledOnce, 'opened event sent');

expect(isVisible(outerPopover)).to.be.true;
expect(closed).to.be.false;

const openedEvent = openedSpy.args[0][0] as CustomEvent<OverlayOpenCloseDetail>;
const openedEvent = openedSpy
.args[0][0] as CustomEvent<OverlayOpenCloseDetail>;
expect(openedEvent.detail.interaction).to.equal('click');

document.body.click();
Expand All @@ -738,14 +741,117 @@ describe('Overlay Trigger', () => {
'inner content returned'
);

await waitUntil(
() => closedSpy.calledOnce,
'closed event sent'
);
await waitUntil(() => closedSpy.calledOnce, 'closed event sent');

const closedEvent = closedSpy.args[0][0] as CustomEvent<OverlayOpenCloseDetail>;
const closedEvent = closedSpy
.args[0][0] as CustomEvent<OverlayOpenCloseDetail>;
expect(closedEvent.detail.interaction).to.equal('click');

expect(isVisible(outerPopover)).to.be.false;
});
it('manages multiple layers of `type="modal"', async () => {
const el = await fixture(html`
<overlay-trigger type="modal" placement="none">
<sp-button slot="trigger" variant="cta">
Toggle Dialog
</sp-button>
<sp-popover dialog slot="click-content">
<overlay-trigger>
<sp-button slot="trigger" variant="primary">
Toggle Dialog
</sp-button>
<sp-popover dialog slot="click-content">
<overlay-trigger type="modal">
<sp-button slot="trigger" variant="secondary">
Toggle Dialog
</sp-button>
<sp-popover dialog slot="click-content">
<p>
When you get this deep, this
ActiveOverlay should be the only one in
[slot="open"].
</p>
<p>
All of the rest of the ActiveOverlay
elements should have had their [slot]
attribute removed.
</p>
<p>
Closing this ActiveOverlay should
replace them...
</p>
</sp-popover>
</overlay-trigger>
</sp-popover>
</overlay-trigger>
</sp-popover>
</overlay-trigger>
`);
const overlayTriggers = [...el.querySelectorAll('overlay-trigger')];
let activeOverlays = [...document.querySelectorAll('active-overlay')];
const triggers = [
...el.querySelectorAll('sp-button[slot="trigger"]'),
] as Button[];

expect(activeOverlays.length).to.equal(0);

triggers[0]?.click();
await elementUpdated(overlayTriggers[0]);
await waitUntil(() => {
activeOverlays = [...document.querySelectorAll('active-overlay')];
return activeOverlays.length === 1;
}, 'The first `active-overlay` element has been added.');

expect(activeOverlays.length).to.equal(1);
expect(activeOverlays[0].slot, 'first overlay, first time').to.equal(
'open'
);

triggers[1]?.click();
await elementUpdated(overlayTriggers[1]);
await waitUntil(() => {
activeOverlays = [...document.querySelectorAll('active-overlay')];
return activeOverlays.length === 2;
}, 'The second `active-overlay` element has been added.');

expect(activeOverlays[0].slot, 'first overlay, second time').to.equal(
'open'
);
expect(activeOverlays[1].slot, 'second overlay, second time').to.equal(
'open'
);

triggers[2]?.click();
await elementUpdated(overlayTriggers[2]);
await waitUntil(() => {
activeOverlays = [...document.querySelectorAll('active-overlay')];
return activeOverlays.length === 3;
}, 'The third `active-overlay` element has been added.');

expect(
activeOverlays[0].hasAttribute('slot'),
'first overlay, third time'
).to.be.false;
expect(
activeOverlays[1].hasAttribute('slot'),
'second overlay, third time'
).to.be.false;
expect(activeOverlays[2].slot, 'third overlay, third time').to.equal(
'open'
);

document.body.click();
await elementUpdated(overlayTriggers[1]);
await waitUntil(() => {
activeOverlays = [...document.querySelectorAll('active-overlay')];
return activeOverlays.length === 2;
}, 'The third `active-overlay` element has been removed.');

await waitUntil(() => {
return activeOverlays[0].slot === 'open';
}, 'first overlay, last time');
expect(activeOverlays[1].slot, 'second overlay, last time').to.equal(
'open'
);
});
});

0 comments on commit dceccb1

Please sign in to comment.