Skip to content

Commit

Permalink
feat(svelte): frame for svelte (#3171)
Browse files Browse the repository at this point in the history
* feat: svelte portal

* feat: Frame for svelte
  • Loading branch information
domuk-k authored Dec 25, 2024
1 parent 51c3873 commit 66804ad
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 0 deletions.
17 changes: 17 additions & 0 deletions packages/svelte/src/lib/components/frame/examples/basic.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script lang="ts">
import { Frame } from '@ark-ui/svelte/frame'
</script>

<Frame title="Custom Frame" style="border: 1px solid #ccc; width: 100%; height: var(--height)">
{#snippet head()}
<style>
body {
color: #f96743;
}
</style>
{/snippet}
<div style="padding: 40px">
<h1>Hello from inside the frame!</h1>
<p>This content is rendered within our custom frame component using a Portal.</p>
</div>
</Frame>
23 changes: 23 additions & 0 deletions packages/svelte/src/lib/components/frame/examples/script.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script lang="ts">
import { Frame } from '@ark-ui/svelte/frame'
let frameRef: HTMLIFrameElement | undefined
</script>

<Frame
title="Custom Frame"
bind:ref={frameRef}
onMount={() => {
const doc = frameRef?.contentDocument
if (!doc) return
const script = doc.createElement('script')
script.innerHTML = 'console.log("Hello from inside the frame!")'
doc.body.appendChild(script)
}}
style="border: 1px solid #ccc; width: 100%; height: var(--height)"
>
<div style="padding: 40px">
<h1>Hello from inside the frame!</h1>
<p>This content is rendered within our custom frame component using a Portal.</p>
</div>
</Frame>
20 changes: 20 additions & 0 deletions packages/svelte/src/lib/components/frame/examples/src-doc.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import { Frame } from '@ark-ui/svelte/frame'
const srcDoc = `<html>
<head>
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<base target=_blank>
</head>
<body style='overflow: hidden'>
<div></div>
</body>
</html>`
</script>

<Frame title="Custom Frame" style="border: 1px solid #ccc; width: 100%;" srcdoc={srcDoc}>
<h1 style="font-family: Open Sans, sans-serif;">Hello from inside the frame!</h1>
<p>This content is rendered within our custom frame component using a Portal.</p>
<p>The frame has custom initial content, including Font Awesome and Open Sans font.</p>
</Frame>
19 changes: 19 additions & 0 deletions packages/svelte/src/lib/components/frame/frame-content.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script lang="ts">
import type { Snippet } from 'svelte'
interface FrameContentProps {
onMount?(): void
onUnmount?(): void
children: Snippet
}
const { onMount, onUnmount, children }: FrameContentProps = $props()
$effect(() => {
onMount?.()
return onUnmount
})
</script>

{@render children()}
28 changes: 28 additions & 0 deletions packages/svelte/src/lib/components/frame/frame.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Meta } from '@storybook/svelte'
import BasicExample from './examples/basic.svelte'
import ScriptExample from './examples/script.svelte'
import SrcDocExample from './examples/src-doc.svelte'

const meta = {
title: 'Components / Frame',
} as Meta

export default meta

export const Basic = {
render: () => ({
Component: BasicExample,
}),
}

export const Script = {
render: () => ({
Component: ScriptExample,
}),
}

export const SrcDoc = {
render: () => ({
Component: SrcDocExample,
}),
}
101 changes: 101 additions & 0 deletions packages/svelte/src/lib/components/frame/frame.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<script module lang="ts">
import type { Snippet } from 'svelte'
import type { HTMLIframeAttributes } from 'svelte/elements'
import type { Assign } from '../../types'
export interface FrameBaseProps {
/** Additional content to be inserted into the frame's <head> */
head?: Snippet
/** Callback function to be executed when the frame is mounted */
onMount?: () => void
/** Callback function to be executed when the frame is unmounted */
onUnmount?: () => void
srcdoc?: string
ref?: HTMLIFrameElement
}
export interface FrameProps extends Assign<HTMLIframeAttributes, FrameBaseProps> {}
const CUSTOM_ROOT_CLASS = 'frame-root'
const resetStyle =
'<style>*,*::before,*::after { margin: 0; padding: 0; box-sizing: border-box; }</style>'
const initialSrcDoc = `<html><head>${resetStyle}</head><body><div class="${CUSTOM_ROOT_CLASS}"></div></body></html>`
function getMountNode(frame: HTMLIFrameElement) {
const doc = frame.contentWindow?.document
if (!doc) return null
return doc.body.querySelector<HTMLElement>(`.${CUSTOM_ROOT_CLASS}`) || doc.body
}
</script>

<script lang="ts">
import { EnvironmentProvider } from '@ark-ui/svelte/environment'
import Portal from '$lib/components/portal/portal.svelte'
import FrameContent from './frame-content.svelte'
let { head, onMount, onUnmount, srcdoc, ref = $bindable(), ...localProps }: FrameProps = $props()
let frameRef: HTMLIFrameElement | undefined = $state()
let mountNode: HTMLElement | null = $derived(frameRef ? getMountNode(frameRef) : null)
$effect(() => {
if (!frameRef) return
const doc = frameRef.contentWindow?.document
if (!doc) return
doc.open()
doc.write(srcdoc ?? initialSrcDoc)
doc.close()
})
$effect(() => {
if (!frameRef || !frameRef.contentDocument) return
const win = frameRef.contentWindow as Window & typeof globalThis
if (!win || !mountNode) return
const exec = () => {
if (!(mountNode && frameRef && frameRef.contentDocument)) return
const rootEl = frameRef.contentDocument?.documentElement
if (!rootEl) return
frameRef.style.setProperty('--width', `${mountNode.scrollWidth}px`)
frameRef.style.setProperty('--height', `${mountNode.scrollHeight}px`)
}
const resizeObserver = new win.ResizeObserver(exec)
exec()
if (frameRef.contentDocument) {
resizeObserver.observe(mountNode)
}
return () => {
resizeObserver.disconnect()
}
})
</script>

<iframe bind:this={frameRef} bind:this={ref} {...localProps}>
<EnvironmentProvider value={() => frameRef?.contentDocument ?? document}>
{#if mountNode}
<Portal container={mountNode}>
<FrameContent {onMount} {onUnmount}>
{#if localProps?.children}
{@render localProps.children()}
{/if}
</FrameContent>
</Portal>
{/if}
{#if mountNode && head && frameRef?.contentDocument?.head}
<Portal container={frameRef.contentDocument.head}>
{@render head()}
</Portal>
{/if}
</EnvironmentProvider>
</iframe>
2 changes: 2 additions & 0 deletions packages/svelte/src/lib/components/frame/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as Frame } from './frame.svelte'
export type { FrameBaseProps, FrameProps } from './frame.svelte'
2 changes: 2 additions & 0 deletions packages/svelte/src/lib/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export * from './avatar'
export * from './factory'
export * from './frame'
export * from './highlight'
export * from './portal'
export * from './presence'
export * from './qr-code'
export * from './slider'
Expand Down
2 changes: 2 additions & 0 deletions packages/svelte/src/lib/components/portal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as Portal } from './portal.svelte'
export type { PortalProps } from './portal.svelte'
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script lang="ts">
import type { Snippet } from 'svelte'
const { children }: { children: Snippet } = $props()
</script>

{#if children}
{@render children()}
{/if}
25 changes: 25 additions & 0 deletions packages/svelte/src/lib/components/portal/portal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script module lang="ts">
import type { Snippet } from 'svelte'
export interface PortalProps {
container?: HTMLElement
children: Snippet
}
</script>

<script lang="ts">
/**
* @see https://github.com/sveltejs/svelte/issues/7082
*/
import { getAllContexts, mount, unmount } from 'svelte'
import PortalConsumer from './portal-consumer.svelte'
const { container = document.body, children }: PortalProps = $props()
const context = getAllContexts()
$effect(() => {
mount(PortalConsumer, { target: container, props: { children }, context })
return () => unmount(PortalConsumer)
})
</script>

0 comments on commit 66804ad

Please sign in to comment.