Anchored Popover
Similar to the Popover component, but this one is anchored to a virtual reference element. This is used in features such as floating toolbars.
Here, you'll need to create a virtual element because text selection and mouse coordinates don't have a DOM element associated with them. So you'll need to create a virtual element that responds to getBoundingClientRect(), retrieve the coordinates of the selection or mouse coordinates, and then position the popover relative to that.
Live islandRuns locally in this page
Page shell
<h1>Anchored Popover</h1>
<p>An anchored popover relative to any coordinate. In this example, we're usign the mouse position as the reference element.</p>
<cami-anchored-popover></cami-anchored-popover>
<style>
h1, p {
text-align: center;
}
.popover__backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
.popover__backdrop--hidden {
display: none;
}
</style>
<!-- <script src="./build/cami.cdn.js"></script> -->
<!-- CDN version below -->
<script src="https://unpkg.com/cami@0.4/build/cami.cdn.js"></script>
<script type="module" src="./island.js"></script>
Island source
const { html, ReactiveElement } = cami
class AnchoredPopoverElement extends ReactiveElement {
isOpen = false;
point = { x: 0, y: 0 };
anchor = null;
openAt(point) {
this.point = point;
this.anchor = {
getBoundingClientRect: () => new DOMRect(point.x, point.y, 0, 0),
};
this.isOpen = true;
}
close() {
this.isOpen = false;
this.anchor = null;
}
handlePointerMove(event) {
this.point = { x: event.clientX, y: event.clientY };
}
handlePointerDown(event) {
this.openAt({ x: event.clientX, y: event.clientY });
}
get positionStyle() {
const rect = this.anchor?.getBoundingClientRect();
if (!rect)
return '';
return `position:fixed;left:${rect.x}px;top:${rect.y - 8}px;transform:translate(-50%, -100%)`;
}
template() {
return html `
<section
class="anchor-surface"
@pointermove=${(event) => this.handlePointerMove(event)}
@pointerdown=${(event) => this.handlePointerDown(event)}
>
Click anywhere to anchor the popover. Pointer: ${this.point.x}, ${this.point.y}
</section>
${this.isOpen ? html `
<aside role="dialog" class="popover" style=${this.positionStyle}>
<p>Virtual anchor at ${this.point.x}, ${this.point.y}</p>
<button @click=${() => this.close()}>Close</button>
</aside>
` : ''}
`;
}
}
customElements.define('cami-anchored-popover', AnchoredPopoverElement);
import { html, ReactiveElement } from 'cami'
interface Point {
x: number
y: number
}
interface VirtualAnchor {
getBoundingClientRect(): DOMRect
}
class AnchoredPopoverElement extends ReactiveElement {
isOpen: boolean = false
point: Point = { x: 0, y: 0 }
anchor: VirtualAnchor | null = null
openAt(point: Point): void {
this.point = point
this.anchor = {
getBoundingClientRect: (): DOMRect => new DOMRect(point.x, point.y, 0, 0),
}
this.isOpen = true
}
close(): void {
this.isOpen = false
this.anchor = null
}
handlePointerMove(event: PointerEvent): void {
this.point = { x: event.clientX, y: event.clientY }
}
handlePointerDown(event: PointerEvent): void {
this.openAt({ x: event.clientX, y: event.clientY })
}
get positionStyle(): string {
const rect = this.anchor?.getBoundingClientRect()
if (!rect) return ''
return `position:fixed;left:${rect.x}px;top:${rect.y - 8}px;transform:translate(-50%, -100%)`
}
template(): ReturnType<typeof html> {
return html`
<section
class="anchor-surface"
@pointermove=${(event: PointerEvent) => this.handlePointerMove(event)}
@pointerdown=${(event: PointerEvent) => this.handlePointerDown(event)}
>
Click anywhere to anchor the popover. Pointer: ${this.point.x}, ${this.point.y}
</section>
${this.isOpen ? html`
<aside role="dialog" class="popover" style=${this.positionStyle}>
<p>Virtual anchor at ${this.point.x}, ${this.point.y}</p>
<button @click=${() => this.close()}>Close</button>
</aside>
` : ''}
`
}
}
declare global {
interface HTMLElementTagNameMap {
'cami-anchored-popover': AnchoredPopoverElement
}
}
customElements.define('cami-anchored-popover', AnchoredPopoverElement)