diff --git a/packages/overlay/local.d.ts b/packages/overlay/local.d.ts index c6237fd608..71412d67ff 100644 --- a/packages/overlay/local.d.ts +++ b/packages/overlay/local.d.ts @@ -20,9 +20,9 @@ declare module '@popperjs/core/dist/esm/popper-lite.js' { } declare module '@popperjs/core/dist/esm/types.js' { - import { Instance } from '@popperjs/core/lib/types.js'; + import { Instance, VirtualElement } from '@popperjs/core/lib/types.js'; - export { Instance }; + export { Instance, VirtualElement }; } declare module '@popperjs/core/dist/esm/enums.js' { diff --git a/packages/overlay/src/ActiveOverlay.ts b/packages/overlay/src/ActiveOverlay.ts index 433d01fa2f..9f316f3d57 100644 --- a/packages/overlay/src/ActiveOverlay.ts +++ b/packages/overlay/src/ActiveOverlay.ts @@ -28,6 +28,7 @@ import { TriggerInteractions, } from './overlay-types.js'; import { applyMaxSize, createPopper, Instance, maxSize } from './popper.js'; +import { VirtualTrigger } from './VirtualTrigger.js'; export interface PositionResult { arrowOffsetLeft: number; @@ -115,6 +116,7 @@ export class ActiveOverlay extends SpectrumElement { public overlayContent!: HTMLElement; public overlayContentTip?: HTMLElement; public trigger!: HTMLElement; + public virtualTrigger?: VirtualTrigger; private popper?: Instance; @@ -250,25 +252,29 @@ export class ActiveOverlay extends SpectrumElement { if (!this.overlayContent || !this.trigger) return; if (this.placement && this.placement !== 'none') { - this.popper = createPopper(this.trigger, this, { - placement: this.placement, - modifiers: [ - maxSize, - applyMaxSize, - { - name: 'arrow', - options: { - element: this.overlayContentTip, + this.popper = createPopper( + this.virtualTrigger || this.trigger, + this, + { + placement: this.placement, + modifiers: [ + maxSize, + applyMaxSize, + { + name: 'arrow', + options: { + element: this.overlayContentTip, + }, }, - }, - { - name: 'offset', - options: { - offset: [0, this.offset], + { + name: 'offset', + options: { + offset: [0, this.offset], + }, }, - }, - ], - }); + ], + } + ); } this.state = 'active'; @@ -324,6 +330,7 @@ export class ActiveOverlay extends SpectrumElement { this.overlayContent = detail.content; this.overlayContentTip = detail.contentTip; this.trigger = detail.trigger; + this.virtualTrigger = detail.virtualTrigger; this.placement = detail.placement; this.offset = detail.offset; this.interaction = detail.interaction; diff --git a/packages/overlay/src/VirtualTrigger.ts b/packages/overlay/src/VirtualTrigger.ts new file mode 100644 index 0000000000..bf375834f9 --- /dev/null +++ b/packages/overlay/src/VirtualTrigger.ts @@ -0,0 +1,45 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import type { VirtualElement } from './popper.js'; +import { Overlay } from './overlay.js'; + +export class VirtualTrigger implements VirtualElement { + private x = 0; + private y = 0; + + public constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + + public updateBoundingClientRect(x: number, y: number): void { + this.x = x; + this.y = y; + Overlay.update(); + } + + public getBoundingClientRect(): DOMRect { + return { + width: 0, + height: 0, + top: this.y, + right: this.x, + y: this.y, + x: this.x, + bottom: this.y, + left: this.x, + toJSON() { + return; + }, + }; + } +} diff --git a/packages/overlay/src/index.ts b/packages/overlay/src/index.ts index b0c1c23bc1..ce36445eb8 100644 --- a/packages/overlay/src/index.ts +++ b/packages/overlay/src/index.ts @@ -14,3 +14,4 @@ export * from './OverlayTrigger.js'; export * from './overlay-types.js'; export * from './ActiveOverlay.js'; export * from './loader.js'; +export * from './VirtualTrigger.js'; diff --git a/packages/overlay/src/overlay-types.ts b/packages/overlay/src/overlay-types.ts index 2b34286f18..4ff5eb6cdf 100644 --- a/packages/overlay/src/overlay-types.ts +++ b/packages/overlay/src/overlay-types.ts @@ -12,6 +12,7 @@ governing permissions and limitations under the License. import { ThemeData } from '@spectrum-web-components/theme'; import { Placement as PopperPlacement } from './popper'; +import { VirtualTrigger } from './VirtualTrigger.js'; export type TriggerInteractions = | 'click' @@ -29,6 +30,7 @@ export interface OverlayOpenDetail { offset: number; placement?: Placement; receivesFocus?: 'auto'; + virtualTrigger?: VirtualTrigger; trigger: HTMLElement; interaction: TriggerInteractions; theme: ThemeData; @@ -59,6 +61,7 @@ export type OverlayOptions = { receivesFocus?: 'auto'; notImmediatelyClosable?: boolean; abortPromise?: Promise; + virtualTrigger?: VirtualTrigger; }; declare global { diff --git a/packages/overlay/src/overlay.ts b/packages/overlay/src/overlay.ts index 5677c046fe..28cb5413e0 100644 --- a/packages/overlay/src/overlay.ts +++ b/packages/overlay/src/overlay.ts @@ -96,6 +96,7 @@ export class Overlay { placement = 'top', receivesFocus, notImmediatelyClosable, + virtualTrigger, }: OverlayOptions): Promise { /* c8 ignore next */ if (this.isOpen) return true; @@ -140,6 +141,7 @@ export class Overlay { theme: queryThemeDetail, receivesFocus, notImmediatelyClosable, + virtualTrigger, ...overlayDetailQuery, }); this.isOpen = true; diff --git a/packages/overlay/src/popper.ts b/packages/overlay/src/popper.ts index 595d69f347..73b6d693a6 100644 --- a/packages/overlay/src/popper.ts +++ b/packages/overlay/src/popper.ts @@ -24,7 +24,10 @@ import { defaultModifiers, popperGenerator, } from '@popperjs/core/dist/esm/popper-lite.js'; -import type { Instance } from '@popperjs/core/dist/esm/types.js'; +import type { + Instance, + VirtualElement, +} from '@popperjs/core/dist/esm/types.js'; import maxSize from 'popper-max-size-modifier'; import { applyMaxSize } from './apply-max-size.js'; @@ -38,5 +41,5 @@ export const createPopper = popperGenerator({ ], }); -export type { Instance, Placement }; +export type { Instance, Placement, VirtualElement }; export { maxSize, applyMaxSize }; diff --git a/packages/overlay/stories/overlay.stories.ts b/packages/overlay/stories/overlay.stories.ts index 5e70de26ea..9327afca50 100644 --- a/packages/overlay/stories/overlay.stories.ts +++ b/packages/overlay/stories/overlay.stories.ts @@ -9,7 +9,13 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ import { html, TemplateResult, ifDefined } from '@spectrum-web-components/base'; -import { OverlayContentTypes, OverlayTrigger, Placement } from '../'; +import { + openOverlay, + OverlayContentTypes, + OverlayTrigger, + Placement, + VirtualTrigger, +} from '../'; import '@spectrum-web-components/action-button/sp-action-button.js'; import '@spectrum-web-components/action-group/sp-action-group.js'; import '@spectrum-web-components/button/sp-button.js'; @@ -20,6 +26,7 @@ import '@spectrum-web-components/icons-workflow/icons/sp-icon-magnify.js'; import '@spectrum-web-components/overlay/overlay-trigger.js'; import { Picker } from '@spectrum-web-components/picker'; import '@spectrum-web-components/picker/sp-picker.js'; +import '@spectrum-web-components/menu/sp-menu.js'; import '@spectrum-web-components/menu/sp-menu-item.js'; import '@spectrum-web-components/menu/sp-menu-divider.js'; import '@spectrum-web-components/popover/sp-popover.js'; @@ -637,3 +644,51 @@ export const superComplexModal = (): TemplateResult => { `; }; + +export const virtualElement = (args: Properties): TemplateResult => { + const pointerenter = async (event: PointerEvent): Promise => { + event.preventDefault(); + const trigger = event.target as HTMLElement; + const virtualTrigger = new VirtualTrigger(event.clientX, event.clientY); + openOverlay( + trigger, + 'modal', + trigger.nextElementSibling as HTMLElement, + { + placement: args.placement, + receivesFocus: 'auto', + virtualTrigger, + } + ); + }; + return html` + +
+ + event.target?.dispatchEvent( + new Event('close', { bubbles: true }) + )} + > + + Deselect + Select inverse + Feather... + Select and mask... + + Save selection + Make work path + + + `; +}; + +virtualElement.args = { + placement: 'right-end', +};