diff --git a/.changeset/cool-cats-compete.md b/.changeset/cool-cats-compete.md new file mode 100644 index 000000000..d0bdd4bba --- /dev/null +++ b/.changeset/cool-cats-compete.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +fix: Combobox `forceMount` diff --git a/.changeset/itchy-drinks-explode.md b/.changeset/itchy-drinks-explode.md new file mode 100644 index 000000000..7e0b6424c --- /dev/null +++ b/.changeset/itchy-drinks-explode.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +fix: Tooltip `forceMount` diff --git a/.changeset/olive-ads-enjoy.md b/.changeset/olive-ads-enjoy.md new file mode 100644 index 000000000..3cccc93d0 --- /dev/null +++ b/.changeset/olive-ads-enjoy.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +fix: DropdownMenu & ContextMenu `forceMount` diff --git a/.changeset/olive-worms-bow.md b/.changeset/olive-worms-bow.md new file mode 100644 index 000000000..29e588dd9 --- /dev/null +++ b/.changeset/olive-worms-bow.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +fix: Select `forceMount` diff --git a/.changeset/stale-weeks-ring.md b/.changeset/stale-weeks-ring.md new file mode 100644 index 000000000..87c60479d --- /dev/null +++ b/.changeset/stale-weeks-ring.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +fix: popover `forceMount` diff --git a/.changeset/wet-beds-hunt.md b/.changeset/wet-beds-hunt.md new file mode 100644 index 000000000..9929b08c4 --- /dev/null +++ b/.changeset/wet-beds-hunt.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +fix: LinkPreview `forceMount` diff --git a/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content-static.svelte b/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content-static.svelte index e048fbb9e..14dc8f06d 100644 --- a/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content-static.svelte @@ -1,11 +1,13 @@ - { + function handleInteractOutside(e: PointerEvent) { onInteractOutside(e); if (e.defaultPrevented) return; contentState.parentMenu.onClose(); - }} - onEscapeKeydown={(e) => { + } + + function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown(e); if (e.defaultPrevented) return; contentState.parentMenu.onClose(); - }} - isValidEvent={(e) => { + } + + function isValidEvent(e: PointerEvent) { if ("button" in e && e.button === 2) { const target = e.target as HTMLElement; if (!target) return false; @@ -62,17 +60,69 @@ return isAnotherContextTrigger; } return false; - }} - {loop} -> - {#snippet popper({ props })} - {#if child} - {@render child({ props, ...contentState.snippetProps })} - {:else} -
- {@render children?.()} -
- {/if} - - {/snippet} -
+ } + + +{#if forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { + style: getFloatingContentCSSVars("context-menu"), + })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
+{:else if !forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { + style: getFloatingContentCSSVars("context-menu"), + })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
+{/if} diff --git a/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content.svelte b/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content.svelte index 1e8e9a65f..b12618e4f 100644 --- a/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content.svelte +++ b/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content.svelte @@ -7,6 +7,7 @@ import PopperLayer from "$lib/bits/utilities/popper-layer/popper-layer.svelte"; import Mounted from "$lib/bits/utilities/mounted.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"; let { id = useId(), @@ -36,26 +37,20 @@ }); const mergedProps = $derived(mergeProps(restProps, contentState.props)); - - { + function handleInteractOutside(e: PointerEvent) { onInteractOutside(e); if (e.defaultPrevented) return; contentState.parentMenu.onClose(); - }} - onEscapeKeydown={(e) => { + } + + function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown(e); if (e.defaultPrevented) return; contentState.parentMenu.onClose(); - }} - isValidEvent={(e) => { + } + + function isValidEvent(e: PointerEvent) { if ("button" in e && e.button === 2) { const target = e.target as HTMLElement; if (!target) return false; @@ -65,21 +60,65 @@ return isAnotherContextTrigger; } return false; - }} - trapFocus - {loop} -> - {#snippet popper({ props })} - {@const finalProps = mergeProps(props, { - style: getFloatingContentCSSVars("context-menu"), - })} - {#if child} - {@render child({ props: finalProps, ...contentState.snippetProps })} - {:else} -
- {@render children?.()} -
- {/if} - - {/snippet} -
+ } + + +{#if forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { + style: getFloatingContentCSSVars("context-menu"), + })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
+{:else if !forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { + style: getFloatingContentCSSVars("context-menu"), + })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
+{/if} diff --git a/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content-static.svelte b/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content-static.svelte index 1065602ac..f8c15ae00 100644 --- a/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content-static.svelte @@ -1,11 +1,13 @@ - { + function handleInteractOutside(e: PointerEvent) { contentState.handleInteractOutside(e); if (e.defaultPrevented) return; onInteractOutside(e); + if (e.defaultPrevented) return; contentState.parentMenu.onClose(); - }} - onEscapeKeydown={(e) => { + } + function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown(e); if (e.defaultPrevented) return; contentState.parentMenu.onClose(); - }} - trapFocus - {loop} -> - {#snippet popper({ props })} - {@const finalProps = mergeProps(props)} - {#if child} - {@render child({ props: finalProps, ...contentState.snippetProps })} - {:else} -
- {@render children?.()} -
- {/if} - - {/snippet} -
+ } + + +{#if forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { + style: getFloatingContentCSSVars("dropdown-menu"), + })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
+{:else if !forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { + style: getFloatingContentCSSVars("dropdown-menu"), + })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
+{/if} diff --git a/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content.svelte b/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content.svelte index 34d3350f2..f9e90724e 100644 --- a/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content.svelte +++ b/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content.svelte @@ -7,6 +7,7 @@ import PopperLayer from "$lib/bits/utilities/popper-layer/popper-layer.svelte"; import Mounted from "$lib/bits/utilities/mounted.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"; let { id = useId(), @@ -33,36 +34,69 @@ }); const mergedProps = $derived(mergeProps(restProps, contentState.props)); - - { + function handleInteractOutside(e: PointerEvent) { contentState.handleInteractOutside(e); if (e.defaultPrevented) return; onInteractOutside(e); + if (e.defaultPrevented) return; contentState.parentMenu.onClose(); - }} - onEscapeKeydown={(e) => { + } + function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown(e); if (e.defaultPrevented) return; contentState.parentMenu.onClose(); - }} - trapFocus - {loop} -> - {#snippet popper({ props })} - {@const finalProps = mergeProps(props, { - style: getFloatingContentCSSVars("dropdown-menu"), - })} - {#if child} - {@render child({ props: finalProps, ...contentState.snippetProps })} - {:else} -
- {@render children?.()} -
- {/if} - - {/snippet} -
+ } + + +{#if forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { + style: getFloatingContentCSSVars("dropdown-menu"), + })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
+{:else if !forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { + style: getFloatingContentCSSVars("dropdown-menu"), + })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
+{/if} diff --git a/packages/bits-ui/src/lib/bits/link-preview/components/link-preview-content-static.svelte b/packages/bits-ui/src/lib/bits/link-preview/components/link-preview-content-static.svelte index 653a55580..c43771340 100644 --- a/packages/bits-ui/src/lib/bits/link-preview/components/link-preview-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/link-preview/components/link-preview-content-static.svelte @@ -1,20 +1,30 @@ + const floatingProps = $derived({ + side, + sideOffset, + align, + avoidCollisions, + arrowPadding, + sticky, + hideWhenDetached, + collisionPadding, + }); + + const mergedProps = $derived(mergeProps(restProps, floatingProps, contentState.props)); - { + function handleInteractOutside(e: PointerEvent) { onInteractOutside?.(e); if (e.defaultPrevented) return; - contentState.root.immediateClose(); - }} - onEscapeKeydown={(e) => { + contentState.root.handleClose(); + } + + function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown?.(e); if (e.defaultPrevented) return; - contentState.root.immediateClose(); - }} - onOpenAutoFocus={(e) => e.preventDefault()} - onCloseAutoFocus={(e) => e.preventDefault()} - trapFocus={false} - loop={false} - preventScroll={false} -> - {#snippet popper({ props })} - {#if child} - {@render child({ props, ...contentState.snippetProps })} - {:else} -
- {@render children?.(contentState.snippetProps)} -
- {/if} - {/snippet} -
+ contentState.root.handleClose(); + } + + +{#if forceMount} + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + trapFocus={false} + loop={false} + preventScroll={false} + forceMount={true} + > + {#snippet popper({ props })} + {@const mergedProps = mergeProps(props, { + style: getFloatingContentCSSVars("link-preview"), + })} + {#if child} + {@render child({ props: mergedProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+{:else if !forceMount} + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + trapFocus={false} + loop={false} + preventScroll={false} + forceMount={false} + > + {#snippet popper({ props })} + {@const mergedProps = mergeProps(props, { + style: getFloatingContentCSSVars("link-preview"), + })} + {#if child} + {@render child({ props: mergedProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+{/if} diff --git a/packages/bits-ui/src/lib/bits/link-preview/components/link-preview-content.svelte b/packages/bits-ui/src/lib/bits/link-preview/components/link-preview-content.svelte index 8c8ee329f..fedfe85db 100644 --- a/packages/bits-ui/src/lib/bits/link-preview/components/link-preview-content.svelte +++ b/packages/bits-ui/src/lib/bits/link-preview/components/link-preview-content.svelte @@ -5,6 +5,7 @@ import { useId } from "$lib/internal/use-id.js"; 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"; let { children, @@ -45,38 +46,72 @@ }); const mergedProps = $derived(mergeProps(restProps, floatingProps, contentState.props)); - - { + function handleInteractOutside(e: PointerEvent) { onInteractOutside?.(e); if (e.defaultPrevented) return; - contentState.root.immediateClose(); - }} - onEscapeKeydown={(e) => { + contentState.root.handleClose(); + } + + function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown?.(e); if (e.defaultPrevented) return; - contentState.root.immediateClose(); - }} - onOpenAutoFocus={(e) => e.preventDefault()} - onCloseAutoFocus={(e) => e.preventDefault()} - trapFocus={false} - loop={false} - preventScroll={false} -> - {#snippet popper({ props })} - {@const mergedProps = mergeProps(props, { - style: getFloatingContentCSSVars("link-preview"), - })} - {#if child} - {@render child({ props: mergedProps, ...contentState.snippetProps })} - {:else} -
- {@render children?.()} -
- {/if} - {/snippet} -
+ contentState.root.handleClose(); + } + + +{#if forceMount} + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + trapFocus={false} + loop={false} + preventScroll={false} + forceMount={true} + > + {#snippet popper({ props })} + {@const mergedProps = mergeProps(props, { + style: getFloatingContentCSSVars("link-preview"), + })} + {#if child} + {@render child({ props: mergedProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+{:else if !forceMount} + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + trapFocus={false} + loop={false} + preventScroll={false} + forceMount={false} + > + {#snippet popper({ props })} + {@const mergedProps = mergeProps(props, { + style: getFloatingContentCSSVars("link-preview"), + })} + {#if child} + {@render child({ props: mergedProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+{/if} diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-content-static.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-content-static.svelte index 86deb746a..f492a7ad8 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-content-static.svelte @@ -1,11 +1,13 @@ - { + function handleInteractOutside(e: PointerEvent) { onInteractOutside(e); if (e.defaultPrevented) return; contentState.parentMenu.onClose(); - }} - onEscapeKeydown={(e) => { + } + + function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown(e); if (e.defaultPrevented) return; contentState.parentMenu.onClose(); - }} - trapFocus - {loop} -> - {#snippet popper({ props })} - {@const finalProps = mergeProps(props, { - style: { - outline: "none", - }, - })} - {#if child} - {@render child({ props: finalProps, ...contentState.snippetProps })} - {:else} -
- {@render children?.()} -
- {/if} - - {/snippet} -
+ } + + +{#if forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { + style: { + outline: "none", + ...getFloatingContentCSSVars("menu"), + }, + })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
+{:else if !forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { + style: { + outline: "none", + ...getFloatingContentCSSVars("menu"), + }, + })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
+{/if} diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-content.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-content.svelte index 9cb9361bf..72a23b1e2 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-content.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-content.svelte @@ -7,6 +7,7 @@ import PopperLayer from "$lib/bits/utilities/popper-layer/popper-layer.svelte"; import Mounted from "$lib/bits/utilities/mounted.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"; let { id = useId(), @@ -37,38 +38,74 @@ style: { outline: "none" }, }) ); - - { + function handleInteractOutside(e: PointerEvent) { onInteractOutside(e); if (e.defaultPrevented) return; contentState.parentMenu.onClose(); - }} - onEscapeKeydown={(e) => { + } + + function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown(e); if (e.defaultPrevented) return; contentState.parentMenu.onClose(); - }} - trapFocus - {loop} -> - {#snippet popper({ props })} - {@const finalProps = mergeProps(props, { - style: { - outline: "none", - ...getFloatingContentCSSVars("menu"), - }, - })} - {#if child} - {@render child({ props: finalProps, ...contentState.snippetProps })} - {:else} -
- {@render children?.()} -
- {/if} - - {/snippet} -
+ } + + +{#if forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { + style: { + outline: "none", + ...getFloatingContentCSSVars("menu"), + }, + })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
+{:else if !forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { + style: { + outline: "none", + ...getFloatingContentCSSVars("menu"), + }, + })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
+{/if} diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content-static.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content-static.svelte index 03c479f08..6fcbace24 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content-static.svelte @@ -9,6 +9,7 @@ import { isHTMLElement } from "$lib/internal/is.js"; import Mounted from "$lib/bits/utilities/mounted.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"; let { id = useId(), @@ -23,7 +24,7 @@ escapeKeydownBehavior = "defer-otherwise-close", onOpenAutoFocus: onOpenAutoFocusProp = noop, onCloseAutoFocus: onCloseAutoFocusProp = noop, - onFocusOutside: onFocusOutsideProp = noop, + onFocusOutside = noop, side = "right", ...restProps }: MenuSubContentProps = $props(); @@ -53,15 +54,19 @@ } } + const dataAttr = $derived(subContentState.parentMenu.root.getAttr("sub-content")); + const mergedProps = $derived( mergeProps(restProps, subContentState.props, { side, onkeydown, - "data-menu-sub-content": "", + [dataAttr]: "", }) ); - function onOpenAutoFocus(e: Event) { + function handleOpenAutoFocus(e: Event) { + onOpenAutoFocusProp(e); + if (e.defaultPrevented) return; afterTick(() => { e.preventDefault(); if (subContentState.parentMenu.root.isUsingKeyboard.current) { @@ -71,40 +76,26 @@ }); } - function onCloseAutoFocus(e: Event) { + function handleCloseAutoFocus(e: Event) { + onCloseAutoFocusProp(e); + if (e.defaultPrevented) return; e.preventDefault(); } - - { - onCloseAutoFocusProp(e); - if (e.defaultPrevented) return; - onCloseAutoFocus(e); - }} - onOpenAutoFocus={(e) => { - onOpenAutoFocusProp(e); - if (e.defaultPrevented) return; - onOpenAutoFocus(e); - }} - present={subContentState.parentMenu.open.current || forceMount} - onInteractOutside={(e) => { + function handleInteractOutside(e: PointerEvent) { onInteractOutside(e); if (e.defaultPrevented) return; subContentState.parentMenu.onClose(); - }} - onEscapeKeydown={(e) => { - // TODO: users should be able to cancel this + } + + function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown(e); if (e.defaultPrevented) return; subContentState.parentMenu.onClose(); - }} - onFocusOutside={(e) => { - onFocusOutsideProp(e); + } + + function handleOnFocusOutside(e: FocusEvent) { + onFocusOutside(e); if (e.defaultPrevented) return; // We prevent closing when the trigger is focused to avoid triggering a re-open animation // on pointer interaction. @@ -112,21 +103,67 @@ if (e.target.id !== subContentState.parentMenu.triggerNode?.id) { subContentState.parentMenu.onClose(); } - }} - preventScroll={false} - {loop} -> - {#snippet popper({ props })} - {@const finalProps = mergeProps(props, mergedProps, { - style: getFloatingContentCSSVars("menu"), - })} - {#if child} - {@render child({ props: finalProps, ...subContentState.snippetProps })} - {:else} -
- {@render children?.()} -
- {/if} - - {/snippet} -
+ } + + +{#if forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, mergedProps, { + style: getFloatingContentCSSVars("menu"), + })} + {#if child} + {@render child({ props: finalProps, ...subContentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
+{:else if !forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, mergedProps, { + style: getFloatingContentCSSVars("menu"), + })} + {#if child} + {@render child({ props: finalProps, ...subContentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
+{/if} diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content.svelte index ab67197ef..c0ccd96f5 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-sub-content.svelte @@ -9,6 +9,7 @@ import { isHTMLElement } from "$lib/internal/is.js"; import Mounted from "$lib/bits/utilities/mounted.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"; let { id = useId(), @@ -63,7 +64,9 @@ }) ); - function onOpenAutoFocus(e: Event) { + function handleOpenAutoFocus(e: Event) { + onOpenAutoFocusProp(e); + if (e.defaultPrevented) return; afterTick(() => { e.preventDefault(); if (subContentState.parentMenu.root.isUsingKeyboard.current) { @@ -73,38 +76,25 @@ }); } - function onCloseAutoFocus(e: Event) { + function handleCloseAutoFocus(e: Event) { + onCloseAutoFocusProp(e); + if (e.defaultPrevented) return; e.preventDefault(); } - - { - onCloseAutoFocusProp(e); - if (e.defaultPrevented) return; - onCloseAutoFocus(e); - }} - onOpenAutoFocus={(e) => { - onOpenAutoFocusProp(e); - if (e.defaultPrevented) return; - onOpenAutoFocus(e); - }} - present={subContentState.parentMenu.open.current || forceMount} - onInteractOutside={(e) => { + function handleInteractOutside(e: PointerEvent) { onInteractOutside(e); if (e.defaultPrevented) return; subContentState.parentMenu.onClose(); - }} - onEscapeKeydown={(e) => { - // TODO: users should be able to cancel this + } + + function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown(e); if (e.defaultPrevented) return; subContentState.parentMenu.onClose(); - }} - onFocusOutside={(e) => { + } + + function handleOnFocusOutside(e: FocusEvent) { onFocusOutside(e); if (e.defaultPrevented) return; // We prevent closing when the trigger is focused to avoid triggering a re-open animation @@ -113,22 +103,65 @@ if (e.target.id !== subContentState.parentMenu.triggerNode?.id) { subContentState.parentMenu.onClose(); } - }} - preventScroll={false} - {loop} - trapFocus={false} -> - {#snippet popper({ props })} - {@const finalProps = mergeProps(props, mergedProps, { - style: getFloatingContentCSSVars("menu"), - })} - {#if child} - {@render child({ props: finalProps, ...subContentState.snippetProps })} - {:else} -
- {@render children?.()} -
- {/if} - - {/snippet} -
+ } + + +{#if forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, mergedProps, { + style: getFloatingContentCSSVars("menu"), + })} + {#if child} + {@render child({ props: finalProps, ...subContentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
+{:else if !forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, mergedProps, { + style: getFloatingContentCSSVars("menu"), + })} + {#if child} + {@render child({ props: finalProps, ...subContentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + + {/snippet} +
+{/if} 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 cceddb733..7d6b7ad66 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 @@ -1,10 +1,12 @@ - { + function handleInteractOutside(e: PointerEvent) { onInteractOutside(e); if (e.defaultPrevented) return; contentState.root.close(); - }} - onEscapeKeydown={(e) => { + } + + function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown(e); if (e.defaultPrevented) return; contentState.root.close(); - }} - onCloseAutoFocus={(e) => { + } + + function handleCloseAutoFocus(e: Event) { onCloseAutoFocus(e); if (e.defaultPrevented) return; e.preventDefault(); contentState.root.triggerNode?.focus(); - }} - {trapFocus} - {preventScroll} - loop - {forceMount} -> - {#snippet popper({ props })} - {@const finalProps = mergeProps(props)} - {#if child} - {@render child({ props: finalProps, ...contentState.snippetProps })} - {:else} -
- {@render children?.()} -
- {/if} - {/snippet} -
+ } + + +{#if forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { + style: getFloatingContentCSSVars("popover"), + })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+{:else if !forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { + style: getFloatingContentCSSVars("popover"), + })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+{/if} 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 f6b6a62d4..1e8f89464 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 @@ -6,6 +6,7 @@ import { noop } from "$lib/internal/noop.js"; 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"; let { child, @@ -30,43 +31,77 @@ }); const mergedProps = $derived(mergeProps(restProps, contentState.props)); - - { + function handleInteractOutside(e: PointerEvent) { onInteractOutside(e); if (e.defaultPrevented) return; contentState.root.close(); - }} - onEscapeKeydown={(e) => { + } + + function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown(e); if (e.defaultPrevented) return; contentState.root.close(); - }} - onCloseAutoFocus={(e) => { + } + + function handleCloseAutoFocus(e: Event) { onCloseAutoFocus(e); if (e.defaultPrevented) return; e.preventDefault(); contentState.root.triggerNode?.focus(); - }} - {trapFocus} - {preventScroll} - loop - {forceMount} -> - {#snippet popper({ props })} - {@const finalProps = mergeProps(props, { - style: getFloatingContentCSSVars("popover"), - })} - {#if child} - {@render child({ props: finalProps, ...contentState.snippetProps })} - {:else} -
- {@render children?.()} -
- {/if} - {/snippet} -
+ } + + +{#if forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { + style: getFloatingContentCSSVars("popover"), + })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+{:else if !forceMount} + + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { + style: getFloatingContentCSSVars("popover"), + })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+{/if} 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 244ac7528..82455159f 100644 --- a/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts +++ b/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts @@ -126,6 +126,9 @@ class PopoverContentState { tabindex: -1, "data-state": getDataOpenClosed(this.root.open.current), "data-popover-content": "", + style: { + pointerEvents: "auto", + }, })); } diff --git a/packages/bits-ui/src/lib/bits/select/components/select-content-static.svelte b/packages/bits-ui/src/lib/bits/select/components/select-content-static.svelte index ea6601dbf..905aa7a72 100644 --- a/packages/bits-ui/src/lib/bits/select/components/select-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/select/components/select-content-static.svelte @@ -1,21 +1,23 @@ - { + function handleInteractOutside(e: PointerEvent) { contentState.handleInteractOutside(e); if (e.defaultPrevented) return; onInteractOutside(e); if (e.defaultPrevented) return; contentState.root.handleClose(); - }} - onEscapeKeydown={(e) => { + } + + function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown(e); if (e.defaultPrevented) return; contentState.root.handleClose(); - }} - onOpenAutoFocus={(e) => e.preventDefault()} - onCloseAutoFocus={(e) => e.preventDefault()} - trapFocus={false} - loop={false} - preventScroll={false} - onPlaced={() => (contentState.isPositioned = true)} - {forceMount} -> - {#snippet popper({ props })} - {@const finalProps = mergeProps(props, { - style: contentState.props.style, - })} - {#if child} - {@render child({ props: finalProps, ...contentState.snippetProps })} - {:else} -
- {@render children?.()} -
- {/if} - {/snippet} -
+ } + + +{#if forceMount} + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + trapFocus={false} + loop={false} + preventScroll={false} + onPlaced={() => (contentState.isPositioned = true)} + forceMount={true} + > + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { style: contentState.props.style })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+{:else if !forceMount} + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + trapFocus={false} + loop={false} + preventScroll={false} + onPlaced={() => (contentState.isPositioned = true)} + forceMount={false} + > + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { style: contentState.props.style })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+{/if} diff --git a/packages/bits-ui/src/lib/bits/select/components/select-content.svelte b/packages/bits-ui/src/lib/bits/select/components/select-content.svelte index 17d7f96e1..defa02c4f 100644 --- a/packages/bits-ui/src/lib/bits/select/components/select-content.svelte +++ b/packages/bits-ui/src/lib/bits/select/components/select-content.svelte @@ -5,6 +5,7 @@ import PopperLayer from "$lib/bits/utilities/popper-layer/popper-layer.svelte"; import { useId } from "$lib/internal/use-id.js"; import { noop } from "$lib/internal/noop.js"; + import PopperLayerForceMount from "$lib/bits/utilities/popper-layer/popper-layer-force-mount.svelte"; let { id = useId(), @@ -27,50 +28,74 @@ }); const mergedProps = $derived(mergeProps(restProps, contentState.props)); - - { + function handleInteractOutside(e: PointerEvent) { contentState.handleInteractOutside(e); if (e.defaultPrevented) return; onInteractOutside(e); if (e.defaultPrevented) return; contentState.root.handleClose(); - }} - onEscapeKeydown={(e) => { + } + + function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown(e); if (e.defaultPrevented) return; contentState.root.handleClose(); - }} - onOpenAutoFocus={(e) => e.preventDefault()} - onCloseAutoFocus={(e) => e.preventDefault()} - trapFocus={false} - loop={false} - preventScroll={false} - onPlaced={() => (contentState.isPositioned = true)} - {forceMount} -> - {#snippet popper({ props })} - {@const finalProps = mergeProps(props, { - style: { - "--bits-select-content-transform-origin": "var(--bits-floating-transform-origin)", - "--bits-select-content-available-width": "var(--bits-floating-available-width)", - "--bits-select-content-available-height": "var(--bits-floating-available-height)", - "--bits-select-anchor-width": "var(--bits-floating-anchor-width)", - "--bits-select-anchor-height": "var(--bits-floating-anchor-height)", - ...contentState.props.style, - }, - })} - {#if child} - {@render child({ props: finalProps, ...contentState.snippetProps })} - {:else} -
- {@render children?.()} -
- {/if} - {/snippet} -
+ } + + +{#if forceMount} + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + trapFocus={false} + loop={false} + preventScroll={false} + onPlaced={() => (contentState.isPositioned = true)} + forceMount={true} + > + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { style: contentState.props.style })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+{:else if !forceMount} + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + trapFocus={false} + loop={false} + preventScroll={false} + onPlaced={() => (contentState.isPositioned = true)} + forceMount={false} + > + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, { style: contentState.props.style })} + {#if child} + {@render child({ props: finalProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+{/if} diff --git a/packages/bits-ui/src/lib/bits/select/select.svelte.ts b/packages/bits-ui/src/lib/bits/select/select.svelte.ts index 48836acdc..dd171d23d 100644 --- a/packages/bits-ui/src/lib/bits/select/select.svelte.ts +++ b/packages/bits-ui/src/lib/bits/select/select.svelte.ts @@ -784,6 +784,7 @@ class SelectContentState { flexDirection: "column", outline: "none", boxSizing: "border-box", + pointerEvents: "auto", ...this.#styles, }, onpointermove: this.#onpointermove, diff --git a/packages/bits-ui/src/lib/bits/tooltip/components/tooltip-content-static.svelte b/packages/bits-ui/src/lib/bits/tooltip/components/tooltip-content-static.svelte index 1e90b8958..148d14253 100644 --- a/packages/bits-ui/src/lib/bits/tooltip/components/tooltip-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/tooltip/components/tooltip-content-static.svelte @@ -1,20 +1,30 @@ + const floatingProps = $derived({ + side, + sideOffset, + align, + avoidCollisions, + arrowPadding, + sticky, + hideWhenDetached, + collisionPadding, + }); + + const mergedProps = $derived(mergeProps(restProps, floatingProps, contentState.props)); - { + function handleInteractOutside(e: PointerEvent) { onInteractOutside?.(e); if (e.defaultPrevented) return; contentState.root.handleClose(); - }} - onEscapeKeydown={(e) => { + } + + function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown?.(e); if (e.defaultPrevented) return; contentState.root.handleClose(); - }} - onOpenAutoFocus={(e) => e.preventDefault()} - onCloseAutoFocus={(e) => e.preventDefault()} - trapFocus={false} - loop={false} - preventScroll={false} -> - {#snippet popper({ props })} - {@const mergedProps = mergeProps(props)} - {#if child} - {@render child({ props: mergedProps, ...contentState.snippetProps })} - {:else} -
- {@render children?.()} -
- {/if} - {/snippet} -
+ } + + +{#if forceMount} + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + trapFocus={false} + loop={false} + preventScroll={false} + forceMount={true} + > + {#snippet popper({ props })} + {@const mergedProps = mergeProps(props, { + style: getFloatingContentCSSVars("tooltip"), + })} + {#if child} + {@render child({ props: mergedProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+{:else if !forceMount} + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + trapFocus={false} + loop={false} + preventScroll={false} + forceMount={false} + > + {#snippet popper({ props })} + {@const mergedProps = mergeProps(props, { + style: getFloatingContentCSSVars("tooltip"), + })} + {#if child} + {@render child({ props: mergedProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+{/if} diff --git a/packages/bits-ui/src/lib/bits/tooltip/components/tooltip-content.svelte b/packages/bits-ui/src/lib/bits/tooltip/components/tooltip-content.svelte index 4b32884e9..f82258566 100644 --- a/packages/bits-ui/src/lib/bits/tooltip/components/tooltip-content.svelte +++ b/packages/bits-ui/src/lib/bits/tooltip/components/tooltip-content.svelte @@ -5,6 +5,7 @@ import { useId } from "$lib/internal/use-id.js"; 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"; let { children, @@ -45,38 +46,72 @@ }); const mergedProps = $derived(mergeProps(restProps, floatingProps, contentState.props)); - - { + function handleInteractOutside(e: PointerEvent) { onInteractOutside?.(e); if (e.defaultPrevented) return; contentState.root.handleClose(); - }} - onEscapeKeydown={(e) => { + } + + function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown?.(e); if (e.defaultPrevented) return; contentState.root.handleClose(); - }} - onOpenAutoFocus={(e) => e.preventDefault()} - onCloseAutoFocus={(e) => e.preventDefault()} - trapFocus={false} - loop={false} - preventScroll={false} -> - {#snippet popper({ props })} - {@const mergedProps = mergeProps(props, { - style: getFloatingContentCSSVars("tooltip"), - })} - {#if child} - {@render child({ props: mergedProps, ...contentState.snippetProps })} - {:else} -
- {@render children?.()} -
- {/if} - {/snippet} -
+ } + + +{#if forceMount} + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + trapFocus={false} + loop={false} + preventScroll={false} + forceMount={true} + > + {#snippet popper({ props })} + {@const mergedProps = mergeProps(props, { + style: getFloatingContentCSSVars("tooltip"), + })} + {#if child} + {@render child({ props: mergedProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+{:else if !forceMount} + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + trapFocus={false} + loop={false} + preventScroll={false} + forceMount={false} + > + {#snippet popper({ props })} + {@const mergedProps = mergeProps(props, { + style: getFloatingContentCSSVars("tooltip"), + })} + {#if child} + {@render child({ props: mergedProps, ...contentState.snippetProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
+{/if} diff --git a/packages/bits-ui/src/lib/bits/tooltip/tooltip.svelte.ts b/packages/bits-ui/src/lib/bits/tooltip/tooltip.svelte.ts index b3a284b82..3788feb44 100644 --- a/packages/bits-ui/src/lib/bits/tooltip/tooltip.svelte.ts +++ b/packages/bits-ui/src/lib/bits/tooltip/tooltip.svelte.ts @@ -337,12 +337,19 @@ class TooltipContentState { snippetProps = $derived.by(() => ({ open: this.root.open.current })); - props = $derived.by(() => ({ - id: this.#id.current, - "data-state": this.root.stateAttr, - "data-disabled": getDataDisabled(this.root.disabled), - [CONTENT_ATTR]: "", - })); + props = $derived.by( + () => + ({ + id: this.#id.current, + "data-state": this.root.stateAttr, + "data-disabled": getDataDisabled(this.root.disabled), + style: { + pointerEvents: "auto", + outline: "none", + }, + [CONTENT_ATTR]: "", + }) as const + ); } // diff --git a/packages/bits-ui/src/lib/bits/utilities/focus-scope/focus-scope-stack.svelte.ts b/packages/bits-ui/src/lib/bits/utilities/focus-scope/focus-scope-stack.svelte.ts index 273f75f0c..1b6be4882 100644 --- a/packages/bits-ui/src/lib/bits/utilities/focus-scope/focus-scope-stack.svelte.ts +++ b/packages/bits-ui/src/lib/bits/utilities/focus-scope/focus-scope-stack.svelte.ts @@ -17,7 +17,7 @@ export function createFocusScopeStack() { add(focusScope: FocusScopeAPI) { // pause the currently active focus scope (top of the stack) const activeFocusScope = stack.current[0]; - if (focusScope !== activeFocusScope) { + if (focusScope.id !== activeFocusScope?.id) { activeFocusScope?.pause(); } diff --git a/packages/bits-ui/src/lib/bits/utilities/focus-scope/focus-scope.svelte b/packages/bits-ui/src/lib/bits/utilities/focus-scope/focus-scope.svelte index 8418da4c6..6cf5273bd 100644 --- a/packages/bits-ui/src/lib/bits/utilities/focus-scope/focus-scope.svelte +++ b/packages/bits-ui/src/lib/bits/utilities/focus-scope/focus-scope.svelte @@ -11,6 +11,7 @@ onCloseAutoFocus = noop, onOpenAutoFocus = noop, focusScope, + forceMount = false, }: FocusScopeImplProps = $props(); const focusScopeState = useFocusScope({ @@ -19,6 +20,7 @@ onCloseAutoFocus: box.with(() => onCloseAutoFocus), onOpenAutoFocus: box.with(() => onOpenAutoFocus), id: box.with(() => id), + forceMount: box.with(() => forceMount), }); diff --git a/packages/bits-ui/src/lib/bits/utilities/focus-scope/types.ts b/packages/bits-ui/src/lib/bits/utilities/focus-scope/types.ts index 181511200..b9f4f25b2 100644 --- a/packages/bits-ui/src/lib/bits/utilities/focus-scope/types.ts +++ b/packages/bits-ui/src/lib/bits/utilities/focus-scope/types.ts @@ -38,4 +38,9 @@ export type FocusScopeImplProps = { * When `true` will loop through the tabbable elements in the focus scope. */ loop?: boolean; + + /** + * Whether the content within the focus trap is being force mounted or not. + */ + forceMount?: boolean; } & FocusScopeProps; diff --git a/packages/bits-ui/src/lib/bits/utilities/focus-scope/useFocusScope.svelte.ts b/packages/bits-ui/src/lib/bits/utilities/focus-scope/useFocusScope.svelte.ts index abfa695b8..8157c6175 100644 --- a/packages/bits-ui/src/lib/bits/utilities/focus-scope/useFocusScope.svelte.ts +++ b/packages/bits-ui/src/lib/bits/utilities/focus-scope/useFocusScope.svelte.ts @@ -1,5 +1,5 @@ import { untrack } from "svelte"; -import { afterTick, box, executeCallbacks, useRefById } from "svelte-toolbelt"; +import { type Getter, afterTick, box, executeCallbacks, useRefById } from "svelte-toolbelt"; import { createFocusScopeAPI, createFocusScopeStack, @@ -45,6 +45,11 @@ type UseFocusScopeProps = ReadableBoxedValues<{ * Can be prevented. */ onCloseAutoFocus: EventCallback; + + /** + * Whether force mount is enabled or not + */ + forceMount: boolean; }>; export type FocusScopeContainerProps = { @@ -59,6 +64,7 @@ export function useFocusScope({ enabled, onOpenAutoFocus, onCloseAutoFocus, + forceMount, }: UseFocusScopeProps) { const focusScopeStack = createFocusScopeStack(); const focusScope = createFocusScopeAPI(); @@ -140,53 +146,71 @@ export function useFocusScope({ }); $effect(() => { + if (forceMount.current) return; let container = ref.current; const previouslyFocusedElement = document.activeElement as HTMLElement | null; untrack(() => { - if (!container) { - container = document.getElementById(id.current); - } + handleMount(container, previouslyFocusedElement); + }); + + return () => { if (!container) return; - focusScopeStack.add(focusScope); - const hasFocusedCandidate = container.contains(previouslyFocusedElement); - - if (!hasFocusedCandidate) { - const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS); - container.addEventListener(AUTOFOCUS_ON_MOUNT, onOpenAutoFocus.current); - container.dispatchEvent(mountEvent); - - if (!mountEvent.defaultPrevented) { - afterTick(() => { - if (!container) return; - focusFirst(removeLinks(getTabbableCandidates(container)), { select: true }); - - if (document.activeElement === previouslyFocusedElement) { - focus(container); - } - }); - } - } + handleDestroy(previouslyFocusedElement); + }; + }); + + $effect(() => { + if (!forceMount.current) return; + let container = ref.current; + enabled.current; + const previouslyFocusedElement = document.activeElement as HTMLElement | null; + untrack(() => { + handleMount(container, previouslyFocusedElement); }); return () => { if (!container) return; - container.removeEventListener(AUTOFOCUS_ON_MOUNT, onOpenAutoFocus.current); + handleDestroy(previouslyFocusedElement); + }; + }); - const destroyEvent = new CustomEvent(AUTOFOCUS_ON_DESTROY, EVENT_OPTIONS); - container.addEventListener(AUTOFOCUS_ON_DESTROY, onCloseAutoFocus.current); - container.dispatchEvent(destroyEvent); + function handleMount(container: HTMLElement | null, prevFocusedElement: HTMLElement | null) { + if (!container) { + container = document.getElementById(id.current); + } + if (!container) return; + focusScopeStack.add(focusScope); + const hasFocusedCandidate = container.contains(prevFocusedElement); + + if (!hasFocusedCandidate) { + const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS); + onOpenAutoFocus.current(mountEvent); + + if (!mountEvent.defaultPrevented) { + afterTick(() => { + if (!container) return; + focusFirst(removeLinks(getTabbableCandidates(container)), { select: true }); + + if (document.activeElement === prevFocusedElement) { + focus(container); + } + }); + } + } + } - setTimeout(() => { - if (!destroyEvent.defaultPrevented && previouslyFocusedElement) { - focus(previouslyFocusedElement ?? document.body, { select: true }); - } + function handleDestroy(prevFocusedElement: HTMLElement | null) { + const destroyEvent = new CustomEvent(AUTOFOCUS_ON_DESTROY, EVENT_OPTIONS); + onCloseAutoFocus.current(destroyEvent); - container?.removeEventListener(AUTOFOCUS_ON_DESTROY, onCloseAutoFocus.current); + setTimeout(() => { + if (!destroyEvent.defaultPrevented && prevFocusedElement) { + focus(prevFocusedElement ?? document.body, { select: true }); + } - focusScopeStack.remove(focusScope); - }, 0); - }; - }); + focusScopeStack.remove(focusScope); + }, 0); + } function handleKeydown(e: KeyboardEvent) { if (!enabled.current) return; @@ -218,11 +242,14 @@ export function useFocusScope({ } } - const props: FocusScopeContainerProps = $derived({ - id: id.current, - tabindex: -1, - onkeydown: handleKeydown, - }); + const props: FocusScopeContainerProps = $derived.by( + () => + ({ + id: id.current, + tabindex: -1, + onkeydown: handleKeydown, + }) as const + ); return { get props() { diff --git a/packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer-force-mount.svelte b/packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer-force-mount.svelte new file mode 100644 index 000000000..850e99c66 --- /dev/null +++ b/packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer-force-mount.svelte @@ -0,0 +1,85 @@ + + + diff --git a/packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer-inner.svelte b/packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer-inner.svelte new file mode 100644 index 000000000..9066d1dc8 --- /dev/null +++ b/packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer-inner.svelte @@ -0,0 +1,126 @@ + + + + {#snippet content({ props: floatingProps })} + {#if restProps.forceMount && enabled} + + {:else if !restProps.forceMount} + + {/if} + + {#snippet focusScope({ props: focusScopeProps })} + + + {#snippet children({ props: dismissibleProps })} + + {@render popper?.({ + props: mergeProps( + restProps, + floatingProps, + dismissibleProps, + focusScopeProps, + { + style: { + pointerEvents: "auto", + }, + } + ), + })} + + {/snippet} + + + {/snippet} + + {/snippet} + diff --git a/packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer.svelte b/packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer.svelte index 102ab5a37..0b4d58348 100644 --- a/packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer.svelte +++ b/packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer.svelte @@ -1,13 +1,7 @@ @@ -422,4 +422,35 @@ To trigger side effects when an item is highlighted or unhighlighted, you can us ``` +## Svelte Transitions + +You can use the `forceMount` prop along with the `child` snippet to forcefully mount the `Combobox.Content` component to use Svelte Transitions or another animation library that requires more control. + +```svelte /forceMount/ /transition:fly/ + + + + {#snippet child({ props, open })} + {#if open} +
+ +
+ {/if} + {/snippet} +
+``` + +Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the [Transitions](/docs/transitions) documentation. + + + +{#snippet preview()} + +{/snippet} + + + diff --git a/sites/docs/content/components/context-menu.md b/sites/docs/content/components/context-menu.md index b8f29aa3c..d8c67efd6 100644 --- a/sites/docs/content/components/context-menu.md +++ b/sites/docs/content/components/context-menu.md @@ -4,7 +4,7 @@ description: Displays options or actions relevant to a specific context or selec --- @@ -329,4 +329,12 @@ You can use the `forceMount` prop along with the `child` snippet to forcefully m Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the [Transitions](/docs/transitions) documentation. + + +{#snippet preview()} + +{/snippet} + + + diff --git a/sites/docs/content/components/dropdown-menu.md b/sites/docs/content/components/dropdown-menu.md index e88b579b3..000cf3711 100644 --- a/sites/docs/content/components/dropdown-menu.md +++ b/sites/docs/content/components/dropdown-menu.md @@ -4,7 +4,7 @@ description: Displays a menu of items that users can select from when triggered. --- @@ -344,6 +344,14 @@ You can use the `forceMount` prop along with the `child` snippet to forcefully m Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the [Transitions](/docs/transitions) documentation. + + +{#snippet preview()} + +{/snippet} + + + ## Custom Anchor By default, the `DropdownMenu.Content` is anchored to the `DropdownMenu.Trigger` component, which determines where the content is positioned. diff --git a/sites/docs/content/components/link-preview.md b/sites/docs/content/components/link-preview.md index b519da514..88a947e12 100644 --- a/sites/docs/content/components/link-preview.md +++ b/sites/docs/content/components/link-preview.md @@ -4,7 +4,7 @@ description: Displays a summarized preview of a linked content's details or info --- @@ -171,4 +171,35 @@ If you wish to instead anchor the content to a different element, you can pass e ``` +## Svelte Transitions + +You can use the `forceMount` prop along with the `child` snippet to forcefully mount the `LinkPreview.Content` component to use Svelte Transitions or another animation library that requires more control. + +```svelte /forceMount/ /transition:fly/ + + + + {#snippet child({ props, open })} + {#if open} +
+ +
+ {/if} + {/snippet} +
+``` + +Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the [Transitions](/docs/transitions) documentation. + + + +{#snippet preview()} + +{/snippet} + + + diff --git a/sites/docs/content/components/popover.md b/sites/docs/content/components/popover.md index af3fc2835..440edcb71 100644 --- a/sites/docs/content/components/popover.md +++ b/sites/docs/content/components/popover.md @@ -4,7 +4,7 @@ description: Display supplementary content or information when users interact wi --- @@ -267,4 +267,35 @@ If you wish to instead anchor the content to a different element, you can pass e ``` +## Svelte Transitions + +You can use the `forceMount` prop along with the `child` snippet to forcefully mount the `Popover.Content` component to use Svelte Transitions or another animation library that requires more control. + +```svelte /forceMount/ /transition:fly/ + + + + {#snippet child({ props, open })} + {#if open} +
+ +
+ {/if} + {/snippet} +
+``` + +Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the [Transitions](/docs/transitions) documentation. + + + +{#snippet preview()} + +{/snippet} + + + diff --git a/sites/docs/content/components/select.md b/sites/docs/content/components/select.md index aa67b5a62..103796616 100644 --- a/sites/docs/content/components/select.md +++ b/sites/docs/content/components/select.md @@ -4,7 +4,7 @@ description: Enables users to choose from a list of options presented in a dropd --- @@ -444,4 +444,35 @@ To trigger side effects when an item is highlighted or unhighlighted, you can us ``` +## Svelte Transitions + +You can use the `forceMount` prop along with the `child` snippet to forcefully mount the `Select.Content` component to use Svelte Transitions or another animation library that requires more control. + +```svelte /forceMount/ /transition:fly/ + + + + {#snippet child({ props, open })} + {#if open} +
+ +
+ {/if} + {/snippet} +
+``` + +Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the [Transitions](/docs/transitions) documentation. + + + +{#snippet preview()} + +{/snippet} + + + diff --git a/sites/docs/content/components/tooltip.md b/sites/docs/content/components/tooltip.md index 5967aec9a..1b8a76bc6 100644 --- a/sites/docs/content/components/tooltip.md +++ b/sites/docs/content/components/tooltip.md @@ -4,7 +4,7 @@ description: Provides additional information or context when users hover over or --- @@ -315,6 +315,14 @@ You can use the `forceMount` prop along with the `child` snippet to forcefully m Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content components that handles this logic if you intend to use this approach throughout your app. For more information on using transitions with Bits UI components, see the [Transitions](/docs/transitions) documentation. + + +{#snippet preview()} + +{/snippet} + + + ## Opt-out of Floating UI When you use the `Tooltip.Content` component, Bits UI uses [Floating UI](https://floating-ui.com/) to position the content relative to the trigger, similar to other popover-like components. diff --git a/sites/docs/src/lib/components/demos/combobox-demo-transition.svelte b/sites/docs/src/lib/components/demos/combobox-demo-transition.svelte new file mode 100644 index 000000000..659e1d597 --- /dev/null +++ b/sites/docs/src/lib/components/demos/combobox-demo-transition.svelte @@ -0,0 +1,105 @@ + + + { + if (!o) searchValue = ""; + }} +> +
+ + (searchValue = e.currentTarget.value)} + class="inline-flex h-input w-[296px] truncate rounded-9px border border-border-input bg-background px-11 text-sm transition-colors placeholder:text-foreground-alt/50 focus:outline-none focus:ring-2 focus:ring-foreground focus:ring-offset-2 focus:ring-offset-background" + placeholder="Search a fruit" + aria-label="Search a fruit" + /> + + + +
+ + + {#snippet child({ props, open })} + {#if open} +
+ + + + + {#each filteredFruits as fruit, i (i + fruit.value)} + + {#snippet children({ selected })} + {fruit.label} + {#if selected} +
+ +
+ {/if} + {/snippet} +
+ {:else} + + No results found, try again. + + {/each} +
+ + + +
+ {/if} + {/snippet} +
+
+
diff --git a/sites/docs/src/lib/components/demos/context-menu-demo-transition.svelte b/sites/docs/src/lib/components/demos/context-menu-demo-transition.svelte new file mode 100644 index 000000000..2cd3d5929 --- /dev/null +++ b/sites/docs/src/lib/components/demos/context-menu-demo-transition.svelte @@ -0,0 +1,134 @@ + + + + +
+ + Right click me +
+
+ + + {#snippet child({ props, open })} + {#if open} +
+ +
+ + Edit +
+
+ + ⌘ + + + E + +
+
+ + +
+ + Add +
+
+ + ⌘ + + + N + +
+
+ + + Header + + + Paragraph + + + Codeblock + + + List + + + Task + + +
+ +
+ + Duplicate +
+
+ + ⌘ + + + D + +
+
+ + +
+ + Delete +
+
+
+ {/if} + {/snippet} +
+
+
diff --git a/sites/docs/src/lib/components/demos/dropdown-menu-demo-transition.svelte b/sites/docs/src/lib/components/demos/dropdown-menu-demo-transition.svelte new file mode 100644 index 000000000..b05c11c73 --- /dev/null +++ b/sites/docs/src/lib/components/demos/dropdown-menu-demo-transition.svelte @@ -0,0 +1,232 @@ + + + + + + + + + {#snippet child({ props, open })} + {#if open} +
+ +
+ + Profile +
+
+ + ⌘ + + + P + +
+
+ +
+ + Billing +
+
+ + ⌘ + + + B + +
+
+ +
+ + Settings +
+
+ + ⌘ + + + S + +
+
+ + {#snippet children({ checked })} +
+ + Notifications +
+
+ {#if checked} + + {/if} +
+ {/snippet} +
+ + +
+ + Workspace +
+
+ +
+
+ + + + {#snippet children({ checked })} + + + HJ + + @huntabyte + {#if checked} + + {/if} + {/snippet} + + + {#snippet children({ checked })} + + + PS + + @pavel_stianko + {#if checked} + + {/if} + {/snippet} + + + {#snippet children({ checked })} + + + CK + + @cokakoala_ + {#if checked} + + {/if} + {/snippet} + + + {#snippet children({ checked })} + + + + TL + + + @thomasglopes + {#if checked} + + {/if} + {/snippet} + + + +
+
+ {/if} + {/snippet} +
+
+
diff --git a/sites/docs/src/lib/components/demos/index.ts b/sites/docs/src/lib/components/demos/index.ts index ce27f1a9b..a4805787e 100644 --- a/sites/docs/src/lib/components/demos/index.ts +++ b/sites/docs/src/lib/components/demos/index.ts @@ -11,9 +11,11 @@ export { default as CheckboxDemoCustom } from "./checkbox-demo-custom.svelte"; export { default as CollapsibleDemo } from "./collapsible-demo.svelte"; export { default as CollapsibleDemoTransitions } from "./collapsible-demo-transitions.svelte"; export { default as ComboboxDemo } from "./combobox-demo.svelte"; +export { default as ComboboxDemoTransition } from "./combobox-demo-transition.svelte"; export { default as CommandDemo } from "./command-demo.svelte"; export { default as CommandDemoDialog } from "./command-demo-dialog.svelte"; export { default as ContextMenuDemo } from "./context-menu-demo.svelte"; +export { default as ContextMenuDemoTransition } from "./context-menu-demo-transition.svelte"; export { default as DateFieldDemo } from "./date-field-demo.svelte"; export { default as DateRangeFieldDemo } from "./date-range-field-demo.svelte"; export { default as DatePickerDemo } from "./date-picker-demo.svelte"; @@ -22,17 +24,21 @@ export { default as DialogDemo } from "./dialog-demo.svelte"; export { default as DialogDemoCustom } from "./dialog-demo-custom.svelte"; export { default as DialogDemoNested } from "./dialog-demo-nested.svelte"; export { default as DropdownMenuDemo } from "./dropdown-menu-demo.svelte"; +export { default as DropdownMenuDemoTransition } from "./dropdown-menu-demo-transition.svelte"; export { default as LabelDemo } from "./label-demo.svelte"; export { default as LinkPreviewDemo } from "./link-preview-demo.svelte"; +export { default as LinkPreviewDemoTransition } from "./link-preview-demo-transition.svelte"; export { default as SelectDemo } from "./select-demo.svelte"; export { default as SelectDemoCustom } from "./select-demo-custom.svelte"; export { default as SelectDemoCustomAnchor } from "./select-demo-custom-anchor.svelte"; export { default as SelectDemoMultiple } from "./select-demo-multiple.svelte"; +export { default as SelectDemoTransition } from "./select-demo-transition.svelte"; export { default as MenubarDemo } from "./menubar-demo.svelte"; export { default as NavigationMenuDemo } from "./navigation-menu-demo.svelte"; export { default as PaginationDemo } from "./pagination-demo.svelte"; export { default as PinInputDemo } from "./pin-input-demo.svelte"; export { default as PopoverDemo } from "./popover-demo.svelte"; +export { default as PopoverDemoTransition } from "./popover-demo-transition.svelte"; export { default as ProgressDemo } from "./progress-demo.svelte"; export { default as RadioGroupDemo } from "./radio-group-demo.svelte"; export { default as RangeCalendarDemo } from "./range-calendar-demo.svelte"; @@ -49,4 +55,5 @@ export { default as ToolbarDemo } from "./toolbar-demo.svelte"; export { default as TooltipDemo } from "./tooltip-demo.svelte"; export { default as TooltipDemoCustom } from "./tooltip-demo-custom.svelte"; export { default as TooltipDemoDelayDuration } from "./tooltip-demo-delay-duration.svelte"; +export { default as TooltipDemoTransition } from "./tooltip-demo-transition.svelte"; export { default as DateFieldDemoCustom } from "./date-field-demo-custom.svelte"; diff --git a/sites/docs/src/lib/components/demos/link-preview-demo-transition.svelte b/sites/docs/src/lib/components/demos/link-preview-demo-transition.svelte new file mode 100644 index 000000000..13cc34629 --- /dev/null +++ b/sites/docs/src/lib/components/demos/link-preview-demo-transition.svelte @@ -0,0 +1,75 @@ + + + + + +
+ + HB +
+
+
+ + {#snippet child({ open, props })} + {#if open} +
+
+ +
+ + HB +
+
+
+

@huntabyte

+

I do things on the internet.

+
+
+ + FL, USA +
+
+ + Joined May 2020 +
+
+
+
+
+ {/if} + {/snippet} +
+
diff --git a/sites/docs/src/lib/components/demos/popover-demo-transition.svelte b/sites/docs/src/lib/components/demos/popover-demo-transition.svelte new file mode 100644 index 000000000..8d8e89fcc --- /dev/null +++ b/sites/docs/src/lib/components/demos/popover-demo-transition.svelte @@ -0,0 +1,84 @@ + + + + + Resize + + + + {#snippet child({ props, open })} + {#if open} +
+
+
+ +
+
+

+ Resize image +

+

+ Resize your photos easily +

+
+
+ +
+
+
+ Width + + +
+
+ Height + + +
+
+ + + +
+
+ {/if} + {/snippet} +
+
+
diff --git a/sites/docs/src/lib/components/demos/popover-demo.svelte b/sites/docs/src/lib/components/demos/popover-demo.svelte index 23c88d5dc..47a06eedd 100644 --- a/sites/docs/src/lib/components/demos/popover-demo.svelte +++ b/sites/docs/src/lib/components/demos/popover-demo.svelte @@ -16,7 +16,6 @@ diff --git a/sites/docs/src/lib/components/demos/select-demo-transition.svelte b/sites/docs/src/lib/components/demos/select-demo-transition.svelte new file mode 100644 index 000000000..f02b9b876 --- /dev/null +++ b/sites/docs/src/lib/components/demos/select-demo-transition.svelte @@ -0,0 +1,86 @@ + + + + + + {selectedLabel} + + + + + {#snippet child({ props, open })} + {#if open} +
+ + + + + {#each themes as theme, i (i + theme.value)} + + {#snippet children({ selected })} + {theme.label} + {#if selected} +
+ +
+ {/if} + {/snippet} +
+ {/each} +
+ + + +
+ {/if} + {/snippet} +
+
+
diff --git a/sites/docs/src/lib/components/demos/tooltip-demo-transition.svelte b/sites/docs/src/lib/components/demos/tooltip-demo-transition.svelte new file mode 100644 index 000000000..76d923dff --- /dev/null +++ b/sites/docs/src/lib/components/demos/tooltip-demo-transition.svelte @@ -0,0 +1,29 @@ + + + + + + + + + {#snippet child({ props, open })} + {#if open} +
+
+ Make some magic! +
+
+ {/if} + {/snippet} +
+
+
diff --git a/sites/docs/src/routes/(main)/sink/+page.svelte b/sites/docs/src/routes/(main)/sink/+page.svelte index e69de29bb..4f2393f4c 100644 --- a/sites/docs/src/routes/(main)/sink/+page.svelte +++ b/sites/docs/src/routes/(main)/sink/+page.svelte @@ -0,0 +1,133 @@ + + + + +
+ + Right click me +
+
+ + + {#snippet child({ props, open })} + {#if open} +
+ +
+ + Edit +
+
+ + ⌘ + + + E + +
+
+ + +
+ + Duplicate +
+
+ + ⌘ + + + D + +
+
+ + +
+ + Delete +
+
+
+ {/if} + {/snippet} +
+
+