Skip to content

Commit 068d388

Browse files
authored
feat(Popover): add Popover.Overlay component (#1851)
1 parent 82e8c3d commit 068d388

File tree

13 files changed

+374
-42
lines changed

13 files changed

+374
-42
lines changed

.changeset/salty-clocks-lay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"bits-ui": minor
3+
---
4+
5+
feat(Popover): add `Popover.Overlay` component

docs/content/components/popover.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: Display supplementary content or information when users interact wi
44
---
55

66
<script>
7-
import { APISection, ComponentPreview, PopoverDemo, PopoverDemoTransition, Callout } from '$lib/components/index.js'
7+
import { APISection, ComponentPreview, PopoverDemo, PopoverDemoTransition, PopoverDemoOverlay } from '$lib/components/index.js'
88
let { schemas } = $props()
99
</script>
1010

@@ -25,10 +25,13 @@ description: Display supplementary content or information when users interact wi
2525
2626
<Popover.Root>
2727
<Popover.Trigger />
28-
<Popover.Content>
29-
<Popover.Close />
30-
<Popover.Arrow />
31-
</Popover.Content>
28+
<Popover.Portal>
29+
<Popover.Overlay />
30+
<Popover.Content>
31+
<Popover.Close />
32+
<Popover.Arrow />
33+
</Popover.Content>
34+
</Popover.Portal>
3235
</Popover.Root>
3336
```
3437

@@ -246,12 +249,24 @@ You can use the `forceMount` prop along with the `child` snippet to forcefully m
246249

247250
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.
248251

249-
<ComponentPreview name="popover-demo-transition" componentName="Popover" containerClass="mt-4">
252+
<ComponentPreview name="popover-demo-transition" componentName="Popover" containerClass="mt-4" size="xs">
250253

251254
{#snippet preview()}
252255
<PopoverDemoTransition />
253256
{/snippet}
254257

255258
</ComponentPreview>
256259

260+
## Overlay Component
261+
262+
You can use the `Popover.Overlay` component to create an overlay behind the popover that appears when the popover is open.
263+
264+
<ComponentPreview name="popover-demo-overlay" componentName="Popover" size="xs">
265+
266+
{#snippet preview()}
267+
<PopoverDemoOverlay />
268+
{/snippet}
269+
270+
</ComponentPreview>
271+
257272
<APISection {schemas} />

docs/src/lib/components/api-ref/data-attrs/data-attrs-table.svelte

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,41 @@
99
let { dataAttrs = [] }: { dataAttrs: DataAttrSchema[] } = $props();
1010
</script>
1111

12-
<Table.Root>
13-
<Table.Header>
14-
<Table.Row>
15-
<Table.Head class="w-[90%] whitespace-nowrap sm:w-[38%]">Data Attribute</Table.Head>
16-
<Table.Head class="hidden w-[22%] whitespace-nowrap sm:table-cell">Value</Table.Head>
17-
<Table.Head class="hidden w-[40%] whitespace-nowrap sm:table-cell"
18-
>Description</Table.Head
19-
>
20-
<Table.Head class="sr-only w-[10%] whitespace-nowrap sm:hidden">Details</Table.Head>
21-
</Table.Row>
22-
</Table.Header>
23-
<Table.Body>
24-
{#each dataAttrs as attr (attr.name)}
12+
{#if dataAttrs.length}
13+
<Table.Root>
14+
<Table.Header>
2515
<Table.Row>
26-
<Table.Cell class="align-baseline">
27-
<Code
28-
class="text-foreground h-fit py-1.5 font-semibold md:py-1 lg:h-[27px] lg:py-0"
29-
>data-{attr.name}</Code
30-
>
31-
</Table.Cell>
32-
<Table.Cell class="hidden pr-1 align-baseline sm:table-cell">
33-
<DataAttrValueContent {attr} />
34-
</Table.Cell>
35-
<Table.Cell class="hidden align-baseline sm:table-cell">
36-
<p class="text-sm leading-[1.5rem]">
37-
{@html parseMarkdown(attr.description)}
38-
</p>
39-
</Table.Cell>
40-
<Table.Cell class="overflow-hidden py-0 sm:hidden">
41-
<DataAttrsValueContentMobile {attr} />
42-
</Table.Cell>
16+
<Table.Head class="w-[90%] whitespace-nowrap sm:w-[38%]">Data Attribute</Table.Head>
17+
<Table.Head class="hidden w-[22%] whitespace-nowrap sm:table-cell">Value</Table.Head
18+
>
19+
<Table.Head class="hidden w-[40%] whitespace-nowrap sm:table-cell"
20+
>Description</Table.Head
21+
>
22+
<Table.Head class="sr-only w-[10%] whitespace-nowrap sm:hidden">Details</Table.Head>
4323
</Table.Row>
44-
{/each}
45-
</Table.Body>
46-
</Table.Root>
24+
</Table.Header>
25+
<Table.Body>
26+
{#each dataAttrs as attr (attr.name)}
27+
<Table.Row>
28+
<Table.Cell class="align-baseline">
29+
<Code
30+
class="text-foreground h-fit py-1.5 font-semibold md:py-1 lg:h-[27px] lg:py-0"
31+
>data-{attr.name}</Code
32+
>
33+
</Table.Cell>
34+
<Table.Cell class="hidden pr-1 align-baseline sm:table-cell">
35+
<DataAttrValueContent {attr} />
36+
</Table.Cell>
37+
<Table.Cell class="hidden align-baseline sm:table-cell">
38+
<p class="text-sm leading-[1.5rem]">
39+
{@html parseMarkdown(attr.description)}
40+
</p>
41+
</Table.Cell>
42+
<Table.Cell class="overflow-hidden py-0 sm:hidden">
43+
<DataAttrsValueContentMobile {attr} />
44+
</Table.Cell>
45+
</Table.Row>
46+
{/each}
47+
</Table.Body>
48+
</Table.Root>
49+
{/if}

docs/src/lib/components/demos/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export { default as PaginationDemo } from "./pagination-demo.svelte";
5656
export { default as PinInputDemo } from "./pin-input-demo.svelte";
5757
export { default as PopoverDemo } from "./popover-demo.svelte";
5858
export { default as PopoverDemoTransition } from "./popover-demo-transition.svelte";
59+
export { default as PopoverDemoOverlay } from "./popover-demo-overlay.svelte";
5960
export { default as PortalDemo } from "./portal-demo.svelte";
6061
export { default as ProgressDemo } from "./progress-demo.svelte";
6162
export { default as ProgressDemoCustom } from "./progress-demo-custom.svelte";
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<script lang="ts">
2+
import { Popover, Separator, Toggle } from "bits-ui";
3+
import ImageSquare from "phosphor-svelte/lib/ImageSquare";
4+
import LinkSimpleHorizontalBreak from "phosphor-svelte/lib/LinkSimpleHorizontalBreak";
5+
6+
let width = $state(1024);
7+
let height = $state(768);
8+
</script>
9+
10+
<Popover.Root>
11+
<Popover.Trigger
12+
class="rounded-input bg-dark
13+
text-background shadow-mini hover:bg-dark/95 inline-flex h-10 select-none items-center justify-center whitespace-nowrap px-[21px] text-[15px] font-medium transition-all hover:cursor-pointer active:scale-[0.98]"
14+
>
15+
Resize
16+
</Popover.Trigger>
17+
<Popover.Portal>
18+
<Popover.Overlay
19+
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80"
20+
/>
21+
<Popover.Content
22+
class="border-dark-10 bg-background shadow-popover data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-popover-content-transform-origin) z-50 w-full max-w-[328px] rounded-[12px] border p-4"
23+
sideOffset={8}
24+
>
25+
<div class="flex items-center">
26+
<div class="bg-muted mr-3 flex size-12 items-center justify-center rounded-full">
27+
<ImageSquare class="size-6" />
28+
</div>
29+
<div class="flex flex-col">
30+
<h4 class="text-[17px] font-semibold leading-5 tracking-[-0.01em]">
31+
Resize image
32+
</h4>
33+
<p class="text-muted-foreground text-sm font-medium">
34+
Resize your photos easily
35+
</p>
36+
</div>
37+
</div>
38+
<Separator.Root class="bg-dark-10 -mx-4 mb-6 mt-[17px] block h-px" />
39+
<div class="flex items-center pb-2">
40+
<div class="mr-2 flex items-center">
41+
<div class="relative mr-2">
42+
<span class="sr-only">Width</span>
43+
<span
44+
aria-hidden="true"
45+
class="text-xxs text-muted-foreground absolute left-5 top-4">W</span
46+
>
47+
<input
48+
type="number"
49+
class="h-input rounded-10px border-border-input bg-background text-foreground w-[119px] border pl-10 pr-2 text-base sm:text-sm"
50+
bind:value={width}
51+
/>
52+
</div>
53+
<div class="relative">
54+
<span class="sr-only">Height</span>
55+
<span
56+
aria-hidden="true"
57+
class="text-xxs text-muted-foreground absolute left-5 top-4">H</span
58+
>
59+
<input
60+
type="number"
61+
class="h-input rounded-10px border-border-input bg-background text-foreground w-[119px] border pl-10 pr-2 text-base sm:text-sm"
62+
bind:value={height}
63+
/>
64+
</div>
65+
</div>
66+
<Toggle.Root
67+
aria-label="toggle constrain portions"
68+
class="bg-background hover:bg-muted data-[state=on]:bg-muted inline-flex size-10 items-center justify-center rounded-[9px] transition-all active:scale-[0.98]"
69+
>
70+
<LinkSimpleHorizontalBreak class="size-6" />
71+
</Toggle.Root>
72+
</div>
73+
</Popover.Content>
74+
</Popover.Portal>
75+
</Popover.Root>

docs/src/lib/content/api-reference/popover.api.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
PopoverClosePropsWithoutHTML,
44
PopoverContentPropsWithoutHTML,
55
PopoverContentStaticPropsWithoutHTML,
6+
PopoverOverlayPropsWithoutHTML,
67
PopoverPortalPropsWithoutHTML,
78
PopoverRootPropsWithoutHTML,
89
PopoverTriggerPropsWithoutHTML,
@@ -165,4 +166,21 @@ export const portal = defineComponentApiSchema<PopoverPortalPropsWithoutHTML>({
165166
props: portalProps,
166167
});
167168

168-
export const popover = [root, trigger, content, contentStatic, close, arrow, portal];
169+
export const overlay = defineComponentApiSchema<PopoverOverlayPropsWithoutHTML>({
170+
title: "Overlay",
171+
description:
172+
"An overlay that can be used to create a semi-transparent overlay behind the popover when open.",
173+
props: {
174+
forceMount: forceMountProp,
175+
...withChildProps({ elType: "HTMLDivElement" }),
176+
},
177+
dataAttributes: [
178+
defineSimpleDataAttr({
179+
name: "popover-overlay",
180+
description: "Present on the overlay element.",
181+
}),
182+
openClosedDataAttr,
183+
],
184+
});
185+
186+
export const popover = [root, trigger, content, contentStatic, overlay, close, arrow, portal];
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<script lang="ts">
2+
import { boxWith, mergeProps } from "svelte-toolbelt";
3+
import { PopoverOverlayState } from "../popover.svelte.js";
4+
import type { PopoverOverlayProps } from "../types.js";
5+
import { createId } from "$lib/internal/create-id.js";
6+
import PresenceLayer from "$lib/bits/utilities/presence-layer/presence-layer.svelte";
7+
8+
const uid = $props.id();
9+
10+
let {
11+
id = createId(uid),
12+
forceMount = false,
13+
child,
14+
children,
15+
ref = $bindable(null),
16+
...restProps
17+
}: PopoverOverlayProps = $props();
18+
19+
const overlayState = PopoverOverlayState.create({
20+
id: boxWith(() => id),
21+
ref: boxWith(
22+
() => ref,
23+
(v) => (ref = v)
24+
),
25+
});
26+
27+
const mergedProps = $derived(mergeProps(restProps, overlayState.props));
28+
</script>
29+
30+
<PresenceLayer open={overlayState.root.opts.open.current || forceMount} ref={overlayState.opts.ref}>
31+
{#snippet presence()}
32+
{#if child}
33+
{@render child({ props: mergeProps(mergedProps), ...overlayState.snippetProps })}
34+
{:else}
35+
<div {...mergeProps(mergedProps)}>
36+
{@render children?.(overlayState.snippetProps)}
37+
</div>
38+
{/if}
39+
{/snippet}
40+
</PresenceLayer>

packages/bits-ui/src/lib/bits/popover/exports.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { default as ContentStatic } from "./components/popover-content-static.sv
55
export { default as Trigger } from "./components/popover-trigger.svelte";
66
export { default as Close } from "./components/popover-close.svelte";
77
export { default as Portal } from "$lib/bits/utilities/portal/portal.svelte";
8+
export { default as Overlay } from "./components/popover-overlay.svelte";
89

910
export type {
1011
PopoverRootProps as RootProps,
@@ -14,4 +15,5 @@ export type {
1415
PopoverTriggerProps as TriggerProps,
1516
PopoverCloseProps as CloseProps,
1617
PopoverPortalProps as PortalProps,
18+
PopoverOverlayProps as OverlayProps,
1719
} from "./types.js";

packages/bits-ui/src/lib/bits/popover/popover.svelte.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type { Measurable } from "$lib/internal/floating-svelte/types.js";
2121

2222
const popoverAttrs = createBitsAttrs({
2323
component: "popover",
24-
parts: ["root", "trigger", "content", "close"],
24+
parts: ["root", "trigger", "content", "close", "overlay"],
2525
});
2626

2727
const PopoverRootContext = new Context<PopoverRootState>("Popover.Root");
@@ -233,3 +233,36 @@ export class PopoverCloseState {
233233
}) as const
234234
);
235235
}
236+
237+
interface PopoverOverlayStateOpts extends WithRefOpts {}
238+
239+
export class PopoverOverlayState {
240+
static create(opts: PopoverOverlayStateOpts) {
241+
return new PopoverOverlayState(opts, PopoverRootContext.get());
242+
}
243+
244+
readonly opts: PopoverOverlayStateOpts;
245+
readonly root: PopoverRootState;
246+
readonly attachment: RefAttachment;
247+
248+
constructor(opts: PopoverOverlayStateOpts, root: PopoverRootState) {
249+
this.opts = opts;
250+
this.root = root;
251+
this.attachment = attachRef(this.opts.ref);
252+
}
253+
254+
readonly snippetProps = $derived.by(() => ({ open: this.root.opts.open.current }));
255+
256+
readonly props = $derived.by(
257+
() =>
258+
({
259+
id: this.opts.id.current,
260+
[popoverAttrs.overlay]: "",
261+
style: {
262+
pointerEvents: "auto",
263+
},
264+
"data-state": getDataOpenClosed(this.root.opts.open.current),
265+
...this.attachment,
266+
}) as const
267+
);
268+
}

packages/bits-ui/src/lib/bits/popover/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
} from "$lib/shared/attributes.js";
1414
import type { FloatingContentSnippetProps, StaticContentSnippetProps } from "$lib/shared/types.js";
1515
import type { PortalProps } from "$lib/types.js";
16+
import type { PresenceLayerProps } from "../utilities/presence-layer/types.js";
1617

1718
export type PopoverRootPropsWithoutHTML = WithChildren<{
1819
/**
@@ -65,3 +66,15 @@ export type PopoverArrowProps = ArrowProps;
6566

6667
export type PopoverPortalPropsWithoutHTML = PortalProps;
6768
export type PopoverPortalProps = PortalProps;
69+
70+
export type PopoverOverlaySnippetProps = {
71+
open: boolean;
72+
};
73+
74+
export type PopoverOverlayPropsWithoutHTML = WithChild<
75+
PresenceLayerProps,
76+
PopoverOverlaySnippetProps
77+
>;
78+
79+
export type PopoverOverlayProps = PopoverOverlayPropsWithoutHTML &
80+
Without<BitsPrimitiveDivAttributes, PopoverOverlayPropsWithoutHTML>;

0 commit comments

Comments
 (0)