Skip to content

Commit 54abe1c

Browse files
committed
feat: item actions
1 parent 97925ca commit 54abe1c

File tree

15 files changed

+970
-28
lines changed

15 files changed

+970
-28
lines changed

.changeset/big-pugs-deny.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": minor
3+
---
4+
5+
Allow to add actions to Item, ItemButton, and ItemBase.

src/components/Item.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ReactElement } from 'react';
22
import { Item, ItemProps } from 'react-stately';
33

4+
import { ItemAction } from './actions/ItemAction';
45
import { CubeItemBaseProps } from './content/ItemBase/ItemBase';
56

67
export interface CubeItemProps<T>
@@ -11,6 +12,11 @@ export interface CubeItemProps<T>
1112
[key: string]: any;
1213
}
1314

14-
const _Item = Item as <T>(props: CubeItemProps<T>) => ReactElement;
15+
const _Item = Object.assign(
16+
Item as <T>(props: CubeItemProps<T>) => ReactElement,
17+
{
18+
Action: ItemAction,
19+
},
20+
);
1521

1622
export { _Item as Item };

src/components/actions/CommandMenu/CommandMenu.stories.tsx

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import {
22
IconArrowBack,
33
IconArrowForward,
4+
IconBook,
5+
IconBulb,
46
IconClipboard,
57
IconCopy,
68
IconCut,
9+
IconPlus,
10+
IconReload,
711
IconSelect,
812
} from '@tabler/icons-react';
913
import React, { useState } from 'react';
1014
import { expect, findByRole, userEvent, waitFor, within } from 'storybook/test';
1115

16+
import { ClearIcon, EditIcon } from '../../../icons';
1217
import { tasty } from '../../../tasty';
1318
import { Card } from '../../content/Card/Card';
1419
import { HotKeys } from '../../content/HotKeys';
@@ -23,6 +28,7 @@ import {
2328
useDialogContainer,
2429
} from '../../overlays/Dialog';
2530
import { Button } from '../Button';
31+
import { ItemAction } from '../ItemAction';
2632
import { Menu } from '../Menu/Menu';
2733
import { useAnchoredMenu } from '../use-anchored-menu';
2834
import { useContextMenu } from '../use-context-menu';
@@ -376,6 +382,107 @@ WithSections.args = {
376382
width: '20x 50x',
377383
};
378384

385+
export const ItemsWithActions = (props) => {
386+
const handleAction = (key) => {
387+
console.log('CommandMenu action:', key);
388+
};
389+
390+
const handleItemAction = (itemKey, actionKey) => {
391+
console.log(`Action "${actionKey}" triggered on item "${itemKey}"`);
392+
};
393+
394+
return (
395+
<CommandMenu
396+
id="command-menu-with-actions"
397+
{...props}
398+
searchPlaceholder="Search files..."
399+
autoFocus={true}
400+
width="20x 50x"
401+
onAction={handleAction}
402+
>
403+
<CommandMenu.Item
404+
key="file1"
405+
icon={<IconBook />}
406+
description="PDF document"
407+
actions={
408+
<>
409+
<ItemAction
410+
icon={<EditIcon />}
411+
aria-label="Edit"
412+
onPress={() => handleItemAction('file1', 'edit')}
413+
/>
414+
<ItemAction
415+
icon={<ClearIcon />}
416+
aria-label="Delete"
417+
onPress={() => handleItemAction('file1', 'delete')}
418+
/>
419+
</>
420+
}
421+
>
422+
Document.pdf
423+
</CommandMenu.Item>
424+
<CommandMenu.Item
425+
key="file2"
426+
icon={<IconReload />}
427+
description="Backup file"
428+
actions={
429+
<>
430+
<ItemAction
431+
icon={<EditIcon />}
432+
aria-label="Edit"
433+
onPress={() => handleItemAction('file2', 'edit')}
434+
/>
435+
<ItemAction
436+
icon={<ClearIcon />}
437+
aria-label="Delete"
438+
onPress={() => handleItemAction('file2', 'delete')}
439+
/>
440+
</>
441+
}
442+
>
443+
Backup.zip
444+
</CommandMenu.Item>
445+
<CommandMenu.Item
446+
key="file3"
447+
icon={<IconPlus />}
448+
description="New file"
449+
actions={
450+
<>
451+
<ItemAction
452+
icon={<EditIcon />}
453+
aria-label="Edit"
454+
onPress={() => handleItemAction('file3', 'edit')}
455+
/>
456+
<ItemAction
457+
icon={<ClearIcon />}
458+
aria-label="Delete"
459+
onPress={() => handleItemAction('file3', 'delete')}
460+
/>
461+
</>
462+
}
463+
>
464+
Project.doc
465+
</CommandMenu.Item>
466+
<CommandMenu.Item
467+
key="file4"
468+
icon={<IconBulb />}
469+
description="No actions"
470+
>
471+
Item without actions
472+
</CommandMenu.Item>
473+
</CommandMenu>
474+
);
475+
};
476+
477+
ItemsWithActions.parameters = {
478+
docs: {
479+
description: {
480+
story:
481+
'Demonstrates CommandMenu.Item with inline actions. Actions are displayed on the right side of each item and inherit the item type through ItemActionProvider context. Search to filter items and hover to see the actions.',
482+
},
483+
},
484+
};
485+
379486
export const WithMenuTrigger: StoryFn<CubeCommandMenuProps<any>> = (args) => (
380487
<CommandMenu.Trigger>
381488
<Button>Open Command Palette</Button>

src/components/actions/ItemAction/ItemAction.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,40 @@ import { forwardRef } from 'react';
33

44
import { tasty } from '../../../tasty';
55
import { Button, CubeButtonProps } from '../Button';
6+
import { useItemActionContext } from '../ItemActionContext';
67

78
export interface CubeItemActionProps extends CubeButtonProps {
89
// All props from Button are inherited
910
}
1011

11-
export const ItemAction = tasty(Button, {
12-
type: 'neutral',
12+
const StyledButton = tasty(Button, {
1313
styles: {
14+
border: 0,
1415
height: '($size - 1x)',
1516
width: '($size - 1x)',
1617
margin: {
1718
'': '0 1bw 0 1bw',
1819
':last-child & !:first-child': '0 (.5x - 1bw) 0 0',
1920
'!:last-child & :first-child': '0 0 0 (.5x - 1bw)',
2021
':last-child & :first-child': '0 (.5x - 1bw)',
22+
context: '0',
2123
},
2224
},
2325
});
26+
27+
export const ItemAction = forwardRef(function ItemAction(
28+
props: CubeItemActionProps,
29+
ref: FocusableRef<HTMLElement>,
30+
) {
31+
const { type: contextType } = useItemActionContext();
32+
const { type = contextType ?? 'neutral', ...rest } = props;
33+
34+
return (
35+
<StyledButton
36+
{...rest}
37+
ref={ref}
38+
mods={{ context: !!contextType }}
39+
type={type}
40+
/>
41+
);
42+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { createContext, ReactNode, useContext } from 'react';
2+
3+
import { CubeItemBaseProps } from '../content/ItemBase';
4+
5+
interface ItemActionContextValue {
6+
type?: CubeItemBaseProps['type'];
7+
}
8+
9+
const ItemActionContext = createContext<ItemActionContextValue | undefined>(
10+
undefined,
11+
);
12+
13+
export interface ItemActionProviderProps {
14+
type?: CubeItemBaseProps['type'];
15+
children: ReactNode;
16+
}
17+
18+
export function ItemActionProvider({
19+
type,
20+
children,
21+
}: ItemActionProviderProps) {
22+
return (
23+
<ItemActionContext.Provider
24+
value={{ type: type === 'item' ? 'neutral' : type }}
25+
>
26+
{children}
27+
</ItemActionContext.Provider>
28+
);
29+
}
30+
31+
export function useItemActionContext(): ItemActionContextValue {
32+
return useContext(ItemActionContext) ?? {};
33+
}

src/components/actions/ItemButton/ItemButton.docs.mdx

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ ItemButton inherits all properties from [ItemBase](/docs/content-itembase--docs)
3737
- Interactive properties: `hotkeys`, `tooltip`, `isSelected`
3838
- Styling properties: `size`, `type`, `theme`, `styles`
3939

40+
### Content Properties
41+
42+
#### actions
43+
- **Type**: `ReactNode`
44+
- **Description**: Inline action buttons displayed on the right side of the button. Use `ItemButton.Action` for consistent styling. Actions automatically inherit the parent button's `type` prop and the component reserves space to prevent content overlap.
45+
4046
### Action Properties
4147

4248
#### onPress
@@ -161,6 +167,27 @@ Inherits all modifiers from [ItemBase](/docs/content-itembase--docs) plus:
161167
</ItemButton>
162168
```
163169

170+
### Button with Actions
171+
172+
ItemButton supports inline actions that appear on the right side. Use the `ItemButton.Action` compound component for consistent styling:
173+
174+
```jsx
175+
<ItemButton
176+
icon={<IconFile />}
177+
actions={
178+
<>
179+
<ItemButton.Action icon={<IconEdit />} aria-label="Edit" onPress={handleEdit} />
180+
<ItemButton.Action icon={<IconTrash />} aria-label="Delete" onPress={handleDelete} />
181+
</>
182+
}
183+
onPress={handleOpen}
184+
>
185+
Document with Actions
186+
</ItemButton>
187+
```
188+
189+
Actions automatically inherit the parent button's `type` prop and adjust their styling accordingly. The component reserves space for actions to prevent content overlap.
190+
164191
## Accessibility
165192

166193
### Keyboard Navigation
@@ -207,13 +234,46 @@ Inherits all modifiers from [ItemBase](/docs/content-itembase--docs) plus:
207234
</ItemButton>
208235
```
209236

210-
4. **Don't**: Use vague or unclear button text
237+
4. **Do**: Use `ItemButton.Action` for inline actions
238+
```jsx
239+
<ItemButton
240+
icon={<IconFile />}
241+
actions={
242+
<>
243+
<ItemButton.Action icon={<IconEdit />} aria-label="Edit" />
244+
<ItemButton.Action icon={<IconTrash />} aria-label="Delete" />
245+
</>
246+
}
247+
onPress={handleOpen}
248+
>
249+
Open Document
250+
</ItemButton>
251+
```
252+
253+
5. **Don't**: Use vague or unclear button text
211254
```jsx
212255
{/* Avoid this - unclear what "OK" does */}
213256
<ItemButton onPress={handleAction}>OK</ItemButton>
214257
```
215258

216-
5. **Don't**: Overcrowd buttons with too many visual elements
259+
6. **Don't**: Add too many actions (limit to 2-3 for clarity)
260+
```jsx
261+
{/* Avoid this - too many action buttons */}
262+
<ItemButton
263+
actions={
264+
<>
265+
<ItemButton.Action icon={<IconEdit />} aria-label="Edit" />
266+
<ItemButton.Action icon={<IconCopy />} aria-label="Copy" />
267+
<ItemButton.Action icon={<IconShare />} aria-label="Share" />
268+
<ItemButton.Action icon={<IconTrash />} aria-label="Delete" />
269+
</>
270+
}
271+
>
272+
Button with too many actions
273+
</ItemButton>
274+
```
275+
276+
7. **Don't**: Overcrowd buttons with too many visual elements
217277
```jsx
218278
{/* Avoid this - too many competing elements */}
219279
<ItemButton
@@ -227,7 +287,7 @@ Inherits all modifiers from [ItemBase](/docs/content-itembase--docs) plus:
227287
</ItemButton>
228288
```
229289

230-
6. **Accessibility**: Always ensure buttons have clear, descriptive labels and proper keyboard support
290+
8. **Accessibility**: Always ensure buttons have clear, descriptive labels, proper keyboard support, and provide `aria-label` for action buttons
231291

232292
## Integration with Forms
233293

@@ -248,6 +308,7 @@ ItemButton integrates seamlessly with forms when using the `buttonType` prop:
248308
## Related Components
249309

250310
- [ItemBase](/docs/content-itembase--docs) - The foundational component that ItemButton extends
311+
- [ItemButton.Action](/docs/actions-itemaction--docs) - Action button component for inline actions (also available as `ItemBase.Action`, `Menu.Item.Action`, etc.)
251312
- [Button](/docs/actions-button--docs) - Traditional button component for simpler use cases
252313
- [Link](/docs/navigation-link--docs) - Text link component for navigation
253314
- [Menu.Item](/docs/actions-menu--docs) - Menu item component that also uses ItemBase

0 commit comments

Comments
 (0)