@spectrum-web-components/overlay
Advanced tools
Comparing version 0.1.0 to 0.2.0
@@ -6,2 +6,8 @@ # Change Log | ||
# [0.2.0](https://github.com/adobe/spectrum-web-components/compare/@spectrum-web-components/overlay@0.1.0...@spectrum-web-components/overlay@0.2.0) (2020-01-30) | ||
### Features | ||
- rework overlays to use popper ([e17d1bb](https://github.com/adobe/spectrum-web-components/commit/e17d1bb)) | ||
# 0.1.0 (2020-01-06) | ||
@@ -8,0 +14,0 @@ |
{ | ||
"version": 2, | ||
"version": "experimental", | ||
"tags": [ | ||
@@ -7,15 +7,17 @@ { | ||
"description": "A overlay trigger component for displaying overlays relative to other content.", | ||
"jsDoc": "/**\n * A overlay trigger component for displaying overlays relative to other content.\n * @element overlay-trigger\n *\n * @slot hover-content - The content that will be displayed on hover\n * @slot click-content - The content that will be displayed on click\n */", | ||
"attributes": [ | ||
{ | ||
"name": "placement", | ||
"type": "Placement" | ||
"type": "Placement", | ||
"default": "\"bottom\"" | ||
}, | ||
{ | ||
"name": "offset", | ||
"type": "number" | ||
"type": "number", | ||
"default": "6" | ||
}, | ||
{ | ||
"name": "disabled", | ||
"type": "boolean" | ||
"type": "boolean", | ||
"default": "false" | ||
} | ||
@@ -27,3 +29,4 @@ ], | ||
"attribute": "placement", | ||
"type": "Placement" | ||
"type": "Placement", | ||
"default": "\"bottom\"" | ||
}, | ||
@@ -33,3 +36,4 @@ { | ||
"attribute": "offset", | ||
"type": "number" | ||
"type": "number", | ||
"default": "6" | ||
}, | ||
@@ -39,24 +43,14 @@ { | ||
"attribute": "disabled", | ||
"type": "boolean" | ||
"type": "boolean", | ||
"default": "false" | ||
} | ||
], | ||
"events": [ | ||
"slots": [ | ||
{ | ||
"name": "sp-overlay-close" | ||
"name": "hover-content", | ||
"description": "The content that will be displayed on hover" | ||
}, | ||
{ | ||
"name": "sp-overlay-open" | ||
}, | ||
{ | ||
"name": "query-theme" | ||
} | ||
], | ||
"slots": [ | ||
{ | ||
"name": "click-content", | ||
"description": "The content that will be displayed on click" | ||
}, | ||
{ | ||
"name": "hover-content", | ||
"description": "The content that will be displayed on hover" | ||
} | ||
@@ -68,15 +62,17 @@ ] | ||
"description": "A overlay trigger component for displaying overlays relative to other content.", | ||
"jsDoc": "/**\n * A overlay trigger component for displaying overlays relative to other content.\n * @element overlay-trigger\n *\n * @slot hover-content - The content that will be displayed on hover\n * @slot click-content - The content that will be displayed on click\n */", | ||
"attributes": [ | ||
{ | ||
"name": "placement", | ||
"type": "Placement" | ||
"type": "Placement", | ||
"default": "\"bottom\"" | ||
}, | ||
{ | ||
"name": "offset", | ||
"type": "number" | ||
"type": "number", | ||
"default": "6" | ||
}, | ||
{ | ||
"name": "disabled", | ||
"type": "boolean" | ||
"type": "boolean", | ||
"default": "false" | ||
} | ||
@@ -88,3 +84,4 @@ ], | ||
"attribute": "placement", | ||
"type": "Placement" | ||
"type": "Placement", | ||
"default": "\"bottom\"" | ||
}, | ||
@@ -94,3 +91,4 @@ { | ||
"attribute": "offset", | ||
"type": "number" | ||
"type": "number", | ||
"default": "6" | ||
}, | ||
@@ -100,24 +98,14 @@ { | ||
"attribute": "disabled", | ||
"type": "boolean" | ||
"type": "boolean", | ||
"default": "false" | ||
} | ||
], | ||
"events": [ | ||
"slots": [ | ||
{ | ||
"name": "sp-overlay-close" | ||
"name": "hover-content", | ||
"description": "The content that will be displayed on hover" | ||
}, | ||
{ | ||
"name": "sp-overlay-open" | ||
}, | ||
{ | ||
"name": "query-theme" | ||
} | ||
], | ||
"slots": [ | ||
{ | ||
"name": "click-content", | ||
"description": "The content that will be displayed on click" | ||
}, | ||
{ | ||
"name": "hover-content", | ||
"description": "The content that will be displayed on hover" | ||
} | ||
@@ -130,4 +118,13 @@ ] | ||
{ | ||
"name": "animating", | ||
"type": "boolean", | ||
"default": "false" | ||
}, | ||
{ | ||
"name": "placement", | ||
"type": "Placement" | ||
"type": "\"auto\" | \"auto-start\" | \"auto-end\" | \"top\" | \"bottom\" | \"right\" | \"left\" | \"top-start\" | \"top-end\" | \"bottom-start\" | \"bottom-end\" | \"right-start\" | \"right-end\" | \"left-start\" | \"left-end\" | \"none\" | undefined" | ||
}, | ||
{ | ||
"name": "data-popper-placement", | ||
"type": "\"auto\" | \"auto-start\" | \"auto-end\" | \"top\" | \"bottom\" | \"right\" | \"left\" | \"top-start\" | \"top-end\" | \"bottom-start\" | \"bottom-end\" | \"right-start\" | \"right-end\" | \"left-start\" | \"left-end\" | \"none\" | undefined" | ||
} | ||
@@ -141,2 +138,6 @@ ], | ||
{ | ||
"name": "overlayContentTip", | ||
"type": "HTMLElement | undefined" | ||
}, | ||
{ | ||
"name": "trigger", | ||
@@ -150,5 +151,11 @@ "type": "HTMLElement | undefined" | ||
{ | ||
"name": "animating", | ||
"attribute": "animating", | ||
"type": "boolean", | ||
"default": "false" | ||
}, | ||
{ | ||
"name": "placement", | ||
"attribute": "placement", | ||
"type": "Placement" | ||
"type": "\"auto\" | \"auto-start\" | \"auto-end\" | \"top\" | \"bottom\" | \"right\" | \"left\" | \"top-start\" | \"top-end\" | \"bottom-start\" | \"bottom-end\" | \"right-start\" | \"right-end\" | \"left-start\" | \"left-end\" | \"none\" | undefined" | ||
}, | ||
@@ -160,12 +167,19 @@ { | ||
{ | ||
"name": "size", | ||
"name": "scale", | ||
"type": "\"medium\" | \"large\" | undefined" | ||
}, | ||
{ | ||
"name": "dataPopperPlacement", | ||
"attribute": "data-popper-placement", | ||
"type": "\"auto\" | \"auto-start\" | \"auto-end\" | \"top\" | \"bottom\" | \"right\" | \"left\" | \"top-start\" | \"top-end\" | \"bottom-start\" | \"bottom-end\" | \"right-start\" | \"right-end\" | \"left-start\" | \"left-end\" | \"none\" | undefined" | ||
}, | ||
{ | ||
"name": "offset", | ||
"type": "number" | ||
"type": "number", | ||
"default": "6" | ||
}, | ||
{ | ||
"name": "interaction", | ||
"type": "TriggerInteractions" | ||
"type": "TriggerInteractions", | ||
"default": "\"hover\"" | ||
} | ||
@@ -172,0 +186,0 @@ ] |
@@ -14,5 +14,5 @@ /* | ||
const styles = css ` | ||
@keyframes spOverlayFadeIn{0%{opacity:0;transform:var(--animation-transform)}to{opacity:1;transform:translate(0)}}@keyframes spOverlayFadeOut{0%{opacity:1;transform:translate(0)}to{opacity:0;transform:var(--animation-transform)}}:host{z-index:2;position:absolute;display:none;opacity:0;top:-999em;left:-999em;animation-duration:var(--spectrum-global-animation-duration-100);animation-timing-function:ease-in-out}:host([state]){display:block}:host([state=visible]){opacity:1;transform:translate(0)!important;visibility:visible;animation-name:spOverlayFadeIn}:host([state=hiding]){animation-name:spOverlayFadeOut}:host([placement=top]){--animation-transform:translateY(6px)}:host([placement=right]){--animation-transform:translate(-6px)}:host([placement=bottom]){--animation-transform:translateY(-6px)}:host([placement=left]){--animation-transform:translate(6px)} | ||
@keyframes spOverlayFadeIn{0%{opacity:0;transform:var(--sp-overlay-from)}to{opacity:1;transform:translate(0)}}@keyframes spOverlayFadeOut{0%{opacity:1;transform:translate(0)}to{opacity:0;transform:var(--sp-overlay-from)}}:host{z-index:2;position:absolute}#contents,:host{display:inline-block;pointer-events:none}#contents{animation-duration:var(--spectrum-global-animation-duration-200);animation-timing-function:var(--spectrum-global-animation-ease-out);opacity:1;visibility:visible}:host([data-popper-placement*=top]) #contents{--sp-overlay-from:translateY(var(--spectrum-global-dimension-size-75))}:host([data-popper-placement*=right]) #contents{--sp-overlay-from:translateX(calc(-1*var(--spectrum-global-dimension-size-75)))}:host([data-popper-placement*=bottom]) #contents{--sp-overlay-from:translateY(calc(-1*var(--spectrum-global-dimension-size-75)))}:host([data-popper-placement*=left]) #contents{--sp-overlay-from:translateX(var(--spectrum-global-dimension-size-75))}::slotted(*){position:relative}:host([animating]) ::slotted(*){pointer-events:none} | ||
`; | ||
export default styles; | ||
//# sourceMappingURL=active-overlay.css.js.map |
@@ -1,39 +0,58 @@ | ||
import { Placement, OverlayOpenDetail, TriggerInteractions } from './overlay.js'; | ||
import { Size, Color } from '@spectrum-web-components/theme'; | ||
import { LitElement, TemplateResult, CSSResultArray } from 'lit-element'; | ||
import { Placement, OverlayOpenDetail, TriggerInteractions } from './overlay-types.js'; | ||
import { Scale, Color } from '@spectrum-web-components/theme'; | ||
import { LitElement, TemplateResult, CSSResultArray, PropertyValues } from 'lit-element'; | ||
export interface PositionResult { | ||
arrowOffsetLeft: number; | ||
arrowOffsetTop: number; | ||
maxHeight: number; | ||
placement: string; | ||
positionLeft: number; | ||
positionTop: number; | ||
} | ||
declare type OverlayStateType = 'idle' | 'active' | 'visible' | 'hiding'; | ||
declare type ContentAnimation = 'spOverlayFadeIn' | 'spOverlayFadeOut'; | ||
export declare class ActiveOverlay extends LitElement { | ||
overlayContent?: HTMLElement; | ||
overlayContentTip?: HTMLElement; | ||
trigger?: HTMLElement; | ||
private placeholder?; | ||
private root?; | ||
private popper?; | ||
private originalSlot; | ||
_state: OverlayStateType; | ||
state: OverlayStateType; | ||
placement: Placement; | ||
animating: boolean; | ||
placement?: Placement; | ||
color?: Color; | ||
size?: Size; | ||
scale?: Scale; | ||
private originalPlacement?; | ||
/** | ||
* @prop Used by the popper library to indicate where the overlay was | ||
* actually rendered. Popper may switch which side an overlay | ||
* is rendered on to fit it on the screen | ||
*/ | ||
dataPopperPlacement?: Placement; | ||
private readonly hasTheme; | ||
offset: number; | ||
private position?; | ||
interaction: TriggerInteractions; | ||
private positionAnimationFrame; | ||
private timeout?; | ||
private hiddenDeferred?; | ||
static readonly styles: CSSResultArray; | ||
firstUpdated(changedProperties: PropertyValues): void; | ||
private updateOverlayPopperPlacement; | ||
updated(changedProperties: PropertyValues): void; | ||
private open; | ||
private extractEventDetail; | ||
private extractDetail; | ||
dispose(): void; | ||
private stealOverlayContent; | ||
private returnOverlayContent; | ||
private readonly hasSlotenOverlayContent; | ||
updateOverlayPosition(): void; | ||
hide(): Promise<void>; | ||
private onAnimationEnd; | ||
updateOverlayPosition(): Promise<void>; | ||
hide(animated?: boolean): Promise<void>; | ||
private schedulePositionUpdate; | ||
private onSlotChange; | ||
connectedCallback(): void; | ||
applyContentAnimation(animation: ContentAnimation): Promise<boolean>; | ||
renderTheme(content: TemplateResult): TemplateResult; | ||
render(): TemplateResult; | ||
static create(openEvent: CustomEvent<OverlayOpenDetail>, root: HTMLElement): ActiveOverlay; | ||
static create(details: OverlayOpenDetail): ActiveOverlay; | ||
} | ||
export {}; |
@@ -13,24 +13,5 @@ /* | ||
import { __decorate } from "tslib"; | ||
import calculatePosition from './calculate-position.js'; | ||
import { createPopper } from './popper'; | ||
import { html, LitElement, property, } from 'lit-element'; | ||
import styles from './active-overlay.css.js'; | ||
class Deferred { | ||
constructor() { | ||
this.promise = new Promise((resolve) => (this.resolveFn = resolve)); | ||
} | ||
resolve(value) { | ||
/* istanbul ignore else */ | ||
if (this.resolveFn) { | ||
this.resolveFn(value); | ||
} | ||
} | ||
} | ||
const defaultOptions = { | ||
containerPadding: 10, | ||
crossOffset: 0, | ||
flip: true, | ||
offset: 0, | ||
placement: 'left', | ||
}; | ||
const FadeOutAnimation = 'spOverlayFadeOut'; | ||
const stateMachine = { | ||
@@ -75,12 +56,8 @@ initial: 'idle', | ||
super(...arguments); | ||
this.originalSlot = null; | ||
this._state = stateTransition(); | ||
this.placement = 'bottom'; | ||
this.animating = false; | ||
this.offset = 6; | ||
this.interaction = 'hover'; | ||
this.positionAnimationFrame = 0; | ||
this.onAnimationEnd = (event) => { | ||
if (this.hiddenDeferred && event.animationName === FadeOutAnimation) { | ||
this.hiddenDeferred.resolve(); | ||
} | ||
}; | ||
} | ||
@@ -104,3 +81,3 @@ get state() { | ||
get hasTheme() { | ||
return !!this.color || !!this.size; | ||
return !!this.color || !!this.scale; | ||
} | ||
@@ -110,28 +87,72 @@ static get styles() { | ||
} | ||
open(openEvent) { | ||
this.extractEventDetail(openEvent); | ||
this.stealOverlayContent(openEvent.detail.content); | ||
firstUpdated(changedProperties) { | ||
super.firstUpdated(changedProperties); | ||
/* istanbul ignore if */ | ||
if (!this.overlayContent) | ||
return; | ||
this.stealOverlayContent(this.overlayContent); | ||
/* istanbul ignore if */ | ||
if (!this.overlayContent || !this.trigger || !this.shadowRoot) | ||
return; | ||
/* istanbul ignore else */ | ||
if (this.placement && this.placement !== 'none') { | ||
this.popper = createPopper(this.trigger, this, { | ||
placement: this.placement, | ||
modifiers: [ | ||
{ | ||
name: 'arrow', | ||
options: { | ||
element: this.overlayContentTip, | ||
}, | ||
}, | ||
{ | ||
name: 'offset', | ||
options: { | ||
offset: [0, this.offset], | ||
}, | ||
}, | ||
], | ||
}); | ||
} | ||
this.state = 'active'; | ||
this.timeout = window.setTimeout(() => { | ||
document.addEventListener('sp-update-overlays', () => { | ||
this.updateOverlayPosition(); | ||
this.state = 'visible'; | ||
delete this.timeout; | ||
}, openEvent.detail.delay); | ||
this.hiddenDeferred = new Deferred(); | ||
this.addEventListener('animationend', this.onAnimationEnd); | ||
this.hiddenDeferred.promise.then(() => { | ||
this.removeEventListener('animationend', this.onAnimationEnd); | ||
}); | ||
this.updateOverlayPosition().then(() => this.applyContentAnimation('spOverlayFadeIn')); | ||
} | ||
extractEventDetail(event) { | ||
this.overlayContent = event.detail.content; | ||
this.trigger = event.detail.trigger; | ||
this.placement = event.detail.placement; | ||
this.offset = event.detail.offset; | ||
this.interaction = event.detail.interaction; | ||
this.color = event.detail.theme.color; | ||
this.size = event.detail.theme.size; | ||
updateOverlayPopperPlacement() { | ||
if (!this.overlayContent) | ||
return; | ||
if (this.dataPopperPlacement) { | ||
// Copy this attribute to the actual overlay node so that it can use | ||
// the attribute for styling shadow DOM elements based on the side | ||
// that popper has chosen for it | ||
this.overlayContent.setAttribute('placement', this.dataPopperPlacement); | ||
} | ||
else if (this.originalPlacement) { | ||
this.overlayContent.setAttribute('placement', this.originalPlacement); | ||
} | ||
else { | ||
this.overlayContent.removeAttribute('placement'); | ||
} | ||
} | ||
updated(changedProperties) { | ||
if (changedProperties.has('dataPopperPlacement')) { | ||
this.updateOverlayPopperPlacement(); | ||
} | ||
} | ||
open(openDetail) { | ||
this.extractDetail(openDetail); | ||
} | ||
extractDetail(detail) { | ||
this.overlayContent = detail.content; | ||
this.overlayContentTip = detail.contentTip; | ||
this.trigger = detail.trigger; | ||
this.placement = detail.placement; | ||
this.offset = detail.offset; | ||
this.interaction = detail.interaction; | ||
this.color = detail.theme.color; | ||
this.scale = detail.theme.scale; | ||
} | ||
dispose() { | ||
@@ -143,2 +164,6 @@ this.state = 'idle'; | ||
} | ||
if (this.popper) { | ||
this.popper.destroy(); | ||
this.popper = undefined; | ||
} | ||
this.returnOverlayContent(); | ||
@@ -159,4 +184,6 @@ } | ||
this.overlayContent = element; | ||
this.originalSlot = this.overlayContent.getAttribute('slot'); | ||
this.overlayContent.setAttribute('slot', 'overlay'); | ||
this.appendChild(this.overlayContent); | ||
this.originalPlacement = this.overlayContent.getAttribute('placement'); | ||
} | ||
@@ -167,3 +194,9 @@ returnOverlayContent() { | ||
return; | ||
this.overlayContent.removeAttribute('slot'); | ||
if (this.originalSlot) { | ||
this.overlayContent.setAttribute('slot', this.originalSlot); | ||
delete this.originalSlot; | ||
} | ||
else { | ||
this.overlayContent.removeAttribute('slot'); | ||
} | ||
/* istanbul ignore else */ | ||
@@ -173,33 +206,19 @@ if (this.placeholder && this.placeholder.parentElement) { | ||
} | ||
if (this.originalPlacement) { | ||
this.overlayContent.setAttribute('placement', this.originalPlacement); | ||
delete this.originalPlacement; | ||
} | ||
delete this.placeholder; | ||
} | ||
get hasSlotenOverlayContent() { | ||
return !!(this.overlayContent && this.overlayContent.parentElement === this); | ||
} | ||
updateOverlayPosition() { | ||
if (!this.trigger || | ||
!this.overlayContent || | ||
!this.hasSlotenOverlayContent || | ||
!this.root || | ||
!this.isConnected) { | ||
return; | ||
async updateOverlayPosition() { | ||
if (this.popper) { | ||
await this.popper.update(); | ||
} | ||
const options = { | ||
containerPadding: 0, | ||
crossOffset: 0, | ||
flip: false, | ||
offset: this.offset, | ||
placement: this.placement, | ||
}; | ||
const positionOptions = Object.assign(Object.assign({}, defaultOptions), options); | ||
this.position = calculatePosition(positionOptions.placement, this.overlayContent, this.trigger, this.root, positionOptions.containerPadding, positionOptions.flip, this.root, positionOptions.offset, positionOptions.crossOffset); | ||
this.style.setProperty('left', `${this.position.positionLeft}px`); | ||
this.style.setProperty('top', `${this.position.positionTop}px`); | ||
} | ||
async hide() { | ||
this.state = 'hiding'; | ||
/* istanbul ignore else */ | ||
if (this.hiddenDeferred) { | ||
return this.hiddenDeferred.promise; | ||
async hide(animated = true) { | ||
if (animated) { | ||
this.state = 'hiding'; | ||
await this.applyContentAnimation('spOverlayFadeOut'); | ||
} | ||
this.state = 'idle'; | ||
} | ||
@@ -218,8 +237,30 @@ schedulePositionUpdate() { | ||
} | ||
applyContentAnimation(animation) { | ||
return new Promise((resolve, reject) => { | ||
/* istanbul ignore if */ | ||
if (!this.shadowRoot) { | ||
reject(); | ||
return; | ||
} | ||
const contents = this.shadowRoot.querySelector('#contents'); | ||
const doneHandler = (event) => { | ||
if (animation !== event.animationName) | ||
return; | ||
contents.removeEventListener('animationend', doneHandler); | ||
contents.removeEventListener('animationcancel', doneHandler); | ||
this.animating = false; | ||
resolve(event.type === 'animationcancel'); | ||
}; | ||
contents.addEventListener('animationend', doneHandler); | ||
contents.addEventListener('animationcancel', doneHandler); | ||
contents.style.animationName = animation; | ||
this.animating = true; | ||
}); | ||
} | ||
renderTheme(content) { | ||
import('@spectrum-web-components/theme'); | ||
const color = this.color; | ||
const size = this.size; | ||
const scale = this.scale; | ||
return html ` | ||
<sp-theme .color=${color} .size=${size}> | ||
<sp-theme .color=${color} .scale=${scale}> | ||
${content} | ||
@@ -231,12 +272,13 @@ </sp-theme> | ||
const content = html ` | ||
<slot @slotchange=${this.onSlotChange} name="overlay"></slot> | ||
<div id="contents"> | ||
<slot @slotchange=${this.onSlotChange} name="overlay"></slot> | ||
</div> | ||
`; | ||
return this.hasTheme ? this.renderTheme(content) : content; | ||
} | ||
static create(openEvent, root) { | ||
static create(details) { | ||
const overlay = new ActiveOverlay(); | ||
/* istanbul ignore else */ | ||
if (openEvent.detail.content) { | ||
overlay.root = root; | ||
overlay.open(openEvent); | ||
if (details.content) { | ||
overlay.open(details); | ||
} | ||
@@ -250,2 +292,5 @@ return overlay; | ||
__decorate([ | ||
property({ reflect: true, type: Boolean }) | ||
], ActiveOverlay.prototype, "animating", void 0); | ||
__decorate([ | ||
property({ reflect: true }) | ||
@@ -258,3 +303,6 @@ ], ActiveOverlay.prototype, "placement", void 0); | ||
property({ attribute: false }) | ||
], ActiveOverlay.prototype, "size", void 0); | ||
], ActiveOverlay.prototype, "scale", void 0); | ||
__decorate([ | ||
property({ attribute: 'data-popper-placement' }) | ||
], ActiveOverlay.prototype, "dataPopperPlacement", void 0); | ||
//# sourceMappingURL=active-overlay.js.map |
import { OverlayTrigger } from './overlay-trigger.js'; | ||
import { ActiveOverlay } from './active-overlay.js'; | ||
export * from './overlay.js'; | ||
export * from './overlay-root.js'; | ||
export * from './overlay-trigger.js'; | ||
export * from './overlay-types'; | ||
declare global { | ||
@@ -7,0 +7,0 @@ interface HTMLElementTagNameMap { |
@@ -14,3 +14,3 @@ /* | ||
import { ActiveOverlay } from './active-overlay.js'; | ||
export * from './overlay-root.js'; | ||
export * from './overlay.js'; | ||
export * from './overlay-trigger.js'; | ||
@@ -17,0 +17,0 @@ /* istanbul ignore else */ |
import { ActiveOverlay } from './active-overlay.js'; | ||
import { OverlayOpenDetail, OverlayCloseDetail } from './overlay.js'; | ||
import { OverlayOpenDetail } from './overlay-types'; | ||
export declare class OverlayStack { | ||
@@ -7,12 +7,13 @@ overlays: ActiveOverlay[]; | ||
private root; | ||
private onChange; | ||
private handlingResize; | ||
constructor(root: HTMLElement, onChange: (overlays: ActiveOverlay[]) => void); | ||
private overlayTimer; | ||
constructor(); | ||
private readonly document; | ||
private readonly topOverlay; | ||
private findOverlayForContent; | ||
private addEventListeners; | ||
private isOverlayActive; | ||
private isClickOverlayActiveForTrigger; | ||
openOverlay(event: CustomEvent<OverlayOpenDetail>): void; | ||
closeOverlay(event: CustomEvent<OverlayCloseDetail>): void; | ||
openOverlay(details: OverlayOpenDetail): Promise<boolean>; | ||
closeOverlay(content: HTMLElement): void; | ||
private handleMouseCapture; | ||
@@ -19,0 +20,0 @@ private closeAllHoverOverlays; |
@@ -13,2 +13,3 @@ /* | ||
import { ActiveOverlay } from './active-overlay.js'; | ||
import { OverlayTimer } from './overlay-timer'; | ||
function isLeftClick(event) { | ||
@@ -21,3 +22,3 @@ return event.button === 0; | ||
export class OverlayStack { | ||
constructor(root, onChange) { | ||
constructor() { | ||
this.overlays = []; | ||
@@ -27,2 +28,3 @@ this.preventMouseRootClose = false; | ||
this.handlingResize = false; | ||
this.overlayTimer = new OverlayTimer(); | ||
this.handleMouseCapture = (event) => { | ||
@@ -61,11 +63,8 @@ const topOverlay = this.topOverlay; | ||
this.handlingResize = true; | ||
requestAnimationFrame(() => { | ||
this.overlays.forEach((overlay) => { | ||
overlay.updateOverlayPosition(); | ||
}); | ||
requestAnimationFrame(async () => { | ||
const promises = this.overlays.map((overlay) => overlay.updateOverlayPosition()); | ||
await Promise.all(promises); | ||
this.handlingResize = false; | ||
}); | ||
}; | ||
this.root = root; | ||
this.onChange = onChange; | ||
this.addEventListeners(); | ||
@@ -79,2 +78,9 @@ } | ||
} | ||
findOverlayForContent(overlayContent) { | ||
for (const item of this.overlays) { | ||
if (overlayContent.isSameNode(item.overlayContent)) { | ||
return item; | ||
} | ||
} | ||
} | ||
addEventListeners() { | ||
@@ -93,24 +99,35 @@ this.document.addEventListener('click', this.handleMouseCapture, true); | ||
} | ||
openOverlay(event) { | ||
if (this.isOverlayActive(event.detail.content)) | ||
return; | ||
requestAnimationFrame(() => { | ||
const interaction = event.detail.interaction; | ||
if (interaction === 'click') { | ||
this.closeAllHoverOverlays(); | ||
async openOverlay(details) { | ||
/* istanbul ignore if */ | ||
if (this.isOverlayActive(details.content)) | ||
return false; | ||
if (details.delayed) { | ||
const promise = this.overlayTimer.openTimer(details.content); | ||
const cancelled = await promise; | ||
if (cancelled) { | ||
return promise; | ||
} | ||
else if (interaction === 'hover' && | ||
this.isClickOverlayActiveForTrigger(event.detail.trigger)) { | ||
// Don't show a hover popover if the click popover is already active | ||
return; | ||
} | ||
const activeOverlay = ActiveOverlay.create(event, this.root); | ||
this.overlays.push(activeOverlay); | ||
this.onChange(this.overlays); | ||
} | ||
return new Promise((resolve) => { | ||
requestAnimationFrame(() => { | ||
if (details.interaction === 'click') { | ||
this.closeAllHoverOverlays(); | ||
} | ||
else if (details.interaction === 'hover' && | ||
this.isClickOverlayActiveForTrigger(details.trigger)) { | ||
// Don't show a hover popover if the click popover is already active | ||
resolve(true); | ||
return; | ||
} | ||
const activeOverlay = ActiveOverlay.create(details); | ||
this.overlays.push(activeOverlay); | ||
document.body.appendChild(activeOverlay); | ||
resolve(false); | ||
}); | ||
}); | ||
} | ||
closeOverlay(event) { | ||
closeOverlay(content) { | ||
this.overlayTimer.close(content); | ||
requestAnimationFrame(() => { | ||
const overlayContent = event.detail.content; | ||
const overlay = this.overlays.find((item) => overlayContent.isSameNode(item.overlayContent)); | ||
const overlay = this.findOverlayForContent(content); | ||
this.hideAndCloseOverlay(overlay); | ||
@@ -122,15 +139,16 @@ }); | ||
if (overlay.interaction === 'hover') { | ||
this.hideAndCloseOverlay(overlay); | ||
this.hideAndCloseOverlay(overlay, false); | ||
} | ||
} | ||
} | ||
async hideAndCloseOverlay(overlay) { | ||
async hideAndCloseOverlay(overlay, animated = true) { | ||
if (overlay) { | ||
await overlay.hide(); | ||
await overlay.hide(animated); | ||
overlay.remove(); | ||
overlay.dispose(); | ||
const index = this.overlays.indexOf(overlay); | ||
/* istanbul ignore else */ | ||
if (index >= 0) { | ||
this.overlays[index].dispose(); | ||
this.overlays.splice(index, 1); | ||
} | ||
this.onChange(this.overlays); | ||
} | ||
@@ -137,0 +155,0 @@ } |
import { LitElement, CSSResultArray, TemplateResult } from 'lit-element'; | ||
import { OverlayRoot } from './overlay-root.js'; | ||
import { TriggerInteractions, Placement } from './overlay.js'; | ||
import { Placement } from './overlay-types'; | ||
/** | ||
@@ -12,4 +11,5 @@ * A overlay trigger component for displaying overlays relative to other content. | ||
export declare class OverlayTrigger extends LitElement { | ||
private closeClickOverlay?; | ||
private closeHoverOverlay?; | ||
static readonly styles: CSSResultArray; | ||
static overlayRoot: OverlayRoot; | ||
placement: Placement; | ||
@@ -20,12 +20,12 @@ offset: number; | ||
private hoverContent?; | ||
onOverlayOpen(event: Event, interaction: TriggerInteractions): void; | ||
onOverlayClose(event: Event, interaction: TriggerInteractions): void; | ||
onTriggerClick(event: Event): void; | ||
onTriggerMouseOver(event: Event): void; | ||
onTriggerMouseLeave(event: Event): void; | ||
private targetContent?; | ||
protected render(): TemplateResult; | ||
onTriggerClick(): void; | ||
onTriggerMouseEnter(): void; | ||
onTriggerMouseLeave(): void; | ||
private onClickSlotChange; | ||
private onHoverSlotChange; | ||
private extractSlotContent; | ||
private onTargetSlotChange; | ||
private extractSlotContentFromEvent; | ||
disconnectedCallback(): void; | ||
} |
@@ -15,4 +15,3 @@ /* | ||
import overlayTriggerStyles from './overlay-trigger.css.js'; | ||
import { OverlayRoot } from './overlay-root.js'; | ||
let overlayRoot; | ||
import { Overlay } from './overlay.js'; | ||
/** | ||
@@ -35,76 +34,2 @@ * A overlay trigger component for displaying overlays relative to other content. | ||
} | ||
onOverlayOpen(event, interaction) { | ||
const isClick = interaction === 'click'; | ||
const overlayElement = isClick ? this.clickContent : this.hoverContent; | ||
/* istanbul ignore if */ | ||
if (!overlayElement) { | ||
return; | ||
} | ||
if (!overlayRoot) { | ||
overlayRoot = new OverlayRoot(); | ||
} | ||
const delayAttribute = overlayElement.getAttribute('delay'); | ||
const delay = delayAttribute ? parseFloat(delayAttribute) : 0; | ||
const queryThemeDetail = { | ||
color: undefined, | ||
size: undefined, | ||
}; | ||
const queryThemeEvent = new CustomEvent('query-theme', { | ||
bubbles: true, | ||
composed: true, | ||
detail: queryThemeDetail, | ||
cancelable: true, | ||
}); | ||
this.dispatchEvent(queryThemeEvent); | ||
const overlayOpenDetail = { | ||
content: overlayElement, | ||
delay: delay, | ||
offset: this.offset, | ||
placement: this.placement, | ||
trigger: this, | ||
interaction: interaction, | ||
theme: queryThemeDetail, | ||
}; | ||
const overlayOpenEvent = new CustomEvent('sp-overlay-open', { | ||
bubbles: true, | ||
composed: true, | ||
detail: overlayOpenDetail, | ||
}); | ||
this.dispatchEvent(overlayOpenEvent); | ||
} | ||
onOverlayClose(event, interaction) { | ||
const isClick = interaction === 'click'; | ||
const overlayElement = isClick ? this.clickContent : this.hoverContent; | ||
/* istanbul ignore if */ | ||
if (!overlayElement) { | ||
return; | ||
} | ||
const overlayCloseDetail = { | ||
content: overlayElement, | ||
}; | ||
const overlayCloseEvent = new CustomEvent('sp-overlay-close', { | ||
bubbles: true, | ||
composed: true, | ||
detail: overlayCloseDetail, | ||
}); | ||
this.dispatchEvent(overlayCloseEvent); | ||
} | ||
onTriggerClick(event) { | ||
/* istanbul ignore else */ | ||
if (this.clickContent) { | ||
this.onOverlayOpen(event, 'click'); | ||
} | ||
} | ||
onTriggerMouseOver(event) { | ||
/* istanbul ignore else */ | ||
if (this.hoverContent) { | ||
this.onOverlayOpen(event, 'hover'); | ||
} | ||
} | ||
onTriggerMouseLeave(event) { | ||
/* istanbul ignore else */ | ||
if (this.hoverContent) { | ||
this.onOverlayClose(event, 'hover'); | ||
} | ||
} | ||
render() { | ||
@@ -115,6 +40,9 @@ return html ` | ||
@click=${this.onTriggerClick} | ||
@mouseenter=${this.onTriggerMouseOver} | ||
@mouseenter=${this.onTriggerMouseEnter} | ||
@mouseleave=${this.onTriggerMouseLeave} | ||
> | ||
<slot name="trigger"></slot> | ||
<slot | ||
@slotchange=${this.onTargetSlotChange} | ||
name="trigger" | ||
></slot> | ||
</div> | ||
@@ -133,14 +61,40 @@ <div id="overlay-content"> | ||
} | ||
onClickSlotChange(event) { | ||
/* istanbul ignore if */ | ||
if (!event.target) { | ||
return; | ||
onTriggerClick() { | ||
/* istanbul ignore else */ | ||
if (this.targetContent && this.clickContent) { | ||
this.closeClickOverlay = Overlay.open(this.targetContent, 'click', this.clickContent, { | ||
offset: this.offset, | ||
placement: this.placement, | ||
}); | ||
} | ||
const slot = event.target; | ||
const content = this.extractSlotContent(slot); | ||
if (content) { | ||
this.clickContent = content; | ||
} | ||
onTriggerMouseEnter() { | ||
/* istanbul ignore else */ | ||
if (this.targetContent && this.hoverContent) { | ||
this.closeHoverOverlay = Overlay.open(this.targetContent, 'hover', this.hoverContent, { | ||
offset: this.offset, | ||
placement: this.placement, | ||
}); | ||
} | ||
} | ||
onTriggerMouseLeave() { | ||
/* istanbul ignore else */ | ||
if (this.closeHoverOverlay) { | ||
this.closeHoverOverlay(); | ||
delete this.closeHoverOverlay; | ||
} | ||
} | ||
onClickSlotChange(event) { | ||
const content = this.extractSlotContentFromEvent(event); | ||
this.clickContent = content; | ||
} | ||
onHoverSlotChange(event) { | ||
const content = this.extractSlotContentFromEvent(event); | ||
this.hoverContent = content; | ||
} | ||
onTargetSlotChange(event) { | ||
const content = this.extractSlotContentFromEvent(event); | ||
this.targetContent = content; | ||
} | ||
extractSlotContentFromEvent(event) { | ||
/* istanbul ignore if */ | ||
@@ -151,21 +105,14 @@ if (!event.target) { | ||
const slot = event.target; | ||
const content = this.extractSlotContent(slot); | ||
if (content) { | ||
this.hoverContent = content; | ||
} | ||
} | ||
extractSlotContent(slot) { | ||
const nodes = slot.assignedNodes(); | ||
if (nodes.length) { | ||
return nodes[0]; | ||
} | ||
return null; | ||
return nodes.find((node) => node instanceof HTMLElement); | ||
} | ||
disconnectedCallback() { | ||
/* istanbul ignore else */ | ||
if (this.clickContent) { | ||
this.onOverlayClose(new Event('remove'), 'click'); | ||
if (this.closeClickOverlay) { | ||
this.closeClickOverlay(); | ||
delete this.closeClickOverlay; | ||
} | ||
if (this.hoverContent) { | ||
this.onOverlayClose(new Event('remove'), 'hover'); | ||
if (this.closeHoverOverlay) { | ||
this.closeHoverOverlay(); | ||
delete this.closeHoverOverlay; | ||
} | ||
@@ -172,0 +119,0 @@ super.disconnectedCallback(); |
@@ -1,15 +0,53 @@ | ||
export declare type TriggerInteractions = 'click' | 'hover'; | ||
export declare type Placement = 'top' | 'right' | 'bottom' | 'left'; | ||
import { ThemeData } from '@spectrum-web-components/theme'; | ||
export interface OverlayOpenDetail { | ||
content: HTMLElement; | ||
delay: number; | ||
offset: number; | ||
placement: Placement; | ||
trigger: HTMLElement; | ||
interaction: TriggerInteractions; | ||
theme: ThemeData; | ||
import { TriggerInteractions, Placement } from './overlay-types'; | ||
declare type OverlayOptions = { | ||
delayed?: boolean; | ||
placement?: Placement; | ||
offset?: number; | ||
}; | ||
/** | ||
* This class allows access to the overlay system which allows a client to | ||
* position an element in the overlay positioned relative to another node. | ||
*/ | ||
export declare class Overlay { | ||
private static overlayStack; | ||
private isOpen; | ||
private overlayElement; | ||
private owner; | ||
private interaction; | ||
/** | ||
* | ||
* @param owner the parent element we will use to position the overlay element | ||
* @param interaction the type of interaction that caused this overlay to be shown | ||
* @param overlayElement the item to display as an overlay | ||
*/ | ||
constructor(owner: HTMLElement, interaction: TriggerInteractions, overlayElement: HTMLElement); | ||
/** | ||
* Open an overlay | ||
* | ||
* @param owner the parent element we will use to position the overlay element | ||
* @param interaction the type of interaction that caused this overlay to be shown | ||
* @param overlayElement the item to display as an overlay | ||
* @param options display parameters | ||
* @param options.delayed if true delay opening of the overlay based on the global warmup/cooldown timer | ||
* @param options.offset distance to offset the overlay | ||
* @param options.placement side on which to position the overlay | ||
* @returns an Overlay object which can be used to close the overlay | ||
*/ | ||
static open(owner: HTMLElement, interaction: TriggerInteractions, overlayElement: HTMLElement, options: OverlayOptions): () => void; | ||
static update(): void; | ||
/** | ||
* Open an overlay | ||
* | ||
* @param options display parameters | ||
* @param options.delayed delay before opening the overlay | ||
* @param options.offset distance to offset the overlay | ||
* @param options.placement side on which to position the overlay | ||
* @returns a Promise that resolves to true if this operation was cancelled | ||
*/ | ||
open({ delayed, offset, placement, }: OverlayOptions): Promise<boolean>; | ||
/** | ||
* Close the overlay if it is open | ||
*/ | ||
close(): void; | ||
} | ||
export interface OverlayCloseDetail { | ||
content: HTMLElement; | ||
} | ||
export {}; |
@@ -12,2 +12,93 @@ /* | ||
*/ | ||
import { OverlayStack } from './overlay-stack'; | ||
/** | ||
* This class allows access to the overlay system which allows a client to | ||
* position an element in the overlay positioned relative to another node. | ||
*/ | ||
export class Overlay { | ||
/** | ||
* | ||
* @param owner the parent element we will use to position the overlay element | ||
* @param interaction the type of interaction that caused this overlay to be shown | ||
* @param overlayElement the item to display as an overlay | ||
*/ | ||
constructor(owner, interaction, overlayElement) { | ||
this.isOpen = false; | ||
this.owner = owner; | ||
this.overlayElement = overlayElement; | ||
this.interaction = interaction; | ||
} | ||
/** | ||
* Open an overlay | ||
* | ||
* @param owner the parent element we will use to position the overlay element | ||
* @param interaction the type of interaction that caused this overlay to be shown | ||
* @param overlayElement the item to display as an overlay | ||
* @param options display parameters | ||
* @param options.delayed if true delay opening of the overlay based on the global warmup/cooldown timer | ||
* @param options.offset distance to offset the overlay | ||
* @param options.placement side on which to position the overlay | ||
* @returns an Overlay object which can be used to close the overlay | ||
*/ | ||
static open(owner, interaction, overlayElement, options) { | ||
const overlay = new Overlay(owner, interaction, overlayElement); | ||
overlay.open(options); | ||
return () => overlay.close(); | ||
} | ||
static update() { | ||
const overlayUpdateEvent = new CustomEvent('sp-update-overlays', { | ||
bubbles: true, | ||
composed: true, | ||
cancelable: true, | ||
}); | ||
document.dispatchEvent(overlayUpdateEvent); | ||
} | ||
/** | ||
* Open an overlay | ||
* | ||
* @param options display parameters | ||
* @param options.delayed delay before opening the overlay | ||
* @param options.offset distance to offset the overlay | ||
* @param options.placement side on which to position the overlay | ||
* @returns a Promise that resolves to true if this operation was cancelled | ||
*/ | ||
async open({ delayed, offset = 0, placement = 'top', }) { | ||
/* istanbul ignore if */ | ||
if (this.isOpen) | ||
return true; | ||
/* istanbul ignore else */ | ||
if (delayed === undefined) { | ||
delayed = this.overlayElement.hasAttribute('delayed'); | ||
} | ||
const queryThemeDetail = { | ||
color: undefined, | ||
scale: undefined, | ||
}; | ||
const queryThemeEvent = new CustomEvent('sp-query-theme', { | ||
bubbles: true, | ||
composed: true, | ||
detail: queryThemeDetail, | ||
cancelable: true, | ||
}); | ||
this.owner.dispatchEvent(queryThemeEvent); | ||
const overlayDetailQuery = {}; | ||
const queryOverlayDetailEvent = new CustomEvent('sp-overlay-query', { | ||
bubbles: true, | ||
composed: true, | ||
detail: overlayDetailQuery, | ||
cancelable: true, | ||
}); | ||
this.overlayElement.dispatchEvent(queryOverlayDetailEvent); | ||
Overlay.overlayStack.openOverlay(Object.assign({ content: this.overlayElement, contentTip: overlayDetailQuery.overlayContentTipElement, delayed, offset: offset, placement: placement, trigger: this.owner, interaction: this.interaction, theme: queryThemeDetail }, overlayDetailQuery)); | ||
this.isOpen = true; | ||
return true; | ||
} | ||
/** | ||
* Close the overlay if it is open | ||
*/ | ||
close() { | ||
Overlay.overlayStack.closeOverlay(this.overlayElement); | ||
} | ||
} | ||
Overlay.overlayStack = new OverlayStack(); | ||
//# sourceMappingURL=overlay.js.map |
@@ -21,3 +21,3 @@ { | ||
], | ||
"version": "0.1.0", | ||
"version": "0.2.0", | ||
"description": "", | ||
@@ -42,6 +42,7 @@ "main": "lib/index.js", | ||
"dependencies": { | ||
"@spectrum-web-components/theme": "^0.1.1", | ||
"@popperjs/core": "^2.0.4", | ||
"@spectrum-web-components/theme": "^0.2.0", | ||
"tslib": "^1.10.0" | ||
}, | ||
"gitHead": "8ccfbecc2e7534567967bbebb5d201e0b16b08bb" | ||
"gitHead": "1b17c4c27d73273f2479f0d8af40d187f2f194fa" | ||
} |
@@ -1,2 +0,2 @@ | ||
## Overview | ||
## Description | ||
@@ -48,6 +48,6 @@ To ensure that content the requires it (modals, menus, etc) can escape overflow rules, the z-index, et al, Spectrum Web Components provides an overlay system that is made up of three interrelated elements, `<overlay-trigger>`, `<active-overlay>`, and `<sp-theme>`. DOM that should be overlaid on _hover_ (`[slot="hover-content"]`) and _click_ (`[slot="click-content"]`) are outlined in the light DOM content of an `<overlay-trigger>`. Said content will be overlayed onto the DOM via an `<active-overlay>` element that will be appended to the `<body>`. Content delivered in this way will acquire CSS Custom Properties for `color` and `size` from the trigger's nearest ancestor `<sp-theme>`. | ||
</sp-popover> | ||
<div slot="hover-content" class="tooltip" delay="100"> | ||
<sp-tooltip slot="hover-content" delayed open> | ||
Tooltip | ||
</div> | ||
</sp-tooltip> | ||
</overlay-trigger> | ||
``` |
@@ -14,4 +14,4 @@ /* | ||
const styles = css` | ||
@keyframes spOverlayFadeIn{0%{opacity:0;transform:var(--animation-transform)}to{opacity:1;transform:translate(0)}}@keyframes spOverlayFadeOut{0%{opacity:1;transform:translate(0)}to{opacity:0;transform:var(--animation-transform)}}:host{z-index:2;position:absolute;display:none;opacity:0;top:-999em;left:-999em;animation-duration:var(--spectrum-global-animation-duration-100);animation-timing-function:ease-in-out}:host([state]){display:block}:host([state=visible]){opacity:1;transform:translate(0)!important;visibility:visible;animation-name:spOverlayFadeIn}:host([state=hiding]){animation-name:spOverlayFadeOut}:host([placement=top]){--animation-transform:translateY(6px)}:host([placement=right]){--animation-transform:translate(-6px)}:host([placement=bottom]){--animation-transform:translateY(-6px)}:host([placement=left]){--animation-transform:translate(6px)} | ||
@keyframes spOverlayFadeIn{0%{opacity:0;transform:var(--sp-overlay-from)}to{opacity:1;transform:translate(0)}}@keyframes spOverlayFadeOut{0%{opacity:1;transform:translate(0)}to{opacity:0;transform:var(--sp-overlay-from)}}:host{z-index:2;position:absolute}#contents,:host{display:inline-block;pointer-events:none}#contents{animation-duration:var(--spectrum-global-animation-duration-200);animation-timing-function:var(--spectrum-global-animation-ease-out);opacity:1;visibility:visible}:host([data-popper-placement*=top]) #contents{--sp-overlay-from:translateY(var(--spectrum-global-dimension-size-75))}:host([data-popper-placement*=right]) #contents{--sp-overlay-from:translateX(calc(-1*var(--spectrum-global-dimension-size-75)))}:host([data-popper-placement*=bottom]) #contents{--sp-overlay-from:translateY(calc(-1*var(--spectrum-global-dimension-size-75)))}:host([data-popper-placement*=left]) #contents{--sp-overlay-from:translateX(var(--spectrum-global-dimension-size-75))}::slotted(*){position:relative}:host([animating]) ::slotted(*){pointer-events:none} | ||
`; | ||
export default styles; |
@@ -13,2 +13,3 @@ /* | ||
import { createPopper, Instance } from './popper'; | ||
import { | ||
@@ -18,5 +19,4 @@ Placement, | ||
TriggerInteractions, | ||
} from './overlay.js'; | ||
import calculatePosition, { PositionResult } from './calculate-position.js'; | ||
import { Size, Color } from '@spectrum-web-components/theme'; | ||
} from './overlay-types.js'; | ||
import { Scale, Color } from '@spectrum-web-components/theme'; | ||
import { | ||
@@ -28,39 +28,17 @@ html, | ||
property, | ||
PropertyValues, | ||
} from 'lit-element'; | ||
import styles from './active-overlay.css.js'; | ||
interface CalculatePositionOptions { | ||
containerPadding: number; | ||
crossOffset: number; | ||
flip: boolean; | ||
offset: number; | ||
export interface PositionResult { | ||
arrowOffsetLeft: number; | ||
arrowOffsetTop: number; | ||
maxHeight: number; | ||
placement: string; | ||
positionLeft: number; | ||
positionTop: number; | ||
} | ||
class Deferred<T> { | ||
private resolveFn?: (value: T) => void; | ||
public promise: Promise<T> = new Promise( | ||
(resolve: (value: T) => void) => (this.resolveFn = resolve) | ||
); | ||
public resolve(value: T): void { | ||
/* istanbul ignore else */ | ||
if (this.resolveFn) { | ||
this.resolveFn(value); | ||
} | ||
} | ||
} | ||
const defaultOptions: CalculatePositionOptions = { | ||
containerPadding: 10, | ||
crossOffset: 0, | ||
flip: true, | ||
offset: 0, | ||
placement: 'left', | ||
}; | ||
const FadeOutAnimation = 'spOverlayFadeOut'; | ||
type OverlayStateType = 'idle' | 'active' | 'visible' | 'hiding'; | ||
type ContentAnimation = 'spOverlayFadeIn' | 'spOverlayFadeOut'; | ||
@@ -117,6 +95,8 @@ const stateMachine: { | ||
public overlayContent?: HTMLElement; | ||
public overlayContentTip?: HTMLElement; | ||
public trigger?: HTMLElement; | ||
private placeholder?: Comment; | ||
private root?: HTMLElement; | ||
private popper?: Instance; | ||
private originalSlot: string | null = null; | ||
@@ -141,15 +121,27 @@ @property() | ||
@property({ reflect: true, type: Boolean }) | ||
public animating = false; | ||
@property({ reflect: true }) | ||
public placement: Placement = 'bottom'; | ||
public placement?: Placement; | ||
@property({ attribute: false }) | ||
public color?: Color; | ||
@property({ attribute: false }) | ||
public size?: Size; | ||
public scale?: Scale; | ||
private originalPlacement?: Placement; | ||
/** | ||
* @prop Used by the popper library to indicate where the overlay was | ||
* actually rendered. Popper may switch which side an overlay | ||
* is rendered on to fit it on the screen | ||
*/ | ||
@property({ attribute: 'data-popper-placement' }) | ||
public dataPopperPlacement?: Placement; | ||
private get hasTheme(): boolean { | ||
return !!this.color || !!this.size; | ||
return !!this.color || !!this.scale; | ||
} | ||
public offset = 6; | ||
private position?: PositionResult; | ||
public interaction: TriggerInteractions = 'hover'; | ||
@@ -159,3 +151,2 @@ private positionAnimationFrame = 0; | ||
private timeout?: number; | ||
private hiddenDeferred?: Deferred<void>; | ||
@@ -166,5 +157,4 @@ public static get styles(): CSSResultArray { | ||
private open(openEvent: CustomEvent<OverlayOpenDetail>): void { | ||
this.extractEventDetail(openEvent); | ||
this.stealOverlayContent(openEvent.detail.content); | ||
public firstUpdated(changedProperties: PropertyValues): void { | ||
super.firstUpdated(changedProperties); | ||
@@ -174,26 +164,82 @@ /* istanbul ignore if */ | ||
this.stealOverlayContent(this.overlayContent); | ||
/* istanbul ignore if */ | ||
if (!this.overlayContent || !this.trigger || !this.shadowRoot) return; | ||
/* istanbul ignore else */ | ||
if (this.placement && this.placement !== 'none') { | ||
this.popper = createPopper(this.trigger, this, { | ||
placement: this.placement, | ||
modifiers: [ | ||
{ | ||
name: 'arrow', | ||
options: { | ||
element: this.overlayContentTip, | ||
}, | ||
}, | ||
{ | ||
name: 'offset', | ||
options: { | ||
offset: [0, this.offset], | ||
}, | ||
}, | ||
], | ||
}); | ||
} | ||
this.state = 'active'; | ||
this.timeout = window.setTimeout(() => { | ||
document.addEventListener('sp-update-overlays', () => { | ||
this.updateOverlayPosition(); | ||
this.state = 'visible'; | ||
delete this.timeout; | ||
}, openEvent.detail.delay); | ||
}); | ||
this.hiddenDeferred = new Deferred<void>(); | ||
this.addEventListener('animationend', this.onAnimationEnd); | ||
this.hiddenDeferred.promise.then(() => { | ||
this.removeEventListener('animationend', this.onAnimationEnd); | ||
}); | ||
this.updateOverlayPosition().then(() => | ||
this.applyContentAnimation('spOverlayFadeIn') | ||
); | ||
} | ||
private extractEventDetail(event: CustomEvent<OverlayOpenDetail>): void { | ||
this.overlayContent = event.detail.content; | ||
this.trigger = event.detail.trigger; | ||
this.placement = event.detail.placement; | ||
this.offset = event.detail.offset; | ||
this.interaction = event.detail.interaction; | ||
this.color = event.detail.theme.color; | ||
this.size = event.detail.theme.size; | ||
private updateOverlayPopperPlacement(): void { | ||
if (!this.overlayContent) return; | ||
if (this.dataPopperPlacement) { | ||
// Copy this attribute to the actual overlay node so that it can use | ||
// the attribute for styling shadow DOM elements based on the side | ||
// that popper has chosen for it | ||
this.overlayContent.setAttribute( | ||
'placement', | ||
this.dataPopperPlacement | ||
); | ||
} else if (this.originalPlacement) { | ||
this.overlayContent.setAttribute( | ||
'placement', | ||
this.originalPlacement | ||
); | ||
} else { | ||
this.overlayContent.removeAttribute('placement'); | ||
} | ||
} | ||
public updated(changedProperties: PropertyValues): void { | ||
if (changedProperties.has('dataPopperPlacement')) { | ||
this.updateOverlayPopperPlacement(); | ||
} | ||
} | ||
private open(openDetail: OverlayOpenDetail): void { | ||
this.extractDetail(openDetail); | ||
} | ||
private extractDetail(detail: OverlayOpenDetail): void { | ||
this.overlayContent = detail.content; | ||
this.overlayContentTip = detail.contentTip; | ||
this.trigger = detail.trigger; | ||
this.placement = detail.placement; | ||
this.offset = detail.offset; | ||
this.interaction = detail.interaction; | ||
this.color = detail.theme.color; | ||
this.scale = detail.theme.scale; | ||
} | ||
public dispose(): void { | ||
@@ -207,2 +253,7 @@ this.state = 'idle'; | ||
if (this.popper) { | ||
this.popper.destroy(); | ||
this.popper = undefined; | ||
} | ||
this.returnOverlayContent(); | ||
@@ -227,4 +278,9 @@ } | ||
this.overlayContent = element; | ||
this.originalSlot = this.overlayContent.getAttribute('slot'); | ||
this.overlayContent.setAttribute('slot', 'overlay'); | ||
this.appendChild(this.overlayContent); | ||
this.originalPlacement = this.overlayContent.getAttribute( | ||
'placement' | ||
) as Placement; | ||
} | ||
@@ -236,3 +292,8 @@ | ||
this.overlayContent.removeAttribute('slot'); | ||
if (this.originalSlot) { | ||
this.overlayContent.setAttribute('slot', this.originalSlot); | ||
delete this.originalSlot; | ||
} else { | ||
this.overlayContent.removeAttribute('slot'); | ||
} | ||
@@ -246,61 +307,27 @@ /* istanbul ignore else */ | ||
} | ||
delete this.placeholder; | ||
} | ||
private get hasSlotenOverlayContent(): boolean { | ||
return !!( | ||
this.overlayContent && this.overlayContent.parentElement === this | ||
); | ||
} | ||
public updateOverlayPosition(): void { | ||
if ( | ||
!this.trigger || | ||
!this.overlayContent || | ||
!this.hasSlotenOverlayContent || | ||
!this.root || | ||
!this.isConnected | ||
) { | ||
return; | ||
if (this.originalPlacement) { | ||
this.overlayContent.setAttribute( | ||
'placement', | ||
this.originalPlacement | ||
); | ||
delete this.originalPlacement; | ||
} | ||
const options: CalculatePositionOptions = { | ||
containerPadding: 0, | ||
crossOffset: 0, | ||
flip: false, | ||
offset: this.offset, | ||
placement: this.placement, | ||
}; | ||
const positionOptions = { ...defaultOptions, ...options }; | ||
this.position = calculatePosition( | ||
positionOptions.placement, | ||
this.overlayContent, | ||
this.trigger, | ||
this.root, | ||
positionOptions.containerPadding, | ||
positionOptions.flip, | ||
this.root, | ||
positionOptions.offset, | ||
positionOptions.crossOffset | ||
); | ||
this.style.setProperty('left', `${this.position.positionLeft}px`); | ||
this.style.setProperty('top', `${this.position.positionTop}px`); | ||
delete this.placeholder; | ||
} | ||
public async hide(): Promise<void> { | ||
this.state = 'hiding'; | ||
/* istanbul ignore else */ | ||
if (this.hiddenDeferred) { | ||
return this.hiddenDeferred.promise; | ||
public async updateOverlayPosition(): Promise<void> { | ||
if (this.popper) { | ||
await this.popper.update(); | ||
} | ||
} | ||
private onAnimationEnd = (event: AnimationEvent): void => { | ||
if (this.hiddenDeferred && event.animationName === FadeOutAnimation) { | ||
this.hiddenDeferred.resolve(); | ||
public async hide(animated = true): Promise<void> { | ||
if (animated) { | ||
this.state = 'hiding'; | ||
await this.applyContentAnimation('spOverlayFadeOut'); | ||
} | ||
}; | ||
this.state = 'idle'; | ||
} | ||
@@ -324,8 +351,36 @@ private schedulePositionUpdate(): void { | ||
public applyContentAnimation( | ||
animation: ContentAnimation | ||
): Promise<boolean> { | ||
return new Promise((resolve, reject): void => { | ||
/* istanbul ignore if */ | ||
if (!this.shadowRoot) { | ||
reject(); | ||
return; | ||
} | ||
const contents = this.shadowRoot.querySelector( | ||
'#contents' | ||
) as HTMLElement; | ||
const doneHandler = (event: AnimationEvent): void => { | ||
if (animation !== event.animationName) return; | ||
contents.removeEventListener('animationend', doneHandler); | ||
contents.removeEventListener('animationcancel', doneHandler); | ||
this.animating = false; | ||
resolve(event.type === 'animationcancel'); | ||
}; | ||
contents.addEventListener('animationend', doneHandler); | ||
contents.addEventListener('animationcancel', doneHandler); | ||
contents.style.animationName = animation; | ||
this.animating = true; | ||
}); | ||
} | ||
public renderTheme(content: TemplateResult): TemplateResult { | ||
import('@spectrum-web-components/theme'); | ||
const color = this.color as Color; | ||
const size = this.size as Size; | ||
const scale = this.scale as Scale; | ||
return html` | ||
<sp-theme .color=${color} .size=${size}> | ||
<sp-theme .color=${color} .scale=${scale}> | ||
${content} | ||
@@ -338,3 +393,5 @@ </sp-theme> | ||
const content = html` | ||
<slot @slotchange=${this.onSlotChange} name="overlay"></slot> | ||
<div id="contents"> | ||
<slot @slotchange=${this.onSlotChange} name="overlay"></slot> | ||
</div> | ||
`; | ||
@@ -344,12 +401,8 @@ return this.hasTheme ? this.renderTheme(content) : content; | ||
public static create( | ||
openEvent: CustomEvent<OverlayOpenDetail>, | ||
root: HTMLElement | ||
): ActiveOverlay { | ||
public static create(details: OverlayOpenDetail): ActiveOverlay { | ||
const overlay = new ActiveOverlay(); | ||
/* istanbul ignore else */ | ||
if (openEvent.detail.content) { | ||
overlay.root = root; | ||
overlay.open(openEvent); | ||
if (details.content) { | ||
overlay.open(details); | ||
} | ||
@@ -356,0 +409,0 @@ |
@@ -16,4 +16,4 @@ /* | ||
export * from './overlay.js'; | ||
export * from './overlay-root.js'; | ||
export * from './overlay-trigger.js'; | ||
export * from './overlay-types'; | ||
@@ -20,0 +20,0 @@ /* istanbul ignore else */ |
@@ -14,3 +14,4 @@ /* | ||
import { ActiveOverlay } from './active-overlay.js'; | ||
import { OverlayOpenDetail, OverlayCloseDetail } from './overlay.js'; | ||
import { OverlayOpenDetail } from './overlay-types'; | ||
import { OverlayTimer } from './overlay-timer'; | ||
@@ -30,11 +31,6 @@ function isLeftClick(event: MouseEvent): boolean { | ||
private root: HTMLElement = document.body; | ||
private onChange: (overlays: ActiveOverlay[]) => void; | ||
private handlingResize = false; | ||
private overlayTimer = new OverlayTimer(); | ||
public constructor( | ||
root: HTMLElement, | ||
onChange: (overlays: ActiveOverlay[]) => void | ||
) { | ||
this.root = root; | ||
this.onChange = onChange; | ||
public constructor() { | ||
this.addEventListeners(); | ||
@@ -51,2 +47,12 @@ } | ||
private findOverlayForContent( | ||
overlayContent: HTMLElement | ||
): ActiveOverlay | undefined { | ||
for (const item of this.overlays) { | ||
if (overlayContent.isSameNode(item.overlayContent as HTMLElement)) { | ||
return item; | ||
} | ||
} | ||
} | ||
private addEventListeners(): void { | ||
@@ -73,30 +79,39 @@ this.document.addEventListener('click', this.handleMouseCapture, true); | ||
public openOverlay(event: CustomEvent<OverlayOpenDetail>): void { | ||
if (this.isOverlayActive(event.detail.content)) return; | ||
public async openOverlay(details: OverlayOpenDetail): Promise<boolean> { | ||
/* istanbul ignore if */ | ||
if (this.isOverlayActive(details.content)) return false; | ||
requestAnimationFrame(() => { | ||
const interaction = event.detail.interaction; | ||
if (interaction === 'click') { | ||
this.closeAllHoverOverlays(); | ||
} else if ( | ||
interaction === 'hover' && | ||
this.isClickOverlayActiveForTrigger(event.detail.trigger) | ||
) { | ||
// Don't show a hover popover if the click popover is already active | ||
return; | ||
if (details.delayed) { | ||
const promise = this.overlayTimer.openTimer(details.content); | ||
const cancelled = await promise; | ||
if (cancelled) { | ||
return promise; | ||
} | ||
} | ||
const activeOverlay = ActiveOverlay.create(event, this.root); | ||
this.overlays.push(activeOverlay); | ||
return new Promise((resolve) => { | ||
requestAnimationFrame(() => { | ||
if (details.interaction === 'click') { | ||
this.closeAllHoverOverlays(); | ||
} else if ( | ||
details.interaction === 'hover' && | ||
this.isClickOverlayActiveForTrigger(details.trigger) | ||
) { | ||
// Don't show a hover popover if the click popover is already active | ||
resolve(true); | ||
return; | ||
} | ||
this.onChange(this.overlays); | ||
const activeOverlay = ActiveOverlay.create(details); | ||
this.overlays.push(activeOverlay); | ||
document.body.appendChild(activeOverlay); | ||
resolve(false); | ||
}); | ||
}); | ||
} | ||
public closeOverlay(event: CustomEvent<OverlayCloseDetail>): void { | ||
public closeOverlay(content: HTMLElement): void { | ||
this.overlayTimer.close(content); | ||
requestAnimationFrame(() => { | ||
const overlayContent = event.detail.content; | ||
const overlay = this.overlays.find((item) => | ||
overlayContent.isSameNode(item.overlayContent as HTMLElement) | ||
); | ||
const overlay = this.findOverlayForContent(content); | ||
this.hideAndCloseOverlay(overlay); | ||
@@ -132,3 +147,3 @@ }); | ||
if (overlay.interaction === 'hover') { | ||
this.hideAndCloseOverlay(overlay); | ||
this.hideAndCloseOverlay(overlay, false); | ||
} | ||
@@ -138,11 +153,16 @@ } | ||
private async hideAndCloseOverlay(overlay?: ActiveOverlay): Promise<void> { | ||
private async hideAndCloseOverlay( | ||
overlay?: ActiveOverlay, | ||
animated = true | ||
): Promise<void> { | ||
if (overlay) { | ||
await overlay.hide(); | ||
await overlay.hide(animated); | ||
overlay.remove(); | ||
overlay.dispose(); | ||
const index = this.overlays.indexOf(overlay); | ||
/* istanbul ignore else */ | ||
if (index >= 0) { | ||
this.overlays[index].dispose(); | ||
this.overlays.splice(index, 1); | ||
} | ||
this.onChange(this.overlays); | ||
} | ||
@@ -171,6 +191,7 @@ } | ||
this.handlingResize = true; | ||
requestAnimationFrame(() => { | ||
this.overlays.forEach((overlay) => { | ||
overlay.updateOverlayPosition(); | ||
}); | ||
requestAnimationFrame(async () => { | ||
const promises = this.overlays.map((overlay) => | ||
overlay.updateOverlayPosition() | ||
); | ||
await Promise.all(promises); | ||
this.handlingResize = false; | ||
@@ -177,0 +198,0 @@ }); |
@@ -23,13 +23,5 @@ /* | ||
import { OverlayRoot } from './overlay-root.js'; | ||
import { | ||
OverlayCloseDetail, | ||
OverlayOpenDetail, | ||
TriggerInteractions, | ||
Placement, | ||
} from './overlay.js'; | ||
import { ThemeData } from '@spectrum-web-components/theme'; | ||
import { Placement } from './overlay-types'; | ||
import { Overlay } from './overlay.js'; | ||
let overlayRoot: OverlayRoot; | ||
/** | ||
@@ -43,2 +35,5 @@ * A overlay trigger component for displaying overlays relative to other content. | ||
export class OverlayTrigger extends LitElement { | ||
private closeClickOverlay?: () => void; | ||
private closeHoverOverlay?: () => void; | ||
public static get styles(): CSSResultArray { | ||
@@ -48,4 +43,2 @@ return [overlayTriggerStyles]; | ||
static overlayRoot: OverlayRoot; | ||
@property({ reflect: true }) | ||
@@ -61,103 +54,5 @@ public placement: Placement = 'bottom'; | ||
private clickContent?: HTMLElement; | ||
private hoverContent?: HTMLElement; | ||
private targetContent?: HTMLElement; | ||
public onOverlayOpen(event: Event, interaction: TriggerInteractions): void { | ||
const isClick = interaction === 'click'; | ||
const overlayElement = isClick ? this.clickContent : this.hoverContent; | ||
/* istanbul ignore if */ | ||
if (!overlayElement) { | ||
return; | ||
} | ||
if (!overlayRoot) { | ||
overlayRoot = new OverlayRoot(); | ||
} | ||
const delayAttribute = overlayElement.getAttribute('delay'); | ||
const delay = delayAttribute ? parseFloat(delayAttribute) : 0; | ||
const queryThemeDetail: ThemeData = { | ||
color: undefined, | ||
size: undefined, | ||
}; | ||
const queryThemeEvent = new CustomEvent<ThemeData>('query-theme', { | ||
bubbles: true, | ||
composed: true, | ||
detail: queryThemeDetail, | ||
cancelable: true, | ||
}); | ||
this.dispatchEvent(queryThemeEvent); | ||
const overlayOpenDetail: OverlayOpenDetail = { | ||
content: overlayElement, | ||
delay: delay, | ||
offset: this.offset, | ||
placement: this.placement, | ||
trigger: this, | ||
interaction: interaction, | ||
theme: queryThemeDetail, | ||
}; | ||
const overlayOpenEvent = new CustomEvent<OverlayOpenDetail>( | ||
'sp-overlay-open', | ||
{ | ||
bubbles: true, | ||
composed: true, | ||
detail: overlayOpenDetail, | ||
} | ||
); | ||
this.dispatchEvent(overlayOpenEvent); | ||
} | ||
public onOverlayClose( | ||
event: Event, | ||
interaction: TriggerInteractions | ||
): void { | ||
const isClick = interaction === 'click'; | ||
const overlayElement = isClick ? this.clickContent : this.hoverContent; | ||
/* istanbul ignore if */ | ||
if (!overlayElement) { | ||
return; | ||
} | ||
const overlayCloseDetail: OverlayCloseDetail = { | ||
content: overlayElement, | ||
}; | ||
const overlayCloseEvent = new CustomEvent<OverlayCloseDetail>( | ||
'sp-overlay-close', | ||
{ | ||
bubbles: true, | ||
composed: true, | ||
detail: overlayCloseDetail, | ||
} | ||
); | ||
this.dispatchEvent(overlayCloseEvent); | ||
} | ||
public onTriggerClick(event: Event): void { | ||
/* istanbul ignore else */ | ||
if (this.clickContent) { | ||
this.onOverlayOpen(event, 'click'); | ||
} | ||
} | ||
public onTriggerMouseOver(event: Event): void { | ||
/* istanbul ignore else */ | ||
if (this.hoverContent) { | ||
this.onOverlayOpen(event, 'hover'); | ||
} | ||
} | ||
public onTriggerMouseLeave(event: Event): void { | ||
/* istanbul ignore else */ | ||
if (this.hoverContent) { | ||
this.onOverlayClose(event, 'hover'); | ||
} | ||
} | ||
protected render(): TemplateResult { | ||
@@ -168,6 +63,9 @@ return html` | ||
@click=${this.onTriggerClick} | ||
@mouseenter=${this.onTriggerMouseOver} | ||
@mouseenter=${this.onTriggerMouseEnter} | ||
@mouseleave=${this.onTriggerMouseLeave} | ||
> | ||
<slot name="trigger"></slot> | ||
<slot | ||
@slotchange=${this.onTargetSlotChange} | ||
name="trigger" | ||
></slot> | ||
</div> | ||
@@ -187,16 +85,56 @@ <div id="overlay-content"> | ||
private onClickSlotChange(event: Event): void { | ||
/* istanbul ignore if */ | ||
if (!event.target) { | ||
return; | ||
public onTriggerClick(): void { | ||
/* istanbul ignore else */ | ||
if (this.targetContent && this.clickContent) { | ||
this.closeClickOverlay = Overlay.open( | ||
this.targetContent, | ||
'click', | ||
this.clickContent, | ||
{ | ||
offset: this.offset, | ||
placement: this.placement, | ||
} | ||
); | ||
} | ||
const slot = event.target as HTMLSlotElement; | ||
const content = this.extractSlotContent(slot); | ||
} | ||
if (content) { | ||
this.clickContent = content; | ||
public onTriggerMouseEnter(): void { | ||
/* istanbul ignore else */ | ||
if (this.targetContent && this.hoverContent) { | ||
this.closeHoverOverlay = Overlay.open( | ||
this.targetContent, | ||
'hover', | ||
this.hoverContent, | ||
{ | ||
offset: this.offset, | ||
placement: this.placement, | ||
} | ||
); | ||
} | ||
} | ||
public onTriggerMouseLeave(): void { | ||
/* istanbul ignore else */ | ||
if (this.closeHoverOverlay) { | ||
this.closeHoverOverlay(); | ||
delete this.closeHoverOverlay; | ||
} | ||
} | ||
private onClickSlotChange(event: Event): void { | ||
const content = this.extractSlotContentFromEvent(event); | ||
this.clickContent = content; | ||
} | ||
private onHoverSlotChange(event: Event): void { | ||
const content = this.extractSlotContentFromEvent(event); | ||
this.hoverContent = content; | ||
} | ||
private onTargetSlotChange(event: Event): void { | ||
const content = this.extractSlotContentFromEvent(event); | ||
this.targetContent = content; | ||
} | ||
private extractSlotContentFromEvent(event: Event): HTMLElement | undefined { | ||
/* istanbul ignore if */ | ||
@@ -207,17 +145,4 @@ if (!event.target) { | ||
const slot = event.target as HTMLSlotElement; | ||
const content = this.extractSlotContent(slot); | ||
if (content) { | ||
this.hoverContent = content; | ||
} | ||
} | ||
private extractSlotContent(slot: HTMLSlotElement): HTMLElement | null { | ||
const nodes = slot.assignedNodes(); | ||
if (nodes.length) { | ||
return nodes[0] as HTMLElement; | ||
} | ||
return null; | ||
return nodes.find((node) => node instanceof HTMLElement) as HTMLElement; | ||
} | ||
@@ -227,7 +152,9 @@ | ||
/* istanbul ignore else */ | ||
if (this.clickContent) { | ||
this.onOverlayClose(new Event('remove'), 'click'); | ||
if (this.closeClickOverlay) { | ||
this.closeClickOverlay(); | ||
delete this.closeClickOverlay; | ||
} | ||
if (this.hoverContent) { | ||
this.onOverlayClose(new Event('remove'), 'hover'); | ||
if (this.closeHoverOverlay) { | ||
this.closeHoverOverlay(); | ||
delete this.closeHoverOverlay; | ||
} | ||
@@ -234,0 +161,0 @@ super.disconnectedCallback(); |
@@ -13,20 +13,142 @@ /* | ||
export type TriggerInteractions = 'click' | 'hover'; | ||
import { ThemeData } from '@spectrum-web-components/theme'; | ||
import { | ||
TriggerInteractions, | ||
Placement, | ||
OverlayDisplayQueryDetail, | ||
} from './overlay-types'; | ||
import { OverlayStack } from './overlay-stack'; | ||
export type Placement = 'top' | 'right' | 'bottom' | 'left'; | ||
type OverlayOptions = { | ||
delayed?: boolean; | ||
placement?: Placement; | ||
offset?: number; | ||
}; | ||
import { ThemeData } from '@spectrum-web-components/theme'; | ||
/** | ||
* This class allows access to the overlay system which allows a client to | ||
* position an element in the overlay positioned relative to another node. | ||
*/ | ||
export class Overlay { | ||
private static overlayStack = new OverlayStack(); | ||
export interface OverlayOpenDetail { | ||
content: HTMLElement; | ||
delay: number; | ||
offset: number; | ||
placement: Placement; | ||
trigger: HTMLElement; | ||
interaction: TriggerInteractions; | ||
theme: ThemeData; | ||
} | ||
private isOpen = false; | ||
private overlayElement: HTMLElement; | ||
private owner: HTMLElement; | ||
private interaction: TriggerInteractions; | ||
export interface OverlayCloseDetail { | ||
content: HTMLElement; | ||
/** | ||
* | ||
* @param owner the parent element we will use to position the overlay element | ||
* @param interaction the type of interaction that caused this overlay to be shown | ||
* @param overlayElement the item to display as an overlay | ||
*/ | ||
constructor( | ||
owner: HTMLElement, | ||
interaction: TriggerInteractions, | ||
overlayElement: HTMLElement | ||
) { | ||
this.owner = owner; | ||
this.overlayElement = overlayElement; | ||
this.interaction = interaction; | ||
} | ||
/** | ||
* Open an overlay | ||
* | ||
* @param owner the parent element we will use to position the overlay element | ||
* @param interaction the type of interaction that caused this overlay to be shown | ||
* @param overlayElement the item to display as an overlay | ||
* @param options display parameters | ||
* @param options.delayed if true delay opening of the overlay based on the global warmup/cooldown timer | ||
* @param options.offset distance to offset the overlay | ||
* @param options.placement side on which to position the overlay | ||
* @returns an Overlay object which can be used to close the overlay | ||
*/ | ||
public static open( | ||
owner: HTMLElement, | ||
interaction: TriggerInteractions, | ||
overlayElement: HTMLElement, | ||
options: OverlayOptions | ||
): () => void { | ||
const overlay = new Overlay(owner, interaction, overlayElement); | ||
overlay.open(options); | ||
return () => overlay.close(); | ||
} | ||
public static update(): void { | ||
const overlayUpdateEvent = new CustomEvent('sp-update-overlays', { | ||
bubbles: true, | ||
composed: true, | ||
cancelable: true, | ||
}); | ||
document.dispatchEvent(overlayUpdateEvent); | ||
} | ||
/** | ||
* Open an overlay | ||
* | ||
* @param options display parameters | ||
* @param options.delayed delay before opening the overlay | ||
* @param options.offset distance to offset the overlay | ||
* @param options.placement side on which to position the overlay | ||
* @returns a Promise that resolves to true if this operation was cancelled | ||
*/ | ||
public async open({ | ||
delayed, | ||
offset = 0, | ||
placement = 'top', | ||
}: OverlayOptions): Promise<boolean> { | ||
/* istanbul ignore if */ | ||
if (this.isOpen) return true; | ||
/* istanbul ignore else */ | ||
if (delayed === undefined) { | ||
delayed = this.overlayElement.hasAttribute('delayed'); | ||
} | ||
const queryThemeDetail: ThemeData = { | ||
color: undefined, | ||
scale: undefined, | ||
}; | ||
const queryThemeEvent = new CustomEvent<ThemeData>('sp-query-theme', { | ||
bubbles: true, | ||
composed: true, | ||
detail: queryThemeDetail, | ||
cancelable: true, | ||
}); | ||
this.owner.dispatchEvent(queryThemeEvent); | ||
const overlayDetailQuery: OverlayDisplayQueryDetail = {}; | ||
const queryOverlayDetailEvent = new CustomEvent< | ||
OverlayDisplayQueryDetail | ||
>('sp-overlay-query', { | ||
bubbles: true, | ||
composed: true, | ||
detail: overlayDetailQuery, | ||
cancelable: true, | ||
}); | ||
this.overlayElement.dispatchEvent(queryOverlayDetailEvent); | ||
Overlay.overlayStack.openOverlay({ | ||
content: this.overlayElement, | ||
contentTip: overlayDetailQuery.overlayContentTipElement, | ||
delayed, | ||
offset: offset, | ||
placement: placement, | ||
trigger: this.owner, | ||
interaction: this.interaction, | ||
theme: queryThemeDetail, | ||
...overlayDetailQuery, | ||
}); | ||
this.isOpen = true; | ||
return true; | ||
} | ||
/** | ||
* Close the overlay if it is open | ||
*/ | ||
public close(): void { | ||
Overlay.overlayStack.closeOverlay(this.overlayElement); | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 3 instances in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 2 instances in 1 package
51
168964
5
6
2452
2
+ Added@popperjs/core@^2.0.4
+ Added@popperjs/core@2.11.8(transitive)
+ Added@spectrum-web-components/theme@0.2.0(transitive)
- Removed@spectrum-web-components/theme@0.1.1(transitive)