Skip to content

Commit

Permalink
feat(svelte): add factory (#3088)
Browse files Browse the repository at this point in the history
  • Loading branch information
cschroeter authored Nov 28, 2024
1 parent d0bf99e commit 8665987
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 22 deletions.
4 changes: 4 additions & 0 deletions packages/svelte/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ description: All notable changes will be documented in this file.

## [Unreleased]

## Added

- Added `factory` component for `asChild` prop.

## [0.1.0] - 2024-11-27

### Added
Expand Down
11 changes: 5 additions & 6 deletions packages/svelte/src/lib/components/avatar/avatar-fallback.svelte
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
<script module lang="ts">
import type { HTMLProps } from '$lib/types'
import type { HTMLProps, PolymorphicProps } from '$lib/types'
export type AvatarFallbackBaseProps = {}
export interface AvatarFallbackProps extends AvatarFallbackBaseProps, HTMLProps<'div'> {}
export interface AvatarFallbackBaseProps extends PolymorphicProps<'span'> {}
export interface AvatarFallbackProps extends HTMLProps<'span'>, AvatarFallbackBaseProps {}
</script>

<script lang="ts">
import { useAvatarContext } from './use-avatar-context'
import { mergeProps } from '@zag-js/svelte'
import { Ark } from '../factory'
const props: AvatarFallbackProps = $props()
const avatar = useAvatarContext()
const mergedProps = $derived(mergeProps(avatar().getFallbackProps(), props))
</script>

<div {...mergedProps}>
{@render props.children?.()}
</div>
<Ark as="span" {...mergedProps} />
11 changes: 6 additions & 5 deletions packages/svelte/src/lib/components/avatar/avatar-image.svelte
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
<script module lang="ts">
import type { HTMLProps } from '$lib/types'
import type { HTMLProps, PolymorphicProps } from '$lib/types'
export type AvatarImageBaseProps = {}
export interface AvatarImageProps extends AvatarImageBaseProps, HTMLProps<'img'> {}
export interface AvatarImageBaseProps extends PolymorphicProps<'img'> {}
export interface AvatarImageProps extends HTMLProps<'img'>, AvatarImageBaseProps {}
</script>

<script lang="ts">
import { useAvatarContext } from './use-avatar-context'
import { Ark } from '../factory'
import { mergeProps } from '@zag-js/svelte'
import { useAvatarContext } from './use-avatar-context'
const props: AvatarImageProps = $props()
const avatar = useAvatarContext()
const mergedProps = $derived(mergeProps(avatar().getImageProps(), props))
</script>

<img {...mergedProps} />
<Ark as="img" {...mergedProps} />
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import { createSplitProps } from '$lib/utils/create-split-props'
import { mergeProps } from '@zag-js/svelte'
import { AvatarProvider } from './use-avatar-context'
import { Ark } from '../factory'
const props: AvatarRootProviderProps = $props()
const [{ value: avatar }, localProps] = createSplitProps<RootProviderProps>()(props, ['value'])
Expand All @@ -24,6 +25,4 @@
const mergedProps = $derived(mergeProps(avatar().getRootProps(), localProps))
</script>

<div {...mergedProps}>
{@render props.children?.()}
</div>
<Ark as="div" {...mergedProps} />
11 changes: 5 additions & 6 deletions packages/svelte/src/lib/components/avatar/avatar-root.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
<script module lang="ts">
import type { Assign, HTMLProps } from '$lib/types'
import type { Assign, HTMLProps, PolymorphicProps } from '$lib/types'
import type { UseAvatarProps } from './use-avatar.svelte'
export interface AvatarRootBaseProps extends UseAvatarProps {}
export interface AvatarRootProps extends Assign<HTMLProps<'div'>, UseAvatarProps> {}
export interface AvatarRootBaseProps extends UseAvatarProps, PolymorphicProps<'div'> {}
export interface AvatarRootProps extends Assign<HTMLProps<'div'>, AvatarRootBaseProps> {}
</script>

<script lang="ts">
import { mergeProps } from '@zag-js/svelte'
import { createSplitProps } from '../../utils/create-split-props'
import { AvatarProvider } from './use-avatar-context'
import { useAvatar } from './use-avatar.svelte'
import { Ark } from '../factory'
let props: AvatarRootProps = $props()
const [useAvatarProps, localProps] = createSplitProps<UseAvatarProps>()(props, [
Expand All @@ -25,6 +26,4 @@
AvatarProvider(avatar)
</script>

<div {...mergedProps}>
{@render props.children?.()}
</div>
<Ark as="div" {...mergedProps} />
31 changes: 31 additions & 0 deletions packages/svelte/src/lib/components/factory/examples/basic.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
import Ark from '../factory.svelte'
interface Props {
onClickParent?: () => void
onClickChild?: () => void
}
const { onClickParent, onClickChild }: Props = $props()
</script>

<Ark
as="div"
asChild
id="parent"
data-testid="parent"
data-part="parent"
class="parent"
style="background: red"
onclick={onClickParent}
>
{#snippet render(props)}
<Ark
as="span"
{...props({ id: 'child', class: 'child', style: 'color: blue', onclick: onClickChild })}
data-testid="child"
data-part="child"
>
Ark UI
</Ark>
{/snippet}
</Ark>
22 changes: 22 additions & 0 deletions packages/svelte/src/lib/components/factory/factory.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script lang="ts" generics="T extends keyof SvelteHTMLElements">
import type { HTMLProps, PolymorphicProps, PropsFn } from '$lib/types'
import { mergeProps } from '@zag-js/svelte'
import type { SvelteHTMLElements } from 'svelte/elements'
type Props = HTMLProps<T> &
PolymorphicProps<T> & {
as: T
}
const { asChild = false, children, render, as, ...rest }: Props = $props()
const propsFn: PropsFn<T> = (props) => mergeProps(rest, props ?? {})
</script>

{#if asChild}
{@render render?.(propsFn)}
{:else}
<svelte:element this={as} {...rest}>
{@render children?.()}
</svelte:element>
{/if}
40 changes: 40 additions & 0 deletions packages/svelte/src/lib/components/factory/factory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { render, screen } from '@testing-library/svelte'
import user from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import ComponentUnderTest from './examples/basic.svelte'

describe('Ark Factory', () => {
it('should render only the child', () => {
render(ComponentUnderTest)

expect(() => screen.getByTestId('parent')).toThrow()
expect(screen.getByTestId('child')).toBeVisible()
})

it('should override existing props', () => {
render(ComponentUnderTest)
const child = screen.getByTestId('child')
expect(child.id).toBe('child')
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
expect(child.dataset['part']).toBe('child')
})

it('should merge styles and classes', () => {
render(ComponentUnderTest)
const child = screen.getByTestId('child')
expect(child).toHaveStyle({ background: 'red' })
expect(child).toHaveStyle({ color: 'rgb(0, 0, 255)' })
expect(child).toHaveClass('child parent')
expect(screen.getByText('Ark UI')).toBeVisible()
})

it('should merge events', async () => {
const onClickParent = vi.fn()
const onClickChild = vi.fn()
render(ComponentUnderTest, { onClickParent, onClickChild })
await user.click(screen.getByTestId('child'))

expect(onClickParent).toHaveBeenCalled()
expect(onClickChild).toHaveBeenCalled()
})
})
1 change: 1 addition & 0 deletions packages/svelte/src/lib/components/factory/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Ark } from './factory.svelte'
1 change: 1 addition & 0 deletions packages/svelte/src/lib/components/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './avatar'
export * from './factory'
14 changes: 12 additions & 2 deletions packages/svelte/src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import type { SvelteHTMLElements } from 'svelte/elements'
import type { Snippet } from 'svelte'
import type { HTMLAttributes, SvelteHTMLElements } from 'svelte/elements'

export type Assign<T, U> = Omit<T, keyof U> & U
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>

export type CollectionItem = string | object

export type HTMLProps<T extends keyof SvelteHTMLElements> = SvelteHTMLElements[T]
export type Accessor<T> = () => T

export type HTMLTag = keyof SvelteHTMLElements
export type PropsFn<T extends HTMLTag> = (props?: HTMLProps<T>) => HTMLAttributes<HTMLElement>

export type HTMLProps<T extends HTMLTag> = SvelteHTMLElements[T]
export type PolymorphicProps<T extends HTMLTag> = {
asChild?: boolean
children?: Snippet
render?: Snippet<[PropsFn<T>]>
}

0 comments on commit 8665987

Please sign in to comment.