Skip to content
Merged
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
21 changes: 20 additions & 1 deletion COMPONENT_INDEX.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Component Index

> 168 components exported from carbon-components-svelte@0.94.0.
> 169 components exported from carbon-components-svelte@0.94.0.

## Components

Expand Down Expand Up @@ -96,6 +96,7 @@
- [`PaginationSkeleton`](#paginationskeleton)
- [`PasswordInput`](#passwordinput)
- [`Popover`](#popover)
- [`Portal`](#portal)
- [`ProgressBar`](#progressbar)
- [`ProgressIndicator`](#progressindicator)
- [`ProgressIndicatorSkeleton`](#progressindicatorskeleton)
Expand Down Expand Up @@ -2890,6 +2891,24 @@ None.
| :------------ | :--------- | :------------------------------------ | :---------- |
| click:outside | dispatched | <code>{ target: HTMLElement; }</code> | -- |

## `Portal`

### Props

| Prop name | Required | Kind | Reactive | Type | Default value | Description |
| :-------- | :------- | :--------------- | :------- | ---------------------------------------- | ------------------ | --------------------- |
| tag | No | <code>let</code> | No | <code>keyof HTMLElementTagNameMap</code> | <code>"div"</code> | Specify the tag name. |

### Slots

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

### Events

None.

## `ProgressBar`

### Props
Expand Down
36 changes: 35 additions & 1 deletion docs/src/COMPONENT_API.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"total": 168,
"total": 169,
"components": [
{
"moduleName": "Accordion",
Expand Down Expand Up @@ -11650,6 +11650,40 @@
},
"contexts": []
},
{
"moduleName": "Portal",
"filePath": "src/Portal/Portal.svelte",
"props": [
{
"name": "tag",
"kind": "let",
"description": "Specify the tag name.",
"type": "keyof HTMLElementTagNameMap",
"value": "\"div\"",
"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,
"rest_props": {
"type": "Element",
"name": "svelte:element"
},
"contexts": []
},
{
"moduleName": "ProgressBar",
"filePath": "src/ProgressBar/ProgressBar.svelte",
Expand Down
6 changes: 6 additions & 0 deletions docs/src/pages/components/ComposedModal.svx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ Create a modal with a header, body, and footer. Each section can be customized i

<FileSource src="/framed/Modal/ComposedModal" />

## With Portal

Wrap `ComposedModal` in a `Portal` to ensure it renders above all z-index stacking contexts and parent overflow constraints, preventing visual clipping and layering issues.

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

## Prevent default close behavior

The modal dispatches a cancelable `close` event, allowing you to prevent the modal from closing using `e.preventDefault()`. The event includes a `trigger` property indicating what triggered the close attempt: `"escape-key"`, `"outside-click"`, or `"close-button"`.
Expand Down
6 changes: 6 additions & 0 deletions docs/src/pages/components/Modal.svx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ Create a basic modal dialog with primary and secondary actions. This variant is

<FileSource src="/framed/Modal/Modal" />

## With Portal

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" />

## Custom focus

Control which element receives focus when the modal opens. Use `selectorPrimaryFocus` to specify the target element using CSS selectors.
Expand Down
30 changes: 30 additions & 0 deletions docs/src/pages/components/Portal.svx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script>
import { Portal } from "carbon-components-svelte";
import Preview from "../../components/Preview.svelte";
</script>

The `Portal` component renders its content directly into `document.body`, allowing you to escape parent overflow constraints and z-index stacking contexts.

## Default Portal

Render content in a portal. This is useful for modals, tooltips, and menus that need to escape parent containers.

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

## Multiple Portals

Each portal instance is independent and creates its own element in `document.body`. Each portal is automatically cleaned up when it is removed.

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

## Custom Tag

Use the `tag` prop to specify a custom HTML element. By default, Portal uses a `div` element.

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

## Modal with Portal

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" />
9 changes: 9 additions & 0 deletions docs/src/pages/framed/Portal/BasicPortal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script>
import { Portal } from "carbon-components-svelte";
</script>

<div>
<div>This is rendered inside the div</div>
<br />
<Portal>This is rendered outside of the div</Portal>
</div>
39 changes: 39 additions & 0 deletions docs/src/pages/framed/Portal/ComposedModalPortal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script>
import {
Button,
ComposedModal,
ModalBody,
ModalFooter,
ModalHeader,
Portal,
Stack,
} from "carbon-components-svelte";

let open = false;
</script>

<Stack gap={5} style="overflow: hidden; position: relative; height: 200px;">
<p>
This container hides overflowing content. Without a portal, the modal would
be clipped.
</p>
<div>
<Button on:click={() => (open = true)}>Open modal</Button>
</div>
<Portal>
<ComposedModal bind:open>
<ModalHeader title="Composed Modal in Portal" />
<ModalBody>
<p>
This composed modal is rendered in a portal, ensuring it appears above
all z-index stacking contexts and parent overflow constraints.
</p>
</ModalBody>
<ModalFooter
primaryButtonText="Confirm"
secondaryButtonText="Cancel"
on:click:button--secondary={() => (open = false)}
/>
</ComposedModal>
</Portal>
</Stack>
7 changes: 7 additions & 0 deletions docs/src/pages/framed/Portal/CustomTagPortal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
import { Portal } from "carbon-components-svelte";
</script>

<Portal tag="section">
This portal uses a section tag.
</Portal>
29 changes: 29 additions & 0 deletions docs/src/pages/framed/Portal/ModalPortal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script>
import { Button, Modal, Portal, Stack } from "carbon-components-svelte";

let open = false;
</script>

<Stack gap={5} style="overflow: hidden; position: relative; height: 200px;">
<p>
This container hides overflowing content. Without a portal, the modal would
be clipped.
</p>
<div>
<Button on:click={() => (open = true)}>Open modal</Button>
</div>
<Portal>
<Modal
bind:open
modalHeading="Modal in Portal"
primaryButtonText="Confirm"
secondaryButtonText="Cancel"
on:click:button--secondary={() => (open = false)}
>
<p>
This modal is rendered in a portal, escaping the parent container's
overflow constraints and ensuring it appears above all other content.
</p>
</Modal>
</Portal>
</Stack>
27 changes: 27 additions & 0 deletions docs/src/pages/framed/Portal/MultiplePortals.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script>
import { Button, ButtonSet, Portal } from "carbon-components-svelte";

let showPortal1 = false;
let showPortal2 = false;
</script>

<ButtonSet>
<Button on:click={() => (showPortal1 = !showPortal1)}>
{showPortal1 ? "Unmount portal 1" : "Mount portal 1"}
</Button>
<Button on:click={() => (showPortal2 = !showPortal2)}>
{showPortal2 ? "Unmount portal 2" : "Mount portal 2"}
</Button>
</ButtonSet>

{#if showPortal1}
<Portal>
Portal content 1
</Portal>
{/if}

{#if showPortal2}
<Portal>
Portal content 2
</Portal>
{/if}
38 changes: 38 additions & 0 deletions src/Portal/Portal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script>
/**
* Specify the tag name.
* @type {keyof HTMLElementTagNameMap}
*/
export let tag = "div";

import { onMount } from "svelte";

/** @type {null | HTMLElement} */
let portal = null;
let mounted = false;

onMount(() => {
mounted = true;

return () => {
mounted = false;

if (portal?.parentNode) {
portal.parentNode.removeChild(portal);
}
};
});

$: if (mounted && portal) {
if (
typeof document !== "undefined" &&
portal.parentNode !== document.body
) {
document.body.appendChild(portal);
}
}
</script>

<svelte:element this={tag} bind:this={portal} data-portal {...$$restProps}>
<slot />
</svelte:element>
1 change: 1 addition & 0 deletions src/Portal/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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 Portal } from "./Portal/Portal.svelte";
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte";
export { default as ProgressIndicator } from "./ProgressIndicator/ProgressIndicator.svelte";
export { default as ProgressIndicatorSkeleton } from "./ProgressIndicator/ProgressIndicatorSkeleton.svelte";
Expand Down
16 changes: 16 additions & 0 deletions tests/Portal/Portal.multiple.test.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
import { Portal } from "carbon-components-svelte";
</script>

<Portal>
Portal content 1
</Portal>

<Portal>
Portal content 2
</Portal>

<Portal>
Portal content 3
</Portal>

14 changes: 14 additions & 0 deletions tests/Portal/Portal.test.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script lang="ts">
import { Portal } from "carbon-components-svelte";

export let showPortal = true;
export let portalContent = "Portal content";
export let tag: keyof HTMLElementTagNameMap = "div";
</script>

{#if showPortal}
<Portal {tag} {...$$restProps}>
{portalContent}
</Portal>
{/if}

Loading