Skip to content

Commit

Permalink
fix(overlay): surface "overlay" property to "sp-opened" and "sp-close…
Browse files Browse the repository at this point in the history
…d" events
  • Loading branch information
Westbrook committed Jan 10, 2024
1 parent 26fa692 commit 957f8e9
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 69 deletions.
14 changes: 13 additions & 1 deletion packages/overlay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,19 @@ The `type` of an Overlay outlines a number of things about the interaction model

### Events

When fully open the `<sp-overlay>` element will dispatch the `sp-opened` event, and when fully closed the `sp-closed` event will be dispatched. "Fully" in this context means that all CSS transitions that have dispatched `transitionrun` events on the direct children of the `<sp-overlay>` element have successfully dispatched their `transitionend` or `transitioncancel` event. Keep in mind the following:
When fully open the `<sp-overlay>` element will dispatch the `sp-opened` event, and when fully closed the `sp-closed` event will be dispatched. Both of these events are of type:

```ts
type OverlayStateEvent = Event & {
overlay: Overlay;
};
```

The `overlay` value in this case will hold a reference to the actual `<sp-overlay>` that is opening or closing to trigger this event. Remember that some `<sp-overlay>` element (like those creates via the imperative API) can be transiently available in the DOM, so if you choose to build a cache of Overlay elements to some end, be sure to leverage a weak reference so that the `<sp-overlay>` can be garbage collected as desired by the browser.

#### When it is "fully" open or closed?

"Fully" in this context means that all CSS transitions that have dispatched `transitionrun` events on the direct children of the `<sp-overlay>` element have successfully dispatched their `transitionend` or `transitioncancel` event. Keep in mind the following:

- `transition*` events bubble; this means that while transition events on light DOM content of those direct children will be heard, those events will not be taken into account
- `transition*` events are not composed; this means that transition events on shadow DOM content of the direct children will not propagate to a level in the DOM where they can be heard
Expand Down
38 changes: 38 additions & 0 deletions packages/overlay/src/AbstractOverlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
OverlayState,
OverlayTypes,
Placement,
TriggerInteractions,
TriggerInteractionsV1,
} from './overlay-types.js';
import type { Overlay } from './Overlay.js';
Expand Down Expand Up @@ -54,6 +55,43 @@ export class BeforetoggleOpenEvent extends Event {
}
}

export class OverlayStateEvent extends Event {
detail!: {
interaction: string;
reason?: 'external-click';
};

constructor(
type: string,
public overlay: HTMLElement,
{
publish,
interaction,
reason,
}: {
publish?: boolean;
interaction: TriggerInteractions;
reason?: 'external-click';
}
) {
super(type, {
bubbles: publish,
composed: publish,
});
this.detail = {
interaction,
reason,
};
}
}

declare global {
interface GlobalEventHandlersEventMap {
'sp-open': OverlayStateEvent;
'sp-close': OverlayStateEvent;
}
}

/**
* Apply a "transitionend" listener to an element that may not transition but
* guarantee the callback will be fired either way.
Expand Down
33 changes: 14 additions & 19 deletions packages/overlay/src/OverlayDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,12 @@ import {
firstFocusableSlottedIn,
} from '@spectrum-web-components/shared/src/first-focusable-in.js';
import { VirtualTrigger } from './VirtualTrigger.js';
import {
Constructor,
OpenableElement,
OverlayOpenCloseDetail,
} from './overlay-types.js';
import { Constructor, OpenableElement } from './overlay-types.js';
import {
BeforetoggleClosedEvent,
BeforetoggleOpenEvent,
guaranteedAllTransitionend,
OverlayStateEvent,
} from './AbstractOverlay.js';
import type { AbstractOverlay } from './AbstractOverlay.js';
import { userFocusableSelector } from '@spectrum-web-components/shared';
Expand Down Expand Up @@ -101,10 +98,9 @@ export function OverlayDialog<T extends Constructor<AbstractOverlay>>(
const eventName = targetOpenState ? 'sp-opened' : 'sp-closed';
if (index > 0) {
el.dispatchEvent(
new CustomEvent<OverlayOpenCloseDetail>(eventName, {
bubbles: false,
composed: false,
detail: { interaction: this.type },
new OverlayStateEvent(eventName, this, {
interaction: this.type,
publish: false,
})
);
return;
Expand All @@ -118,23 +114,22 @@ export function OverlayDialog<T extends Constructor<AbstractOverlay>>(
const hasVirtualTrigger =
this.triggerElement instanceof VirtualTrigger;
this.dispatchEvent(
new Event(eventName, {
bubbles: hasVirtualTrigger,
composed: hasVirtualTrigger,
new OverlayStateEvent(eventName, this, {
interaction: this.type,
publish: hasVirtualTrigger,
})
);
el.dispatchEvent(
new Event(eventName, {
bubbles: false,
composed: false,
new OverlayStateEvent(eventName, this, {
interaction: this.type,
publish: false,
})
);
if (this.triggerElement && !hasVirtualTrigger) {
(this.triggerElement as HTMLElement).dispatchEvent(
new CustomEvent<OverlayOpenCloseDetail>(eventName, {
bubbles: true,
composed: true,
detail: { interaction: this.type },
new OverlayStateEvent(eventName, this, {
interaction: this.type,
publish: true,
})
);
}
Expand Down
26 changes: 10 additions & 16 deletions packages/overlay/src/OverlayNoPopover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,13 @@ import {
} from '@spectrum-web-components/shared/src/first-focusable-in.js';
import type { SpectrumElement } from '@spectrum-web-components/base';
import { VirtualTrigger } from './VirtualTrigger.js';
import {
Constructor,
OpenableElement,
OverlayOpenCloseDetail,
} from './overlay-types.js';
import { Constructor, OpenableElement } from './overlay-types.js';
import {
BeforetoggleClosedEvent,
BeforetoggleOpenEvent,
forcePaint,
guaranteedAllTransitionend,
OverlayStateEvent,
overlayTimer,
} from './AbstractOverlay.js';
import type { AbstractOverlay } from './AbstractOverlay.js';
Expand Down Expand Up @@ -100,10 +97,8 @@ export function OverlayNoPopover<T extends Constructor<AbstractOverlay>>(
}
const eventName = targetOpenState ? 'sp-opened' : 'sp-closed';
el.dispatchEvent(
new CustomEvent<OverlayOpenCloseDetail>(eventName, {
bubbles: false,
composed: false,
detail: { interaction: this.type },
new OverlayStateEvent(eventName, this, {
interaction: this.type,
})
);
if (index > 0) {
Expand All @@ -112,17 +107,16 @@ export function OverlayNoPopover<T extends Constructor<AbstractOverlay>>(
const hasVirtualTrigger =
this.triggerElement instanceof VirtualTrigger;
this.dispatchEvent(
new Event(eventName, {
bubbles: hasVirtualTrigger,
composed: hasVirtualTrigger,
new OverlayStateEvent(eventName, this, {
interaction: this.type,
publish: hasVirtualTrigger,
})
);
if (this.triggerElement && !hasVirtualTrigger) {
(this.triggerElement as HTMLElement).dispatchEvent(
new CustomEvent<OverlayOpenCloseDetail>(eventName, {
bubbles: true,
composed: true,
detail: { interaction: this.type },
new OverlayStateEvent(eventName, this, {
interaction: this.type,
publish: true,
})
);
}
Expand Down
39 changes: 15 additions & 24 deletions packages/overlay/src/OverlayPopover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,13 @@ import {
} from '@spectrum-web-components/shared/src/first-focusable-in.js';
import type { SpectrumElement } from '@spectrum-web-components/base';
import { VirtualTrigger } from './VirtualTrigger.js';
import {
Constructor,
OpenableElement,
OverlayOpenCloseDetail,
} from './overlay-types.js';
import { Constructor, OpenableElement } from './overlay-types.js';
import {
BeforetoggleClosedEvent,
BeforetoggleOpenEvent,
guaranteedAllTransitionend,
nextFrame,
OverlayStateEvent,
overlayTimer,
} from './AbstractOverlay.js';
import type { AbstractOverlay } from './AbstractOverlay.js';
Expand Down Expand Up @@ -173,10 +170,9 @@ export function OverlayPopover<T extends Constructor<AbstractOverlay>>(
: 'sp-closed';
if (index > 0) {
el.dispatchEvent(
new CustomEvent<OverlayOpenCloseDetail>(eventName, {
bubbles: false,
composed: false,
detail: { interaction: this.type },
new OverlayStateEvent(eventName, this, {
interaction: this.type,
publish: false,
})
);
return;
Expand All @@ -189,28 +185,23 @@ export function OverlayPopover<T extends Constructor<AbstractOverlay>>(
const hasVirtualTrigger =
this.triggerElement instanceof VirtualTrigger;
this.dispatchEvent(
new Event(eventName, {
bubbles: hasVirtualTrigger,
composed: hasVirtualTrigger,
new OverlayStateEvent(eventName, this, {
interaction: this.type,
publish: hasVirtualTrigger,
})
);
el.dispatchEvent(
new CustomEvent<OverlayOpenCloseDetail>(eventName, {
bubbles: false,
composed: false,
detail: { interaction: this.type },
new OverlayStateEvent(eventName, this, {
interaction: this.type,
publish: false,
})
);
if (this.triggerElement && !hasVirtualTrigger) {
(this.triggerElement as HTMLElement).dispatchEvent(
new CustomEvent<OverlayOpenCloseDetail>(
eventName,
{
bubbles: true,
composed: true,
detail: { interaction: this.type },
}
)
new OverlayStateEvent(eventName, this, {
interaction: this.type,
publish: true,
})
);
}
this.state = targetOpenState ? 'opened' : 'closed';
Expand Down
7 changes: 0 additions & 7 deletions packages/overlay/src/overlay-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,6 @@ export type OverlayOptionsV1 = {
virtualTrigger?: VirtualTrigger;
};

declare global {
interface GlobalEventHandlersEventMap {
'sp-open': CustomEvent<OverlayOpenCloseDetail>;
'sp-close': CustomEvent<OverlayOpenCloseDetail>;
}
}

export type OpenableElement = HTMLElement & {
open: boolean;
tipElement?: HTMLElement;
Expand Down
6 changes: 4 additions & 2 deletions packages/overlay/test/overlay-element.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,8 @@ describe('sp-overlay', () => {

const opened = oneEvent(el, 'sp-opened');
el.open = true;
await opened;
let { overlay } = await opened;
expect(el === overlay).to.be.true;

await sendMouse({
steps: [
Expand All @@ -830,7 +831,8 @@ describe('sp-overlay', () => {

const closed = oneEvent(el, 'sp-closed');
el.open = false;
await closed;
({ overlay } = await closed);
expect(el === overlay).to.be.true;

expect(el.open).to.be.false;
});
Expand Down

0 comments on commit 957f8e9

Please sign in to comment.