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
8 changes: 8 additions & 0 deletions .changeset/dry-coats-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@skeletonlabs/skeleton-common": minor
"@skeletonlabs/skeleton-svelte": minor
"@skeletonlabs/skeleton-react": minor
---

feat: tabs

10 changes: 10 additions & 0 deletions packages/skeleton-common/src/classes/tabs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineSkeletonClasses } from '../internal/define-skeleton-classes' with { type: 'macro' };

export const classesTabs = defineSkeletonClasses({
root: 'w-full flex data-[orientation=horizontal]:flex-col data-[orientation=vertical]:flex-row',
list: 'relative data-[orientation=horizontal]:mb-4 data-[orientation=horizontal]:pb-2 data-[orientation=vertical]:pe-2 data-[orientation=vertical]:me-4 flex data-[orientation=horizontal]:flex-row data-[orientation=vertical]:flex-col gap-2 data-[orientation=horizontal]:border-b data-[orientation=vertical]:border-e border-surface-200-800',
trigger: 'btn hover:preset-tonal-primary data-disabled:opacity-50 data-disabled:pointer-events-none',
indicator:
'bg-surface-950-50 data-[orientation=horizontal]:w-(--width) data-[orientation=horizontal]:h-0.5 data-[orientation=horizontal]:bottom-0 data-[orientation=vertical]:w-0.5 data-[orientation=vertical]:h-(--height) data-[orientation=vertical]:end-0',
content: ''
});
1 change: 1 addition & 0 deletions packages/skeleton-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './themes.js';
export * from './classes/accordion.js';
export * from './classes/avatar.js';
export * from './classes/rating-group.js';
export * from './classes/tabs.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { classesTabs } from '@skeletonlabs/skeleton-common';
import { splitContentProps, type ContentProps } from '@zag-js/tabs';
import { mergeProps } from '@zag-js/react';
import { useContext, type ComponentProps } from 'react';
import { TabsRootContext } from '../modules/tabs-root-context.js';
import type { PropsWithElement } from '@/internal/props-with-element.js';

export interface TabsContentProps extends PropsWithElement, ContentProps, ComponentProps<'div'> {}

export default function (props: TabsContentProps) {
const rootContext = useContext(TabsRootContext);
const [itemProps, componentProps] = splitContentProps(props);
const { element, children, ...restAttributes } = componentProps;
const attributes = mergeProps(
rootContext.api.getContentProps(itemProps),
{
className: classesTabs.content
},
restAttributes
);
return element ? element({ attributes }) : <div {...attributes}>{children}</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { classesTabs } from '@skeletonlabs/skeleton-common';
import { mergeProps } from '@zag-js/react';
import { useContext, type ComponentProps } from 'react';
import { TabsRootContext } from '../modules/tabs-root-context.js';
import type { PropsWithElement } from '@/internal/props-with-element.js';

export interface TabsIndicatorProps extends PropsWithElement, Omit<ComponentProps<'div'>, 'children'> {}

export default function (props: TabsIndicatorProps) {
const rootContext = useContext(TabsRootContext);
const { element, ...restAttributes } = props;
const attributes = mergeProps(
rootContext.api.getIndicatorProps(),
{
className: classesTabs.indicator
},
restAttributes
);
return element ? element({ attributes }) : <div {...attributes}></div>;
}
20 changes: 20 additions & 0 deletions packages/skeleton-react/src/components/tabs/anatomy/tabs-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { classesTabs } from '@skeletonlabs/skeleton-common';
import { mergeProps } from '@zag-js/react';
import { useContext, type ComponentProps } from 'react';
import { TabsRootContext } from '../modules/tabs-root-context.js';
import type { PropsWithElement } from '@/internal/props-with-element.js';

export interface TabsListProps extends PropsWithElement, Omit<ComponentProps<'div'>, 'id' | 'defaultValue' | 'dir'> {}

export default function (props: TabsListProps) {
const rootContext = useContext(TabsRootContext);
const { element, children, ...restAttributes } = props;
const attributes = mergeProps(
rootContext.api.getListProps(),
{
className: classesTabs.list
},
restAttributes
);
return element ? element({ attributes }) : <div {...attributes}>{children}</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useContext, type ReactNode } from 'react';
import { type TabsRootContextType, TabsRootContext } from '../modules/tabs-root-context.js';

export interface TabsRootContextProps {
children: (context: TabsRootContextType) => ReactNode;
}

export default function (props: TabsRootContextProps) {
const rootContext = useContext(TabsRootContext);
return props.children(rootContext);
}
30 changes: 30 additions & 0 deletions packages/skeleton-react/src/components/tabs/anatomy/tabs-root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { splitProps, machine, connect, type Props } from '@zag-js/tabs';
import { mergeProps, normalizeProps, useMachine } from '@zag-js/react';
import { useId, type ComponentProps } from 'react';
import { TabsRootContext } from '../modules/tabs-root-context.js';
import type { PropsWithElement } from '@/internal/props-with-element';
import { classesTabs } from '@skeletonlabs/skeleton-common';

export interface TabsRootProps extends PropsWithElement, Omit<Props, 'id'>, Omit<ComponentProps<'div'>, 'id' | 'defaultValue' | 'dir'> {}

export default function (props: TabsRootProps) {
const [machineProps, componentProps] = splitProps(props);
const { element, children, ...restAttributes } = componentProps;
const service = useMachine(machine, {
id: useId(),
...machineProps
});
const api = connect(service, normalizeProps);
const attributes = mergeProps(
api.getRootProps(),
{
className: classesTabs.root
},
restAttributes
);
return (
<TabsRootContext.Provider value={{ api }}>
{element ? element({ attributes }) : <div {...attributes}>{children}</div>}
</TabsRootContext.Provider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { classesTabs } from '@skeletonlabs/skeleton-common';
import { splitTriggerProps, type TriggerProps } from '@zag-js/tabs';
import { mergeProps } from '@zag-js/react';
import { useContext, type ComponentProps } from 'react';
import { TabsRootContext } from '../modules/tabs-root-context.js';
import type { PropsWithElement } from '@/internal/props-with-element.js';

export interface TabsTriggerProps extends PropsWithElement, TriggerProps, Omit<ComponentProps<'button'>, 'value'> {}

export default function (props: TabsTriggerProps) {
const rootContext = useContext(TabsRootContext);
const [itemProps, componentProps] = splitTriggerProps(props);
const { element, children, ...restAttributes } = componentProps;
const attributes = mergeProps(
rootContext.api.getTriggerProps(itemProps),
{
className: classesTabs.trigger
},
restAttributes
);
return element ? element({ attributes }) : <button {...attributes}>{children}</button>;
}
8 changes: 8 additions & 0 deletions packages/skeleton-react/src/components/tabs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export { Tabs } from './modules/tabs-anatomy.js';
export type { TabsRootProps } from './anatomy/tabs-root.js';
export type { TabsRootContextProps } from './anatomy/tabs-root-context.js';
export type { TabsListProps } from './anatomy/tabs-list.js';
export type { TabsTriggerProps } from './anatomy/tabs-trigger.js';
export type { TabsIndicatorProps } from './anatomy/tabs-indicator.js';
export type { TabsContentProps } from './anatomy/tabs-content.js';
export type { TabsRootContextType as TabsRootContext } from './modules/tabs-root-context.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import TabsRoot from '../anatomy/tabs-root.jsx';
import TabsRootContext from '../anatomy/tabs-root-context.jsx';
import TabsList from '../anatomy/tabs-list.jsx';
import TabsTrigger from '../anatomy/tabs-trigger.jsx';
import tabsIndicator from '../anatomy/tabs-indicator.js';
import TabsContent from '../anatomy/tabs-content.jsx';

export const Tabs = Object.assign(TabsRoot, {
Context: TabsRootContext,
List: TabsList,
Trigger: TabsTrigger,
Indicator: tabsIndicator,
Content: TabsContent
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createContext } from 'react';
import type { Api } from '@zag-js/tabs';

export interface TabsRootContextType {
api: Api;
}

export const TabsRootContext = createContext<TabsRootContextType>(null!);
1 change: 1 addition & 0 deletions packages/skeleton-react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './components/accordion/index.js';
export * from './components/avatar/index.js';
export * from './components/rating-group/index.js';
export * from './components/tabs/index.js';
40 changes: 40 additions & 0 deletions packages/skeleton-react/test/components/tabs/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import Tabs from './tabs.js';

describe('tabs', () => {
describe('root', () => {
it('renders', () => {
render(<Tabs />);
expect(screen.getByTestId('root')).toBeInTheDocument();
});
});

describe('list', () => {
it('renders', () => {
render(<Tabs />);
expect(screen.getByTestId('list')).toBeInTheDocument();
});
});

describe('trigger', () => {
it('renders', () => {
render(<Tabs />);
expect(screen.getByTestId('trigger')).toBeInTheDocument();
});
});

describe('indicator', () => {
it('renders', () => {
render(<Tabs />);
expect(screen.getByTestId('indicator')).toBeInTheDocument();
});
});

describe('content', () => {
it('renders', () => {
render(<Tabs />);
expect(screen.getByTestId('content')).toBeInTheDocument();
});
});
});
13 changes: 13 additions & 0 deletions packages/skeleton-react/test/components/tabs/tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Tabs } from '@skeletonlabs/skeleton-react';

export default function () {
return (
<Tabs defaultValue="tab-1" data-testid="root">
<Tabs.List data-testid="list">
<Tabs.Trigger value="tab" data-testid="trigger" />
<Tabs.Indicator data-testid="indicator" />
</Tabs.List>
<Tabs.Content value="tab" data-testid="content" />
</Tabs>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script lang="ts" module>
import type { ContentProps } from '@zag-js/tabs';
import type { HTMLAttributes } from 'svelte/elements';
import type { PropsWithElement } from '@/internal/props-with-element.js';

export interface TabsContentProps extends ContentProps, PropsWithElement, HTMLAttributes<HTMLDivElement> {}
</script>

<script lang="ts">
import { TabsRootContext } from '../modules/tabs-root-context.js';
import { mergeProps } from '@zag-js/svelte';
import { classesTabs } from '@skeletonlabs/skeleton-common';
import { splitContentProps } from '@zag-js/tabs';

const props: TabsContentProps = $props();
const rootContext = TabsRootContext.consume();
const [contentProps, componentProps] = $derived(splitContentProps(props));
const { element, children, ...restAttributes } = $derived(componentProps);
const attributes = $derived(
mergeProps(
rootContext.api.getContentProps(contentProps),
{
class: classesTabs.content
},
restAttributes
)
);
</script>

{#if element}
{@render element({ attributes })}
{:else}
<div {...attributes}>
{@render children?.()}
</div>
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts" module>
import type { HTMLAttributes } from 'svelte/elements';
import type { PropsWithElement } from '@/internal/props-with-element.js';

export interface TabsIndicatorProps extends PropsWithElement, Omit<HTMLAttributes<HTMLDivElement>, 'children'> {}
</script>

<script lang="ts">
import { TabsRootContext } from '../modules/tabs-root-context.js';
import { mergeProps } from '@zag-js/svelte';
import { classesTabs } from '@skeletonlabs/skeleton-common';

const props: TabsIndicatorProps = $props();
const rootContext = TabsRootContext.consume();
const { element, ...restAttributes } = $derived(props);
const attributes = $derived(
mergeProps(
rootContext.api.getIndicatorProps(),
{
class: classesTabs.indicator
},
restAttributes
)
);
</script>

{#if element}
{@render element({ attributes })}
{:else}
<div {...attributes}></div>
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script lang="ts" module>
import type { HTMLAttributes } from 'svelte/elements';
import type { PropsWithElement } from '@/internal/props-with-element.js';

export interface TabsListProps extends PropsWithElement, HTMLAttributes<HTMLDivElement> {}
</script>

<script lang="ts">
import { TabsRootContext } from '../modules/tabs-root-context';
import { mergeProps } from '@zag-js/svelte';
import { classesTabs } from '@skeletonlabs/skeleton-common';

const props: TabsListProps = $props();
const rootContext = TabsRootContext.consume();
const { element, children, ...restAttributes } = $derived(props);
const attributes = $derived(
mergeProps(
rootContext.api.getListProps(),
{
class: classesTabs.list
},
restAttributes
)
);
</script>

{#if element}
{@render element({ attributes })}
{:else}
<div {...attributes}>
{@render children?.()}
</div>
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script lang="ts" module>
import type { Snippet } from 'svelte';
import type { TabsRootContextType } from '../modules/tabs-root-context.js';

export interface TabsRootContextProps {
children: Snippet<[TabsRootContextType]>;
}
</script>

<script lang="ts">
import { TabsRootContext } from '../modules/tabs-root-context.js';

const props: TabsRootContextProps = $props();
const rootContext = TabsRootContext.consume();
</script>

{@render props.children(rootContext)}
Loading