feat: test coverage of ComboControl and CheckboxFilter

This commit is contained in:
Ilia Mashkov
2026-01-08 13:14:04 +03:00
parent 36a326817d
commit fc00717359
16 changed files with 2300 additions and 357 deletions

View File

@@ -0,0 +1,112 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import CheckboxFilter from './CheckboxFilter.svelte';
const { Story } = defineMeta({
title: 'Shared/UI/CheckboxFilter',
component: CheckboxFilter,
tags: ['autodocs'],
argTypes: {
displayedLabel: { control: 'text' },
// filter is complex, use stories for examples
},
});
</script>
<script lang="ts">
import WithFilterDecorator from './WithFilterDecorator.svelte';
import type { Property } from '$shared/lib';
// Define initial values for each story
const basicProperties: Property[] = [
{ id: 'serif', name: 'Serif', selected: false },
{ id: 'sans-serif', name: 'Sans-serif', selected: false },
{ id: 'display', name: 'Display', selected: false },
{ id: 'handwriting', name: 'Handwriting', selected: false },
{ id: 'monospace', name: 'Monospace', selected: false },
];
const withSelectedProperties: Property[] = [
{ id: 'serif', name: 'Serif', selected: true },
{ id: 'sans-serif', name: 'Sans-serif', selected: false },
{ id: 'display', name: 'Display', selected: true },
{ id: 'handwriting', name: 'Handwriting', selected: false },
];
const allSelectedProperties: Property[] = [
{ id: 'serif', name: 'Serif', selected: true },
{ id: 'sans-serif', name: 'Sans-serif', selected: true },
{ id: 'display', name: 'Display', selected: true },
{ id: 'handwriting', name: 'Handwriting', selected: true },
];
const emptyProperties: Property[] = [];
const singleProperty: Property[] = [{ id: 'serif', name: 'Serif', selected: false }];
const multipleProperties: Property[] = [
{ id: 'thin', name: 'Thin', selected: false },
{ id: 'extra-light', name: 'Extra Light', selected: false },
{ id: 'light', name: 'Light', selected: false },
{ id: 'regular', name: 'Regular', selected: false },
{ id: 'medium', name: 'Medium', selected: false },
{ id: 'semi-bold', name: 'Semi Bold', selected: false },
{ id: 'bold', name: 'Bold', selected: false },
{ id: 'extra-bold', name: 'Extra Bold', selected: false },
{ id: 'black', name: 'Black', selected: false },
];
</script>
<!-- Basic usage - multiple properties -->
<Story name="Basic Usage" args={{ displayedLabel: 'Font Category' }}>
<WithFilterDecorator initialValues={basicProperties}>
{#snippet children({ filter })}
<CheckboxFilter displayedLabel={'Font Category'} {filter} />
{/snippet}
</WithFilterDecorator>
</Story>
<!-- With some items pre-selected -->
<Story name="With Selected Items" args={{ displayedLabel: 'Font Category' }}>
<WithFilterDecorator initialValues={withSelectedProperties}>
{#snippet children({ filter })}
<CheckboxFilter displayedLabel={'Font Category'} {filter} />
{/snippet}
</WithFilterDecorator>
</Story>
<!-- All items selected -->
<Story name="All Selected" args={{ displayedLabel: 'Font Category' }}>
<WithFilterDecorator initialValues={allSelectedProperties}>
{#snippet children({ filter })}
<CheckboxFilter displayedLabel={'Font Category'} {filter} />
{/snippet}
</WithFilterDecorator>
</Story>
<!-- Empty filter (no properties) -->
<Story name="Empty Filter" args={{ displayedLabel: 'Empty Filter' }}>
<WithFilterDecorator initialValues={emptyProperties}>
{#snippet children({ filter })}
<CheckboxFilter displayedLabel={'Empty Filter'} {filter} />
{/snippet}
</WithFilterDecorator>
</Story>
<!-- Single property -->
<Story name="Single Property" args={{ displayedLabel: 'Font Category' }}>
<WithFilterDecorator initialValues={singleProperty}>
{#snippet children({ filter })}
<CheckboxFilter displayedLabel={'Font Category'} {filter} />
{/snippet}
</WithFilterDecorator>
</Story>
<!-- Large number of properties -->
<Story name="Multiple Properties" args={{ displayedLabel: 'Font Weight' }}>
<WithFilterDecorator initialValues={multipleProperties}>
{#snippet children({ filter })}
<CheckboxFilter displayedLabel={'Font Weight'} {filter} />
{/snippet}
</WithFilterDecorator>
</Story>

View File

@@ -63,8 +63,6 @@ const slideConfig = $derived({
// Derived for reactive updates when properties change - avoids recomputing on every render
const selectedCount = $derived(filter.selectedCount);
const hasSelection = $derived(selectedCount > 0);
$inspect(filter.properties).with(console.trace);
</script>
<!-- Collapsible card wrapper with subtle hover state for affordance -->

View File

@@ -1,85 +1,571 @@
import type { Property } from '$shared/lib/store/createFilterStore/createFilterStore';
import {
type Property,
createFilter,
} from '$shared/lib';
import {
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/svelte';
import {
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
import CheckboxFilter from './CheckboxFilter.svelte';
describe('CheckboxFilter', () => {
const mockProperties: Property[] = [
{ id: '1', name: 'Sans-serif', selected: false },
{ id: '2', name: 'Serif', selected: true },
{ id: '3', name: 'Display', selected: false },
];
/**
* Test Suite for CheckboxFilter Component
*
* This suite tests the actual Svelte component rendering, interactions, and behavior
* using a real browser environment (Playwright) via @vitest/browser-playwright.
*
* Tests for the createFilter helper function are in createFilter.test.ts
*
* IMPORTANT: These tests use the browser environment because Svelte 5's $state,
* $derived, and onMount lifecycle require a browser environment. The bits-ui
* Checkbox component renders as <button type="button"> with role="checkbox",
* not as <input type="checkbox">.
*/
const mockOnPropertyToggle = vi.fn();
describe('CheckboxFilter Component', () => {
/**
* Helper function to create a filter for testing
*/
function createTestFilter(properties: Property[]) {
return createFilter({ properties });
}
beforeEach(() => {
mockOnPropertyToggle.mockClear();
});
/**
* Helper function to create mock properties
*/
function createMockProperties(count: number, selectedIndices: number[] = []) {
return Array.from({ length: count }, (_, i) => ({
id: `prop-${i}`,
name: `Property ${i}`,
selected: selectedIndices.includes(i),
}));
}
it('renders with correct label', () => {
render(CheckboxFilter, {
displayedLabel: 'Font Categories',
properties: mockProperties,
onPropertyToggle: mockOnPropertyToggle,
describe('Rendering', () => {
it('displays the label', () => {
const filter = createTestFilter(createMockProperties(3));
render(CheckboxFilter, {
displayedLabel: 'Test Label',
filter,
});
expect(screen.getByText('Test Label')).toBeInTheDocument();
});
expect(screen.getByText('Font Categories')).toBeInTheDocument();
});
it('renders all properties as checkboxes with labels', () => {
const properties = createMockProperties(3);
const filter = createTestFilter(properties);
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
it('displays all properties as checkboxes', () => {
render(CheckboxFilter, {
displayedLabel: 'Categories',
properties: mockProperties,
onPropertyToggle: mockOnPropertyToggle,
// Check that all property names are rendered
expect(screen.getByText('Property 0')).toBeInTheDocument();
expect(screen.getByText('Property 1')).toBeInTheDocument();
expect(screen.getByText('Property 2')).toBeInTheDocument();
});
expect(screen.getByLabelText('Sans-serif')).toBeInTheDocument();
expect(screen.getByLabelText('Serif')).toBeInTheDocument();
expect(screen.getByLabelText('Display')).toBeInTheDocument();
});
it('shows selected count badge when items are selected', () => {
const properties = createMockProperties(3, [0, 2]); // Select 2 items
const filter = createTestFilter(properties);
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
it('shows selected count badge when items selected', () => {
render(CheckboxFilter, {
displayedLabel: 'Categories',
properties: mockProperties,
onPropertyToggle: mockOnPropertyToggle,
expect(screen.getByText('2')).toBeInTheDocument();
});
expect(screen.getByText('1')).toBeInTheDocument();
});
it('hides badge when no items selected', () => {
const properties = createMockProperties(3);
const filter = createTestFilter(properties);
const { container } = render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
it('does not show badge when no items selected', () => {
const allUnselected = mockProperties.map(p => ({ ...p, selected: false }));
render(CheckboxFilter, {
displayedLabel: 'Categories',
properties: allUnselected,
onPropertyToggle: mockOnPropertyToggle,
// Badge should not be in the document
const badges = container.querySelectorAll('[class*="badge"]');
expect(badges).toHaveLength(0);
});
expect(screen.queryByText('0')).not.toBeInTheDocument();
it('renders with no properties', () => {
const filter = createTestFilter([]);
render(CheckboxFilter, {
displayedLabel: 'Empty Filter',
filter,
});
expect(screen.getByText('Empty Filter')).toBeInTheDocument();
});
});
it('calls onPropertyToggle when checkbox clicked', async () => {
render(CheckboxFilter, {
displayedLabel: 'Categories',
properties: mockProperties,
onPropertyToggle: mockOnPropertyToggle,
describe('Checkbox Interactions', () => {
it('checkboxes reflect initial selected state', async () => {
const properties = createMockProperties(3, [0, 2]);
const filter = createTestFilter(properties);
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
// Wait for component to render
// bits-ui Checkbox renders as <button type="button"> with role="checkbox"
const checkboxes = screen.getAllByRole('checkbox');
expect(checkboxes).toHaveLength(3);
// Check that the correct checkboxes are checked using aria-checked attribute
expect(checkboxes[0]).toHaveAttribute('data-state', 'checked');
expect(checkboxes[1]).toHaveAttribute('data-state', 'unchecked');
expect(checkboxes[2]).toHaveAttribute('data-state', 'checked');
});
const checkbox = screen.getByLabelText('Sans-serif');
await checkbox.click();
it('clicking checkbox toggles property.selected state', async () => {
const properties = createMockProperties(3, [0]);
const filter = createTestFilter(properties);
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
expect(mockOnPropertyToggle).toHaveBeenCalledWith('1');
const checkboxes = await screen.findAllByRole('checkbox');
// Initially, first checkbox is checked
expect(checkboxes[0]).toHaveAttribute('data-state', 'checked');
expect(filter.selectedCount).toBe(1);
// Click to uncheck it
await fireEvent.click(checkboxes[0]);
// Now it should be unchecked
await waitFor(() => {
expect(checkboxes[0]).toHaveAttribute('data-state', 'unchecked');
});
expect(filter.selectedCount).toBe(0);
// Click it again to re-check
await fireEvent.click(checkboxes[0]);
await waitFor(() => {
expect(checkboxes[0]).toHaveAttribute('data-state', 'checked');
});
expect(filter.selectedCount).toBe(1);
});
it('label styling changes based on selection state', async () => {
const properties = createMockProperties(2, [0]);
const filter = createTestFilter(properties);
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
const checkboxes = await screen.findAllByRole('checkbox');
// Find label elements - they are siblings of checkboxes
const labels = checkboxes.map(cb => cb.nextElementSibling);
// First label should have font-medium and text-foreground classes
expect(labels[0]).toHaveClass('font-medium', 'text-foreground');
// Second label should not have these classes
expect(labels[1]).not.toHaveClass('font-medium', 'text-foreground');
// Uncheck the first checkbox
await fireEvent.click(checkboxes[0]);
await waitFor(() => {
// Now first label should not have these classes
expect(labels[0]).not.toHaveClass('font-medium', 'text-foreground');
});
});
it('multiple checkboxes can be toggled independently', async () => {
const properties = createMockProperties(3);
const filter = createTestFilter(properties);
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
const checkboxes = await screen.findAllByRole('checkbox');
// Check all three checkboxes
await fireEvent.click(checkboxes[0]);
await fireEvent.click(checkboxes[1]);
await fireEvent.click(checkboxes[2]);
await waitFor(() => {
expect(filter.selectedCount).toBe(3);
});
// Uncheck middle one
await fireEvent.click(checkboxes[1]);
await waitFor(() => {
expect(filter.selectedCount).toBe(2);
expect(checkboxes[0]).toHaveAttribute('data-state', 'checked');
expect(checkboxes[1]).toHaveAttribute('data-state', 'unchecked');
expect(checkboxes[2]).toHaveAttribute('data-state', 'checked');
});
});
});
describe('Collapsible Behavior', () => {
it('is open by default', () => {
const filter = createTestFilter(createMockProperties(2));
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
// Check that properties are visible (content is expanded)
expect(screen.getByText('Property 0')).toBeInTheDocument();
expect(screen.getByText('Property 1')).toBeInTheDocument();
});
it('clicking trigger toggles open/close state', async () => {
const filter = createTestFilter(createMockProperties(2));
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
// Content is initially visible
expect(screen.getByText('Property 0')).toBeVisible();
// Click the trigger (button) - use role and text to find it
const trigger = screen.getByRole('button', { name: /Test/ });
await fireEvent.click(trigger);
// Content should now be hidden
await waitFor(() => {
expect(screen.queryByText('Property 0')).not.toBeInTheDocument();
});
// Click again to open
await fireEvent.click(trigger);
// Content should be visible again
await waitFor(() => {
expect(screen.getByText('Property 0')).toBeInTheDocument();
});
});
it('chevron icon rotates based on open state', async () => {
const filter = createTestFilter(createMockProperties(2));
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
const trigger = screen.getByRole('button', { name: /Test/ });
const chevronContainer = trigger.querySelector('.lucide-chevron-down')
?.parentElement as HTMLElement;
// Initially open, transform should be rotate(0deg) or no rotation
expect(chevronContainer?.style.transform).toContain('0deg');
// Click to close
await fireEvent.click(trigger);
await waitFor(() => {
// Now should be rotated -90deg
expect(chevronContainer?.style.transform).toContain('-90deg');
});
// Click to open again
await fireEvent.click(trigger);
await waitFor(() => {
// Back to 0deg
expect(chevronContainer?.style.transform).toContain('0deg');
});
});
});
describe('Count Display', () => {
it('badge shows correct count based on filter.selectedCount', async () => {
const properties = createMockProperties(5, [0, 2, 4]);
const filter = createTestFilter(properties);
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
// Should show 3
expect(screen.getByText('3')).toBeInTheDocument();
// Click a checkbox to change selection
const checkboxes = await screen.findAllByRole('checkbox');
await fireEvent.click(checkboxes[1]);
// Should now show 4
await waitFor(() => {
expect(screen.getByText('4')).toBeInTheDocument();
});
});
it('badge visibility changes with hasSelection (selectedCount > 0)', async () => {
const properties = createMockProperties(2, [0]);
const filter = createTestFilter(properties);
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
// Initially has 1 selection, badge should be visible
expect(screen.getByText('1')).toBeInTheDocument();
// Uncheck the selected item
const checkboxes = await screen.findAllByRole('checkbox');
await fireEvent.click(checkboxes[0]);
// Now 0 selections, badge should be hidden
await waitFor(() => {
expect(screen.queryByText('0')).not.toBeInTheDocument();
});
// Check it again
await fireEvent.click(checkboxes[0]);
// Badge should be visible again
await waitFor(() => {
expect(screen.getByText('1')).toBeInTheDocument();
});
});
it('badge shows count correctly when all items are selected', () => {
const properties = createMockProperties(5, [0, 1, 2, 3, 4]);
const filter = createTestFilter(properties);
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
expect(screen.getByText('5')).toBeInTheDocument();
});
});
describe('Accessibility', () => {
it('provides proper ARIA labels on buttons', () => {
const filter = createTestFilter(createMockProperties(2));
render(CheckboxFilter, {
displayedLabel: 'Test Label',
filter,
});
// The trigger button should be findable by its text
const trigger = screen.getByRole('button', { name: /Test Label/ });
expect(trigger).toBeInTheDocument();
});
it('labels are properly associated with checkboxes', async () => {
const properties = createMockProperties(3);
const filter = createTestFilter(properties);
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
const checkboxes = await screen.findAllByRole('checkbox');
checkboxes.forEach((checkbox, index) => {
// Each checkbox should have an id
expect(checkbox).toHaveAttribute('id', `prop-${index}`);
// Find the label element (Label component wraps checkbox)
const labelElement = checkbox.closest('label');
expect(labelElement).toHaveAttribute('for', `prop-${index}`);
});
});
it('checkboxes have proper role', async () => {
const filter = createTestFilter(createMockProperties(2));
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
const checkboxes = await screen.findAllByRole('checkbox');
checkboxes.forEach(checkbox => {
expect(checkbox).toHaveAttribute('role', 'checkbox');
expect(checkbox).toHaveAttribute('type', 'button');
});
});
it('labels are clickable and toggle associated checkboxes', async () => {
const properties = createMockProperties(2);
const filter = createTestFilter(properties);
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
const checkboxes = await screen.findAllByRole('checkbox');
// Find the label text element (span inside label)
const firstLabelText = screen.getByText('Property 0');
// Initially unchecked
expect(checkboxes[0]).toHaveAttribute('data-state', 'unchecked');
// Click the label text
await fireEvent.click(firstLabelText);
// Checkbox should now be checked
await waitFor(() => {
expect(checkboxes[0]).toHaveAttribute('data-state', 'checked');
});
// Click again
await fireEvent.click(firstLabelText);
// Should be unchecked again
await waitFor(() => {
expect(checkboxes[0]).toHaveAttribute('data-state', 'unchecked');
});
});
});
describe('Edge Cases', () => {
it('handles long property names', () => {
const properties: Property[] = [
{
id: '1',
name: 'This is a very long property name that might wrap to multiple lines',
selected: false,
},
];
const filter = createTestFilter(properties);
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
expect(
screen.getByText(
'This is a very long property name that might wrap to multiple lines',
),
).toBeInTheDocument();
});
it('handles special characters in property names', () => {
const properties: Property[] = [
{ id: '1', name: 'Café & Restaurant', selected: true },
{ id: '2', name: '100% Organic', selected: false },
{ id: '3', name: '(Special) <Characters>', selected: false },
];
const filter = createTestFilter(properties);
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
expect(screen.getByText('Café & Restaurant')).toBeInTheDocument();
expect(screen.getByText('100% Organic')).toBeInTheDocument();
expect(screen.getByText('(Special) <Characters>')).toBeInTheDocument();
});
it('handles single property filter', () => {
const properties: Property[] = [
{ id: '1', name: 'Only One', selected: true },
];
const filter = createTestFilter(properties);
render(CheckboxFilter, {
displayedLabel: 'Single',
filter,
});
expect(screen.getByText('Only One')).toBeInTheDocument();
expect(screen.getByText('1')).toBeInTheDocument();
});
it('handles very large number of properties', async () => {
const properties = createMockProperties(50, [0, 25, 49]);
const filter = createTestFilter(properties);
render(CheckboxFilter, {
displayedLabel: 'Large List',
filter,
});
const checkboxes = await screen.findAllByRole('checkbox');
expect(checkboxes).toHaveLength(50);
expect(screen.getByText('3')).toBeInTheDocument();
});
it('updates badge when filter is manipulated externally', async () => {
const properties = createMockProperties(3);
const filter = createTestFilter(properties);
render(CheckboxFilter, {
displayedLabel: 'Test',
filter,
});
// Initially no badge (0 selections)
expect(screen.queryByText('0')).not.toBeInTheDocument();
// Externally select properties
filter.selectProperty('prop-0');
filter.selectProperty('prop-1');
// Badge should now show 2
// Note: This might not update immediately in the DOM due to Svelte reactivity
// In a real browser environment, this would update
await waitFor(() => {
expect(screen.getByText('2')).toBeInTheDocument();
});
});
});
describe('Component Integration', () => {
it('works correctly with real filter data', async () => {
const realProperties: Property[] = [
{ id: 'sans-serif', name: 'Sans-serif', selected: true },
{ id: 'serif', name: 'Serif', selected: false },
{ id: 'display', name: 'Display', selected: false },
{ id: 'handwriting', name: 'Handwriting', selected: true },
{ id: 'monospace', name: 'Monospace', selected: false },
];
const filter = createTestFilter(realProperties);
render(CheckboxFilter, {
displayedLabel: 'Font Category',
filter,
});
// Check label
expect(screen.getByText('Font Category')).toBeInTheDocument();
// Check count badge
expect(screen.getByText('2')).toBeInTheDocument();
// Check property names
expect(screen.getByText('Sans-serif')).toBeInTheDocument();
expect(screen.getByText('Serif')).toBeInTheDocument();
expect(screen.getByText('Display')).toBeInTheDocument();
expect(screen.getByText('Handwriting')).toBeInTheDocument();
expect(screen.getByText('Monospace')).toBeInTheDocument();
// Check initial checkbox states
const checkboxes = await screen.findAllByRole('checkbox');
expect(checkboxes[0]).toHaveAttribute('data-state', 'checked');
expect(checkboxes[1]).toHaveAttribute('data-state', 'unchecked');
expect(checkboxes[2]).toHaveAttribute('data-state', 'unchecked');
expect(checkboxes[3]).toHaveAttribute('data-state', 'checked');
expect(checkboxes[4]).toHaveAttribute('data-state', 'unchecked');
// Interact with checkboxes
await fireEvent.click(checkboxes[1]);
await waitFor(() => {
expect(filter.selectedCount).toBe(3);
});
});
});
});

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { createFilter } from '$shared/lib';
import type { Property, Filter } from '$shared/lib';
/**
* Props for the WithFilter decorator component.
*/
let {
children,
/** Initial properties to create the filter from */
initialValues,
}: {
children: Snippet<[props: { filter: Filter }]>;
initialValues: Property[];
} = $props();
// Create filter inside component body so Svelte 5 runes work correctly
const filter = createFilter({ properties: initialValues });
</script>
{@render children({ filter })}

View File

@@ -1,64 +1,76 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import ComboControl from './ComboControl.svelte';
const { Story } = defineMeta({
title: 'Shared/UI/ComboControl',
component: ComboControl,
tags: ['autodocs'],
argTypes: {
value: { control: 'number' },
minValue: { control: 'number' },
maxValue: { control: 'number' },
step: { control: 'number' },
increaseDisabled: { control: 'boolean' },
decreaseDisabled: { control: 'boolean' },
onChange: { action: 'onChange' },
onIncrease: { action: 'onIncrease' },
onDecrease: { action: 'onDecrease' },
},
});
const { Story } = defineMeta({
title: 'Shared/UI/ComboControl',
component: ComboControl,
tags: ['autodocs'],
argTypes: {
controlLabel: { control: 'text' },
increaseLabel: { control: 'text' },
decreaseLabel: { control: 'text' },
},
});
</script>
<script lang="ts">
import ComboControl from './ComboControl.svelte';
import WithControlDecorator from './WithControlDecorator.svelte';
let integerStep = 1;
let decimalStep = 0.05;
// Define initial values for each story
const fontSizeInitial = { value: 16, min: 8, max: 100, step: 1 };
let integerValue = 16;
let decimalValue = 1.5;
const letterSpacingInitial = { value: 0, min: -2, max: 4, step: 0.05 };
let integerMinValue = 8;
let decimalMinValue = 1;
const atMinimumInitial = { value: 10, min: 10, max: 100, step: 1 };
let integerMaxValue = 100;
let decimalMaxValue = 2;
function onChange() {}
function onIncrease() {}
function onDecrease() {}
const atMaximumInitial = { value: 100, min: 10, max: 100, step: 1 };
</script>
<Story name="Integer Step">
<ComboControl
value={integerValue}
step={integerStep}
onChange={onChange}
onIncrease={onIncrease}
onDecrease={onDecrease}
minValue={integerMinValue}
maxValue={integerMaxValue}
/>
<Story name="Integer Step" args={{ controlLabel: 'Font size' }}>
<WithControlDecorator initialValues={fontSizeInitial}>
{#snippet children({ control })}
<ComboControl controlLabel={'Font size'} {control} />
{/snippet}
</WithControlDecorator>
</Story>
<Story name="Decimal Step">
<ComboControl
value={decimalValue}
step={decimalStep}
onChange={onChange}
onIncrease={onIncrease}
onDecrease={onDecrease}
minValue={decimalMinValue}
maxValue={decimalMaxValue}
/>
<Story name="Decimal Step" args={{ controlLabel: 'Letter spacing' }}>
<WithControlDecorator initialValues={letterSpacingInitial}>
{#snippet children({ control })}
<ComboControl controlLabel={'Letter spacing'} {control} />
{/snippet}
</WithControlDecorator>
</Story>
<Story
name="At Minimum"
args={{ controlLabel: 'Font size', increaseLabel: 'Increase', decreaseLabel: 'Decrease' }}
>
<WithControlDecorator initialValues={atMinimumInitial}>
{#snippet children({ control })}
<ComboControl
controlLabel={'Font size'}
increaseLabel={'Increase'}
decreaseLabel={'Decrease'}
{control}
/>
{/snippet}
</WithControlDecorator>
</Story>
<Story
name="At Maximum"
args={{ controlLabel: 'Font size', increaseLabel: 'Increase', decreaseLabel: 'Decrease' }}
>
<WithControlDecorator initialValues={atMaximumInitial}>
{#snippet children({ control })}
<ComboControl
controlLabel={'Font size'}
increaseLabel={'Increase'}
decreaseLabel={'Decrease'}
{control}
/>
{/snippet}
</WithControlDecorator>
</Story>

View File

@@ -36,6 +36,7 @@ const {
}: ComboControlProps = $props();
// Local state for the slider to prevent infinite loops
// svelte-ignore state_referenced_locally - $state captures initial value, $effect syncs updates
let sliderValue = $state(Number(control.value));
// Sync sliderValue when external value changes

File diff suppressed because it is too large Load Diff