Skip to content

Commit 465e4eb

Browse files
authored
feat(ComboBox): new component (#818)
1 parent 6e73bb9 commit 465e4eb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+8689
-1898
lines changed

.changeset/clever-crabs-cover.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cube-dev/ui-kit": patch
3+
---
4+
5+
Add DisplayTransition helper component.

.changeset/rare-jars-pretend.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cube-dev/ui-kit": patch
3+
---
4+
5+
Fix Tooltip position and transition.

.cursor/rules/coding.mdc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ alwaysApply: true
1313
# Coding rules
1414

1515
- Use named imports from react (like `useCallback`) instead of using the `React` instance. Avoid: `React.useCallback`.
16+
- Prefer stable `useEvent` callbacks when it's possible.

DOCUMENTATION_GUIDELINES.md renamed to .cursor/rules/documentation.mdc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
---
2+
globs: *.docs.mdx
3+
alwaysApply: false
4+
---
5+
16
# Component Documentation Guidelines
27

38
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.

.cursor/rules/guidelines.mdc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,6 @@ Specific test: `$ pnpm test -- {TestFileName}`
192192

193193
# Recomendations
194194

195-
- Use `DOCUMENTATION_GUIDELINES.md` for writing documentation for components.
196195
- 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.
197196

198197
## Form System

.cursor/rules/storybook.mdc

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
---
2+
globs: *.stories.tsx,*.docs.mdx
3+
alwaysApply: false
4+
---
5+
## Imports
6+
7+
### Stories Files (.stories.tsx)
8+
- Import types: `import type { Meta, StoryObj } from '@storybook/react-vite';`
9+
- Import `StoryFn` for custom template functions
10+
- For interactive tests: `import { userEvent, within } from 'storybook/test';` (NOT from `@testing-library/react`)
11+
12+
### Documentation Files (.docs.mdx)
13+
- `import { Meta, Canvas, Story, Controls } from '@storybook/addon-docs/blocks';`
14+
- `Meta` - Define meta information with `<Meta of={StoriesImport} />`
15+
- `Canvas` - Display story with code panel
16+
- `Story` - Reference specific story with `<Story of={StoriesImport.StoryName} />`
17+
- `Controls` - Display interactive controls table with `<Controls of={StoriesImport.StoryName} />`
18+
- `ArgTypes` - Display argument types documentation
19+
- `Source` - Show code examples
20+
- Import stories: `import * as ComponentStories from './Component.stories';`
21+
22+
## Meta Configuration
23+
24+
Use `satisfies Meta<typeof Component>` or `as Meta<typeof Component>`:
25+
26+
```tsx
27+
const meta = {
28+
title: 'Category/ComponentName',
29+
component: ComponentName,
30+
subcomponents: { Item: Component.Item }, // For compound components
31+
args: { /* common default args */ },
32+
parameters: { controls: { exclude: baseProps } }, // Exclude base design system props
33+
argTypes: { /* ... */ }
34+
} satisfies Meta<typeof Component>;
35+
36+
export default meta;
37+
```
38+
39+
## ArgTypes Structure
40+
41+
Group by categories with comments:
42+
- `/* Content */` - children, labels, placeholders, icons
43+
- `/* Selection */` - selectedKey, defaultSelectedKey
44+
- `/* Behavior */` - filter, trigger modes, loading states
45+
- `/* Presentation */` - type, theme, size, direction
46+
- `/* State */` - isDisabled, isRequired, isReadOnly, validationState, autoFocus
47+
- `/* Events */` - onPress, onChange, onSelectionChange, onBlur, onFocus
48+
49+
### ArgType Format
50+
51+
```tsx
52+
propName: {
53+
control: { type: 'radio' | 'boolean' | 'text' | 'number' | null },
54+
options: ['option1', 'option2'], // For radio/select
55+
description: 'Clear description',
56+
table: {
57+
defaultValue: { summary: 'value' },
58+
type: { summary: 'string' }
59+
}
60+
}
61+
```
62+
63+
- Use `control: { type: null }` to disable controls (for functions, complex types)
64+
- Use `action: 'event-name'` for event handlers
65+
- Use `action: (e) => ({ type: 'event', data })` for custom action logging
66+
67+
## Stories
68+
69+
### Named Exports (Preferred)
70+
```tsx
71+
export const StoryName = (args) => <Component {...args} />;
72+
```
73+
74+
### Story Objects with CSF3
75+
```tsx
76+
export const StoryName: StoryObj<typeof Component> = {
77+
render: (args) => <Component {...args} />,
78+
args: { /* story-specific args */ },
79+
play: async ({ canvasElement }) => {
80+
// Interactive test
81+
}
82+
};
83+
```
84+
85+
### Templates (Legacy Pattern)
86+
```tsx
87+
const Template: StoryFn<ComponentProps> = (args) => <Component {...args} />;
88+
89+
export const Story = Template.bind({});
90+
Story.args = { /* ... */ };
91+
```
92+
93+
## Testing with Play Functions
94+
95+
```tsx
96+
export const Interactive: StoryObj = {
97+
render: () => { /* ... */ },
98+
play: async ({ canvasElement }) => {
99+
const canvas = within(canvasElement);
100+
const element = canvas.getByRole('button');
101+
await userEvent.click(element);
102+
}
103+
};
104+
```
105+
106+
**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.
107+
108+
## MDX Documentation Structure
109+
110+
```mdx
111+
import { Meta, Canvas, Story, Controls } from '@storybook/addon-docs/blocks';
112+
import * as ComponentStories from './Component.stories';
113+
114+
<Meta of={ComponentStories} />
115+
116+
# ComponentName
117+
118+
Component description
119+
120+
## When to Use
121+
- Use case 1
122+
- Use case 2
123+
124+
## Component
125+
126+
<Story of={ComponentStories.Default} />
127+
128+
---
129+
130+
### Properties
131+
132+
<Controls of={ComponentStories.Default} />
133+
134+
### Base Properties
135+
136+
Reference base properties support here.
137+
138+
## Examples
139+
140+
### Example Section
141+
142+
<Story of={ComponentStories.ExampleStory} />
143+
```
144+
145+
## Common Patterns
146+
147+
- Use `baseProps` exclusion for design system props
148+
- Define default `width` in meta `args` for form components
149+
- Create size/state matrix stories to show all variants
150+
- Use `Space` component for layout in template functions
151+
- Export type: `type Story = StoryObj<typeof meta>;`

.cursor/rules/tests.mdc

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
---
2+
globs: *.test.tsx
3+
alwaysApply: false
4+
---
5+
6+
# Testing Rules for UI Kit
7+
8+
## Setup
9+
10+
- Place test files next to components with `.test.tsx` extension
11+
- Mock internal warnings: `jest.mock('../../../_internal/hooks/use-warn')`
12+
- Define test data at the top of `describe` blocks
13+
14+
## Render Functions
15+
16+
**Choose based on component requirements:**
17+
- `render()` - Simple components (Button, Text, standalone UI elements)
18+
- `renderWithRoot()` - Components using overlays, popovers, portals, modals, or complex interactions (Select, ComboBox, Menu, Dialog, etc.)
19+
- `renderWithForm()` - Form-integrated components (returns `{ formInstance, ...renderResult }`)
20+
21+
Note: Root provides ModalProvider, PortalProvider, EventBusProvider, NotificationsProvider, and styled-components context. Most interactive components need it, but simple presentational components don't.
22+
23+
## User Interactions
24+
25+
**Always use `userEvent` for interactions:**
26+
- `await userEvent.click(element)`
27+
- `await userEvent.type(input, 'text')`
28+
- `await userEvent.clear(input)`
29+
- `await userEvent.keyboard('{Enter}')` / `'{Escape}'` / `'{ArrowDown}'` / `'{Home}'` / etc.
30+
- `await userEvent.tab()`
31+
32+
**Focus management:**
33+
```tsx
34+
await act(async () => {
35+
element.focus();
36+
element.blur();
37+
});
38+
```
39+
40+
## Async Testing
41+
42+
Use `waitFor()` for async state changes:
43+
```tsx
44+
await userEvent.click(button);
45+
46+
await waitFor(() => {
47+
expect(element).toBeInTheDocument();
48+
});
49+
```
50+
51+
Wait for removal: `await waitForElementToBeRemoved(() => queryByRole('dialog'))`
52+
53+
## Common Patterns
54+
55+
### Basic Rendering & User Interactions
56+
```tsx
57+
it('should handle button press', async () => {
58+
const onPress = jest.fn();
59+
const { getByRole } = render(<Button onPress={onPress}>Label</Button>);
60+
61+
await userEvent.click(getByRole('button'));
62+
63+
expect(onPress).toHaveBeenCalled();
64+
});
65+
```
66+
67+
### Popover/Overlay State
68+
```tsx
69+
it('should open and close popover', async () => {
70+
const { getByRole, queryByRole } = renderWithRoot(<Select label="test">...</Select>);
71+
72+
expect(queryByRole('listbox')).not.toBeInTheDocument();
73+
74+
await userEvent.click(getByRole('button'));
75+
await waitFor(() => expect(queryByRole('listbox')).toBeInTheDocument());
76+
77+
await userEvent.keyboard('{Escape}');
78+
await waitFor(() => expect(queryByRole('listbox')).not.toBeInTheDocument());
79+
});
80+
```
81+
82+
### Form Integration
83+
```tsx
84+
// Modern Form integration
85+
it('should integrate with Form', async () => {
86+
const { getByRole, formInstance } = renderWithForm(
87+
<TextInput name="test" label="test" />
88+
);
89+
90+
await userEvent.type(getByRole('textbox'), 'Hello');
91+
expect(formInstance.getFieldValue('test')).toBe('Hello');
92+
});
93+
94+
// Legacy Field wrapper
95+
it('should interop with legacy <Field />', async () => {
96+
const { getByRole, formInstance } = renderWithForm(
97+
<Field name="test"><Switch aria-label="test" /></Field>
98+
);
99+
100+
await userEvent.click(getByRole('switch'));
101+
expect(formInstance.getFieldValue('test')).toBe(true);
102+
});
103+
```
104+
105+
### Props & State Changes (Rerender)
106+
```tsx
107+
it('should respect state props', async () => {
108+
const { getByRole, rerender } = renderWithRoot(<Component isDisabled />);
109+
110+
expect(getByRole('button')).toBeDisabled();
111+
112+
rerender(<Component isReadOnly />);
113+
expect(getByRole('button')).toHaveAttribute('readonly');
114+
});
115+
```
116+
117+
### Keyboard Navigation
118+
```tsx
119+
it('should handle keyboard navigation', async () => {
120+
const { getByRole } = renderWithRoot(<ComboBox label="test">...</ComboBox>);
121+
const input = getByRole('combobox');
122+
123+
await act(async () => {
124+
input.focus();
125+
await userEvent.keyboard('{ArrowDown}');
126+
});
127+
128+
await waitFor(() => expect(input).toHaveAttribute('aria-expanded', 'true'));
129+
});
130+
```
131+
132+
### Filtering/Search
133+
```tsx
134+
it('should filter options', async () => {
135+
const { getByRole, getAllByRole } = renderWithRoot(<ComboBox label="test">{items}</ComboBox>);
136+
137+
await userEvent.type(getByRole('combobox'), 'red');
138+
139+
await waitFor(() => {
140+
expect(getAllByRole('option')).toHaveLength(2);
141+
});
142+
});
143+
```
144+
145+
### Validation
146+
```tsx
147+
it('should display validation errors', async () => {
148+
const { getByRole, getByText } = renderWithForm(
149+
<TextInput name="test" label="test" rules={[{ required: true, message: 'Required' }]} />
150+
);
151+
152+
await userEvent.type(getByRole('textbox'), 'a');
153+
await userEvent.clear(getByRole('textbox'));
154+
await userEvent.tab();
155+
156+
await waitFor(() => expect(getByText('Required')).toBeInTheDocument());
157+
});
158+
```
159+
160+
### Parameterized Tests
161+
```tsx
162+
it.each([
163+
['aria-label', { 'aria-label': 'test' }],
164+
['label', { label: 'test' }],
165+
])('should not warn if %s is provided', (_, props) => {
166+
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
167+
render(<Button {...props} />);
168+
expect(spy).not.toHaveBeenCalled();
169+
spy.mockRestore();
170+
});
171+
```
172+
173+
## Query Selectors Priority
174+
175+
1. `getByRole()` - most accessible
176+
2. `getByLabelText()` - form fields
177+
3. `getByTestId()` - QA attributes (`qa` prop)
178+
4. `getByText()` - text content
179+
180+
Use `query*` when checking non-existence, `getAll*` for multiple elements.
181+
182+
## Common Matchers
183+
184+
- `toBeInTheDocument()`, `toHaveTextContent()`, `toHaveAttribute()`, `toHaveValue()`
185+
- `toBeDisabled()`, `toBeChecked()`
186+
- `toHaveBeenCalled()`, `toHaveBeenCalledWith()`, `toHaveBeenCalledTimes()`
187+
188+
## Best Practices
189+
190+
### Do
191+
- ✅ Test user-facing behavior, not implementation
192+
- ✅ Use semantic queries (`getByRole`)
193+
- ✅ Wait for async changes with `waitFor()`
194+
- ✅ Test accessibility (ARIA, keyboard navigation)
195+
- ✅ Test form integration when supported
196+
- ✅ Use descriptive test names
197+
198+
### Don't
199+
- ❌ Don't test implementation details
200+
- ❌ Don't overuse `act()` (userEvent handles it)
201+
- ❌ Don't query by class names when semantic queries work
202+
- ❌ Don't test `styles` or simple layout props (e.g., `icon` that just renders to slot)
203+
- ❌ Don't forget to mock warnings for clean output
204+
205+
## Debugging
206+
207+
- `screen.debug()` - print DOM
208+
- `waitFor(() => {...}, { timeout: 5000 })` - custom timeout

0 commit comments

Comments
 (0)