Skip to content

Commit

Permalink
fix: link preview timer issue (#889)
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte authored Nov 6, 2024
1 parent 733f39a commit bb6098f
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 72 deletions.
5 changes: 5 additions & 0 deletions .changeset/many-clouds-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"bits-ui": patch
---

fix: issue with `LinkPreview` where content wouldn't close when the trigger was entered and exited quickly
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import PopperLayer from "$lib/bits/utilities/popper-layer/popper-layer.svelte";
import { getFloatingContentCSSVars } from "$lib/internal/floating-svelte/floating-utils.svelte.js";
import PopperLayerForceMount from "$lib/bits/utilities/popper-layer/popper-layer-force-mount.svelte";
import Mounted from "$lib/bits/utilities/mounted.svelte";
let {
children,
Expand Down Expand Up @@ -112,6 +113,7 @@
{@render children?.()}
</div>
{/if}
<Mounted onMountedChange={(m) => (contentState.root.contentMounted = m)} />
{/snippet}
</PopperLayer>
{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ class LinkPreviewRootState {
timeout: number | null = null;
contentNode = $state<HTMLElement | null>(null);
contentId = $state<string | undefined>(undefined);
contentMounted = $state(false);
triggerNode = $state<HTMLElement | null>(null);
isPointerInTransit = box(false);
isOpening = $state(false);

constructor(props: LinkPreviewRootStateProps) {
this.open = props.open;
Expand Down Expand Up @@ -87,17 +89,23 @@ class LinkPreviewRootState {
handleOpen = () => {
this.clearTimeout();
if (this.open.current) return;
this.isOpening = true;
this.timeout = window.setTimeout(() => {
this.open.current = true;
if (this.isOpening) {
this.open.current = true;
this.isOpening = false;
}
}, this.openDelay.current);
};

immediateClose = () => {
this.clearTimeout();
this.isOpening = false;
this.open.current = false;
};

handleClose = () => {
this.isOpening = false;
this.clearTimeout();

if (!this.isPointerDownOnContent && !this.hasSelection) {
Expand Down Expand Up @@ -134,6 +142,13 @@ class LinkPreviewTriggerState {
this.#root.handleOpen();
};

#onpointerleave = (e: PointerEvent) => {
if (isTouch(e)) return;
if (!this.#root.contentMounted) {
this.#root.immediateClose();
}
};

#onfocus = (e: FocusEvent & { currentTarget: HTMLElement }) => {
if (!isFocusVisible(e.currentTarget)) return;
this.#root.handleOpen();
Expand All @@ -156,6 +171,7 @@ class LinkPreviewTriggerState {
onpointerenter: this.#onpointerenter,
onfocus: this.#onfocus,
onblur: this.#onblur,
onpointerleave: this.#onpointerleave,
}) as const
);
}
Expand Down
22 changes: 10 additions & 12 deletions packages/bits-ui/src/lib/internal/use-grace-area.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { addEventListener } from "./events.js";
import type { Side } from "$lib/bits/utilities/floating-layer/useFloatingLayer.svelte.js";

export function useGraceArea(
triggerNode: Getter<HTMLElement | null>,
contentNode: Getter<HTMLElement | null>
getTriggerNode: Getter<HTMLElement | null>,
getContentNode: Getter<HTMLElement | null>
) {
const isPointerInTransit = boxAutoReset(false, 300);
const triggerNode = $derived(getTriggerNode());
const contentNode = $derived(getContentNode());

let pointerGraceArea = $state<Polygon | null>(null);
const pointerExit = createEventHook<void>();
Expand All @@ -32,21 +34,19 @@ export function useGraceArea(
}

$effect(() => {
const trigger = triggerNode();
const content = contentNode();
if (!trigger || !content) return;
if (!triggerNode || !contentNode) return;

const handleTriggerLeave = (e: PointerEvent) => {
handleCreateGraceArea(e, content!);
handleCreateGraceArea(e, contentNode!);
};

const handleContentLeave = (e: PointerEvent) => {
handleCreateGraceArea(e, trigger!);
handleCreateGraceArea(e, triggerNode!);
};

const unsub = executeCallbacks(
addEventListener(trigger, "pointerleave", handleTriggerLeave),
addEventListener(content, "pointerleave", handleContentLeave)
addEventListener(triggerNode, "pointerleave", handleTriggerLeave),
addEventListener(contentNode, "pointerleave", handleContentLeave)
);
return unsub;
});
Expand All @@ -58,10 +58,8 @@ export function useGraceArea(
if (!pointerGraceArea) return;
const target = e.target;
if (!isElement(target)) return;
const trigger = triggerNode();
const content = contentNode();
const pointerPosition = { x: e.clientX, y: e.clientY };
const hasEnteredTarget = trigger?.contains(target) || content?.contains(target);
const hasEnteredTarget = triggerNode?.contains(target) || contentNode?.contains(target);
const isPointerOutsideGraceArea = !isPointInPolygon(pointerPosition, pointerGraceArea);

if (hasEnteredTarget) {
Expand Down
80 changes: 21 additions & 59 deletions sites/docs/src/routes/(main)/sink/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,64 +1,26 @@
<script lang="ts">
import { Dialog, Label, Separator } from "bits-ui";
import LockKeyOpen from "phosphor-svelte/lib/LockKeyOpen";
import X from "phosphor-svelte/lib/X";
import SelectDemo from "$lib/components/demos/select-demo.svelte";
import PopoverDemo from "$lib/components/demos/popover-demo.svelte";
import { LinkPreview } from "bits-ui";
let numbers = [1, 2, 3, 4, 5];
</script>

<Dialog.Root>
<Dialog.Trigger
class="inline-flex h-12 items-center
justify-center whitespace-nowrap rounded-input bg-dark px-[21px]
text-[15px] font-semibold text-background shadow-mini transition-colors hover:bg-dark/95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background active:scale-98"
>
New API key
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay
class="fixed inset-0 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<Dialog.Content
class="fixed left-[50%] top-[50%] w-full max-w-[94%] translate-x-[-50%] translate-y-[-50%] rounded-card-lg border bg-background p-5 shadow-popover outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:max-w-[490px] md:w-full"
>
<Dialog.Title
class="flex w-full items-center justify-center text-lg font-semibold tracking-tight"
>Create API key</Dialog.Title
<div class="flex w-full flex-col">
{#each numbers as number}
<LinkPreview.Root>
<LinkPreview.Trigger
href="https://github.com/sveltejs"
target="_blank"
rel="noreferrer noopener"
class="w-full border border-border p-2"
>
<Separator.Root class="-mx-5 mb-6 mt-5 block h-px bg-muted" />
<Dialog.Description class="text-sm text-foreground-alt">
Create and manage API keys. You can create multiple keys to organize your
applications.
</Dialog.Description>
<SelectDemo />
<PopoverDemo />
<div class="flex flex-col items-start gap-1 pb-11 pt-7">
<Label.Root for="apiKey" class="text-sm font-medium">API Key</Label.Root>
<div class="relative w-full">
<input
id="apiKey"
class="inline-flex h-input w-full items-center rounded-card-sm border border-border-input bg-background px-4 text-sm placeholder:text-foreground-alt/50 hover:border-dark-40 focus:outline-none focus:ring-2 focus:ring-foreground focus:ring-offset-2 focus:ring-offset-background"
placeholder="secret_api_key"
name="name"
/>
<LockKeyOpen class="absolute right-4 top-[14px] size-[22px] text-dark/30" />
</div>
</div>
<div class="flex w-full justify-end">
<Dialog.Close
class="inline-flex h-input items-center justify-center rounded-input bg-dark px-[50px] text-[15px] font-semibold text-background shadow-mini hover:bg-dark/95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-dark focus-visible:ring-offset-2 focus-visible:ring-offset-background active:scale-98"
>
Save
</Dialog.Close>
</div>
<Dialog.Close
class="absolute right-5 top-5 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background active:scale-98"
{number}
</LinkPreview.Trigger>
<LinkPreview.Content
class="w-[331px] rounded-xl border border-muted bg-background p-[17px] shadow-popover"
sideOffset={8}
>
<div>
<X class="size-5 text-foreground" />
<span class="sr-only">Close</span>
</div>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
PREVIEW_CONTENT {number}
</LinkPreview.Content>
</LinkPreview.Root>
{/each}
</div>

0 comments on commit bb6098f

Please sign in to comment.