From b781c9278da204e8d92e35b5e67e78e00d04b48f Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Wed, 30 Oct 2024 16:06:53 -0400 Subject: [PATCH 1/6] fix: popover forcemount --- .../components/popover-content-static.svelte | 101 +++++++++----- .../popover/components/popover-content.svelte | 93 +++++++++---- .../focus-scope/focus-scope-stack.svelte.ts | 2 +- .../utilities/focus-scope/focus-scope.svelte | 2 + .../lib/bits/utilities/focus-scope/types.ts | 5 + .../focus-scope/useFocusScope.svelte.ts | 109 +++++++++------ .../popper-layer-force-mount.svelte | 84 ++++++++++++ .../popper-layer/popper-layer-inner.svelte | 126 ++++++++++++++++++ .../popper-layer/popper-layer.svelte | 83 +++--------- .../docs/src/routes/(main)/sink/+page.svelte | 86 ++++++++++++ 10 files changed, 527 insertions(+), 164 deletions(-) create mode 100644 packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer-force-mount.svelte create mode 100644 packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer-inner.svelte 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..ed6ea2fd1 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..02899b213 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/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..8866a790b --- /dev/null +++ b/packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer-force-mount.svelte @@ -0,0 +1,84 @@ + + + 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..68b7f42ac 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 @@ + + + + Resize + + + + {#snippet child({ props, open })} + {#if open} +
+
+
+ +
+
+

+ Resize image +

+

+ Resize your photos easily +

+
+
+ +
+
+
+ Width + + +
+
+ Height + + +
+
+ + + +
+
+ {/if} + {/snippet} +
+
+
From 780ebfc4aef9b84bbd6e9e721402bf03762af798 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Wed, 30 Oct 2024 16:44:48 -0400 Subject: [PATCH 2/6] fix: dropdown/context menu forcemount --- .changeset/stale-weeks-ring.md | 5 + .../context-menu-content-static.svelte | 102 ++++++-- .../components/context-menu-content.svelte | 99 +++++--- .../dropdown-menu-content-static.svelte | 88 +++++-- .../components/dropdown-menu-content.svelte | 82 +++++-- .../bits/menu/components/menu-content.svelte | 91 ++++--- .../menu/components/menu-sub-content.svelte | 119 +++++---- .../popper-layer-force-mount.svelte | 1 + .../popper-layer/popper-layer.svelte | 1 + sites/docs/content/components/context-menu.md | 10 +- .../docs/content/components/dropdown-menu.md | 10 +- .../demos/context-menu-demo-transition.svelte | 134 ++++++++++ .../dropdown-menu-demo-transition.svelte | 232 ++++++++++++++++++ sites/docs/src/lib/components/demos/index.ts | 2 + .../lib/components/demos/popover-demo.svelte | 1 - .../docs/src/routes/(main)/sink/+page.svelte | 185 ++++++++------ 16 files changed, 910 insertions(+), 252 deletions(-) create mode 100644 .changeset/stale-weeks-ring.md create mode 100644 sites/docs/src/lib/components/demos/context-menu-demo-transition.svelte create mode 100644 sites/docs/src/lib/components/demos/dropdown-menu-demo-transition.svelte 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/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..3e1555667 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,67 @@ 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..cbed0f2b9 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,63 @@ 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..8346cf048 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..68b17d61f 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,67 @@ }); 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/menu/components/menu-content.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-content.svelte index 9cb9361bf..44bd3a841 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,72 @@ 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.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/utilities/popper-layer/popper-layer-force-mount.svelte b/packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer-force-mount.svelte index 8866a790b..850e99c66 100644 --- 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 @@ -81,4 +81,5 @@ {isValidEvent} {onFocusOutside} {...restProps} + forceMount={true} /> 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 68b7f42ac..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 @@ -81,6 +81,7 @@ {trapFocus} {isValidEvent} {onFocusOutside} + forceMount={false} {...restProps} /> {/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..36a5b9fba 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/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..f4c5a76f6 100644 --- a/sites/docs/src/lib/components/demos/index.ts +++ b/sites/docs/src/lib/components/demos/index.ts @@ -14,6 +14,7 @@ export { default as ComboboxDemo } from "./combobox-demo.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,6 +23,7 @@ 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 SelectDemo } from "./select-demo.svelte"; 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/routes/(main)/sink/+page.svelte b/sites/docs/src/routes/(main)/sink/+page.svelte index 1f56338e7..4f2393f4c 100644 --- a/sites/docs/src/routes/(main)/sink/+page.svelte +++ b/sites/docs/src/routes/(main)/sink/+page.svelte @@ -1,86 +1,133 @@ - - + - Resize - - - + + Right click me + + + + {#snippet child({ props, open })} {#if open} -
-
-
- +
+ +
+ + Edit
-
-

- Resize image -

-

- Resize your photos easily -

+
+ + ⌘ + + + E +
-
- -
-
-
- Width - - + + + +
+ + Duplicate +
+
+ + ⌘ + + + D + +
+
+ + +
+ + Delete +
+
{/if} {/snippet} - - - + + + From 1129d7133f140cc46c39718c08db9350c8fc8c50 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Wed, 30 Oct 2024 16:52:00 -0400 Subject: [PATCH 3/6] fix: link preview forcemount --- .changeset/olive-ads-enjoy.md | 5 + .changeset/wet-beds-hunt.md | 5 + .../components/link-preview-content.svelte | 95 +++++++++---- .../components/menu-content-static.svelte | 98 +++++++++---- .../components/menu-sub-content-static.svelte | 129 +++++++++++------- .../docs/content/components/dropdown-menu.md | 2 +- sites/docs/content/components/link-preview.md | 34 ++++- sites/docs/src/lib/components/demos/index.ts | 1 + .../demos/link-preview-demo-transition.svelte | 75 ++++++++++ 9 files changed, 336 insertions(+), 108 deletions(-) create mode 100644 .changeset/olive-ads-enjoy.md create mode 100644 .changeset/wet-beds-hunt.md create mode 100644 sites/docs/src/lib/components/demos/link-preview-demo-transition.svelte diff --git a/.changeset/olive-ads-enjoy.md b/.changeset/olive-ads-enjoy.md new file mode 100644 index 000000000..d75fe4667 --- /dev/null +++ b/.changeset/olive-ads-enjoy.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +fix: DropdownMenu & ContextMenu forcemount diff --git a/.changeset/wet-beds-hunt.md b/.changeset/wet-beds-hunt.md new file mode 100644 index 000000000..e55b26175 --- /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/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..d414995d9 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-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/sites/docs/content/components/dropdown-menu.md b/sites/docs/content/components/dropdown-menu.md index 36a5b9fba..000cf3711 100644 --- a/sites/docs/content/components/dropdown-menu.md +++ b/sites/docs/content/components/dropdown-menu.md @@ -344,7 +344,7 @@ 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()} diff --git a/sites/docs/content/components/link-preview.md b/sites/docs/content/components/link-preview.md index b519da514..9e256d145 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,36 @@ 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} +
+ Item 1 + Item 2 +
+ {/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/src/lib/components/demos/index.ts b/sites/docs/src/lib/components/demos/index.ts index f4c5a76f6..cbe15f16f 100644 --- a/sites/docs/src/lib/components/demos/index.ts +++ b/sites/docs/src/lib/components/demos/index.ts @@ -26,6 +26,7 @@ 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"; 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} +
+
From d6e041c5d14b2e61c9830ba11eeb2622ff96bbcb Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Wed, 30 Oct 2024 17:07:22 -0400 Subject: [PATCH 4/6] fix: select forcemount --- .changeset/olive-worms-bow.md | 5 + .../context-menu-content-static.svelte | 2 + .../components/context-menu-content.svelte | 2 + .../dropdown-menu-content-static.svelte | 2 + .../components/dropdown-menu-content.svelte | 2 + .../link-preview-content-static.svelte | 121 +++++++++++++----- .../components/menu-content-static.svelte | 2 + .../bits/menu/components/menu-content.svelte | 2 + .../components/popover-content-static.svelte | 6 +- .../popover/components/popover-content.svelte | 6 +- .../src/lib/bits/popover/popover.svelte.ts | 3 + .../components/select-content-static.svelte | 101 ++++++++++----- .../select/components/select-content.svelte | 101 +++++++++------ .../src/lib/bits/select/select.svelte.ts | 1 + sites/docs/content/components/link-preview.md | 3 +- sites/docs/content/components/popover.md | 33 ++++- sites/docs/content/components/select.md | 33 ++++- sites/docs/src/lib/components/demos/index.ts | 2 + .../demos/popover-demo-transition.svelte | 84 ++++++++++++ .../demos/select-demo-transition.svelte | 86 +++++++++++++ 20 files changed, 485 insertions(+), 112 deletions(-) create mode 100644 .changeset/olive-worms-bow.md create mode 100644 sites/docs/src/lib/components/demos/popover-demo-transition.svelte create mode 100644 sites/docs/src/lib/components/demos/select-demo-transition.svelte 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/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 3e1555667..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 @@ -78,6 +78,7 @@ trapFocus {loop} {forceMount} + {id} > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { @@ -108,6 +109,7 @@ trapFocus {loop} forceMount={false} + {id} > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { 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 cbed0f2b9..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 @@ -76,6 +76,7 @@ {isValidEvent} trapFocus {loop} + {id} > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { @@ -104,6 +105,7 @@ {isValidEvent} trapFocus {loop} + {id} > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { 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 8346cf048..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 @@ -59,6 +59,7 @@ {loop} forceMount={true} isStatic + {id} > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { @@ -84,6 +85,7 @@ {loop} forceMount={false} isStatic + {id} > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { 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 68b17d61f..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 @@ -58,6 +58,7 @@ trapFocus {loop} forceMount={true} + {id} > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { @@ -82,6 +83,7 @@ trapFocus {loop} forceMount={false} + {id} > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { 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/menu/components/menu-content-static.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-content-static.svelte index d414995d9..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 @@ -62,6 +62,7 @@ {loop} forceMount={true} isStatic + {id} > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { @@ -90,6 +91,7 @@ {loop} forceMount={false} isStatic + {id} > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { 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 44bd3a841..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 @@ -61,6 +61,7 @@ trapFocus {loop} forceMount={true} + {id} > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { @@ -88,6 +89,7 @@ trapFocus {loop} forceMount={false} + {id} > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { 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 ed6ea2fd1..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 @@ -64,7 +64,7 @@ {trapFocus} {preventScroll} loop - {forceMount} + forceMount={true} > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { @@ -83,7 +83,7 @@ {#snippet popper({ props })} {@const finalProps = mergeProps(props, { 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 02899b213..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 @@ -63,7 +63,7 @@ {trapFocus} {preventScroll} loop - {forceMount} + forceMount={true} > {#snippet popper({ props })} {@const finalProps = mergeProps(props, { @@ -81,7 +81,7 @@ {:else if !forceMount} {#snippet popper({ props })} {@const finalProps = mergeProps(props, { 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/sites/docs/content/components/link-preview.md b/sites/docs/content/components/link-preview.md index 9e256d145..88a947e12 100644 --- a/sites/docs/content/components/link-preview.md +++ b/sites/docs/content/components/link-preview.md @@ -185,8 +185,7 @@ You can use the `forceMount` prop along with the `child` snippet to forcefully m {#snippet child({ props, open })} {#if open}
- Item 1 - Item 2 +
{/if} {/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/src/lib/components/demos/index.ts b/sites/docs/src/lib/components/demos/index.ts index cbe15f16f..4f45be32c 100644 --- a/sites/docs/src/lib/components/demos/index.ts +++ b/sites/docs/src/lib/components/demos/index.ts @@ -31,11 +31,13 @@ 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"; 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/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} +
+
+
From f36c5e412dd8088d50f5a25053a796f85a29b1c9 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Wed, 30 Oct 2024 17:09:51 -0400 Subject: [PATCH 5/6] fix: combobox forcemount --- .changeset/cool-cats-compete.md | 5 + sites/docs/content/components/combobox.md | 33 +++++- .../demos/combobox-demo-transition.svelte | 105 ++++++++++++++++++ sites/docs/src/lib/components/demos/index.ts | 1 + 4 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 .changeset/cool-cats-compete.md create mode 100644 sites/docs/src/lib/components/demos/combobox-demo-transition.svelte 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/sites/docs/content/components/combobox.md b/sites/docs/content/components/combobox.md index 393992ba6..7f65ed8da 100644 --- a/sites/docs/content/components/combobox.md +++ b/sites/docs/content/components/combobox.md @@ -4,7 +4,7 @@ description: Enables users to pick from a list of options displayed in a dropdow --- @@ -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/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/index.ts b/sites/docs/src/lib/components/demos/index.ts index 4f45be32c..ecb874bf9 100644 --- a/sites/docs/src/lib/components/demos/index.ts +++ b/sites/docs/src/lib/components/demos/index.ts @@ -11,6 +11,7 @@ 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"; From 604369834901e8d7526de8869c39a203c6994a17 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Wed, 30 Oct 2024 17:15:07 -0400 Subject: [PATCH 6/6] update changesets --- .changeset/itchy-drinks-explode.md | 5 + .changeset/olive-ads-enjoy.md | 2 +- .changeset/wet-beds-hunt.md | 2 +- .../components/tooltip-content-static.svelte | 118 +++++++++++++----- .../tooltip/components/tooltip-content.svelte | 91 +++++++++----- .../src/lib/bits/tooltip/tooltip.svelte.ts | 19 ++- sites/docs/content/components/tooltip.md | 10 +- sites/docs/src/lib/components/demos/index.ts | 1 + .../demos/tooltip-demo-transition.svelte | 29 +++++ 9 files changed, 210 insertions(+), 67 deletions(-) create mode 100644 .changeset/itchy-drinks-explode.md create mode 100644 sites/docs/src/lib/components/demos/tooltip-demo-transition.svelte 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 index d75fe4667..3cccc93d0 100644 --- a/.changeset/olive-ads-enjoy.md +++ b/.changeset/olive-ads-enjoy.md @@ -2,4 +2,4 @@ "bits-ui": patch --- -fix: DropdownMenu & ContextMenu forcemount +fix: DropdownMenu & ContextMenu `forceMount` diff --git a/.changeset/wet-beds-hunt.md b/.changeset/wet-beds-hunt.md index e55b26175..9929b08c4 100644 --- a/.changeset/wet-beds-hunt.md +++ b/.changeset/wet-beds-hunt.md @@ -2,4 +2,4 @@ "bits-ui": patch --- -fix: LinkPreview forceMount +fix: LinkPreview `forceMount` 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/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/index.ts b/sites/docs/src/lib/components/demos/index.ts index ecb874bf9..a4805787e 100644 --- a/sites/docs/src/lib/components/demos/index.ts +++ b/sites/docs/src/lib/components/demos/index.ts @@ -55,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/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} +
+
+