Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
98a55dd
feat(ComboBox): new component
tenphi Oct 8, 2025
18fe995
feat(ComboBox): new component * 2
tenphi Oct 8, 2025
1f600d1
feat(ComboBox): new component * 3
tenphi Oct 8, 2025
d41bea4
feat(ComboBox): new component * 4
tenphi Oct 8, 2025
f192470
feat(ComboBox): new component * 5
tenphi Oct 8, 2025
9c4cac6
feat(ComboBox): new component * 6
tenphi Oct 8, 2025
d0b9631
feat(ComboBox): new component * 7
tenphi Oct 8, 2025
d6f9278
feat(ComboBox): add tests
tenphi Oct 8, 2025
12e4211
feat(ComboBox): add tests and documentation
tenphi Oct 8, 2025
2d38183
fix(ItemAction): styles
tenphi Oct 9, 2025
fa55b78
feat(DisplayTransition): add helper
tenphi Oct 9, 2025
fdfabbd
fix: combobox and tooltip fixes
tenphi Oct 9, 2025
7fe3575
fix(DisplayTransition): api
tenphi Oct 10, 2025
b5c655b
fix: multiple
tenphi Oct 10, 2025
9e0f81c
fix(ComboBox): add tests and stories
tenphi Oct 10, 2025
cf0859b
fix: listbox optimizations
tenphi Oct 10, 2025
aa2c526
fix: listbox collection simplification
tenphi Oct 10, 2025
97f2fcd
fix: listbox collection simplification * 2
tenphi Oct 10, 2025
d9513ce
fix: ref types
tenphi Oct 10, 2025
6e177a5
fix: combobox tests
tenphi Oct 10, 2025
d30c2d4
fix: combobox tests * 2
tenphi Oct 10, 2025
9bd05b1
fix(ComboBox): input id linking
tenphi Oct 13, 2025
af24747
fix(ListBox): input id linking
tenphi Oct 13, 2025
c409174
chore: add documentation rule
tenphi Oct 13, 2025
01b5771
fix(ComboBox): sort selected to top be default
tenphi Oct 13, 2025
b2fe3ac
chore: update documentation
tenphi Oct 13, 2025
8a184b2
chore(ComboBox): clear final props
tenphi Oct 13, 2025
f9a848e
fix(ComboBox): minor
tenphi Oct 13, 2025
8ac6ce6
fix(ComboBox): initial value rendering
tenphi Oct 13, 2025
7e0f574
Merge remote-tracking branch 'origin' into feat-combobox
tenphi Oct 13, 2025
0abd66e
fix(ComboBox): focus events support
tenphi Oct 13, 2025
9e9ee69
fix(ComboBox): minor fixes
tenphi Oct 13, 2025
9f24b6b
fix(ComboBox): use portal
tenphi Oct 13, 2025
ef6477b
fix(Select): use portal
tenphi Oct 13, 2025
237da26
fix(ComboBox): portal usage
tenphi Oct 13, 2025
fea4349
feat(ComboBox): clear on blur
tenphi Oct 14, 2025
d3e6ebc
fix(ComboBox): focus and blur handling
tenphi Oct 14, 2025
1cd7ece
fix(ComboBox): controlled mode
tenphi Oct 14, 2025
8169c0d
fix(ComboBox): popover size
tenphi Oct 14, 2025
2903fa9
fix(ComboBox): popover transition
tenphi Oct 14, 2025
a537732
fix(ComboBox): popover transition * 2
tenphi Oct 14, 2025
571a033
fix(ComboBox): onKeyDown handler
tenphi Oct 14, 2025
69936e5
fix(ComboBox): popover trigger logic
tenphi Oct 14, 2025
dbef0b0
feat(ComboBox): default input value
tenphi Oct 14, 2025
062f869
fix(FilterPicker): select custom value without duplicates
tenphi Oct 15, 2025
7f85f26
fix(Select): popover size
tenphi Oct 15, 2025
ed7aad2
fix(ComboBox): better accessibility
tenphi Oct 15, 2025
ac2074d
chore: update stories
tenphi Oct 15, 2025
8251ab2
chore: update stories * 2
tenphi Oct 15, 2025
730681f
chore: update stories * 3
tenphi Oct 15, 2025
c38c02b
Revert "chore: update stories * 3"
tenphi Oct 15, 2025
0c70f78
fix(FilterPicker): label extraction
tenphi Oct 15, 2025
7e0db85
fix(Select): update popover position
tenphi Oct 15, 2025
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: 5 additions & 0 deletions .changeset/clever-crabs-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cube-dev/ui-kit": patch
---

Add DisplayTransition helper component.
5 changes: 5 additions & 0 deletions .changeset/rare-jars-pretend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cube-dev/ui-kit": patch
---

Fix Tooltip position and transition.
1 change: 1 addition & 0 deletions .cursor/rules/coding.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ alwaysApply: true
# Coding rules

- Use named imports from react (like `useCallback`) instead of using the `React` instance. Avoid: `React.useCallback`.
- Prefer stable `useEvent` callbacks when it's possible.
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
globs: *.docs.mdx
alwaysApply: false
---

# Component Documentation Guidelines

This guide outlines the standards and structure for documenting components in the Cube UI Kit. Following these guidelines ensures consistency, clarity, and comprehensive coverage of component features.
Expand Down
1 change: 0 additions & 1 deletion .cursor/rules/guidelines.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,6 @@ Specific test: `$ pnpm test -- {TestFileName}`

# Recomendations

- Use `DOCUMENTATION_GUIDELINES.md` for writing documentation for components.
- Use icons from `/src/icons` if they have a required one. If not - use `tabler/icons-react`. If we need to customize the size or color of the icon, then wrap it with `<Icon/>` component and pass all required props there. Do not add any props to the tabler icons directly.

## Form System
Expand Down
151 changes: 151 additions & 0 deletions .cursor/rules/storybook.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
globs: *.stories.tsx,*.docs.mdx
alwaysApply: false
---
## Imports

### Stories Files (.stories.tsx)
- Import types: `import type { Meta, StoryObj } from '@storybook/react-vite';`
- Import `StoryFn` for custom template functions
- For interactive tests: `import { userEvent, within } from 'storybook/test';` (NOT from `@testing-library/react`)

### Documentation Files (.docs.mdx)
- `import { Meta, Canvas, Story, Controls } from '@storybook/addon-docs/blocks';`
- `Meta` - Define meta information with `<Meta of={StoriesImport} />`
- `Canvas` - Display story with code panel
- `Story` - Reference specific story with `<Story of={StoriesImport.StoryName} />`
- `Controls` - Display interactive controls table with `<Controls of={StoriesImport.StoryName} />`
- `ArgTypes` - Display argument types documentation
- `Source` - Show code examples
- Import stories: `import * as ComponentStories from './Component.stories';`

## Meta Configuration

Use `satisfies Meta<typeof Component>` or `as Meta<typeof Component>`:

```tsx
const meta = {
title: 'Category/ComponentName',
component: ComponentName,
subcomponents: { Item: Component.Item }, // For compound components
args: { /* common default args */ },
parameters: { controls: { exclude: baseProps } }, // Exclude base design system props
argTypes: { /* ... */ }
} satisfies Meta<typeof Component>;

export default meta;
```

## ArgTypes Structure

Group by categories with comments:
- `/* Content */` - children, labels, placeholders, icons
- `/* Selection */` - selectedKey, defaultSelectedKey
- `/* Behavior */` - filter, trigger modes, loading states
- `/* Presentation */` - type, theme, size, direction
- `/* State */` - isDisabled, isRequired, isReadOnly, validationState, autoFocus
- `/* Events */` - onPress, onChange, onSelectionChange, onBlur, onFocus

### ArgType Format

```tsx
propName: {
control: { type: 'radio' | 'boolean' | 'text' | 'number' | null },
options: ['option1', 'option2'], // For radio/select
description: 'Clear description',
table: {
defaultValue: { summary: 'value' },
type: { summary: 'string' }
}
}
```

- Use `control: { type: null }` to disable controls (for functions, complex types)
- Use `action: 'event-name'` for event handlers
- Use `action: (e) => ({ type: 'event', data })` for custom action logging

## Stories

### Named Exports (Preferred)
```tsx
export const StoryName = (args) => <Component {...args} />;
```

### Story Objects with CSF3
```tsx
export const StoryName: StoryObj<typeof Component> = {
render: (args) => <Component {...args} />,
args: { /* story-specific args */ },
play: async ({ canvasElement }) => {
// Interactive test
}
};
```

### Templates (Legacy Pattern)
```tsx
const Template: StoryFn<ComponentProps> = (args) => <Component {...args} />;

export const Story = Template.bind({});
Story.args = { /* ... */ };
```

## Testing with Play Functions

```tsx
export const Interactive: StoryObj = {
render: () => { /* ... */ },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const element = canvas.getByRole('button');
await userEvent.click(element);
}
};
```

**Important:** Always import `userEvent` and `within` from `'storybook/test'` in story files. This ensures they respect Storybook's configuration (e.g., `testIdAttribute: 'data-qa'` set in `.storybook/preview.jsx`). Do NOT use `@testing-library/react` imports in stories.

## MDX Documentation Structure

```mdx
import { Meta, Canvas, Story, Controls } from '@storybook/addon-docs/blocks';
import * as ComponentStories from './Component.stories';

<Meta of={ComponentStories} />

# ComponentName

Component description

## When to Use
- Use case 1
- Use case 2

## Component

<Story of={ComponentStories.Default} />

---

### Properties

<Controls of={ComponentStories.Default} />

### Base Properties

Reference base properties support here.

## Examples

### Example Section

<Story of={ComponentStories.ExampleStory} />
```

## Common Patterns

- Use `baseProps` exclusion for design system props
- Define default `width` in meta `args` for form components
- Create size/state matrix stories to show all variants
- Use `Space` component for layout in template functions
- Export type: `type Story = StoryObj<typeof meta>;`
208 changes: 208 additions & 0 deletions .cursor/rules/tests.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
---
globs: *.test.tsx
alwaysApply: false
---

# Testing Rules for UI Kit

## Setup

- Place test files next to components with `.test.tsx` extension
- Mock internal warnings: `jest.mock('../../../_internal/hooks/use-warn')`
- Define test data at the top of `describe` blocks

## Render Functions

**Choose based on component requirements:**
- `render()` - Simple components (Button, Text, standalone UI elements)
- `renderWithRoot()` - Components using overlays, popovers, portals, modals, or complex interactions (Select, ComboBox, Menu, Dialog, etc.)
- `renderWithForm()` - Form-integrated components (returns `{ formInstance, ...renderResult }`)

Note: Root provides ModalProvider, PortalProvider, EventBusProvider, NotificationsProvider, and styled-components context. Most interactive components need it, but simple presentational components don't.

## User Interactions

**Always use `userEvent` for interactions:**
- `await userEvent.click(element)`
- `await userEvent.type(input, 'text')`
- `await userEvent.clear(input)`
- `await userEvent.keyboard('{Enter}')` / `'{Escape}'` / `'{ArrowDown}'` / `'{Home}'` / etc.
- `await userEvent.tab()`

**Focus management:**
```tsx
await act(async () => {
element.focus();
element.blur();
});
```

## Async Testing

Use `waitFor()` for async state changes:
```tsx
await userEvent.click(button);

await waitFor(() => {
expect(element).toBeInTheDocument();
});
```

Wait for removal: `await waitForElementToBeRemoved(() => queryByRole('dialog'))`

## Common Patterns

### Basic Rendering & User Interactions
```tsx
it('should handle button press', async () => {
const onPress = jest.fn();
const { getByRole } = render(<Button onPress={onPress}>Label</Button>);

await userEvent.click(getByRole('button'));

expect(onPress).toHaveBeenCalled();
});
```

### Popover/Overlay State
```tsx
it('should open and close popover', async () => {
const { getByRole, queryByRole } = renderWithRoot(<Select label="test">...</Select>);

expect(queryByRole('listbox')).not.toBeInTheDocument();

await userEvent.click(getByRole('button'));
await waitFor(() => expect(queryByRole('listbox')).toBeInTheDocument());

await userEvent.keyboard('{Escape}');
await waitFor(() => expect(queryByRole('listbox')).not.toBeInTheDocument());
});
```

### Form Integration
```tsx
// Modern Form integration
it('should integrate with Form', async () => {
const { getByRole, formInstance } = renderWithForm(
<TextInput name="test" label="test" />
);

await userEvent.type(getByRole('textbox'), 'Hello');
expect(formInstance.getFieldValue('test')).toBe('Hello');
});

// Legacy Field wrapper
it('should interop with legacy <Field />', async () => {
const { getByRole, formInstance } = renderWithForm(
<Field name="test"><Switch aria-label="test" /></Field>
);

await userEvent.click(getByRole('switch'));
expect(formInstance.getFieldValue('test')).toBe(true);
});
```

### Props & State Changes (Rerender)
```tsx
it('should respect state props', async () => {
const { getByRole, rerender } = renderWithRoot(<Component isDisabled />);

expect(getByRole('button')).toBeDisabled();

rerender(<Component isReadOnly />);
expect(getByRole('button')).toHaveAttribute('readonly');
});
```

### Keyboard Navigation
```tsx
it('should handle keyboard navigation', async () => {
const { getByRole } = renderWithRoot(<ComboBox label="test">...</ComboBox>);
const input = getByRole('combobox');

await act(async () => {
input.focus();
await userEvent.keyboard('{ArrowDown}');
});

await waitFor(() => expect(input).toHaveAttribute('aria-expanded', 'true'));
});
```

### Filtering/Search
```tsx
it('should filter options', async () => {
const { getByRole, getAllByRole } = renderWithRoot(<ComboBox label="test">{items}</ComboBox>);

await userEvent.type(getByRole('combobox'), 'red');

await waitFor(() => {
expect(getAllByRole('option')).toHaveLength(2);
});
});
```

### Validation
```tsx
it('should display validation errors', async () => {
const { getByRole, getByText } = renderWithForm(
<TextInput name="test" label="test" rules={[{ required: true, message: 'Required' }]} />
);

await userEvent.type(getByRole('textbox'), 'a');
await userEvent.clear(getByRole('textbox'));
await userEvent.tab();

await waitFor(() => expect(getByText('Required')).toBeInTheDocument());
});
```

### Parameterized Tests
```tsx
it.each([
['aria-label', { 'aria-label': 'test' }],
['label', { label: 'test' }],
])('should not warn if %s is provided', (_, props) => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
render(<Button {...props} />);
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
});
```

## Query Selectors Priority

1. `getByRole()` - most accessible
2. `getByLabelText()` - form fields
3. `getByTestId()` - QA attributes (`qa` prop)
4. `getByText()` - text content

Use `query*` when checking non-existence, `getAll*` for multiple elements.

## Common Matchers

- `toBeInTheDocument()`, `toHaveTextContent()`, `toHaveAttribute()`, `toHaveValue()`
- `toBeDisabled()`, `toBeChecked()`
- `toHaveBeenCalled()`, `toHaveBeenCalledWith()`, `toHaveBeenCalledTimes()`

## Best Practices

### Do
- ✅ Test user-facing behavior, not implementation
- ✅ Use semantic queries (`getByRole`)
- ✅ Wait for async changes with `waitFor()`
- ✅ Test accessibility (ARIA, keyboard navigation)
- ✅ Test form integration when supported
- ✅ Use descriptive test names

### Don't
- ❌ Don't test implementation details
- ❌ Don't overuse `act()` (userEvent handles it)
- ❌ Don't query by class names when semantic queries work
- ❌ Don't test `styles` or simple layout props (e.g., `icon` that just renders to slot)
- ❌ Don't forget to mock warnings for clean output

## Debugging

- `screen.debug()` - print DOM
- `waitFor(() => {...}, { timeout: 5000 })` - custom timeout
Loading
Loading