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
5 changes: 4 additions & 1 deletion .changeset/feat-collapse-redesign.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
---
'@tiny-design/react': minor
'@tiny-design/icons': minor
'@tiny-design/tokens': minor
'@tiny-design/charts': minor
---

Redesign the Collapse component API, styles, and docs, and align the related tokens.
Redesign the Collapse component API, styles, and docs, align the related tokens, and keep
the fixed-version package group in sync for release.
20 changes: 15 additions & 5 deletions apps/docs/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1712,21 +1712,31 @@ import React from 'react';
import { BaseProps, SizeType } from '../_utils/props';

export interface SegmentedOption {
label: React.ReactNode;
value: string | number;
value: SegmentedValue;
label?: React.ReactNode;
disabled?: boolean;
icon?: React.ReactNode;
title?: string;
className?: string;
}

export type SegmentedValue = string | number;

export interface SegmentedProps
extends BaseProps,
Omit<React.PropsWithoutRef<JSX.IntrinsicElements['div']>, 'onChange'> {
options: (string | number | SegmentedOption)[];
Omit<
React.PropsWithoutRef<JSX.IntrinsicElements['div']>,
'children' | 'defaultValue' | 'onChange'
> {
options: SegmentedOption[];
name?: string;
value?: SegmentedValue;
defaultValue?: SegmentedValue;
onChange?: (value: SegmentedValue) => void;
onChange?: (
value: SegmentedValue,
option: SegmentedOption,
event: React.ChangeEvent<HTMLInputElement>
) => void;
block?: boolean;
disabled?: boolean;
size?: SizeType;
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Components that accept sizes use: `'sm' | 'md' | 'lg'`
- **NativeSelect** — Native HTML select wrapper.
- **Radio** — Single selection. `Radio.Group` for groups.
- **Rate** — Star rating component.
- **Segmented** — Toggle between a set of options.
- **Segmented** — Segmented single-choice control for switching between mutually exclusive options.
- **Select** — Select value from dropdown options. Props: `options`, `mode` (`'multiple'|'tags'`), `searchable`.
- **Slider** — Drag slider within range. Props: `min`, `max`, `step`, `range`.
- **SplitButton** — Button with attached dropdown menu.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,11 @@ export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocum
'input-number.height.md': fields.fieldHeightMd,
'input-number.height.lg': fields.fieldHeightLg,
'segmented.bg': fields.muted,
'segmented.active-bg': fields.card,
'segmented.item-bg-hover': fields.secondary,
'segmented.item-bg-selected': fields.card,
'segmented.item-color': fields.mutedForeground,
'segmented.item-color-selected': fields.baseForeground,
'segmented.item-shadow-focus': fields.shadowFocus,
'segmented.radius': fields.radius,
'tag.bg': fields.secondary,
'tag.color': fields.secondaryForeground,
Expand Down
178 changes: 120 additions & 58 deletions packages/mcp/src/data/components.json

Large diffs are not rendered by default.

18 changes: 11 additions & 7 deletions packages/react/src/flex/demo/Align.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import type { SegmentedValue } from '@tiny-design/react';

export default function AlignDemo() {
const justifyOptions = [
'flex-start',
'center',
'flex-end',
'space-between',
'space-around',
'space-evenly',
{ label: 'flex-start', value: 'flex-start' },
{ label: 'center', value: 'center' },
{ label: 'flex-end', value: 'flex-end' },
{ label: 'space-between', value: 'space-between' },
{ label: 'space-around', value: 'space-around' },
{ label: 'space-evenly', value: 'space-evenly' },
];
const alignOptions = [
{ label: 'flex-start', value: 'flex-start' },
{ label: 'center', value: 'center' },
{ label: 'flex-end', value: 'flex-end' },
];
const alignOptions = ['flex-start', 'center', 'flex-end'];

const [justify, setJustify] = React.useState('flex-start');
const [align, setAlign] = React.useState('flex-start');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,63 @@ exports[`<Segmented /> should match the snapshot 1`] = `
role="radiogroup"
>
<label
class="ty-segmented__item ty-segmented__item_active"
class="ty-segmented__item"
>
<input
checked=""
aria-label="Daily"
class="ty-segmented__input"
name=":r0:"
type="radio"
value="Daily"
value="daily"
/>
<span
class="ty-segmented__label"
class="ty-segmented__item-content"
>
Daily
<span
class="ty-segmented__label"
>
Daily
</span>
</span>
</label>
<label
class="ty-segmented__item"
>
<input
aria-label="Weekly"
class="ty-segmented__input"
name=":r0:"
type="radio"
value="Weekly"
value="weekly"
/>
<span
class="ty-segmented__label"
class="ty-segmented__item-content"
>
Weekly
<span
class="ty-segmented__label"
>
Weekly
</span>
</span>
</label>
<label
class="ty-segmented__item"
>
<input
aria-label="Monthly"
class="ty-segmented__input"
name=":r0:"
type="radio"
value="Monthly"
value="monthly"
/>
<span
class="ty-segmented__label"
class="ty-segmented__item-content"
>
Monthly
<span
class="ty-segmented__label"
>
Monthly
</span>
</span>
</label>
</div>
Expand Down
102 changes: 82 additions & 20 deletions packages/react/src/segmented/__tests__/segmented.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,127 @@ import { render, fireEvent } from '@testing-library/react';
import Segmented from '../index';

describe('<Segmented />', () => {
const options = [
{ label: 'Daily', value: 'daily' },
{ label: 'Weekly', value: 'weekly' },
{ label: 'Monthly', value: 'monthly' },
];

it('should match the snapshot', () => {
const { asFragment } = render(<Segmented options={['Daily', 'Weekly', 'Monthly']} />);
const { asFragment } = render(<Segmented options={options} />);
expect(asFragment()).toMatchSnapshot();
});

it('should render correctly', () => {
const { container } = render(<Segmented options={['A', 'B', 'C']} />);
const { container } = render(
<Segmented
options={[
{ label: 'A', value: 'a' },
{ label: 'B', value: 'b' },
{ label: 'C', value: 'c' },
]}
/>
);
expect(container.firstChild).toHaveClass('ty-segmented');
});

it('should render options', () => {
const { getByText } = render(<Segmented options={['Foo', 'Bar']} />);
const { getByText } = render(
<Segmented
options={[
{ label: 'Foo', value: 'foo' },
{ label: 'Bar', value: 'bar' },
]}
/>
);
expect(getByText('Foo')).toBeInTheDocument();
expect(getByText('Bar')).toBeInTheDocument();
});

it('should select default value', () => {
const { container } = render(
<Segmented options={['A', 'B', 'C']} defaultValue="B" />
<Segmented
options={[
{ label: 'A', value: 'a' },
{ label: 'B', value: 'b' },
{ label: 'C', value: 'c' },
]}
defaultValue="b"
/>
);
const active = container.querySelector('.ty-segmented__item_active');
expect(active).toBeTruthy();
expect(active!).toHaveTextContent('B');
});

it('should handle onChange', () => {
const onChange = jest.fn();
const { getByText } = render(
<Segmented options={['A', 'B']} onChange={onChange} />
);
fireEvent.click(getByText('B'));
expect(onChange).toHaveBeenCalledWith('B');
it('should not select any option by default', () => {
const { container } = render(<Segmented options={options} />);
expect(container.querySelector('.ty-segmented__item_active')).toBeNull();
});

it('should support object options', () => {
const { getByText } = render(
it('should handle onChange', () => {
const onChange = jest.fn();
const { getByLabelText } = render(
<Segmented
options={[
{ label: 'Option A', value: 'a' },
{ label: 'Option B', value: 'b' },
{ label: 'A', value: 'a' },
{ label: 'B', value: 'b' },
]}
onChange={onChange}
/>
);
expect(getByText('Option A')).toBeInTheDocument();
fireEvent.click(getByLabelText('B'));
expect(onChange).toHaveBeenCalledWith(
'b',
{ label: 'B', value: 'b' },
expect.any(Object)
);
});

it('should support block mode', () => {
const { container } = render(
<Segmented options={['A', 'B']} block />
<Segmented
options={[
{ label: 'A', value: 'a' },
{ label: 'B', value: 'b' },
]}
block
/>
);
expect(container.firstChild).toHaveClass('ty-segmented_block');
});

it('should support disabled', () => {
const onChange = jest.fn();
const { getByText } = render(
<Segmented options={['A', 'B']} disabled onChange={onChange} />
const { getByLabelText } = render(
<Segmented
options={[
{ label: 'A', value: 'a' },
{ label: 'B', value: 'b' },
]}
disabled
onChange={onChange}
/>
);
fireEvent.click(getByText('B'));
fireEvent.click(getByLabelText('B'));
expect(onChange).not.toHaveBeenCalled();
});

it('should reset uncontrolled selection when option is removed', () => {
const { container, rerender } = render(
<Segmented options={options} defaultValue="weekly" />
);

rerender(
<Segmented
options={[
{ label: 'Daily', value: 'daily' },
{ label: 'Monthly', value: 'monthly' },
]}
defaultValue="weekly"
/>
);

expect(container.querySelector('.ty-segmented__item_active')).toBeNull();
});
});
20 changes: 17 additions & 3 deletions packages/react/src/segmented/demo/Basic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import { Segmented } from '@tiny-design/react';
export default function BasicDemo() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<Segmented options={['Daily', 'Weekly', 'Monthly', 'Yearly']} />
<Segmented
options={[
{ label: 'Daily', value: 'daily' },
{ label: 'Weekly', value: 'weekly' },
{ label: 'Monthly', value: 'monthly' },
{ label: 'Yearly', value: 'yearly' },
]}
/>
<Segmented
options={[
{ label: 'Small', value: 'sm' },
Expand All @@ -13,7 +20,14 @@ export default function BasicDemo() {
]}
size="sm"
/>
<Segmented options={['Map', 'Transit', 'Satellite']} block />
<Segmented
options={[
{ label: 'Map', value: 'map' },
{ label: 'Transit', value: 'transit' },
{ label: 'Satellite', value: 'satellite' },
]}
block
/>
</div>
);
}
}
23 changes: 23 additions & 0 deletions packages/react/src/segmented/demo/Controlled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { Segmented, Text } from '@tiny-design/react';

const options = [
{ label: 'List', value: 'list' },
{ label: 'Board', value: 'board' },
{ label: 'Timeline', value: 'timeline' },
];

export default function ControlledDemo() {
const [value, setValue] = React.useState('list');

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Segmented
options={options}
value={value}
onChange={(nextValue) => setValue(String(nextValue))}
/>
<Text type="secondary">Current value: {value}</Text>
</div>
);
}
18 changes: 18 additions & 0 deletions packages/react/src/segmented/demo/DefaultValue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { Segmented, Text } from '@tiny-design/react';

export default function DefaultValueDemo() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Segmented
options={[
{ label: 'Draft', value: 'draft' },
{ label: 'Review', value: 'review' },
{ label: 'Published', value: 'published' },
]}
defaultValue="review"
/>
<Text type="secondary">Initial selection is set with defaultValue.</Text>
</div>
);
}
Loading
Loading