Chore: Improve dropdown draggable list (#1738)
* draggable menu item component * Menu item isDragged prop removed * Droppable list component * Draggablee item component * Drag and drop use refactor * lint fix * isDragDisabled check on DraggableItem * revert changes on non visibility items * MenuItemDraggable stroybook * DraggableItem storybook * lint fix * lint fix * BoardColumnMenu css fix * showGrip prop addition * isDragged css fix
This commit is contained in:
@ -24,7 +24,6 @@ import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope';
|
||||
|
||||
import { BoardColumnEditTitleMenu } from './BoardColumnEditTitleMenu';
|
||||
const StyledMenuContainer = styled.div`
|
||||
left: 26.5px;
|
||||
position: absolute;
|
||||
top: ${({ theme }) => theme.spacing(10)};
|
||||
width: 200px;
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { IconBell } from '@/ui/icon';
|
||||
import { MenuItemDraggable } from '@/ui/menu-item/components/MenuItemDraggable';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { DraggableItem } from '../components/DraggableItem';
|
||||
import { DroppableList } from '../components/DroppableList';
|
||||
|
||||
const meta: Meta<typeof DraggableItem> = {
|
||||
title: 'ui/draggable-list/DraggableItem',
|
||||
component: DraggableItem,
|
||||
decorators: [
|
||||
(Story, { parameters }) => (
|
||||
<DroppableList
|
||||
droppableId={parameters.droppableId}
|
||||
onDragEnd={parameters.onDragEnd}
|
||||
draggableItems={<Story />}
|
||||
/>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
parameters: {
|
||||
droppableId: 'droppable',
|
||||
onDragEnd: () => console.log('dragged'),
|
||||
},
|
||||
args: {
|
||||
draggableId: 'draggable-1',
|
||||
key: 'key-1',
|
||||
index: 0,
|
||||
isDragDisabled: false,
|
||||
itemComponent: (
|
||||
<>
|
||||
<MenuItemDraggable
|
||||
LeftIcon={IconBell}
|
||||
key="key-1"
|
||||
text="Draggable item 1"
|
||||
/>
|
||||
<MenuItemDraggable
|
||||
LeftIcon={IconBell}
|
||||
key="key-2"
|
||||
text="Draggable item 2"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof DraggableItem>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { Draggable } from '@hello-pangea/dnd';
|
||||
|
||||
type DraggableItemProps = {
|
||||
key: string;
|
||||
draggableId: string;
|
||||
isDragDisabled?: boolean;
|
||||
index: number;
|
||||
itemComponent: JSX.Element;
|
||||
};
|
||||
|
||||
export const DraggableItem = ({
|
||||
key,
|
||||
draggableId,
|
||||
isDragDisabled = false,
|
||||
index,
|
||||
itemComponent,
|
||||
}: DraggableItemProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Draggable
|
||||
key={key}
|
||||
draggableId={draggableId}
|
||||
index={index}
|
||||
isDragDisabled={isDragDisabled}
|
||||
>
|
||||
{(draggableProvided, draggableSnapshot) => {
|
||||
const draggableStyle = draggableProvided.draggableProps.style;
|
||||
const isDragged = draggableSnapshot.isDragging;
|
||||
return (
|
||||
<div
|
||||
ref={draggableProvided.innerRef}
|
||||
{...{
|
||||
...draggableProvided.draggableProps,
|
||||
style: {
|
||||
...draggableStyle,
|
||||
left: 'auto',
|
||||
top: 'auto',
|
||||
transform: draggableStyle?.transform?.replace(
|
||||
/\(-?\d+px,/,
|
||||
'(0,',
|
||||
),
|
||||
background: isDragged
|
||||
? theme.background.transparent.light
|
||||
: 'none',
|
||||
},
|
||||
}}
|
||||
{...draggableProvided.dragHandleProps}
|
||||
>
|
||||
{itemComponent}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,37 @@
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
DragDropContext,
|
||||
Droppable,
|
||||
OnDragEndResponder,
|
||||
} from '@hello-pangea/dnd';
|
||||
|
||||
type DroppableListProps = {
|
||||
droppableId: string;
|
||||
draggableItems: React.ReactNode;
|
||||
onDragEnd: OnDragEndResponder;
|
||||
};
|
||||
|
||||
const StyledDragDropItemsWrapper = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const DroppableList = ({
|
||||
droppableId,
|
||||
draggableItems,
|
||||
onDragEnd,
|
||||
}: DroppableListProps) => {
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<StyledDragDropItemsWrapper>
|
||||
<Droppable droppableId={droppableId}>
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{draggableItems}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</StyledDragDropItemsWrapper>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
@ -1,11 +1,13 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { FloatingIconButtonGroup } from '@/ui/button/components/FloatingIconButtonGroup';
|
||||
import { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||
|
||||
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
|
||||
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
|
||||
import {
|
||||
StyledHoverableMenuItemBase,
|
||||
StyledMenuItemLeftContent,
|
||||
} from '../internals/components/StyledMenuItemBase';
|
||||
import { MenuItemAccent } from '../types/MenuItemAccent';
|
||||
|
||||
export type MenuItemIconButton = {
|
||||
@ -14,7 +16,6 @@ export type MenuItemIconButton = {
|
||||
};
|
||||
|
||||
export type MenuItemProps = {
|
||||
isDraggable?: boolean;
|
||||
LeftIcon?: IconComponent | null;
|
||||
accent?: MenuItemAccent;
|
||||
text: string;
|
||||
@ -24,25 +25,7 @@ export type MenuItemProps = {
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
const StyledHoverableMenuItemBase = styled(StyledMenuItemBase)`
|
||||
& .hoverable-buttons {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
right: ${({ theme }) => theme.spacing(2)};
|
||||
transition: opacity ${({ theme }) => theme.animation.duration.instant}s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
& .hoverable-buttons {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const MenuItem = ({
|
||||
isDraggable,
|
||||
LeftIcon,
|
||||
accent = 'default',
|
||||
text,
|
||||
@ -60,11 +43,9 @@ export const MenuItem = ({
|
||||
className={className}
|
||||
accent={accent}
|
||||
>
|
||||
<MenuItemLeftContent
|
||||
isDraggable={isDraggable ? true : false}
|
||||
LeftIcon={LeftIcon ?? undefined}
|
||||
text={text}
|
||||
/>
|
||||
<StyledMenuItemLeftContent>
|
||||
<MenuItemLeftContent LeftIcon={LeftIcon ?? undefined} text={text} />
|
||||
</StyledMenuItemLeftContent>
|
||||
<div className="hoverable-buttons">
|
||||
{showIconButtons && (
|
||||
<FloatingIconButtonGroup iconButtons={iconButtons} />
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
import { FloatingIconButtonGroup } from '@/ui/button/components/FloatingIconButtonGroup';
|
||||
import { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||
|
||||
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
|
||||
import { StyledHoverableMenuItemBase } from '../internals/components/StyledMenuItemBase';
|
||||
import { MenuItemAccent } from '../types/MenuItemAccent';
|
||||
|
||||
import { MenuItemIconButton } from './MenuItem';
|
||||
|
||||
export type MenuItemDraggableProps = {
|
||||
key: string;
|
||||
LeftIcon: IconComponent | undefined;
|
||||
accent?: MenuItemAccent;
|
||||
iconButtons?: MenuItemIconButton[];
|
||||
onClick?: () => void;
|
||||
text: string;
|
||||
isDragDisabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
export const MenuItemDraggable = ({
|
||||
key,
|
||||
LeftIcon,
|
||||
accent = 'default',
|
||||
iconButtons,
|
||||
onClick,
|
||||
text,
|
||||
isDragDisabled = false,
|
||||
className,
|
||||
}: MenuItemDraggableProps) => {
|
||||
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
|
||||
|
||||
return (
|
||||
<StyledHoverableMenuItemBase
|
||||
data-testid={key ?? undefined}
|
||||
onClick={onClick}
|
||||
accent={accent}
|
||||
className={className}
|
||||
>
|
||||
<MenuItemLeftContent
|
||||
LeftIcon={LeftIcon}
|
||||
text={text}
|
||||
key={key}
|
||||
showGrip={!isDragDisabled}
|
||||
/>
|
||||
<div className="hoverable-buttons">
|
||||
{showIconButtons && (
|
||||
<FloatingIconButtonGroup iconButtons={iconButtons} />
|
||||
)}
|
||||
</div>
|
||||
</StyledHoverableMenuItemBase>
|
||||
);
|
||||
};
|
||||
@ -4,7 +4,10 @@ import { IconChevronRight } from '@/ui/icon';
|
||||
import { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||
|
||||
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
|
||||
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
|
||||
import {
|
||||
StyledMenuItemBase,
|
||||
StyledMenuItemLeftContent,
|
||||
} from '../internals/components/StyledMenuItemBase';
|
||||
|
||||
export type MenuItemProps = {
|
||||
LeftIcon?: IconComponent;
|
||||
@ -23,7 +26,9 @@ export const MenuItemNavigate = ({
|
||||
|
||||
return (
|
||||
<StyledMenuItemBase onClick={onClick} className={className}>
|
||||
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
|
||||
<StyledMenuItemLeftContent>
|
||||
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
|
||||
</StyledMenuItemLeftContent>
|
||||
<IconChevronRight size={theme.icon.size.sm} />
|
||||
</StyledMenuItemBase>
|
||||
);
|
||||
|
||||
@ -0,0 +1,107 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { IconBell, IconMinus } from '@/ui/icon';
|
||||
import {
|
||||
CatalogDecorator,
|
||||
CatalogDimension,
|
||||
CatalogOptions,
|
||||
} from '~/testing/decorators/CatalogDecorator';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { MenuItemAccent } from '../../types/MenuItemAccent';
|
||||
import { MenuItemDraggable } from '../MenuItemDraggable';
|
||||
|
||||
const meta: Meta<typeof MenuItemDraggable> = {
|
||||
title: 'ui/MenuItem/MenuItemDraggable',
|
||||
component: MenuItemDraggable,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof MenuItemDraggable>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
key: 'key-1',
|
||||
LeftIcon: IconBell,
|
||||
accent: 'default',
|
||||
iconButtons: [{ Icon: IconMinus, onClick: () => console.log('Clicked') }],
|
||||
onClick: () => console.log('Clicked'),
|
||||
text: 'Menu item draggable',
|
||||
isDragDisabled: false,
|
||||
},
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export const Catalog: Story = {
|
||||
args: { ...Default.args },
|
||||
argTypes: {
|
||||
accent: { control: false },
|
||||
iconButtons: { control: false },
|
||||
},
|
||||
parameters: {
|
||||
pseudo: { hover: ['.hover'] },
|
||||
catalog: {
|
||||
dimensions: [
|
||||
{
|
||||
name: 'isDragDisabled',
|
||||
values: [true, false],
|
||||
props: (isDragDisabled: boolean) => ({
|
||||
isDragDisabled: isDragDisabled,
|
||||
}),
|
||||
labels: (isDragDisabled: boolean) =>
|
||||
isDragDisabled ? 'Without drag icon' : 'With drag icon',
|
||||
},
|
||||
{
|
||||
name: 'accents',
|
||||
values: ['default', 'danger', 'placeholder'] as MenuItemAccent[],
|
||||
props: (accent: MenuItemAccent) => ({ accent }),
|
||||
},
|
||||
{
|
||||
name: 'states',
|
||||
values: ['default', 'hover'],
|
||||
props: (state: string) => {
|
||||
switch (state) {
|
||||
case 'default':
|
||||
return {};
|
||||
case 'hover':
|
||||
return { className: state };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'iconButtons',
|
||||
values: ['no icon button', 'minus icon buttons'],
|
||||
props: (choice: string) => {
|
||||
switch (choice) {
|
||||
case 'no icon button': {
|
||||
return {
|
||||
iconButtons: [],
|
||||
};
|
||||
}
|
||||
case 'minus icon buttons': {
|
||||
return {
|
||||
iconButtons: [
|
||||
{
|
||||
Icon: IconMinus,
|
||||
onClick: () =>
|
||||
console.log('Clicked on minus icon button'),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
] as CatalogDimension[],
|
||||
options: {
|
||||
elementContainer: {
|
||||
width: 200,
|
||||
},
|
||||
} as CatalogOptions,
|
||||
},
|
||||
},
|
||||
decorators: [CatalogDecorator],
|
||||
};
|
||||
@ -10,21 +10,21 @@ import {
|
||||
} from './StyledMenuItemBase';
|
||||
|
||||
type OwnProps = {
|
||||
isDraggable?: boolean;
|
||||
LeftIcon: IconComponent | null | undefined;
|
||||
showGrip?: boolean;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export const MenuItemLeftContent = ({
|
||||
isDraggable,
|
||||
LeftIcon,
|
||||
text,
|
||||
showGrip = false,
|
||||
}: OwnProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledMenuItemLeftContent>
|
||||
{isDraggable && (
|
||||
{showGrip && (
|
||||
<IconGripVertical
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
|
||||
@ -88,3 +88,20 @@ export const StyledMenuItemRightContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
export const StyledHoverableMenuItemBase = styled(StyledMenuItemBase)`
|
||||
& .hoverable-buttons {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
right: ${({ theme }) => theme.spacing(2)};
|
||||
transition: opacity ${({ theme }) => theme.animation.duration.instant}s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
& .hoverable-buttons {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -1,21 +1,20 @@
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
DragDropContext,
|
||||
Draggable,
|
||||
Droppable,
|
||||
DropResult,
|
||||
OnDragEndResponder,
|
||||
ResponderProvided,
|
||||
} from '@hello-pangea/dnd';
|
||||
|
||||
import { DraggableItem } from '@/ui/draggable-list/components/DraggableItem';
|
||||
import { DroppableList } from '@/ui/draggable-list/components/DroppableList';
|
||||
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
|
||||
import { StyledDropdownMenuSubheader } from '@/ui/dropdown/components/StyledDropdownMenuSubheader';
|
||||
import { IconMinus, IconPlus } from '@/ui/icon';
|
||||
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
|
||||
import { MenuItemDraggable } from '@/ui/menu-item/components/MenuItemDraggable';
|
||||
|
||||
import { ViewFieldForVisibility } from '../types/ViewFieldForVisibility';
|
||||
|
||||
type OwnProps = {
|
||||
type ViewFieldsVisibilityDropdownSectionProps = {
|
||||
fields: ViewFieldForVisibility[];
|
||||
onVisibilityChange: (field: ViewFieldForVisibility) => void;
|
||||
title: string;
|
||||
@ -23,17 +22,13 @@ type OwnProps = {
|
||||
onDragEnd?: OnDragEndResponder;
|
||||
};
|
||||
|
||||
const StyledDropdownMenuItemWrapper = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const ViewFieldsVisibilityDropdownSection = ({
|
||||
fields,
|
||||
onVisibilityChange,
|
||||
title,
|
||||
isDraggable,
|
||||
onDragEnd,
|
||||
}: OwnProps) => {
|
||||
}: ViewFieldsVisibilityDropdownSectionProps) => {
|
||||
const handleOnDrag = (result: DropResult, provided: ResponderProvided) => {
|
||||
onDragEnd?.(result, provided);
|
||||
};
|
||||
@ -54,57 +49,31 @@ export const ViewFieldsVisibilityDropdownSection = ({
|
||||
<StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
|
||||
<StyledDropdownMenuItemsContainer>
|
||||
{isDraggable && (
|
||||
<DragDropContext onDragEnd={handleOnDrag}>
|
||||
<StyledDropdownMenuItemWrapper>
|
||||
<Droppable droppableId="droppable">
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{fields.map((field, index) => (
|
||||
<Draggable
|
||||
<DroppableList
|
||||
droppableId="droppable"
|
||||
onDragEnd={handleOnDrag}
|
||||
draggableItems={
|
||||
<>
|
||||
{fields.map((field, index) => (
|
||||
<DraggableItem
|
||||
key={field.key}
|
||||
draggableId={field.key}
|
||||
index={index}
|
||||
isDragDisabled={index === 0}
|
||||
itemComponent={
|
||||
<MenuItemDraggable
|
||||
key={field.key}
|
||||
draggableId={field.key}
|
||||
index={index}
|
||||
LeftIcon={field.Icon}
|
||||
iconButtons={getIconButtons(index, field)}
|
||||
text={field.name}
|
||||
isDragDisabled={index === 0}
|
||||
>
|
||||
{(draggableProvided) => {
|
||||
const draggableStyle =
|
||||
draggableProvided.draggableProps.style;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={draggableProvided.innerRef}
|
||||
{...{
|
||||
...draggableProvided.draggableProps,
|
||||
style: {
|
||||
...draggableStyle,
|
||||
left: 'auto',
|
||||
top: 'auto',
|
||||
transform: draggableStyle?.transform?.replace(
|
||||
/\(-?\d+px,/,
|
||||
'(0,',
|
||||
),
|
||||
},
|
||||
}}
|
||||
{...draggableProvided.dragHandleProps}
|
||||
>
|
||||
<MenuItem
|
||||
isDraggable={index !== 0 && isDraggable}
|
||||
key={field.key}
|
||||
LeftIcon={field.Icon}
|
||||
iconButtons={getIconButtons(index, field)}
|
||||
text={field.name}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</StyledDropdownMenuItemWrapper>
|
||||
</DragDropContext>
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!isDraggable &&
|
||||
fields.map((field) => (
|
||||
|
||||
Reference in New Issue
Block a user