Skip to content

Commit

Permalink
fix(overlay): traverse up through shadow roots when determining paren…
Browse files Browse the repository at this point in the history
…t overlay
  • Loading branch information
Westbrook committed Oct 5, 2021
1 parent a8cd64c commit 27f232c
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 5 deletions.
16 changes: 14 additions & 2 deletions packages/overlay/src/ActiveOverlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ const stateTransition = (
return stateMachine.states[state].on[event] || state;
};

const parentOverlayOf = (el: Element): ActiveOverlay | null => {
const closestOverlay = el.closest('active-overlay');
if (closestOverlay) {
return closestOverlay;
}
const rootNode = el.getRootNode() as ShadowRoot;
if (rootNode.host) {
return parentOverlayOf(rootNode.host);
}
return null;
};

/**
* @element active-overlay
*
Expand Down Expand Up @@ -216,7 +228,7 @@ export class ActiveOverlay extends SpectrumElement {

public feature(): void {
this.tabIndex = -1;
const parentOverlay = this.trigger.closest('active-overlay');
const parentOverlay = parentOverlayOf(this.trigger);
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.
Expand All @@ -237,7 +249,7 @@ export class ActiveOverlay extends SpectrumElement {
this.removeAttribute('slot');
// Obscure upto and including the next modal root.
if (this.interaction !== 'modal') {
const parentOverlay = this.trigger.closest('active-overlay');
const parentOverlay = parentOverlayOf(this.trigger);
this._modalRoot = parentOverlay?.obscure(
nextOverlayInteraction
);
Expand Down
72 changes: 72 additions & 0 deletions packages/overlay/stories/overlay-story-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import '@spectrum-web-components/popover/sp-popover.js';
import '@spectrum-web-components/radio/sp-radio.js';
import '@spectrum-web-components/radio/sp-radio-group.js';
import '@spectrum-web-components/overlay/overlay-trigger.js';
import { Picker } from '@spectrum-web-components/picker';

// Prevent infinite recursion in browser
const MAX_DEPTH = 7;
Expand Down Expand Up @@ -304,3 +305,74 @@ class RecursivePopover extends LitElement {
}
}
customElements.define('recursive-popover', RecursivePopover);

export class PopoverContent extends LitElement {
@query('sp-picker')
public picker!: Picker;

render(): TemplateResult {
return html`
<sp-field-label for="picker1">Test</sp-field-label>
<sp-picker id="picker1" focusable .label=${'test'}>
<sp-menu-item value=${'timeSinceLastSynced'}>
${'test'}
</sp-menu-item>
<sp-menu-item value=${'timeSinceLastSynced'}>
${'test'}
</sp-menu-item>
<sp-menu-item value=${'timeSinceLastSynced'}>
${'test'}
</sp-menu-item>
<sp-menu-item value=${'timeSinceLastSynced'}>
${'test'}
</sp-menu-item>
<sp-menu-item value=${'timeSinceLastSynced'}>
${'test'}
</sp-menu-item>
</sp-picker>
<sp-field-label for="picker2">Test2</sp-field-label>
<sp-picker id="picker2" focusable .label=${'test'}>
<sp-menu-item value=${'timeSinceLastSynced'}>
${'test'}
</sp-menu-item>
<sp-menu-item value=${'timeSinceLastSynced'}>
${'test'}
</sp-menu-item>
<sp-menu-item value=${'timeSinceLastSynced'}>
${'test'}
</sp-menu-item>
<sp-menu-item value=${'timeSinceLastSynced'}>
${'test'}
</sp-menu-item>
<sp-menu-item value=${'timeSinceLastSynced'}>
${'test'}
</sp-menu-item>
</sp-picker>
<sp-field-label for="picker3">Test 3</sp-field-label>
<sp-picker id="picker3" focusable .label=${'test'}>
<sp-menu-item value=${'timeSinceLastSynced'}>
${'test'}
</sp-menu-item>
<sp-menu-item value=${'timeSinceLastSynced'}>
${'test'}
</sp-menu-item>
<sp-menu-item value=${'timeSinceLastSynced'}>
${'test'}
</sp-menu-item>
<sp-menu-item value=${'timeSinceLastSynced'}>
${'test'}
</sp-menu-item>
<sp-menu-item value=${'timeSinceLastSynced'}>
${'test'}
</sp-menu-item>
</sp-picker>
`;
}
}

customElements.define('popover-content', PopoverContent);
11 changes: 11 additions & 0 deletions packages/overlay/stories/overlay.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,3 +792,14 @@ export const detachedElement = (): TemplateResult => {
</sp-action-button>
`;
};

export const definedOverlayElement = (): TemplateResult => {
return html`
<overlay-trigger placement="bottom" type="modal">
<sp-button variant="primary" slot="trigger">Open popover</sp-button>
<sp-popover slot="click-content" direction="bottom" dialog>
<popover-content></popover-content>
</sp-popover>
</overlay-trigger>
`;
};
35 changes: 32 additions & 3 deletions packages/overlay/test/overlay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import {
nextFrame,
} from '@open-wc/testing';
import { executeServerCommand, sendKeys } from '@web/test-runner-commands';
import { virtualElement } from '../stories/overlay.stories';
import {
virtualElement,
definedOverlayElement,
} from '../stories/overlay.stories';
import { PopoverContent } from '../stories/overlay-story-components.js';

describe('Overlays', () => {
let testDiv!: HTMLDivElement;
Expand Down Expand Up @@ -520,8 +524,8 @@ describe('Overlays', () => {
content.remove();
});
});
describe('Overlay - contextmenu', () => {
it('closes "modal" overlays on `contextmenu` and passes that to the underlying page', async () => {
describe('Overlay - type="modal"', () => {
it('closes on `contextmenu` and passes that to the underlying page', async () => {
await fixture<HTMLDivElement>(html`
${virtualElement({
...virtualElement.args,
Expand Down Expand Up @@ -591,6 +595,31 @@ describe('Overlay - contextmenu', () => {
expect(secondOverlay.isConnected).to.be.true;
expect(secondHeadline.textContent).to.equal('Menu source: start');
});
it('opens children in the modal stack through shadow roots', async () => {
const el = await fixture<OverlayTrigger>(definedOverlayElement());
const trigger = el.querySelector(
'[slot="trigger"]'
) as HTMLButtonElement;
let open = oneEvent(el, 'sp-opened');
trigger.click();
await open;
const content = document.querySelector(
'popover-content'
) as PopoverContent;
open = oneEvent(content, 'sp-opened');
content.picker.click();
await open;
const activeOverlays = document.querySelectorAll('active-overlay');
activeOverlays.forEach((overlay) => {
expect(overlay.slot).to.equal('open');
});
let close = oneEvent(content, 'sp-closed');
content.picker.open = false;
await close;
close = oneEvent(el, 'sp-closed');
el.removeAttribute('open');
await close;
});
});
describe('Overlay - timing', () => {
it('manages multiple modals in a row without preventing them from closing', async () => {
Expand Down

0 comments on commit 27f232c

Please sign in to comment.