// // Copyright 2026 DXOS.org // import { type Meta, type StoryObj } from '@storybook/react-vite'; import React, { type PropsWithChildren, useState } from 'react'; import { mx } from '@dxos/ui-theme'; import { withLayout, withTheme } from '../../testing'; import { type ThemedClassName } from '../../util'; import { Focus } from './Focus'; type Item = { id: string; label: string }; const ITEMS: Item[] = [ { id: '1', label: 'Item 1' }, { id: '2', label: 'Item 2' }, { id: '3', label: 'Item 3' }, { id: '4', label: 'Item 4' }, { id: '5', label: 'Item 5' }, ]; const itemClassName = 'flex items-center gap-3 px-3 py-2 aria-current:bg-neutral-75 dark:aria-current:bg-neutral-800'; const Container = ({ classNames, children }: ThemedClassName) => { return (
{children}
); }; const Text = ({ children }: PropsWithChildren) => { return

{children}

; }; // // Default (vertical list) // const DefaultStory = () => { const [current, setCurrent] = useState('1'); return ( Tab into the group, then use arrow keys to navigate. Press Enter to select. {ITEMS.map((item) => ( setCurrent(item.id)} className={itemClassName} > {item.label} ))} Selected: {current ?? 'none'} ); }; // // Horizontal group // const HorizontalStory = () => { const [current, setCurrent] = useState(); return ( Horizontal arrow-key navigation between cards. {ITEMS.map((item) => ( setCurrent(item.id)} className='flex flex-col items-center justify-center w-20 h-20 border border-separator aria-current:border-primary-500 aria-current:bg-primary-50 dark:aria-current:bg-primary-900/20 cursor-pointer' > {item.label} ))} Selected: {current ?? 'none'} ); }; // // Grid (demonstrates border prop and overflow-hidden clipping) // const GridCell = ({ border, items }: { border?: boolean; items: Item[] }) => { const [current, setCurrent] = useState(); return (
{items.map((item) => ( setCurrent(item.id)} className={itemClassName} > {item.label} ))}
); }; const GridStory = () => { return (
Tab between groups to compare. Each cell has overflow-hidden. {/* Default vs border. */}

Default (invisible until focused)

border (always visible)

{/* Tight grid — border. */}

Tight grid — border

{[0, 1, 2].map((col) => (
))}
{/* Tight grid — default. */}

Tight grid — default

{[0, 1, 2].map((col) => (
))}
{/* Item-level border. */}

Item-level border

{ITEMS.slice(0, 3).map((item) => ( {item.label} ))}
{/* Error state. */}

Error state

{ITEMS.slice(0, 3).map((item) => ( {item.label} ))}
); }; // // asChild (slot) usage // const AsChildStory = () => { const [current, setCurrent] = useState(); return ( Focus.Item rendered as a custom element via asChild. {ITEMS.slice(0, 3).map((item) => ( setCurrent(item.id)}> ))} ); }; // // Meta // const meta: Meta = { title: 'ui/react-ui-core/components/Focus', decorators: [withTheme(), withLayout({ layout: 'fullscreen' })], }; export default meta; type Story = StoryObj; export const Default: Story = { render: DefaultStory }; export const Horizontal: Story = { render: HorizontalStory }; export const Grid: Story = { render: GridStory }; export const AsChild: Story = { render: AsChildStory };