Skip to content

Commit e3b1ff2

Browse files
committed
feat: add simpler API for controlling overlays
The current implementation uses events to open and close overlays. This would be hard for users to adopt and is not easy to use in our other components that need overlay behaviour. This change refactors how our overlays are opened and closed to address issue #321.
1 parent fa417db commit e3b1ff2

File tree

9 files changed

+466
-251
lines changed

9 files changed

+466
-251
lines changed

packages/overlay/src/active-overlay.ts

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
Placement,
1515
OverlayOpenDetail,
1616
TriggerInteractions,
17-
} from './overlay.js';
17+
} from './overlay-types.js';
1818
import calculatePosition, { PositionResult } from './calculate-position.js';
1919
import { Size, Color } from '@spectrum-web-components/theme';
2020
import {
@@ -114,7 +114,7 @@ export class ActiveOverlay extends LitElement {
114114
public trigger?: HTMLElement;
115115

116116
private placeholder?: Comment;
117-
private root?: HTMLElement;
117+
private root?: HTMLElement = document.body;
118118

119119
@property()
120120
public _state = stateTransition();
@@ -157,9 +157,9 @@ export class ActiveOverlay extends LitElement {
157157
return [styles];
158158
}
159159

160-
private open(openEvent: CustomEvent<OverlayOpenDetail>): void {
161-
this.extractEventDetail(openEvent);
162-
this.stealOverlayContent(openEvent.detail.content);
160+
private open(openDetail: OverlayOpenDetail): void {
161+
this.extractDetail(openDetail);
162+
this.stealOverlayContent(openDetail.content);
163163

164164
/* istanbul ignore if */
165165
if (!this.overlayContent) return;
@@ -169,7 +169,7 @@ export class ActiveOverlay extends LitElement {
169169
this.timeout = window.setTimeout(() => {
170170
this.state = 'visible';
171171
delete this.timeout;
172-
}, openEvent.detail.delay);
172+
}, openDetail.delay);
173173

174174
this.hiddenDeferred = new Deferred<void>();
175175
this.addEventListener('animationend', this.onAnimationEnd);
@@ -178,14 +178,14 @@ export class ActiveOverlay extends LitElement {
178178
});
179179
}
180180

181-
private extractEventDetail(event: CustomEvent<OverlayOpenDetail>): void {
182-
this.overlayContent = event.detail.content;
183-
this.trigger = event.detail.trigger;
184-
this.placement = event.detail.placement;
185-
this.offset = event.detail.offset;
186-
this.interaction = event.detail.interaction;
187-
this.color = event.detail.theme.color;
188-
this.size = event.detail.theme.size;
181+
private extractDetail(detail: OverlayOpenDetail): void {
182+
this.overlayContent = detail.content;
183+
this.trigger = detail.trigger;
184+
this.placement = detail.placement;
185+
this.offset = detail.offset;
186+
this.interaction = detail.interaction;
187+
this.color = detail.theme.color;
188+
this.size = detail.theme.size;
189189
}
190190

191191
public dispose(): void {
@@ -327,16 +327,12 @@ export class ActiveOverlay extends LitElement {
327327
return this.hasTheme ? this.renderTheme(content) : content;
328328
}
329329

330-
public static create(
331-
openEvent: CustomEvent<OverlayOpenDetail>,
332-
root: HTMLElement
333-
): ActiveOverlay {
330+
public static create(details: OverlayOpenDetail): ActiveOverlay {
334331
const overlay = new ActiveOverlay();
335332

336333
/* istanbul ignore else */
337-
if (openEvent.detail.content) {
338-
overlay.root = root;
339-
overlay.open(openEvent);
334+
if (details.content) {
335+
overlay.open(details);
340336
}
341337

342338
return overlay;

packages/overlay/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import { OverlayTrigger } from './overlay-trigger.js';
1313
import { ActiveOverlay } from './active-overlay.js';
1414

1515
export * from './overlay.js';
16-
export * from './overlay-root.js';
1716
export * from './overlay-trigger.js';
17+
export * from './overlay-types';
1818

1919
/* istanbul ignore else */
2020
if (!customElements.get('overlay-trigger')) {

packages/overlay/src/overlay-root.ts

Lines changed: 0 additions & 69 deletions
This file was deleted.

packages/overlay/src/overlay-stack.ts

Lines changed: 58 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ governing permissions and limitations under the License.
1111
*/
1212

1313
import { ActiveOverlay } from './active-overlay.js';
14-
import { OverlayOpenDetail, OverlayCloseDetail } from './overlay.js';
14+
import { OverlayOpenDetail } from './overlay-types';
1515

1616
function isLeftClick(event: MouseEvent): boolean {
1717
return event.button === 0;
@@ -22,19 +22,11 @@ function hasModifier(event: MouseEvent): boolean {
2222
}
2323

2424
export class OverlayStack {
25-
public overlays: ActiveOverlay[] = [];
26-
2725
private preventMouseRootClose = false;
2826
private root: HTMLElement = document.body;
29-
private onChange: (overlays: ActiveOverlay[]) => void;
3027
private handlingResize = false;
3128

32-
public constructor(
33-
root: HTMLElement,
34-
onChange: (overlays: ActiveOverlay[]) => void
35-
) {
36-
this.root = root;
37-
this.onChange = onChange;
29+
public constructor() {
3830
this.addEventListeners();
3931
}
4032

@@ -43,7 +35,44 @@ export class OverlayStack {
4335
}
4436

4537
private get topOverlay(): ActiveOverlay | undefined {
46-
return this.overlays.slice(-1)[0];
38+
for (
39+
let index = document.body.children.length - 1;
40+
index >= 0;
41+
index--
42+
) {
43+
const element = document.body.children[index];
44+
if (element instanceof ActiveOverlay) {
45+
return element;
46+
}
47+
}
48+
}
49+
50+
private *overlays(): Generator<ActiveOverlay, void, undefined> {
51+
for (const item of document.body.children) {
52+
if (item instanceof ActiveOverlay) {
53+
yield item;
54+
}
55+
}
56+
}
57+
58+
private findOverlayForContent(
59+
overlayContent: HTMLElement
60+
): ActiveOverlay | undefined {
61+
for (const item of this.overlays()) {
62+
if (overlayContent.isSameNode(item.overlayContent as HTMLElement)) {
63+
return item;
64+
}
65+
}
66+
}
67+
68+
private findOverlayForTrigger(
69+
overlayContent: HTMLElement
70+
): ActiveOverlay | undefined {
71+
for (const item of this.overlays()) {
72+
if (overlayContent.isSameNode(item.trigger as HTMLElement)) {
73+
return item;
74+
}
75+
}
4776
}
4877

4978
private addEventListeners(): void {
@@ -54,47 +83,37 @@ export class OverlayStack {
5483
}
5584

5685
private isOverlayActive(overlayContent: HTMLElement): boolean {
57-
return !!this.overlays.find((item) =>
58-
overlayContent.isSameNode(item.overlayContent as HTMLElement)
59-
);
86+
return !!this.findOverlayForContent(overlayContent);
6087
}
6188

6289
private isClickOverlayActiveForTrigger(trigger: HTMLElement): boolean {
63-
return this.overlays.some(
64-
(item) =>
65-
trigger.isSameNode(item.trigger as HTMLElement) &&
66-
item.interaction === 'click'
67-
);
90+
const overlay = this.findOverlayForTrigger(trigger);
91+
return overlay != null && overlay.interaction === 'click';
6892
}
6993

70-
public openOverlay(event: CustomEvent<OverlayOpenDetail>): void {
71-
if (this.isOverlayActive(event.detail.content)) return;
94+
public openOverlay(details: OverlayOpenDetail): void {
95+
/* istanbul ignore if */
96+
if (this.isOverlayActive(details.content)) return;
7297

7398
requestAnimationFrame(() => {
74-
const interaction = event.detail.interaction;
75-
if (interaction === 'click') {
99+
if (details.interaction === 'click') {
76100
this.closeAllHoverOverlays();
77101
} else if (
78-
interaction === 'hover' &&
79-
this.isClickOverlayActiveForTrigger(event.detail.trigger)
102+
details.interaction === 'hover' &&
103+
this.isClickOverlayActiveForTrigger(details.trigger)
80104
) {
81105
// Don't show a hover popover if the click popover is already active
82106
return;
83107
}
84108

85-
const activeOverlay = ActiveOverlay.create(event, this.root);
86-
this.overlays.push(activeOverlay);
87-
88-
this.onChange(this.overlays);
109+
const activeOverlay = ActiveOverlay.create(details);
110+
document.body.appendChild(activeOverlay);
89111
});
90112
}
91113

92-
public closeOverlay(event: CustomEvent<OverlayCloseDetail>): void {
114+
public closeOverlay(content: HTMLElement): void {
93115
requestAnimationFrame(() => {
94-
const overlayContent = event.detail.content;
95-
const overlay = this.overlays.find((item) =>
96-
overlayContent.isSameNode(item.overlayContent as HTMLElement)
97-
);
116+
const overlay = this.findOverlayForContent(content);
98117
this.hideAndCloseOverlay(overlay);
99118
});
100119
}
@@ -123,7 +142,7 @@ export class OverlayStack {
123142
};
124143

125144
private closeAllHoverOverlays(): void {
126-
for (const overlay of this.overlays) {
145+
for (const overlay of this.overlays()) {
127146
if (overlay.interaction === 'hover') {
128147
this.hideAndCloseOverlay(overlay);
129148
}
@@ -133,13 +152,8 @@ export class OverlayStack {
133152
private async hideAndCloseOverlay(overlay?: ActiveOverlay): Promise<void> {
134153
if (overlay) {
135154
await overlay.hide();
136-
const index = this.overlays.indexOf(overlay);
137-
/* istanbul ignore else */
138-
if (index >= 0) {
139-
this.overlays[index].dispose();
140-
this.overlays.splice(index, 1);
141-
}
142-
this.onChange(this.overlays);
155+
overlay.remove();
156+
overlay.dispose();
143157
}
144158
}
145159

@@ -164,9 +178,9 @@ export class OverlayStack {
164178

165179
this.handlingResize = true;
166180
requestAnimationFrame(() => {
167-
this.overlays.forEach((overlay) => {
181+
for (const overlay of this.overlays()) {
168182
overlay.updateOverlayPosition();
169-
});
183+
}
170184
this.handlingResize = false;
171185
});
172186
};

0 commit comments

Comments
 (0)