Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion COMPONENT_INDEX.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Component Index

> 169 components exported from [email protected].
> 170 components exported from [email protected].

## Components

Expand Down Expand Up @@ -45,6 +45,7 @@
- [`FileUploaderItem`](#fileuploaderitem)
- [`FileUploaderSkeleton`](#fileuploaderskeleton)
- [`Filename`](#filename)
- [`FloatingPortal`](#floatingportal)
- [`FluidForm`](#fluidform)
- [`Form`](#form)
- [`FormGroup`](#formgroup)
Expand Down Expand Up @@ -1441,6 +1442,27 @@ None.
| click | forwarded | -- | -- |
| keydown | forwarded | -- | -- |

## `FloatingPortal`

### Props

| Prop name | Required | Kind | Reactive | Type | Default value | Description |
| :--------- | :------- | :--------------- | :------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ------------------------------------------------------------------ |
| reference | No | <code>let</code> | No | <code>HTMLElement &#124; null</code> | <code>null</code> | Reference element to position the portal relative to |
| placement | No | <code>let</code> | No | <code>"top" &#124; "top-start" &#124; "top-end" &#124; "right" &#124; "right-start" &#124; "right-end" &#124; "bottom" &#124; "bottom-start" &#124; "bottom-end" &#124; "left" &#124; "left-start" &#124; "left-end"</code> | <code>"bottom"</code> | Placement of the floating portal relative to the reference element |
| offset | No | <code>let</code> | No | <code>number</code> | <code>0</code> | Offset in pixels from the reference element |
| autoUpdate | No | <code>let</code> | No | <code>boolean</code> | <code>true</code> | Set to `true` to enable auto-update positioning on scroll/resize |

### Slots

| Slot name | Default | Props | Fallback |
| :-------- | :------ | :---------------------------------- | :------- |
| -- | Yes | <code>Record<string, never> </code> | -- |

### Events

None.

## `FluidForm`

### Props
Expand Down
7 changes: 7 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"": {
"name": "carbon-components-svelte",
"dependencies": {
"@floating-ui/dom": "^1.7.4",
"@ibm/telemetry-js": "^1.5.0",
"flatpickr": "4.6.9",
},
Expand Down Expand Up @@ -135,6 +136,12 @@

"@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="],

"@floating-ui/core": ["@floating-ui/[email protected]", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],

"@floating-ui/dom": ["@floating-ui/[email protected]", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],

"@floating-ui/utils": ["@floating-ui/[email protected]", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],

"@hutson/parse-repository-url": ["@hutson/[email protected]", "", {}, "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q=="],

"@ibm/telemetry-js": ["@ibm/[email protected]", "", { "bin": { "ibmtelemetry": "dist/collect.js" } }, "sha512-F8+/NNUwtm8BuFz18O9KPvIFTFDo8GUSoyhPxPjEpk7nEyEzWGfhIiEPhL00B2NdHRLDSljh3AiCfSnL/tutiQ=="],
Expand Down
68 changes: 67 additions & 1 deletion docs/src/COMPONENT_API.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"total": 169,
"total": 170,
"components": [
{
"moduleName": "Accordion",
Expand Down Expand Up @@ -5663,6 +5663,72 @@
},
"contexts": []
},
{
"moduleName": "FloatingPortal",
"filePath": "src/Portal/FloatingPortal.svelte",
"props": [
{
"name": "reference",
"kind": "let",
"description": "Reference element to position the portal relative to",
"type": "HTMLElement | null",
"value": "null",
"isFunction": false,
"isFunctionDeclaration": false,
"isRequired": false,
"constant": false,
"reactive": false
},
{
"name": "placement",
"kind": "let",
"description": "Placement of the floating portal relative to the reference element",
"type": "\"top\" | \"top-start\" | \"top-end\" | \"right\" | \"right-start\" | \"right-end\" | \"bottom\" | \"bottom-start\" | \"bottom-end\" | \"left\" | \"left-start\" | \"left-end\"",
"value": "\"bottom\"",
"isFunction": false,
"isFunctionDeclaration": false,
"isRequired": false,
"constant": false,
"reactive": false
},
{
"name": "offset",
"kind": "let",
"description": "Offset in pixels from the reference element",
"type": "number",
"value": "0",
"isFunction": false,
"isFunctionDeclaration": false,
"isRequired": false,
"constant": false,
"reactive": false
},
{
"name": "autoUpdate",
"kind": "let",
"description": "Set to `true` to enable auto-update positioning on scroll/resize",
"type": "boolean",
"value": "true",
"isFunction": false,
"isFunctionDeclaration": false,
"isRequired": false,
"constant": false,
"reactive": false
}
],
"moduleExports": [],
"slots": [
{
"name": null,
"default": true,
"slot_props": "Record<string, never>"
}
],
"events": [],
"typedefs": [],
"generics": null,
"contexts": []
},
{
"moduleName": "FluidForm",
"filePath": "src/FluidForm/FluidForm.svelte",
Expand Down
12 changes: 12 additions & 0 deletions docs/src/pages/components/Portal.svx
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,15 @@ Use the `tag` prop to specify a custom HTML element. By default, Portal uses a `
Wrap `Modal` in a `Portal` to escape parent containers with `overflow: hidden` or z-index stacking contexts. This ensures the modal appears above all content and isn't clipped by parent boundaries.

<FileSource src="/framed/Portal/ModalPortal" />

## Floating portal

The `FloatingPortal` component automatically positions content relative to a reference element using [Floating UI](https://floating-ui.com/). This is useful for dropdowns, popovers, and tooltips.

<FileSource src="/framed/Portal/FloatingPortal" />

## Floating portal placement

Control the placement of the floating portal using the `placement` prop.

<FileSource src="/framed/Portal/FloatingPortalPlacement" />
22 changes: 22 additions & 0 deletions docs/src/pages/framed/Portal/FloatingPortal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script>
import { Button, FloatingPortal } from "carbon-components-svelte";

let buttonRef = null;
let showPortal = false;
</script>

<Button bind:this={buttonRef} on:click={() => (showPortal = !showPortal)}>
Toggle Floating Portal
</Button>

{#if showPortal && buttonRef}
<FloatingPortal reference={buttonRef} placement="bottom" offset={8}>
<div style="padding: 1rem; background: var(--cds-layer-01); border: 1px solid var(--cds-border-subtle-01); box-shadow: var(--cds-shadow-03); border-radius: 4px; min-width: 200px;">
<p style="margin: 0 0 0.5rem 0;">Floating portal content</p>
<p style="margin: 0; font-size: 0.875rem; color: var(--cds-text-secondary);">
This content is positioned relative to the button above.
</p>
</div>
</FloatingPortal>
{/if}

53 changes: 53 additions & 0 deletions docs/src/pages/framed/Portal/FloatingPortalPlacement.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script>
import { Button, FloatingPortal } from "carbon-components-svelte";

let buttonRefs = {};
let openPlacements = {};

const placements = [
"top",
"top-start",
"top-end",
"right",
"right-start",
"right-end",
"bottom",
"bottom-start",
"bottom-end",
"left",
"left-start",
"left-end",
];

function togglePlacement(placement) {
openPlacements[placement] = !openPlacements[placement];
openPlacements = { ...openPlacements };
}
</script>

<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 2rem; padding: 4rem;">
{#each placements as placement}
<div style="display: flex; justify-content: center; align-items: center;">
<Button
bind:this={buttonRefs[placement]}
on:click={() => togglePlacement(placement)}
>
{placement}
</Button>
{#if openPlacements[placement] && buttonRefs[placement]}
<FloatingPortal
reference={buttonRefs[placement]}
placement={placement}
offset={8}
>
<div
style="padding: 0.75rem; background: var(--cds-layer-01); border: 1px solid var(--cds-border-subtle-01); box-shadow: var(--cds-shadow-03); border-radius: 4px; white-space: nowrap;"
>
{placement}
</div>
</FloatingPortal>
{/if}
</div>
{/each}
</div>

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"release": "standard-version && bun build:docs"
},
"dependencies": {
"@floating-ui/dom": "^1.7.4",
"@ibm/telemetry-js": "^1.5.0",
"flatpickr": "4.6.9"
},
Expand Down
83 changes: 83 additions & 0 deletions src/Portal/FloatingPortal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<script>
import {
computePosition,
autoUpdate as floatingAutoUpdate,
offset as offsetMiddleware,
} from "@floating-ui/dom";
import { onMount, tick } from "svelte";
import Portal from "./Portal.svelte";

/**
* Reference element to position the portal relative to
* @type {HTMLElement | null}
*/
export let reference = null;

/**
* Placement of the floating portal relative to the reference element
* @type {"top" | "top-start" | "top-end" | "right" | "right-start" | "right-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end"}
*/
export let placement = "bottom";

/**
* Offset in pixels from the reference element
* @type {number}
*/
export let offset = 0;

/**
* Set to `true` to enable auto-update positioning on scroll/resize
* @type {boolean}
*/
export let autoUpdate = true;

/** @type {null | HTMLElement} */
let floatingElement = null;

/** @type {null | ReturnType<typeof floatingAutoUpdate>} */
let cleanup = null;

async function updatePosition() {
if (!reference || !floatingElement) return;

await tick();

computePosition(reference, floatingElement, {
placement,
middleware: [offsetMiddleware(offset)],
}).then(({ x, y }) => {
if (!floatingElement) return;
Object.assign(floatingElement.style, {
position: "absolute",
left: `${x}px`,
top: `${y}px`,
});
});
}

$: if (reference && floatingElement) {
updatePosition();

// Re-setup auto-update if enabled
if (autoUpdate) {
cleanup?.();
cleanup = floatingAutoUpdate(reference, floatingElement, updatePosition);
}
}

onMount(() => {
if (autoUpdate && reference && floatingElement) {
cleanup = floatingAutoUpdate(reference, floatingElement, updatePosition);
}

return () => {
cleanup?.();
};
});
</script>

<Portal>
<div bind:this={floatingElement}>
<slot />
</div>
</Portal>
1 change: 1 addition & 0 deletions src/Portal/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as FloatingPortal } from "./FloatingPortal.svelte";
export { default as Portal } from "./Portal.svelte";
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export { default as Pagination } from "./Pagination/Pagination.svelte";
export { default as PaginationSkeleton } from "./Pagination/PaginationSkeleton.svelte";
export { default as PaginationNav } from "./PaginationNav/PaginationNav.svelte";
export { default as Popover } from "./Popover/Popover.svelte";
export { default as FloatingPortal } from "./Portal/FloatingPortal.svelte";
export { default as Portal } from "./Portal/Portal.svelte";
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte";
export { default as ProgressIndicator } from "./ProgressIndicator/ProgressIndicator.svelte";
Expand Down
31 changes: 31 additions & 0 deletions tests/Portal/FloatingPortal.test.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
import { FloatingPortal } from "carbon-components-svelte";

export let showPortal = true;
export let reference: HTMLElement | null = null;
export let placement:
| "top"
| "top-start"
| "top-end"
| "right"
| "right-start"
| "right-end"
| "bottom"
| "bottom-start"
| "bottom-end"
| "left"
| "left-start"
| "left-end" = "bottom";
export let offset = 0;
export let autoUpdate = true;
export let portalContent = "Floating portal content";
</script>

<button bind:this={reference} type="button">Reference button</button>

{#if showPortal && reference}
<FloatingPortal {reference} {placement} {offset} {autoUpdate}>
{portalContent}
</FloatingPortal>
{/if}

Loading