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:
@@ -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';
|
||||
+2
-2
@@ -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 {
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+1
-1
@@ -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,
|
||||
};
|
||||
|
||||
+1
-1
@@ -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,
|
||||
};
|
||||
|
||||
+1
-1
@@ -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,
|
||||
};
|
||||
|
||||
+1
-1
@@ -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,
|
||||
};
|
||||
|
||||
+1
-1
@@ -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,
|
||||
};
|
||||
|
||||
+1
-1
@@ -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,
|
||||
};
|
||||
|
||||
+1
-1
@@ -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,
|
||||
};
|
||||
|
||||
+1
-1
@@ -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,
|
||||
};
|
||||
|
||||
+1
-1
@@ -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,
|
||||
};
|
||||
|
||||
+1
-1
@@ -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,
|
||||
};
|
||||
|
||||
+1
-1
@@ -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
@@ -1,3 +1,4 @@
|
||||
export * from '../../components/MenuPicker';
|
||||
export * from './MenuItem';
|
||||
export * from './MenuItemAvatar';
|
||||
export * from './MenuItemCommand';
|
||||
Reference in New Issue
Block a user