178 lines
4.5 KiB
Svelte
178 lines
4.5 KiB
Svelte
<script context="module" lang="ts">
|
|
export interface PopoverAPI {
|
|
show: () => void
|
|
hide: () => void
|
|
}
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import "@spectrum-css/popover/dist/index-vars.css"
|
|
import Portal from "svelte-portal"
|
|
import { createEventDispatcher, getContext, onDestroy } from "svelte"
|
|
import positionDropdown, {
|
|
type UpdateHandler,
|
|
} from "../Actions/position_dropdown"
|
|
import clickOutside from "../Actions/click_outside"
|
|
import { fly } from "svelte/transition"
|
|
import Context from "../context"
|
|
import type { KeyboardEventHandler } from "svelte/elements"
|
|
import { PopoverAlignment } from "../constants"
|
|
|
|
export let anchor: HTMLElement
|
|
export let align: PopoverAlignment = PopoverAlignment.Right
|
|
export let portalTarget: string | undefined = undefined
|
|
export let minWidth: number | undefined = undefined
|
|
export let maxWidth: number | undefined = undefined
|
|
export let maxHeight: number | undefined = undefined
|
|
export let open = false
|
|
export let useAnchorWidth = false
|
|
export let dismissible = true
|
|
export let offset = 4
|
|
export let customHeight: string | undefined = undefined
|
|
export let animate = true
|
|
export let customZIndex: number | undefined = undefined
|
|
export let handlePositionUpdate: UpdateHandler | undefined = undefined
|
|
export let showPopover = true
|
|
export let clickOutsideOverride = false
|
|
export let resizable = true
|
|
export let wrap = false
|
|
|
|
const dispatch = createEventDispatcher<{ open: void; close: void }>()
|
|
const animationDuration = 260
|
|
|
|
let timeout: ReturnType<typeof setTimeout>
|
|
let blockPointerEvents = false
|
|
|
|
// Portal library lacks types, so we have to type this as any even though it's
|
|
// actually a string
|
|
$: target = (portalTarget ||
|
|
getContext(Context.PopoverRoot) ||
|
|
".spectrum") as any
|
|
$: {
|
|
// Disable pointer events for the initial part of the animation, because we
|
|
// fly from top to bottom and initially can be positioned under the cursor,
|
|
// causing a flashing hover state in the content
|
|
if (open && animate) {
|
|
blockPointerEvents = true
|
|
clearTimeout(timeout)
|
|
timeout = setTimeout(() => {
|
|
blockPointerEvents = false
|
|
}, animationDuration / 2)
|
|
}
|
|
}
|
|
|
|
export const show = () => {
|
|
dispatch("open")
|
|
open = true
|
|
}
|
|
|
|
export const hide = () => {
|
|
dispatch("close")
|
|
open = false
|
|
}
|
|
|
|
export const toggle = () => {
|
|
if (!open) {
|
|
show()
|
|
} else {
|
|
hide()
|
|
}
|
|
}
|
|
|
|
const handleOutsideClick = (e: MouseEvent) => {
|
|
if (clickOutsideOverride) {
|
|
return
|
|
}
|
|
if (open) {
|
|
// Stop propagation if the source is the anchor
|
|
let node = e.target as Node | null
|
|
let fromAnchor = false
|
|
while (!fromAnchor && node && node.parentNode) {
|
|
fromAnchor = node === anchor
|
|
node = node.parentNode
|
|
}
|
|
if (fromAnchor) {
|
|
e.stopPropagation()
|
|
}
|
|
|
|
// Hide the popover
|
|
hide()
|
|
}
|
|
}
|
|
|
|
const handleEscape: KeyboardEventHandler<HTMLDivElement> = e => {
|
|
if (!clickOutsideOverride) {
|
|
return
|
|
}
|
|
if (open && e.key === "Escape") {
|
|
hide()
|
|
}
|
|
}
|
|
|
|
onDestroy(() => {
|
|
clearTimeout(timeout)
|
|
})
|
|
</script>
|
|
|
|
{#if open}
|
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
|
<Portal {target}>
|
|
<div
|
|
tabindex="0"
|
|
use:positionDropdown={{
|
|
anchor,
|
|
align,
|
|
maxHeight,
|
|
maxWidth,
|
|
minWidth,
|
|
useAnchorWidth,
|
|
offset,
|
|
customUpdate: handlePositionUpdate,
|
|
resizable,
|
|
wrap,
|
|
}}
|
|
use:clickOutside={{
|
|
callback: dismissible ? handleOutsideClick : () => {},
|
|
anchor,
|
|
}}
|
|
on:keydown={handleEscape}
|
|
class="spectrum-Popover is-open"
|
|
class:customZIndex
|
|
class:hidden={!showPopover}
|
|
class:blockPointerEvents
|
|
role="presentation"
|
|
style="height: {customHeight}; --customZIndex: {customZIndex};"
|
|
transition:fly|local={{
|
|
y: -20,
|
|
duration: animate ? animationDuration : 0,
|
|
}}
|
|
on:mouseenter
|
|
on:mouseleave
|
|
>
|
|
<slot />
|
|
</div>
|
|
</Portal>
|
|
{/if}
|
|
|
|
<style>
|
|
.spectrum-Popover {
|
|
min-width: var(--spectrum-global-dimension-size-2000);
|
|
border-color: var(--spectrum-global-color-gray-300);
|
|
overflow: auto;
|
|
transition: opacity 260ms ease-out;
|
|
filter: none;
|
|
-webkit-filter: none;
|
|
box-shadow: 0 1px 4px var(--drop-shadow);
|
|
}
|
|
.blockPointerEvents {
|
|
pointer-events: none;
|
|
}
|
|
.hidden {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
.customZIndex {
|
|
z-index: var(--customZIndex) !important;
|
|
}
|
|
</style>
|