From b8769a5d61021d2fccb49418426fa270d0de73bd Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Mon, 30 Dec 2024 19:48:55 -0500 Subject: [PATCH 1/2] fix: popover trigger close detection --- .changeset/six-dodos-visit.md | 5 +++ .../popover/components/popover-content.svelte | 36 +++++------------ .../src/lib/bits/popover/popover.svelte.ts | 40 ++++++++++++++++++- 3 files changed, 53 insertions(+), 28 deletions(-) create mode 100644 .changeset/six-dodos-visit.md diff --git a/.changeset/six-dodos-visit.md b/.changeset/six-dodos-visit.md new file mode 100644 index 000000000..0614e1f64 --- /dev/null +++ b/.changeset/six-dodos-visit.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +fix: `Popover` trigger close detection diff --git a/packages/bits-ui/src/lib/bits/popover/components/popover-content.svelte b/packages/bits-ui/src/lib/bits/popover/components/popover-content.svelte index 7813fa1e4..80d8cc6ed 100644 --- a/packages/bits-ui/src/lib/bits/popover/components/popover-content.svelte +++ b/packages/bits-ui/src/lib/bits/popover/components/popover-content.svelte @@ -7,7 +7,6 @@ import { useId } from "$lib/internal/use-id.js"; 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 { isHTMLElement } from "$lib/internal/is.js"; let { child, @@ -29,29 +28,12 @@ () => ref, (v) => (ref = v) ), + onInteractOutside: box.with(() => onInteractOutside), + onEscapeKeydown: box.with(() => onEscapeKeydown), + onCloseAutoFocus: box.with(() => onCloseAutoFocus), }); const mergedProps = $derived(mergeProps(restProps, contentState.props)); - - function handleInteractOutside(e: PointerEvent) { - onInteractOutside(e); - if (e.defaultPrevented) return; - if (isHTMLElement(e.target) && e.target.closest("[data-popover-trigger")) return; - contentState.root.handleClose(); - } - - function handleEscapeKeydown(e: KeyboardEvent) { - onEscapeKeydown(e); - if (e.defaultPrevented) return; - contentState.root.handleClose(); - } - - function handleCloseAutoFocus(e: Event) { - onCloseAutoFocus(e); - if (e.defaultPrevented) return; - e.preventDefault(); - contentState.root.triggerNode?.focus(); - } {#if forceMount} @@ -59,9 +41,9 @@ {...mergedProps} enabled={contentState.root.open.current} {id} - onInteractOutside={handleInteractOutside} - onEscapeKeydown={handleEscapeKeydown} - onCloseAutoFocus={handleCloseAutoFocus} + onInteractOutside={contentState.handleInteractOutside} + onEscapeKeydown={contentState.handleEscapeKeydown} + onCloseAutoFocus={contentState.handleCloseAutoFocus} {trapFocus} {preventScroll} loop @@ -87,9 +69,9 @@ {...mergedProps} present={contentState.root.open.current} {id} - onInteractOutside={handleInteractOutside} - onEscapeKeydown={handleEscapeKeydown} - onCloseAutoFocus={handleCloseAutoFocus} + onInteractOutside={contentState.handleInteractOutside} + onEscapeKeydown={contentState.handleEscapeKeydown} + onCloseAutoFocus={contentState.handleCloseAutoFocus} {trapFocus} {preventScroll} loop diff --git a/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts b/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts index 16ce16ff4..6b572ec9e 100644 --- a/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts +++ b/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts @@ -9,6 +9,7 @@ import type { BitsPointerEvent, WithRefProps, } from "$lib/internal/types.js"; +import { isElement } from "$lib/internal/is.js"; type PopoverRootStateProps = WritableBoxedValues<{ open: boolean; @@ -106,16 +107,27 @@ class PopoverTriggerState { ); } -type PopoverContentStateProps = WithRefProps; +type PopoverContentStateProps = WithRefProps & + ReadableBoxedValues<{ + onInteractOutside: (e: PointerEvent) => void; + onEscapeKeydown: (e: KeyboardEvent) => void; + onCloseAutoFocus: (e: Event) => void; + }>; class PopoverContentState { #id: PopoverContentStateProps["id"]; #ref: PopoverContentStateProps["ref"]; root: PopoverRootState; + #onInteractOutside: PopoverContentStateProps["onInteractOutside"]; + #onEscapeKeydown: PopoverContentStateProps["onEscapeKeydown"]; + #onCloseAutoFocus: PopoverContentStateProps["onCloseAutoFocus"]; constructor(props: PopoverContentStateProps, root: PopoverRootState) { this.#id = props.id; this.root = root; this.#ref = props.ref; + this.#onEscapeKeydown = props.onEscapeKeydown; + this.#onInteractOutside = props.onInteractOutside; + this.#onCloseAutoFocus = props.onCloseAutoFocus; useRefById({ id: this.#id, @@ -125,6 +137,32 @@ class PopoverContentState { this.root.contentNode = node; }, }); + + this.handleInteractOutside = this.handleInteractOutside.bind(this); + this.handleEscapeKeydown = this.handleEscapeKeydown.bind(this); + this.handleCloseAutoFocus = this.handleCloseAutoFocus.bind(this); + } + + handleInteractOutside(e: PointerEvent) { + this.#onInteractOutside.current(e); + if (e.defaultPrevented) return; + if (!isElement(e.target)) return; + const closestTrigger = e.target.closest(`[data-popover-trigger]`); + if (closestTrigger === this.root.triggerNode) return; + this.root.handleClose(); + } + + handleEscapeKeydown(e: KeyboardEvent) { + this.#onEscapeKeydown.current(e); + if (e.defaultPrevented) return; + this.root.handleClose(); + } + + handleCloseAutoFocus(e: Event) { + this.#onCloseAutoFocus.current(e); + if (e.defaultPrevented) return; + e.preventDefault(); + this.root.triggerNode?.focus(); } snippetProps = $derived.by(() => ({ open: this.root.open.current })); From 0be22090041ee80685ef01a89d6500b07b4cdbee Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Mon, 30 Dec 2024 19:55:25 -0500 Subject: [PATCH 2/2] update static --- .../components/popover-content-static.svelte | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/packages/bits-ui/src/lib/bits/popover/components/popover-content-static.svelte b/packages/bits-ui/src/lib/bits/popover/components/popover-content-static.svelte index 353357137..bc3aeaed2 100644 --- a/packages/bits-ui/src/lib/bits/popover/components/popover-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/popover/components/popover-content-static.svelte @@ -19,6 +19,7 @@ onInteractOutside = noop, trapFocus = true, preventScroll = false, + ...restProps }: PopoverContentStaticProps = $props(); @@ -28,28 +29,12 @@ () => ref, (v) => (ref = v) ), + onInteractOutside: box.with(() => onInteractOutside), + onEscapeKeydown: box.with(() => onEscapeKeydown), + onCloseAutoFocus: box.with(() => onCloseAutoFocus), }); const mergedProps = $derived(mergeProps(restProps, contentState.props)); - - function handleInteractOutside(e: PointerEvent) { - onInteractOutside(e); - if (e.defaultPrevented) return; - contentState.root.handleClose(); - } - - function handleEscapeKeydown(e: KeyboardEvent) { - onEscapeKeydown(e); - if (e.defaultPrevented) return; - contentState.root.handleClose(); - } - - function handleCloseAutoFocus(e: Event) { - onCloseAutoFocus(e); - if (e.defaultPrevented) return; - e.preventDefault(); - contentState.root.triggerNode?.focus(); - } {#if forceMount} @@ -58,9 +43,9 @@ isStatic enabled={contentState.root.open.current} {id} - onInteractOutside={handleInteractOutside} - onEscapeKeydown={handleEscapeKeydown} - onCloseAutoFocus={handleCloseAutoFocus} + onInteractOutside={contentState.handleInteractOutside} + onEscapeKeydown={contentState.handleEscapeKeydown} + onCloseAutoFocus={contentState.handleCloseAutoFocus} {trapFocus} {preventScroll} loop @@ -85,9 +70,9 @@ isStatic present={contentState.root.open.current} {id} - onInteractOutside={handleInteractOutside} - onEscapeKeydown={handleEscapeKeydown} - onCloseAutoFocus={handleCloseAutoFocus} + onInteractOutside={contentState.handleInteractOutside} + onEscapeKeydown={contentState.handleEscapeKeydown} + onCloseAutoFocus={contentState.handleCloseAutoFocus} {trapFocus} {preventScroll} loop