Skip to content

Refactor Listbox stories for improved structure#22

Merged
marklearst merged 1 commit into
mainfrom
marklearst-patch-listbox-story
Jan 8, 2026
Merged

Refactor Listbox stories for improved structure#22
marklearst merged 1 commit into
mainfrom
marklearst-patch-listbox-story

Conversation

@marklearst
Copy link
Copy Markdown
Owner

No description provided.

Copilot AI review requested due to automatic review settings January 8, 2026 04:44
@marklearst marklearst merged commit c261832 into main Jan 8, 2026
6 of 10 checks passed
@marklearst marklearst deleted the marklearst-patch-listbox-story branch January 8, 2026 04:45
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the Listbox component stories to improve their structure and interactivity. The refactoring introduces Storybook controls for label, description, placeholder, width, and disabled state, allowing for better interactive documentation and testing.

Key changes:

  • Added typed story args with controls for dynamic story configuration
  • Refactored story components to accept props from Storybook controls instead of hardcoded values
  • Improved component naming (e.g., ListboxTextWithMultiplePropWithHooksListboxMultiWithHooks)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

import type { Meta, StoryObj } from "@storybook/react";
import React, { FC } from "react";
import { FormField } from "../form-field";
import type { Meta, StoryObj } from '@storybook/react-vite'
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import should use '@storybook/react' instead of '@storybook/react-vite' to maintain consistency with all other story files in the codebase. All other story files import from '@storybook/react'.

Suggested change
import type { Meta, StoryObj } from '@storybook/react-vite'
import type { Meta, StoryObj } from '@storybook/react'

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +30
const meta: Meta<ListboxStoryArgs> = {
title: 'Input/Listbox',
args: {
label: 'Label',
description: 'Description',
placeholder: 'Select...',
width: 288,
disabled: false,
},
argTypes: {
label: { control: 'text' },
description: { control: 'text' },
placeholder: { control: 'text' },
width: { control: { type: 'range', min: 200, max: 360, step: 16 } },
disabled: { control: 'boolean' },
},
}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The meta configuration is missing the 'component' property. While using custom args for story controls is valid, the meta object should still include a reference to the component being documented. Add component: FormField.Listbox to the meta configuration to maintain consistency with Storybook best practices and other story files in the codebase.

Copilot uses AI. Check for mistakes.
Comment on lines +2 to +230
import type { Meta, StoryObj } from '@storybook/react-vite'
import { FC, useState } from 'react'
import { FormField } from '../form-field'

type ListboxStoryArgs = {
label: string
description: string
placeholder: string
width: number
disabled: boolean
}

const meta: Meta<typeof FormField.Listbox> = {
title: "Input/Listbox",
component: FormField.Listbox,
};
const meta: Meta<ListboxStoryArgs> = {
title: 'Input/Listbox',
args: {
label: 'Label',
description: 'Description',
placeholder: 'Select...',
width: 288,
disabled: false,
},
argTypes: {
label: { control: 'text' },
description: { control: 'text' },
placeholder: { control: 'text' },
width: { control: { type: 'range', min: 200, max: 360, step: 16 } },
disabled: { control: 'boolean' },
},
}

export default meta;
export default meta

type Story = StoryObj<typeof FormField.Listbox>;
type Story = StoryObj<ListboxStoryArgs>

interface Person {
id: number;
name: string;
isDead?: boolean;
id: number
name: string
isDead?: boolean
}

const people: Person[] = [
{ id: 1, name: "John Lennon", isDead: true },
{ id: 2, name: "Kenton Towne" },
{ id: 3, name: "Therese Wunsch" },
{ id: 4, name: "Benedict Kessler" },
{ id: 5, name: "Katelyn Rohan" },
];

const ListboxTextWithHooks = () => {
const [selectedPerson, setSelectedPerson] = React.useState<null | Person>(null);

return (
<FormField>
<FormField.LabelGroup>
<FormField.Label htmlFor="value">Label</FormField.Label>
<FormField.Description id="value-description">Description</FormField.Description>
</FormField.LabelGroup>
<FormField.Listbox value={selectedPerson} onChange={setSelectedPerson}>
<FormField.Listbox.Button>
<FormField.Listbox.Button.TextValue
value={selectedPerson?.name ?? null}
placeholder="Select..."
/>
</FormField.Listbox.Button>
<FormField.Listbox.Options>
{people.map((person) => (
<FormField.Listbox.Option
value={person}
key={person.id}
disabled={person.isDead}
>
<FormField.Listbox.Option.TextOption>
{person.name}
</FormField.Listbox.Option.TextOption>
</FormField.Listbox.Option>
))}
</FormField.Listbox.Options>
</FormField.Listbox>
</FormField>
);
};

const ListboxTextWithMultiplePropWithHooks = () => {
const [selectedPeople, setSelectedPeople] = React.useState<Person[]>([]);

return (
<FormField>
<FormField.LabelGroup>
<FormField.Label htmlFor="value">Label</FormField.Label>
<FormField.Description id="value-description">Description</FormField.Description>
</FormField.LabelGroup>
<FormField.Listbox<Person> value={selectedPeople} onChange={setSelectedPeople} multiple>
<FormField.Listbox.Button>
<FormField.Listbox.Button.TextValue
value={
selectedPeople.length > 0
? selectedPeople.map((person) => person.name).join(", ")
: null
}
placeholder="Select..."
/>
</FormField.Listbox.Button>
<FormField.Listbox.Options>
{people.map((person) => (
<FormField.Listbox.Option
value={person}
key={person.id}
disabled={person.isDead}
>
<FormField.Listbox.Option.TextOption>
{person.name}
</FormField.Listbox.Option.TextOption>
</FormField.Listbox.Option>
))}
</FormField.Listbox.Options>
</FormField.Listbox>
</FormField>
);
};

const ListboxBadgeWithHooks: FC<{ disabled?: boolean }> = ({ disabled }) => {
const [selectedPerson, setSelectedPerson] = React.useState<null | Person>(null);

return (
<FormField>
<FormField.LabelGroup>
<FormField.Label htmlFor="value">Label</FormField.Label>
<FormField.Description id="value-description">Description</FormField.Description>
</FormField.LabelGroup>

<FormField.Listbox value={selectedPerson} onChange={setSelectedPerson}>
<FormField.Listbox.Button disabled={disabled}>
<FormField.Listbox.Button.BadgeValue
value={selectedPerson?.name ?? null}
placeholder="Select …"
/>
</FormField.Listbox.Button>

<FormField.Listbox.Options>
{people.map((person) => (
<FormField.Listbox.Option value={person} key={person.id}>
<FormField.Listbox.Option.BadgeOption>
{person.name}
</FormField.Listbox.Option.BadgeOption>
</FormField.Listbox.Option>
))}
</FormField.Listbox.Options>
</FormField.Listbox>
</FormField>
);
};
{ id: 1, name: 'John Lennon', isDead: true },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]

const ListboxTextWithHooks = ({
label,
description,
placeholder,
}: {
label: string
description: string
placeholder: string
}) => {
const [selectedPerson, setSelectedPerson] = useState<null | Person>(null)

return (
<FormField>
<FormField.LabelGroup>
<FormField.Label htmlFor='value'>{label}</FormField.Label>
<FormField.Description id='value-description'>
{description}
</FormField.Description>
</FormField.LabelGroup>
<FormField.Listbox value={selectedPerson} onChange={setSelectedPerson}>
<FormField.Listbox.Button>
<FormField.Listbox.Button.TextValue
value={selectedPerson?.name ?? null}
placeholder={placeholder}
/>
</FormField.Listbox.Button>
<FormField.Listbox.Options>
{people.map((person) => (
<FormField.Listbox.Option
value={person}
key={person.id}
disabled={person.isDead}
>
<FormField.Listbox.Option.TextOption>
{person.name}
</FormField.Listbox.Option.TextOption>
</FormField.Listbox.Option>
))}
</FormField.Listbox.Options>
</FormField.Listbox>
</FormField>
)
}

const ListboxBadgeWithHooks: FC<{
disabled?: boolean
label: string
description: string
placeholder: string
}> = ({ disabled, label, description, placeholder }) => {
const [selectedPerson, setSelectedPerson] = useState<null | Person>(null)

return (
<FormField>
<FormField.LabelGroup>
<FormField.Label htmlFor='value'>{label}</FormField.Label>
<FormField.Description id='value-description'>
{description}
</FormField.Description>
</FormField.LabelGroup>

<FormField.Listbox value={selectedPerson} onChange={setSelectedPerson}>
<FormField.Listbox.Button disabled={disabled}>
<FormField.Listbox.Button.BadgeValue
value={selectedPerson?.name ?? null}
placeholder={placeholder}
/>
</FormField.Listbox.Button>

<FormField.Listbox.Options>
{people.map((person) => (
<FormField.Listbox.Option value={person} key={person.id}>
<FormField.Listbox.Option.BadgeOption>
{person.name}
</FormField.Listbox.Option.BadgeOption>
</FormField.Listbox.Option>
))}
</FormField.Listbox.Options>
</FormField.Listbox>
</FormField>
)
}

const ListboxMultiWithHooks = ({
label,
description,
placeholder,
}: {
label: string
description: string
placeholder: string
}) => {
const [selectedPeople, setSelectedPeople] = useState<Person[]>([])
const selectedLabels = selectedPeople.map((person) => person.name).join(', ')

return (
<FormField>
<FormField.LabelGroup>
<FormField.Label htmlFor='value'>{label}</FormField.Label>
<FormField.Description id='value-description'>
{description}
</FormField.Description>
</FormField.LabelGroup>
<FormField.Listbox
value={selectedPeople}
onChange={setSelectedPeople}
multiple
>
<FormField.Listbox.Button>
<FormField.Listbox.Button.TextValue
value={selectedLabels || null}
placeholder={placeholder}
/>
</FormField.Listbox.Button>
<FormField.Listbox.Options>
{people.map((person) => (
<FormField.Listbox.Option
value={person}
key={person.id}
disabled={person.isDead}
>
<FormField.Listbox.Option.TextOption>
{person.name}
</FormField.Listbox.Option.TextOption>
</FormField.Listbox.Option>
))}
</FormField.Listbox.Options>
</FormField.Listbox>
</FormField>
)
}

export const Default: Story = {
render: () => (
<div className="w-72">
<ListboxTextWithHooks />
</div>
),
};
render: ({ label, description, placeholder, width }) => (
<div style={{ width }}>
<ListboxTextWithHooks
label={label}
description={description}
placeholder={placeholder}
/>
</div>
),
}

export const Badge: Story = {
render: () => (
<div className="w-72">
<ListboxBadgeWithHooks />
</div>
),
};
render: ({ label, description, placeholder, width, disabled }) => (
<div style={{ width }}>
<ListboxBadgeWithHooks
label={label}
description={description}
placeholder={placeholder}
disabled={disabled}
/>
</div>
),
}

export const Disabled: Story = {
render: () => (
<div className="w-72">
<ListboxBadgeWithHooks disabled />
</div>
),
};
render: ({ label, description, placeholder, width }) => (
<div style={{ width }}>
<ListboxBadgeWithHooks
label={label}
description={description}
placeholder={placeholder}
disabled
/>
</div>
),
}

export const Multiple: Story = {
render: () => (
<div className="w-72">
<ListboxTextWithMultiplePropWithHooks />
</div>
),
};
render: ({ label, description, placeholder, width }) => (
<div style={{ width }}>
<ListboxMultiWithHooks
label={label}
description={description}
placeholder={placeholder}
/>
</div>
),
}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code uses single quotes for string literals, which is inconsistent with the rest of the codebase that predominantly uses double quotes. Update all string literals to use double quotes to maintain consistency with the project's coding style.

Copilot uses AI. Check for mistakes.
Comment on lines +2 to +230
import type { Meta, StoryObj } from '@storybook/react-vite'
import { FC, useState } from 'react'
import { FormField } from '../form-field'

type ListboxStoryArgs = {
label: string
description: string
placeholder: string
width: number
disabled: boolean
}

const meta: Meta<typeof FormField.Listbox> = {
title: "Input/Listbox",
component: FormField.Listbox,
};
const meta: Meta<ListboxStoryArgs> = {
title: 'Input/Listbox',
args: {
label: 'Label',
description: 'Description',
placeholder: 'Select...',
width: 288,
disabled: false,
},
argTypes: {
label: { control: 'text' },
description: { control: 'text' },
placeholder: { control: 'text' },
width: { control: { type: 'range', min: 200, max: 360, step: 16 } },
disabled: { control: 'boolean' },
},
}

export default meta;
export default meta

type Story = StoryObj<typeof FormField.Listbox>;
type Story = StoryObj<ListboxStoryArgs>

interface Person {
id: number;
name: string;
isDead?: boolean;
id: number
name: string
isDead?: boolean
}

const people: Person[] = [
{ id: 1, name: "John Lennon", isDead: true },
{ id: 2, name: "Kenton Towne" },
{ id: 3, name: "Therese Wunsch" },
{ id: 4, name: "Benedict Kessler" },
{ id: 5, name: "Katelyn Rohan" },
];

const ListboxTextWithHooks = () => {
const [selectedPerson, setSelectedPerson] = React.useState<null | Person>(null);

return (
<FormField>
<FormField.LabelGroup>
<FormField.Label htmlFor="value">Label</FormField.Label>
<FormField.Description id="value-description">Description</FormField.Description>
</FormField.LabelGroup>
<FormField.Listbox value={selectedPerson} onChange={setSelectedPerson}>
<FormField.Listbox.Button>
<FormField.Listbox.Button.TextValue
value={selectedPerson?.name ?? null}
placeholder="Select..."
/>
</FormField.Listbox.Button>
<FormField.Listbox.Options>
{people.map((person) => (
<FormField.Listbox.Option
value={person}
key={person.id}
disabled={person.isDead}
>
<FormField.Listbox.Option.TextOption>
{person.name}
</FormField.Listbox.Option.TextOption>
</FormField.Listbox.Option>
))}
</FormField.Listbox.Options>
</FormField.Listbox>
</FormField>
);
};

const ListboxTextWithMultiplePropWithHooks = () => {
const [selectedPeople, setSelectedPeople] = React.useState<Person[]>([]);

return (
<FormField>
<FormField.LabelGroup>
<FormField.Label htmlFor="value">Label</FormField.Label>
<FormField.Description id="value-description">Description</FormField.Description>
</FormField.LabelGroup>
<FormField.Listbox<Person> value={selectedPeople} onChange={setSelectedPeople} multiple>
<FormField.Listbox.Button>
<FormField.Listbox.Button.TextValue
value={
selectedPeople.length > 0
? selectedPeople.map((person) => person.name).join(", ")
: null
}
placeholder="Select..."
/>
</FormField.Listbox.Button>
<FormField.Listbox.Options>
{people.map((person) => (
<FormField.Listbox.Option
value={person}
key={person.id}
disabled={person.isDead}
>
<FormField.Listbox.Option.TextOption>
{person.name}
</FormField.Listbox.Option.TextOption>
</FormField.Listbox.Option>
))}
</FormField.Listbox.Options>
</FormField.Listbox>
</FormField>
);
};

const ListboxBadgeWithHooks: FC<{ disabled?: boolean }> = ({ disabled }) => {
const [selectedPerson, setSelectedPerson] = React.useState<null | Person>(null);

return (
<FormField>
<FormField.LabelGroup>
<FormField.Label htmlFor="value">Label</FormField.Label>
<FormField.Description id="value-description">Description</FormField.Description>
</FormField.LabelGroup>

<FormField.Listbox value={selectedPerson} onChange={setSelectedPerson}>
<FormField.Listbox.Button disabled={disabled}>
<FormField.Listbox.Button.BadgeValue
value={selectedPerson?.name ?? null}
placeholder="Select …"
/>
</FormField.Listbox.Button>

<FormField.Listbox.Options>
{people.map((person) => (
<FormField.Listbox.Option value={person} key={person.id}>
<FormField.Listbox.Option.BadgeOption>
{person.name}
</FormField.Listbox.Option.BadgeOption>
</FormField.Listbox.Option>
))}
</FormField.Listbox.Options>
</FormField.Listbox>
</FormField>
);
};
{ id: 1, name: 'John Lennon', isDead: true },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]

const ListboxTextWithHooks = ({
label,
description,
placeholder,
}: {
label: string
description: string
placeholder: string
}) => {
const [selectedPerson, setSelectedPerson] = useState<null | Person>(null)

return (
<FormField>
<FormField.LabelGroup>
<FormField.Label htmlFor='value'>{label}</FormField.Label>
<FormField.Description id='value-description'>
{description}
</FormField.Description>
</FormField.LabelGroup>
<FormField.Listbox value={selectedPerson} onChange={setSelectedPerson}>
<FormField.Listbox.Button>
<FormField.Listbox.Button.TextValue
value={selectedPerson?.name ?? null}
placeholder={placeholder}
/>
</FormField.Listbox.Button>
<FormField.Listbox.Options>
{people.map((person) => (
<FormField.Listbox.Option
value={person}
key={person.id}
disabled={person.isDead}
>
<FormField.Listbox.Option.TextOption>
{person.name}
</FormField.Listbox.Option.TextOption>
</FormField.Listbox.Option>
))}
</FormField.Listbox.Options>
</FormField.Listbox>
</FormField>
)
}

const ListboxBadgeWithHooks: FC<{
disabled?: boolean
label: string
description: string
placeholder: string
}> = ({ disabled, label, description, placeholder }) => {
const [selectedPerson, setSelectedPerson] = useState<null | Person>(null)

return (
<FormField>
<FormField.LabelGroup>
<FormField.Label htmlFor='value'>{label}</FormField.Label>
<FormField.Description id='value-description'>
{description}
</FormField.Description>
</FormField.LabelGroup>

<FormField.Listbox value={selectedPerson} onChange={setSelectedPerson}>
<FormField.Listbox.Button disabled={disabled}>
<FormField.Listbox.Button.BadgeValue
value={selectedPerson?.name ?? null}
placeholder={placeholder}
/>
</FormField.Listbox.Button>

<FormField.Listbox.Options>
{people.map((person) => (
<FormField.Listbox.Option value={person} key={person.id}>
<FormField.Listbox.Option.BadgeOption>
{person.name}
</FormField.Listbox.Option.BadgeOption>
</FormField.Listbox.Option>
))}
</FormField.Listbox.Options>
</FormField.Listbox>
</FormField>
)
}

const ListboxMultiWithHooks = ({
label,
description,
placeholder,
}: {
label: string
description: string
placeholder: string
}) => {
const [selectedPeople, setSelectedPeople] = useState<Person[]>([])
const selectedLabels = selectedPeople.map((person) => person.name).join(', ')

return (
<FormField>
<FormField.LabelGroup>
<FormField.Label htmlFor='value'>{label}</FormField.Label>
<FormField.Description id='value-description'>
{description}
</FormField.Description>
</FormField.LabelGroup>
<FormField.Listbox
value={selectedPeople}
onChange={setSelectedPeople}
multiple
>
<FormField.Listbox.Button>
<FormField.Listbox.Button.TextValue
value={selectedLabels || null}
placeholder={placeholder}
/>
</FormField.Listbox.Button>
<FormField.Listbox.Options>
{people.map((person) => (
<FormField.Listbox.Option
value={person}
key={person.id}
disabled={person.isDead}
>
<FormField.Listbox.Option.TextOption>
{person.name}
</FormField.Listbox.Option.TextOption>
</FormField.Listbox.Option>
))}
</FormField.Listbox.Options>
</FormField.Listbox>
</FormField>
)
}

export const Default: Story = {
render: () => (
<div className="w-72">
<ListboxTextWithHooks />
</div>
),
};
render: ({ label, description, placeholder, width }) => (
<div style={{ width }}>
<ListboxTextWithHooks
label={label}
description={description}
placeholder={placeholder}
/>
</div>
),
}

export const Badge: Story = {
render: () => (
<div className="w-72">
<ListboxBadgeWithHooks />
</div>
),
};
render: ({ label, description, placeholder, width, disabled }) => (
<div style={{ width }}>
<ListboxBadgeWithHooks
label={label}
description={description}
placeholder={placeholder}
disabled={disabled}
/>
</div>
),
}

export const Disabled: Story = {
render: () => (
<div className="w-72">
<ListboxBadgeWithHooks disabled />
</div>
),
};
render: ({ label, description, placeholder, width }) => (
<div style={{ width }}>
<ListboxBadgeWithHooks
label={label}
description={description}
placeholder={placeholder}
disabled
/>
</div>
),
}

export const Multiple: Story = {
render: () => (
<div className="w-72">
<ListboxTextWithMultiplePropWithHooks />
</div>
),
};
render: ({ label, description, placeholder, width }) => (
<div style={{ width }}>
<ListboxMultiWithHooks
label={label}
description={description}
placeholder={placeholder}
/>
</div>
),
}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code omits semicolons at the end of statements, which is inconsistent with the rest of the codebase that uses semicolons. Add semicolons to maintain consistency with the project's coding style.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants