Create MenuPicker (#14681)

Closes https://github.com/twentyhq/core-team-issues/issues/1534

<img width="746" height="324" alt="CleanShot 2025-09-24 at 14 32 52@2x"
src="https://github.com/user-attachments/assets/6c36fe37-b945-4d43-917c-424e7b346902"
/>
This commit is contained in:
Raphaël Bosi
2025-09-24 14:43:05 +02:00
committed by GitHub
parent 21b7cda7af
commit b0e56e4717
36 changed files with 284 additions and 40 deletions
+28 -26
View File
@@ -18,39 +18,41 @@ export { UndecoratedLink } from './link/components/UndecoratedLink';
export { CAL_LINK } from './link/constants/Cal';
export { GITHUB_LINK } from './link/constants/GithubLink';
export { TWENTY_PRICING_LINK } from './link/constants/TwentyPricingLink';
export type { MenuPickerProps } from './menu/components/MenuPicker';
export { MenuPicker } from './menu/components/MenuPicker';
export type {
MenuItemIconButton,
MenuItemProps,
} from './menu-item/components/MenuItem';
export { MenuItem } from './menu-item/components/MenuItem';
export type { MenuItemAvatarProps } from './menu-item/components/MenuItemAvatar';
export { MenuItemAvatar } from './menu-item/components/MenuItemAvatar';
export type { MenuItemCommandProps } from './menu-item/components/MenuItemCommand';
export { MenuItemCommand } from './menu-item/components/MenuItemCommand';
export type { MenuItemCommandHotKeysProps } from './menu-item/components/MenuItemCommandHotKeys';
export { MenuItemCommandHotKeys } from './menu-item/components/MenuItemCommandHotKeys';
export type { MenuItemDraggableProps } from './menu-item/components/MenuItemDraggable';
export { MenuItemDraggable } from './menu-item/components/MenuItemDraggable';
export { MenuItemMultiSelect } from './menu-item/components/MenuItemMultiSelect';
export { MenuItemMultiSelectAvatar } from './menu-item/components/MenuItemMultiSelectAvatar';
export { MenuItemMultiSelectTag } from './menu-item/components/MenuItemMultiSelectTag';
export type { MenuItemNavigateProps } from './menu-item/components/MenuItemNavigate';
export { MenuItemNavigate } from './menu-item/components/MenuItemNavigate';
} from './menu/menu-item/components/MenuItem';
export { MenuItem } from './menu/menu-item/components/MenuItem';
export type { MenuItemAvatarProps } from './menu/menu-item/components/MenuItemAvatar';
export { MenuItemAvatar } from './menu/menu-item/components/MenuItemAvatar';
export type { MenuItemCommandProps } from './menu/menu-item/components/MenuItemCommand';
export { MenuItemCommand } from './menu/menu-item/components/MenuItemCommand';
export type { MenuItemCommandHotKeysProps } from './menu/menu-item/components/MenuItemCommandHotKeys';
export { MenuItemCommandHotKeys } from './menu/menu-item/components/MenuItemCommandHotKeys';
export type { MenuItemDraggableProps } from './menu/menu-item/components/MenuItemDraggable';
export { MenuItemDraggable } from './menu/menu-item/components/MenuItemDraggable';
export { MenuItemMultiSelect } from './menu/menu-item/components/MenuItemMultiSelect';
export { MenuItemMultiSelectAvatar } from './menu/menu-item/components/MenuItemMultiSelectAvatar';
export { MenuItemMultiSelectTag } from './menu/menu-item/components/MenuItemMultiSelectTag';
export type { MenuItemNavigateProps } from './menu/menu-item/components/MenuItemNavigate';
export { MenuItemNavigate } from './menu/menu-item/components/MenuItemNavigate';
export {
StyledMenuItemSelect,
MenuItemSelect,
} from './menu-item/components/MenuItemSelect';
export { MenuItemSelectAvatar } from './menu-item/components/MenuItemSelectAvatar';
} from './menu/menu-item/components/MenuItemSelect';
export { MenuItemSelectAvatar } from './menu/menu-item/components/MenuItemSelectAvatar';
export {
colorLabels,
MenuItemSelectColor,
} from './menu-item/components/MenuItemSelectColor';
export { MenuItemSelectTag } from './menu-item/components/MenuItemSelectTag';
export type { MenuItemSuggestionProps } from './menu-item/components/MenuItemSuggestion';
export { MenuItemSuggestion } from './menu-item/components/MenuItemSuggestion';
export { MenuItemToggle } from './menu-item/components/MenuItemToggle';
export { MenuItemLeftContent } from './menu-item/internals/components/MenuItemLeftContent';
export type { MenuItemBaseProps } from './menu-item/internals/components/StyledMenuItemBase';
} from './menu/menu-item/components/MenuItemSelectColor';
export { MenuItemSelectTag } from './menu/menu-item/components/MenuItemSelectTag';
export type { MenuItemSuggestionProps } from './menu/menu-item/components/MenuItemSuggestion';
export { MenuItemSuggestion } from './menu/menu-item/components/MenuItemSuggestion';
export { MenuItemToggle } from './menu/menu-item/components/MenuItemToggle';
export { MenuItemLeftContent } from './menu/menu-item/internals/components/MenuItemLeftContent';
export type { MenuItemBaseProps } from './menu/menu-item/internals/components/StyledMenuItemBase';
export {
StyledMenuItemBase,
StyledMenuItemLabel,
@@ -60,8 +62,8 @@ export {
StyledDraggableItem,
StyledHoverableMenuItemBase,
StyledMenuItemIconCheck,
} from './menu-item/internals/components/StyledMenuItemBase';
export type { MenuItemAccent } from './menu-item/types/MenuItemAccent';
} from './menu/menu-item/internals/components/StyledMenuItemBase';
export type { MenuItemAccent } from './menu/menu-item/types/MenuItemAccent';
export { NavigationBar } from './navigation-bar/components/NavigationBar';
export { NavigationBarItem } from './navigation-bar/components/NavigationBarItem';
export { NotificationCounter } from './notification-counter/components/NotificationCounter';
@@ -0,0 +1,152 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { type IconComponent } from '@ui/display';
const StyledMenuItemPicker = styled.button<{
selected: boolean;
disabled: boolean;
}>`
box-sizing: border-box;
background: none;
font: inherit;
outline: inherit;
color: inherit;
align-items: center;
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
cursor: pointer;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(1)};
justify-content: center;
min-height: ${({ theme }) => theme.spacing(8)};
padding: ${({ theme }) => `${theme.spacing(1.5)} ${theme.spacing(1)}`};
transition: all ${({ theme }) => theme.animation.duration.instant}s ease;
user-select: none;
width: 100%;
${({ theme, selected, disabled }) => {
if (disabled) {
return css`
background: inherit;
border-color: ${theme.border.color.medium};
color: ${theme.font.color.tertiary};
cursor: default;
&:hover {
background: inherit;
}
`;
}
if (selected) {
return css`
background: transparent;
border-color: ${theme.color.blue};
color: ${theme.color.blue};
&:hover {
background: ${theme.background.transparent.primary};
}
`;
}
return css`
background: transparent;
border-color: ${theme.border.color.medium};
color: ${theme.font.color.tertiary};
&:hover {
background: ${theme.background.transparent.light};
}
`;
}}
`;
const StyledIconContainer = styled.div`
align-items: center;
display: flex;
flex-shrink: 0;
height: ${({ theme }) => theme.icon.size.md}px;
justify-content: center;
width: ${({ theme }) => theme.icon.size.md}px;
`;
const StyledLabel = styled.div<{
disabled: boolean;
selected: boolean;
}>`
font-family: ${({ theme }) => theme.font.family};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
line-height: 1;
max-width: 100%;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
${({ theme, selected, disabled }) => {
if (disabled) {
return css`
color: ${theme.font.color.tertiary};
`;
}
if (selected) {
return css`
color: ${theme.color.blue};
`;
}
return css`
color: ${theme.font.color.tertiary};
`;
}}
`;
export type MenuPickerProps = {
className?: string;
disabled?: boolean;
icon: IconComponent;
label: string;
onClick?: () => void;
selected?: boolean;
showLabel?: boolean;
testId?: string;
};
export const MenuPicker = ({
icon: Icon,
label,
selected = false,
disabled = false,
showLabel = true,
onClick,
className,
testId,
}: MenuPickerProps) => {
const theme = useTheme();
return (
<StyledMenuItemPicker
selected={selected}
disabled={disabled}
onClick={onClick}
className={className}
data-testid={testId}
aria-pressed={selected}
aria-disabled={disabled}
>
<StyledIconContainer>
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
</StyledIconContainer>
{showLabel && (
<StyledLabel selected={selected} disabled={disabled}>
{label}
</StyledLabel>
)}
</StyledMenuItemPicker>
);
};
@@ -0,0 +1,86 @@
import styled from '@emotion/styled';
import { type Meta, type StoryObj } from '@storybook/react';
import { IconChartPie } from '@ui/display';
import { MenuPicker } from '@ui/navigation/menu/components/MenuPicker';
import { ComponentDecorator } from '@ui/testing';
const meta: Meta<typeof MenuPicker> = {
title: 'UI/Navigation/Menu/MenuPicker',
component: MenuPicker,
decorators: [ComponentDecorator],
args: {
icon: IconChartPie,
label: 'Chart',
selected: false,
disabled: false,
showLabel: true,
},
argTypes: {
icon: {
control: false,
},
onClick: {
action: 'clicked',
},
},
};
export default meta;
type Story = StoryObj<typeof MenuPicker>;
export const Default: Story = {};
export const Selected: Story = {
args: {
selected: true,
},
};
export const Disabled: Story = {
args: {
disabled: true,
},
};
export const WithoutLabel: Story = {
args: {
showLabel: false,
},
};
const StyledTitle = styled.h4`
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
font-size: 12px;
margin-bottom: 8px;
`;
export const AllStates: Story = {
render: () => (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
width: '100%',
gap: '16px',
}}
>
<div>
<StyledTitle>Default</StyledTitle>
<MenuPicker icon={IconChartPie} label="Default" />
</div>
<div>
<StyledTitle>Selected</StyledTitle>
<MenuPicker icon={IconChartPie} label="Selected" selected />
</div>
<div>
<StyledTitle>Disabled</StyledTitle>
<MenuPicker icon={IconChartPie} label="Disabled" disabled />
</div>
<div>
<StyledTitle>No Label</StyledTitle>
<MenuPicker icon={IconChartPie} label="No Label" showLabel={false} />
</div>
</div>
),
};
@@ -0,0 +1 @@
export * from './MenuPicker';
@@ -0,0 +1,2 @@
export * from './components';
export * from './menu-item';
@@ -1,8 +1,8 @@
import { useTheme } from '@emotion/react';
import { Avatar, type AvatarProps, IconChevronRight } from '@ui/display';
import { LightIconButtonGroup } from '@ui/input';
import { type MenuItemIconButton } from '@ui/navigation/menu-item/components/MenuItem';
import { MenuItemLeftContent } from '@ui/navigation/menu-item/internals/components/MenuItemLeftContent';
import { type MenuItemIconButton } from '@ui/navigation/menu/menu-item/components/MenuItem';
import { MenuItemLeftContent } from '@ui/navigation/menu/menu-item/internals/components/MenuItemLeftContent';
import { type MouseEvent, type ReactNode } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
@@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import { Tag } from '@ui/components';
import { type IconComponent } from '@ui/display';
import { Checkbox } from '@ui/input/components/Checkbox';
import { MenuItemLeftContent } from '@ui/navigation/menu-item/internals/components/MenuItemLeftContent';
import { MenuItemLeftContent } from '@ui/navigation/menu/menu-item/internals/components/MenuItemLeftContent';
import { type ThemeColor } from '@ui/theme';
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
@@ -11,7 +11,7 @@ import { type MenuItemAccent } from '../../types/MenuItemAccent';
import { MenuItem } from '../MenuItem';
const meta: Meta<typeof MenuItem> = {
title: 'UI/Navigation/MenuItem/MenuItem',
title: 'UI/Navigation/Menu/MenuItem/MenuItem',
component: MenuItem,
};
@@ -9,7 +9,7 @@ import {
import { MenuItemCommand } from '../MenuItemCommand';
const meta: Meta<typeof MenuItemCommand> = {
title: 'UI/Navigation/MenuItem/MenuItemCommand',
title: 'UI/Navigation/Menu/MenuItem/MenuItemCommand',
component: MenuItemCommand,
};
@@ -12,7 +12,7 @@ import { type MenuItemAccent } from '../../types/MenuItemAccent';
import { MenuItemDraggable } from '../MenuItemDraggable';
const meta: Meta<typeof MenuItemDraggable> = {
title: 'ui/Navigation/MenuItem/MenuItemDraggable',
title: 'UI/Navigation/Menu/MenuItem/MenuItemDraggable',
component: MenuItemDraggable,
};
@@ -11,7 +11,7 @@ import {
import { MenuItemMultiSelect } from '../MenuItemMultiSelect';
const meta: Meta<typeof MenuItemMultiSelect> = {
title: 'UI/Navigation/MenuItem/MenuItemMultiSelect',
title: 'UI/Navigation/Menu/MenuItem/MenuItemMultiSelect',
component: MenuItemMultiSelect,
};
@@ -13,7 +13,7 @@ import {
import { MenuItemMultiSelectAvatar } from '../MenuItemMultiSelectAvatar';
const meta: Meta<typeof MenuItemMultiSelectAvatar> = {
title: 'UI/Navigation/MenuItem/MenuItemMultiSelectAvatar',
title: 'UI/Navigation/Menu/MenuItem/MenuItemMultiSelectAvatar',
component: MenuItemMultiSelectAvatar,
};
@@ -12,7 +12,7 @@ import {
} from '@ui/testing';
const meta: Meta<typeof MenuItemNavigate> = {
title: 'UI/Navigation/MenuItem/MenuItemNavigate',
title: 'UI/Navigation/Menu/MenuItem/MenuItemNavigate',
component: MenuItemNavigate,
};
@@ -11,7 +11,7 @@ import { MenuItemSelect } from '../MenuItemSelect';
import { IconBell } from '@ui/display';
const meta: Meta<typeof MenuItemSelect> = {
title: 'UI/Navigation/MenuItem/MenuItemSelect',
title: 'UI/Navigation/Menu/MenuItem/MenuItemSelect',
component: MenuItemSelect,
};
@@ -13,7 +13,7 @@ import {
import { MenuItemSelectAvatar } from '../MenuItemSelectAvatar';
const meta: Meta<typeof MenuItemSelectAvatar> = {
title: 'UI/Navigation/MenuItem/MenuItemSelectAvatar',
title: 'UI/Navigation/Menu/MenuItem/MenuItemSelectAvatar',
component: MenuItemSelectAvatar,
};
@@ -12,7 +12,7 @@ import { MAIN_COLOR_NAMES, type ThemeColor } from '@ui/theme';
import { MenuItemSelectColor } from '../MenuItemSelectColor';
const meta: Meta<typeof MenuItemSelectColor> = {
title: 'UI/Navigation/MenuItem/MenuItemSelectColor',
title: 'UI/Navigation/Menu/MenuItem/MenuItemSelectColor',
component: MenuItemSelectColor,
};
@@ -11,7 +11,7 @@ import { type ThemeColor } from '@ui/theme';
import { MenuItemSelectTag } from '../MenuItemSelectTag';
const meta: Meta<typeof MenuItemSelectTag> = {
title: 'UI/Navigation/MenuItem/MenuItemSelectTag',
title: 'UI/Navigation/Menu/MenuItem/MenuItemSelectTag',
component: MenuItemSelectTag,
};
@@ -11,7 +11,7 @@ import {
import { MenuItemToggle } from '../MenuItemToggle';
const meta: Meta<typeof MenuItemToggle> = {
title: 'UI/Navigation/MenuItem/MenuItemToggle',
title: 'UI/Navigation/Menu/MenuItem/MenuItemToggle',
component: MenuItemToggle,
};
@@ -1,3 +1,4 @@
export * from '../../components/MenuPicker';
export * from './MenuItem';
export * from './MenuItemAvatar';
export * from './MenuItemCommand';