imperative-api

Examples

Description #

While an <sp-overlay> element is the recommended entry point to the Spectrum Web Components Overlay API, you can also interact with this set of features via an imperative API, Overlay.open.

Usage #

See it on NPM! How big is this package in your project?

yarn add @spectrum-web-components/overlay

Import the Overlay class to leverage its capabilities within your application or custom element:

import { Overlay } from '@spectrum-web-components/overlay';

Primarily, this class gives you access to the open method that will allow you to open an overlay:

Overlay.open(
    (overlayElement: HTMLElement), // the element that will be projected into the overlay, "content",
    (options?: OverlayOptions)
);

Overlay.open() is an asynchronous method that returns an <sp-overlay> element that wraps the HTMLElement provided as the overlayElement. While this process will set the <sp-overlay> element to open, consumers of this API will need to choose where to append this element to the DOM in order for the content to be made available in the browser.

(async () => {
    const content = document.querySelector('#content');
    const options = {
        offset: 0,
        placement: 'bottom',
        trigger: document.querySelector('#trigger'),
        type: 'auto',
    };
    const overlay = await Overlay.open(content, options);
    document.body.append(overlay);
})();

Keep in mind that a changing DOM tree is likely to alter the relationship between existing content. Without proper care this can negatively effect the CSS that you have applied to existing content. DOM events and DOM selection methods can also perform differently than expected as the tree shape changes.

OverlayOptions #

When leveraging Overlay.open(), you can provide an optional second argument of OverlayOptions, with the following type:

type OverlayOptions = {
    delayed?: boolean;
    notImmediatelyCloseable?: boolean;
    offset?: number | [number, number];
    placement?: Placement;
    receivesFocus?: 'auto' | 'true' | 'false';
    trigger?: HTMLElement | VirtualTrigger;
    type?: 'modal' | 'page' | 'hint' | 'auto' | 'manual';
};

delayed #

An Overlay that is delayed will wait until a warm-up period of 1000ms has completed before opening. Once the warmup period has completed, all subsequent Overlays will open immediately. When no Overlays are opened, a cooldown period of 1000ms will begin. Once the cooldown has completed, the next Overlay to be opened will be subject to the warm-up period if provided that option.

notImmediatelyCloseable #

When an Overlay is notImmediatelyCloseable that means that the first interaction that would lead to the closure of the Overlay in question will be ignored. This is useful when working with non-"click" mouse interactions, like contextmenu, where the trigger event (e.g. contextmenu) occurs before an event that would close said overlay (e.g. pointerup).

offset #

The offset property accepts either a single number, to define the offset of the Overlay along the main axis from the trigger, or 2-tuple, to define the offset along the main axis and the cross axis. This option has no effect when there is no trigger element.

placement #

A placement of "auto-start" | "auto-end" | "top" | "bottom" | "right" | "left" | "top-start" | "top-end" | "bottom-start" | "bottom-end" | "right-start" | "right-end" | "left-start" | "left-end" will instruct the Overlay where to place itself in relationship to the trigger element.

receivesFocus #

Some Overlays will always be passed focus (e.g. modal or page Overlays). When this is not true, the receivesFocus option will inform the API whether to attempt to pass focus into the Overlay once it is open. 'true' will pass focus, 'false' will not (when possible), and "auto" (the default), will make a decision based on the type of the Overlay.

trigger #

The trigger option accepts an HTMLElement or a VirtualTrigger from which to position the Overlay.

  • You can import the VirtualTrigger class from the overlay package to create a virtual trigger that can be used to position an Overlay. This is useful when you want to position an Overlay relative to a point on the screen that is not an element in the DOM, like the mouse cursor.

Using a virtual trigger #

<style>
    #root {
        position: relative;
        width: 100%;
        height: 20vh;
        background-color: var(--spectrum-gray-100);
        border: 1px solid var(--spectrum-gray-400);
    }
</style>

<p>Right click anywhere in bounded rectangle to open the menu</p>
<div id="root"></div>

<script type="module">

    import { html, render } from '@spectrum-web-components/base';
    import { VirtualTrigger, openOverlay } from '@spectrum-web-components/overlay';

    const contextMenuTemplate = () => html`
          <sp-popover
            style="width:300px;"
            @change=${(event) => {
                    event.target.dispatchEvent(
                    new Event('close', { bubbles: true })
                );
            }}
        >
            <sp-menu>
                <sp-menu-item>Deselect</sp-menu-item>
                <sp-menu-item>Select Inverse</sp-menu-item>
                <sp-menu-item>Select All</sp-menu-item>
                <sp-menu-divider></sp-menu-divider>
                <sp-menu-item disabled>Copy</sp-menu-item>
                <sp-menu-item disabled>Cut</sp-menu-item>
                <sp-menu-item disabled>Paste</sp-menu-item>
            </sp-menu>
        </sp-popover>
    `;

    const init = () => {
        const appRoot = document.querySelector('#root');
        appRoot.addEventListener('contextmenu', async (event) => {
            event.preventDefault();
            event.stopPropagation();

            const source = event.composedPath()[0];
            const { id } = source;
            const trigger = event.target;
            const virtualTrigger = new VirtualTrigger(event.clientX, event.clientY);
            const fragment = document.createDocumentFragment();
            render(contextMenuTemplate(), fragment);
            const popover = fragment.querySelector('sp-popover');

            const overlay = await openOverlay(popover, {
                trigger: virtualTrigger,
                placement: 'right-start',
                offset: 0,
                notImmediatelyClosable: true,
                type: 'auto',
            });
            trigger.insertAdjacentElement('afterend', overlay);
        });
    }

    customElements.whenDefined('code-example').then(() => {
        Promise.all([...document.querySelectorAll('code-example')].map(example => example.updateComplete)).then(() => {
            init();
        });
    });
</script>

type #

The type of an Overlay outlines a number of things about the interaction model within which is works.

  • 'modal' Overlays are opened with the showModal() method on a <dialog> element, which causes the Overlay to accept focus and trap the tab stop within the content of said Overlay.
  • 'page' Overlays will act in a similar manner to a 'modal' Overlay, however they will not be allowed to close via the "light dismiss" algorithm (e.g. the Escape key).
  • 'hint' Overlays are much like tooltips so they are not just ephemeral, but they are delivered primarily as a visual helper and exist outside of the tab order. In this way, be sure not to place interactive content within this type of Overlay.
  • 'auto' Overlays provide a place for content that is ephemeral and interactive. These Overlays can accept focus but will close when losing that focus, or when interacting with other parts of the page.
  • 'manual' Overlays act much like "auto" Overlays, but do not close when losing focus or interacting with other parts of the page. When a "manual" Overlay is at the top of the "overlay stack", it will still respond to the Escape key and close.