feat: add DropdownMenuInput and use as view name input in board (#1680)
Closes #1510
This commit is contained in:
@ -1,43 +1,23 @@
|
||||
import { forwardRef, InputHTMLAttributes } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { rgba } from '@/ui/theme/constants/colors';
|
||||
import { textInputStyle } from '@/ui/theme/constants/effects';
|
||||
|
||||
const StyledDropdownMenuInputContainer = styled.div`
|
||||
--vertical-padding: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
align-items: center;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: calc(36px - 2 * var(--vertical-padding));
|
||||
padding: var(--vertical-padding) 0;
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
const StyledViewNameInput = styled.input`
|
||||
${textInputStyle}
|
||||
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-sizing: border-box;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
height: 32px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&[type='number']::-webkit-outer-spin-button,
|
||||
&[type='number']::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
&:focus {
|
||||
border-color: ${({ theme }) => theme.color.blue};
|
||||
box-shadow: 0px 0px 0px 3px ${({ theme }) => rgba(theme.color.blue, 0.1)};
|
||||
}
|
||||
`;
|
||||
|
||||
export const DropdownMenuInput = forwardRef<
|
||||
HTMLInputElement,
|
||||
InputHTMLAttributes<HTMLInputElement>
|
||||
>((props, ref) => (
|
||||
<StyledDropdownMenuInputContainer>
|
||||
<StyledInput autoComplete="off" placeholder="Search" {...props} ref={ref} />
|
||||
</StyledDropdownMenuInputContainer>
|
||||
));
|
||||
export { StyledViewNameInput as DropdownMenuInput };
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
box-sizing: border-box;
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export { StyledInputContainer as DropdownMenuInputContainer };
|
||||
@ -0,0 +1,43 @@
|
||||
import { forwardRef, InputHTMLAttributes } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { textInputStyle } from '@/ui/theme/constants/effects';
|
||||
|
||||
const StyledDropdownMenuSearchInputContainer = styled.div`
|
||||
--vertical-padding: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
align-items: center;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: calc(36px - 2 * var(--vertical-padding));
|
||||
padding: var(--vertical-padding) 0;
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
${textInputStyle}
|
||||
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
width: 100%;
|
||||
|
||||
&[type='number']::-webkit-outer-spin-button,
|
||||
&[type='number']::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DropdownMenuSearchInput = forwardRef<
|
||||
HTMLInputElement,
|
||||
InputHTMLAttributes<HTMLInputElement>
|
||||
>((props, ref) => (
|
||||
<StyledDropdownMenuSearchInputContainer>
|
||||
<StyledInput autoComplete="off" placeholder="Search" {...props} ref={ref} />
|
||||
</StyledDropdownMenuSearchInputContainer>
|
||||
));
|
||||
@ -1,8 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { IconPlus, IconUser } from '@/ui/icon';
|
||||
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
|
||||
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
|
||||
import { MenuItemMultiSelectAvatar } from '@/ui/menu-item/components/MenuItemMultiSelectAvatar';
|
||||
@ -12,6 +11,8 @@ import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { DropdownMenuHeader } from '../DropdownMenuHeader';
|
||||
import { DropdownMenuInput } from '../DropdownMenuInput';
|
||||
import { DropdownMenuInputContainer } from '../DropdownMenuInputContainer';
|
||||
import { DropdownMenuSearchInput } from '../DropdownMenuSearchInput';
|
||||
import { StyledDropdownMenu } from '../StyledDropdownMenu';
|
||||
import { StyledDropdownMenuItemsContainer } from '../StyledDropdownMenuItemsContainer';
|
||||
import { StyledDropdownMenuSeparator } from '../StyledDropdownMenuSeparator';
|
||||
@ -23,7 +24,9 @@ const meta: Meta<typeof StyledDropdownMenu> = {
|
||||
decorators: [ComponentDecorator],
|
||||
argTypes: {
|
||||
as: { table: { disable: true } },
|
||||
children: { control: false },
|
||||
theme: { table: { disable: true } },
|
||||
width: { type: 'number', defaultValue: undefined },
|
||||
},
|
||||
};
|
||||
|
||||
@ -122,21 +125,22 @@ const FakeSelectableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => {
|
||||
};
|
||||
|
||||
const FakeCheckableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => {
|
||||
const [selectedItems, setSelectedItems] = useState<string[]>([]);
|
||||
const [selectedItemsById, setSelectedItemsById] = useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
|
||||
return (
|
||||
<>
|
||||
{mockSelectArray.map((item) => (
|
||||
<MenuItemMultiSelectAvatar
|
||||
key={item.id}
|
||||
selected={selectedItems.includes(item.id)}
|
||||
onSelectChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedItems([...selectedItems, item.id]);
|
||||
} else {
|
||||
setSelectedItems(selectedItems.filter((id) => id !== item.id));
|
||||
}
|
||||
}}
|
||||
selected={selectedItemsById[item.id]}
|
||||
onSelectChange={(checked) =>
|
||||
setSelectedItemsById((previous) => ({
|
||||
...previous,
|
||||
[item.id]: checked,
|
||||
}))
|
||||
}
|
||||
avatar={
|
||||
hasAvatar ? (
|
||||
<Avatar
|
||||
@ -154,165 +158,96 @@ const FakeCheckableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const WithContentBelowDecorator: Decorator = (Story) => (
|
||||
<StyledFakeBelowContainer>
|
||||
<FakeContentBelow />
|
||||
<StyledMenuAbsolutePositionWrapper>
|
||||
<Story />
|
||||
</StyledMenuAbsolutePositionWrapper>
|
||||
</StyledFakeBelowContainer>
|
||||
);
|
||||
|
||||
export const Empty: Story = {
|
||||
render: (args) => (
|
||||
<StyledDropdownMenu {...args}>
|
||||
<StyledFakeMenuContent />
|
||||
</StyledDropdownMenu>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithContentBelow: Story = {
|
||||
...Empty,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<StyledFakeBelowContainer>
|
||||
<FakeContentBelow />
|
||||
<StyledMenuAbsolutePositionWrapper>
|
||||
<Story />
|
||||
</StyledMenuAbsolutePositionWrapper>
|
||||
</StyledFakeBelowContainer>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export const SimpleMenuItem: Story = {
|
||||
...WithContentBelow,
|
||||
render: (args) => (
|
||||
<StyledDropdownMenu {...args}>
|
||||
<StyledDropdownMenuItemsContainer hasMaxHeight>
|
||||
{mockSelectArray.map(({ name }) => (
|
||||
<MenuItem text={name} />
|
||||
))}
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
),
|
||||
args: { children: <StyledFakeMenuContent /> },
|
||||
};
|
||||
|
||||
export const WithHeaders: Story = {
|
||||
...WithContentBelow,
|
||||
render: (args) => (
|
||||
<StyledDropdownMenu {...args}>
|
||||
<DropdownMenuHeader>Header</DropdownMenuHeader>
|
||||
<StyledDropdownMenuSeparator />
|
||||
<StyledDropdownMenuSubheader>Subheader 1</StyledDropdownMenuSubheader>
|
||||
<StyledDropdownMenuItemsContainer>
|
||||
{mockSelectArray.slice(0, 3).map(({ name }) => (
|
||||
<MenuItem text={name} />
|
||||
))}
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
<StyledDropdownMenuSeparator />
|
||||
<StyledDropdownMenuSubheader>Subheader 2</StyledDropdownMenuSubheader>
|
||||
<StyledDropdownMenuItemsContainer>
|
||||
{mockSelectArray.slice(3).map(({ name }) => (
|
||||
<MenuItem text={name} />
|
||||
))}
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithIcons: Story = {
|
||||
...WithContentBelow,
|
||||
render: (args) => (
|
||||
<StyledDropdownMenu {...args}>
|
||||
<StyledDropdownMenuItemsContainer hasMaxHeight>
|
||||
{mockSelectArray.map(({ name }) => (
|
||||
<MenuItem text={name} LeftIcon={IconUser} />
|
||||
))}
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithActions: Story = {
|
||||
...WithContentBelow,
|
||||
render: (args) => (
|
||||
<StyledDropdownMenu {...args}>
|
||||
<StyledDropdownMenuItemsContainer hasMaxHeight>
|
||||
{mockSelectArray.map(({ name }, index) => (
|
||||
<MenuItem
|
||||
className={index === 0 ? 'hover' : undefined}
|
||||
iconButtons={[{ Icon: IconUser }, { Icon: IconPlus }]}
|
||||
text={name}
|
||||
/>
|
||||
))}
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
),
|
||||
parameters: {
|
||||
pseudo: { hover: ['.hover'] },
|
||||
decorators: [WithContentBelowDecorator],
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<DropdownMenuHeader>Header</DropdownMenuHeader>
|
||||
<StyledDropdownMenuSeparator />
|
||||
<StyledDropdownMenuSubheader>Subheader 1</StyledDropdownMenuSubheader>
|
||||
<StyledDropdownMenuItemsContainer>
|
||||
{mockSelectArray.slice(0, 3).map(({ name }) => (
|
||||
<MenuItem text={name} />
|
||||
))}
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
<StyledDropdownMenuSeparator />
|
||||
<StyledDropdownMenuSubheader>Subheader 2</StyledDropdownMenuSubheader>
|
||||
<StyledDropdownMenuItemsContainer>
|
||||
{mockSelectArray.slice(3).map(({ name }) => (
|
||||
<MenuItem text={name} />
|
||||
))}
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const LoadingMenu: Story = {
|
||||
...WithContentBelow,
|
||||
render: () => (
|
||||
<StyledDropdownMenu>
|
||||
<DropdownMenuInput value={'query'} autoFocus />
|
||||
<StyledDropdownMenuSeparator />
|
||||
<StyledDropdownMenuItemsContainer hasMaxHeight>
|
||||
<DropdownMenuSkeletonItem />
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
),
|
||||
export const SearchWithLoadingMenu: Story = {
|
||||
decorators: [WithContentBelowDecorator],
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<DropdownMenuSearchInput value="query" autoFocus />
|
||||
<StyledDropdownMenuSeparator />
|
||||
<StyledDropdownMenuItemsContainer hasMaxHeight>
|
||||
<DropdownMenuSkeletonItem />
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const Search: Story = {
|
||||
...WithContentBelow,
|
||||
render: (args) => (
|
||||
<StyledDropdownMenu {...args}>
|
||||
<DropdownMenuInput />
|
||||
<StyledDropdownMenuSeparator />
|
||||
<StyledDropdownMenuItemsContainer hasMaxHeight>
|
||||
{mockSelectArray.map(({ name }) => (
|
||||
<MenuItem text={name} />
|
||||
))}
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
),
|
||||
};
|
||||
|
||||
export const SelectableMenuItem: Story = {
|
||||
...WithContentBelow,
|
||||
render: (args) => (
|
||||
<StyledDropdownMenu {...args}>
|
||||
<StyledDropdownMenuItemsContainer hasMaxHeight>
|
||||
<FakeSelectableMenuItemList />
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
),
|
||||
export const WithInput: Story = {
|
||||
decorators: [WithContentBelowDecorator],
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<DropdownMenuInputContainer>
|
||||
<DropdownMenuInput defaultValue="Lorem ipsum" autoFocus />
|
||||
</DropdownMenuInputContainer>
|
||||
<StyledDropdownMenuSeparator />
|
||||
<StyledDropdownMenuItemsContainer hasMaxHeight>
|
||||
{mockSelectArray.map(({ name }) => (
|
||||
<MenuItem text={name} />
|
||||
))}
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const SelectableMenuItemWithAvatar: Story = {
|
||||
...WithContentBelow,
|
||||
render: (args) => (
|
||||
<StyledDropdownMenu {...args}>
|
||||
decorators: [WithContentBelowDecorator],
|
||||
args: {
|
||||
children: (
|
||||
<StyledDropdownMenuItemsContainer hasMaxHeight>
|
||||
<FakeSelectableMenuItemList hasAvatar />
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
),
|
||||
};
|
||||
|
||||
export const CheckableMenuItem: Story = {
|
||||
...WithContentBelow,
|
||||
render: (args) => (
|
||||
<StyledDropdownMenu {...args}>
|
||||
<StyledDropdownMenuItemsContainer hasMaxHeight>
|
||||
<FakeCheckableMenuItemList />
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const CheckableMenuItemWithAvatar: Story = {
|
||||
...WithContentBelow,
|
||||
render: (args) => (
|
||||
<StyledDropdownMenu {...args}>
|
||||
decorators: [WithContentBelowDecorator],
|
||||
args: {
|
||||
children: (
|
||||
<StyledDropdownMenuItemsContainer hasMaxHeight>
|
||||
<FakeCheckableMenuItemList hasAvatar />
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { DropdownMenuInput } from '../DropdownMenuInput';
|
||||
|
||||
const meta: Meta<typeof DropdownMenuInput> = {
|
||||
title: 'UI/Dropdown/DropdownMenuInput',
|
||||
component: DropdownMenuInput,
|
||||
decorators: [ComponentDecorator],
|
||||
args: { defaultValue: 'Lorem ipsum' },
|
||||
argTypes: {
|
||||
as: { table: { disable: true } },
|
||||
theme: { table: { disable: true } },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof DropdownMenuInput>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Focused: Story = {
|
||||
args: { autoFocus: true },
|
||||
};
|
||||
Reference in New Issue
Block a user