diff --git a/packages/overlay/package.json b/packages/overlay/package.json
index b0500e5a3a8..14d78ac0b74 100644
--- a/packages/overlay/package.json
+++ b/packages/overlay/package.json
@@ -171,6 +171,7 @@
"@spectrum-web-components/reactive-controllers": "1.7.0",
"@spectrum-web-components/shared": "1.7.0",
"@spectrum-web-components/theme": "1.7.0",
+ "@spectrum-web-components/underlay": "1.7.0",
"focus-trap": "^7.6.4"
},
"types": "./src/index.d.ts",
diff --git a/packages/overlay/src/Overlay.ts b/packages/overlay/src/Overlay.ts
index 8535f4db638..c78a0545f1a 100644
--- a/packages/overlay/src/Overlay.ts
+++ b/packages/overlay/src/Overlay.ts
@@ -55,6 +55,7 @@ import {
import styles from './overlay.css.js';
import { FocusTrap } from 'focus-trap';
+import '@spectrum-web-components/underlay/sp-underlay.js';
const browserSupportsPopover = 'showPopover' in document.createElement('div');
@@ -420,9 +421,9 @@ export class Overlay extends ComputedOverlayBase {
* Determines the value for the popover attribute based on the overlay type.
*
* @private
- * @returns {'auto' | 'manual' | undefined} The popover value or undefined if not applicable.
+ * @returns {'auto' | 'manual' | 'hint' | undefined} The popover value or undefined if not applicable.
*/
- private get popoverValue(): 'auto' | 'manual' | undefined {
+ private get popoverValue(): 'auto' | 'manual' | 'hint' | undefined {
const hasPopoverAttribute = 'popover' in this;
if (!hasPopoverAttribute) {
@@ -431,10 +432,8 @@ export class Overlay extends ComputedOverlayBase {
switch (this.type) {
case 'modal':
- return 'auto';
- case 'page':
return 'manual';
- case 'hint':
+ case 'page':
return 'manual';
default:
return this.type;
@@ -542,6 +541,9 @@ export class Overlay extends ComputedOverlayBase {
const focusTrap = await import('focus-trap');
this._focusTrap = focusTrap.createFocusTrap(this.dialogEl, {
initialFocus: focusEl || undefined,
+ allowOutsideClick: (event) => {
+ return !event.isTrusted;
+ },
tabbableOptions: {
getShadowRoot: true,
},
@@ -554,7 +556,10 @@ export class Overlay extends ComputedOverlayBase {
escapeDeactivates: false,
});
- if (this.type === 'modal' || this.type === 'page') {
+ if (
+ (this.type === 'modal' || this.type === 'page') &&
+ this.receivesFocus !== 'false'
+ ) {
this._focusTrap.activate();
}
}
@@ -1141,6 +1146,19 @@ export class Overlay extends ComputedOverlayBase {
*/
public override render(): TemplateResult {
return html`
+ ${this.type === 'modal' || this.type === 'page'
+ ? html`
+
+ {
+ this.open = false;
+ }}
+ style="--spectrum-underlay-background-color: transparent"
+ >
+
+ `
+ : ''}
${this.renderPopover()}
`;
diff --git a/packages/overlay/src/OverlayStack.ts b/packages/overlay/src/OverlayStack.ts
index 4c174840f20..44193a101ea 100644
--- a/packages/overlay/src/OverlayStack.ts
+++ b/packages/overlay/src/OverlayStack.ts
@@ -1,3 +1,4 @@
+/* eslint-disable no-console */
/**
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
@@ -27,6 +28,10 @@ class OverlayStack {
stack: Overlay[] = [];
+ private originalBodyOverflow = '';
+
+ private bodyScrollBlocked = false;
+
constructor() {
this.bindEvents();
}
@@ -79,6 +84,25 @@ class OverlayStack {
this.stack.splice(overlayIndex, 1);
}
overlay.open = false;
+
+ this.manageBodyScroll();
+ }
+
+ /**
+ * Manage body scroll blocking based on modal/page overlays
+ */
+ private manageBodyScroll(): void {
+ const shouldBlock = this.stack.some(
+ (overlay) => overlay.type === 'modal' || overlay.type === 'page'
+ );
+ if (shouldBlock && !this.bodyScrollBlocked) {
+ this.originalBodyOverflow = document.body.style.overflow || '';
+ document.body.style.overflow = 'hidden';
+ this.bodyScrollBlocked = true;
+ } else if (!shouldBlock && this.bodyScrollBlocked) {
+ document.body.style.overflow = this.originalBodyOverflow;
+ this.bodyScrollBlocked = false;
+ }
}
/**
@@ -151,14 +175,14 @@ class OverlayStack {
private handleKeydown = (event: KeyboardEvent): void => {
if (event.code !== 'Escape') return;
+ if (event.defaultPrevented) return; // Don't handle if already handled
if (!this.stack.length) return;
const last = this.stack[this.stack.length - 1];
if (last?.type === 'page') {
event.preventDefault();
return;
}
- if (last?.type === 'manual') {
- // Manual overlays should close on "Escape" key, but not when losing focus or interacting with other parts of the page.
+ if (last?.type === 'manual' || last?.type === 'modal') {
this.closeOverlay(last);
return;
}
@@ -213,11 +237,29 @@ class OverlayStack {
const path = event.composedPath();
this.stack.forEach((overlayEl) => {
const inPath = path.find((el) => el === overlayEl);
+
+ // Check if the trigger element is inside this overlay
+ const triggerInOverlay =
+ overlay.triggerElement &&
+ overlay.triggerElement instanceof HTMLElement &&
+ overlayEl.contains &&
+ overlayEl.contains(overlay.triggerElement);
+ console.log(
+ 'overlayEl.type:',
+ overlayEl.type,
+ 'triggerInOverlay:',
+ triggerInOverlay,
+ 'inPath:',
+ !!inPath
+ );
+
if (
!inPath &&
+ !triggerInOverlay &&
overlayEl.type !== 'manual' &&
overlayEl.type !== 'modal'
) {
+ console.log('Closing overlay:', overlayEl);
this.closeOverlay(overlayEl);
}
});
@@ -248,6 +290,7 @@ class OverlayStack {
overlay.addEventListener('beforetoggle', this.handleBeforetoggle, {
once: true,
});
+ this.manageBodyScroll();
});
}
diff --git a/packages/overlay/stories/overlay.stories.ts b/packages/overlay/stories/overlay.stories.ts
index 52031d0cefe..e39dc249e83 100644
--- a/packages/overlay/stories/overlay.stories.ts
+++ b/packages/overlay/stories/overlay.stories.ts
@@ -33,6 +33,7 @@ import '@spectrum-web-components/overlay/overlay-trigger.js';
import '@spectrum-web-components/accordion/sp-accordion-item.js';
import '@spectrum-web-components/accordion/sp-accordion.js';
+import '@spectrum-web-components/action-menu/sp-action-menu.js';
import '@spectrum-web-components/button-group/sp-button-group.js';
import '@spectrum-web-components/menu/sp-menu-divider.js';
import '@spectrum-web-components/menu/sp-menu-group.js';
@@ -634,6 +635,208 @@ export const deepChildTooltip = (): TemplateResult => html`
`;
+export const debug = (): TemplateResult => {
+ return html`
+
+
+ Button popover
+
+
+
+
+ Deselect
+
+ Select inverse
+
+ Feather...
+
+ Select and mask...
+
+
+
+ Save selection
+
+
+ Make work path
+
+
+
+
+
+ I'm a tooltip in a different direction
+
+
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
+
+
+ Button
+
+ Popover content
+
+
+ Tooltip content
+
+
+
+ Open menu
+
+
+ Item 1
+ Item 2
+
+
+ More Actions
+ Deselect
+ Select inverse
+ Feather...
+ Select and mask...
+
+ Save selection
+ Make work path
+
+
+
+
+
+ Modal overlay
+
+
+ Open menu
+
+
+ Max 20
+
+
+
+ `;
+};
+
+// Helper functions for the overlay functionality
+const getModalOverlayContent = (): HTMLElement => {
+ const fragment = document.createDocumentFragment();
+ render(
+ html`
+
+ Modal overlay content
+ openAutoOverlay(event)}>
+ Auto overlay
+
+
+ `,
+ fragment
+ );
+ return fragment.children[0] as HTMLElement;
+};
+
+const getAutoOverlayContent = (): HTMLElement => {
+ const fragment = document.createDocumentFragment();
+ render(
+ html`
+ Auto overlay
+ `,
+ fragment
+ );
+ return fragment.children[0] as HTMLElement;
+};
+
+const openModalOverlay = async (event: Event) => {
+ const trigger = event.target as HTMLElement;
+ const overlay = await Overlay.open(getModalOverlayContent(), {
+ trigger,
+ type: 'modal',
+ placement: 'bottom',
+ });
+ const container = document.querySelector('.container');
+ container?.insertAdjacentElement('afterend', overlay);
+};
+
+const openAutoOverlay = async (event: Event) => {
+ const trigger = event.target as HTMLElement;
+ const overlay = await Overlay.open(getAutoOverlayContent(), {
+ trigger,
+ type: 'auto',
+ placement: 'bottom',
+ });
+ const container = document.querySelector('.container');
+ container?.insertAdjacentElement('afterend', overlay);
+};
export const deepNesting = (): TemplateResult => {
const color = window.__swc_hack_knobs__.defaultColor;
@@ -1068,30 +1271,6 @@ export const modalManaged = (): TemplateResult => {
`;
};
-export const modalWithinNonModal = (): TemplateResult => {
- return html`
-
-
- Open inline overlay
-
-
-
-
-
- Open modal overlay
-
-
-
- Modal overlay
-
-
-
-
-
-
- `;
-};
-
export const noCloseOnResize = (args: Properties): TemplateResult => html`