feat(portal): support portal

This commit is contained in:
Eric Liu 2025-04-13 13:48:44 -07:00
commit 1ddd9ba1a5
14 changed files with 240 additions and 2 deletions

View file

@ -1,6 +1,6 @@
# Component Index # Component Index
> 165 components exported from carbon-components-svelte@0.89.2. > 167 components exported from carbon-components-svelte@0.89.2.
## Components ## Components
@ -45,6 +45,7 @@
- [`FileUploaderItem`](#fileuploaderitem) - [`FileUploaderItem`](#fileuploaderitem)
- [`FileUploaderSkeleton`](#fileuploaderskeleton) - [`FileUploaderSkeleton`](#fileuploaderskeleton)
- [`Filename`](#filename) - [`Filename`](#filename)
- [`FloatingPortal`](#floatingportal)
- [`FluidForm`](#fluidform) - [`FluidForm`](#fluidform)
- [`Form`](#form) - [`Form`](#form)
- [`FormGroup`](#formgroup) - [`FormGroup`](#formgroup)
@ -95,6 +96,7 @@
- [`PaginationSkeleton`](#paginationskeleton) - [`PaginationSkeleton`](#paginationskeleton)
- [`PasswordInput`](#passwordinput) - [`PasswordInput`](#passwordinput)
- [`Popover`](#popover) - [`Popover`](#popover)
- [`Portal`](#portal)
- [`ProgressBar`](#progressbar) - [`ProgressBar`](#progressbar)
- [`ProgressIndicator`](#progressindicator) - [`ProgressIndicator`](#progressindicator)
- [`ProgressIndicatorSkeleton`](#progressindicatorskeleton) - [`ProgressIndicatorSkeleton`](#progressindicatorskeleton)
@ -1435,6 +1437,20 @@ None.
| click | forwarded | -- | | click | forwarded | -- |
| keydown | forwarded | -- | | keydown | forwarded | -- |
## `FloatingPortal`
### Props
None.
### Slots
None.
### Events
None.
## `FluidForm` ## `FluidForm`
### Props ### Props
@ -2835,6 +2851,22 @@ None.
| :------------ | :--------- | :------------------------------------ | | :------------ | :--------- | :------------------------------------ |
| click:outside | dispatched | <code>{ target: HTMLElement; }</code> | | click:outside | dispatched | <code>{ target: HTMLElement; }</code> |
## `Portal`
### Props
None.
### Slots
| Slot name | Default | Props | Fallback |
| :-------- | :------ | :---- | :------- |
| -- | Yes | -- | -- |
### Events
None.
## `ProgressBar` ## `ProgressBar`
### Props ### Props

View file

@ -30,6 +30,7 @@
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@floating-ui/dom": "^1.6.13",
"@ibm/telemetry-js": "^1.5.0", "@ibm/telemetry-js": "^1.5.0",
"flatpickr": "4.6.9" "flatpickr": "4.6.9"
}, },

View file

@ -1,5 +1,5 @@
{ {
"total": 165, "total": 167,
"components": [ "components": [
{ {
"moduleName": "Accordion", "moduleName": "Accordion",
@ -5241,6 +5241,16 @@
"name": "div | button | svg" "name": "div | button | svg"
} }
}, },
{
"moduleName": "FloatingPortal",
"filePath": "src/Portal/FloatingPortal.svelte",
"props": [],
"moduleExports": [],
"slots": [],
"events": [],
"typedefs": [],
"generics": null
},
{ {
"moduleName": "FluidForm", "moduleName": "FluidForm",
"filePath": "src/FluidForm/FluidForm.svelte", "filePath": "src/FluidForm/FluidForm.svelte",
@ -10851,6 +10861,22 @@
"name": "div" "name": "div"
} }
}, },
{
"moduleName": "Portal",
"filePath": "src/Portal/Portal.svelte",
"props": [],
"moduleExports": [],
"slots": [
{
"name": "__default__",
"default": true,
"slot_props": "{}"
}
],
"events": [],
"typedefs": [],
"generics": null
},
{ {
"moduleName": "ProgressBar", "moduleName": "ProgressBar",
"filePath": "src/ProgressBar/ProgressBar.svelte", "filePath": "src/ProgressBar/ProgressBar.svelte",

View file

@ -0,0 +1,8 @@
<script>
import { Portal } from "carbon-components-svelte";
import Preview from "../../components/Preview.svelte";
</script>
## Default
<FileSource src="/framed/Portal/BasicPortal" />

View file

@ -0,0 +1,6 @@
<script>
import { Portal } from "carbon-components-svelte";
</script>
<Portal>Hello world</Portal>
<Portal>Another portal</Portal>

26
package-lock.json generated
View file

@ -10,6 +10,7 @@
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@floating-ui/dom": "^1.6.13",
"@ibm/telemetry-js": "^1.5.0", "@ibm/telemetry-js": "^1.5.0",
"flatpickr": "4.6.9" "flatpickr": "4.6.9"
}, },
@ -623,6 +624,31 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@floating-ui/core": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@hutson/parse-repository-url": { "node_modules/@hutson/parse-repository-url": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz", "resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz",

View file

@ -41,6 +41,7 @@
"release": "standard-version && npm run build:docs" "release": "standard-version && npm run build:docs"
}, },
"dependencies": { "dependencies": {
"@floating-ui/dom": "^1.6.13",
"@ibm/telemetry-js": "^1.5.0", "@ibm/telemetry-js": "^1.5.0",
"flatpickr": "4.6.9" "flatpickr": "4.6.9"
}, },

View file

@ -0,0 +1,62 @@
<script>
import { onMount } from "svelte";
import { computePosition, autoUpdate } from "@floating-ui/dom";
import Portal from "./Portal.svelte";
/** @type {null | HTMLButtonElement} */
let button = null;
/** @type {null | HTMLDivElement} */
let tooltip = null;
let togglePortal = false;
let padding = 10;
/** @type {null | ReturnType<typeof autoUpdate>} */
let cleanup = null;
function updatePosition() {
if (!button || !tooltip) return;
computePosition(button, tooltip).then(({ x, y }) => {
if (!tooltip) return;
Object.assign(tooltip.style, {
left: `${x}px`,
top: `${y}px`,
});
});
}
onMount(() => {
updatePosition();
if (button && tooltip) {
cleanup = autoUpdate(button, tooltip, updatePosition);
}
return () => {
return cleanup?.();
};
});
</script>
<button
type="button"
style="padding: {padding}px"
bind:this={button}
on:click={() => (padding += 2)}
>
Click to increase padding
</button>
<button
on:click={() => {
togglePortal = !togglePortal;
}}
>
Toggle portal
</button>
{#if togglePortal}
<Portal>
<div bind:this={tooltip}>Floating menu</div>
</Portal>
{/if}

53
src/Portal/Portal.svelte Normal file
View file

@ -0,0 +1,53 @@
<script context="module">
/** @type {HTMLDivElement | null} */
let portalContainer = null;
let instances = 0;
/**
* Creates or returns the shared portal container
* @returns {HTMLDivElement}
*/
function getPortalContainer() {
if (!portalContainer && typeof document !== "undefined") {
portalContainer = document.createElement("div");
portalContainer.setAttribute("data-portal", "");
document.body.appendChild(portalContainer);
}
return portalContainer;
}
</script>
<script>
import { onMount } from "svelte";
/** @type {null | HTMLDivElement} */
let portal = null;
onMount(() => {
instances++;
const container = getPortalContainer();
if (portal && container) {
container.appendChild(portal);
}
return () => {
instances--;
if (portal?.parentNode) {
portal.parentNode.removeChild(portal);
}
if (instances === 0 && portalContainer?.parentNode) {
portalContainer.parentNode.removeChild(portalContainer);
portalContainer = null;
}
};
});
</script>
<div bind:this={portal}>
<slot />
</div>

2
src/Portal/index.js Normal file
View file

@ -0,0 +1,2 @@
export { default as Portal } from "./Portal.svelte";
export { default as FloatingPortal } from "./FloatingPortal.svelte";

View file

@ -91,6 +91,7 @@ export {
ProgressIndicatorSkeleton, ProgressIndicatorSkeleton,
ProgressStep, ProgressStep,
} from "./ProgressIndicator"; } from "./ProgressIndicator";
export { Portal, FloatingPortal } from "./Portal";
export { RadioButton, RadioButtonSkeleton } from "./RadioButton"; export { RadioButton, RadioButtonSkeleton } from "./RadioButton";
export { RadioButtonGroup } from "./RadioButtonGroup"; export { RadioButtonGroup } from "./RadioButtonGroup";
export { RecursiveList } from "./RecursiveList"; export { RecursiveList } from "./RecursiveList";

View file

@ -0,0 +1,9 @@
import type { SvelteComponentTyped } from "svelte";
export type FloatingPortalProps = {};
export default class FloatingPortal extends SvelteComponentTyped<
FloatingPortalProps,
Record<string, any>,
{}
> {}

9
types/Portal/Portal.svelte.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
import type { SvelteComponentTyped } from "svelte";
export type PortalProps = {};
export default class Portal extends SvelteComponentTyped<
PortalProps,
Record<string, any>,
{ default: {} }
> {}

2
types/index.d.ts vendored
View file

@ -95,6 +95,8 @@ export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte";
export { default as ProgressIndicator } from "./ProgressIndicator/ProgressIndicator.svelte"; export { default as ProgressIndicator } from "./ProgressIndicator/ProgressIndicator.svelte";
export { default as ProgressIndicatorSkeleton } from "./ProgressIndicator/ProgressIndicatorSkeleton.svelte"; export { default as ProgressIndicatorSkeleton } from "./ProgressIndicator/ProgressIndicatorSkeleton.svelte";
export { default as ProgressStep } from "./ProgressIndicator/ProgressStep.svelte"; export { default as ProgressStep } from "./ProgressIndicator/ProgressStep.svelte";
export { default as Portal } from "./Portal/Portal.svelte";
export { default as FloatingPortal } from "./Portal/FloatingPortal.svelte";
export { default as RadioButton } from "./RadioButton/RadioButton.svelte"; export { default as RadioButton } from "./RadioButton/RadioButton.svelte";
export { default as RadioButtonSkeleton } from "./RadioButton/RadioButtonSkeleton.svelte"; export { default as RadioButtonSkeleton } from "./RadioButton/RadioButtonSkeleton.svelte";
export { default as RadioButtonGroup } from "./RadioButtonGroup/RadioButtonGroup.svelte"; export { default as RadioButtonGroup } from "./RadioButtonGroup/RadioButtonGroup.svelte";