From 93a01c7292401d4cfe570d1515a0736e8dcb30d7 Mon Sep 17 00:00:00 2001 From: Aditya Pimpalkar Date: Wed, 4 Oct 2023 14:56:25 +0100 Subject: [PATCH] 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 --- .../ui/board/components/BoardColumnMenu.tsx | 1 - .../__stories__/DraggableItem.stories.tsx | 53 +++++++++ .../components/DraggableItem.tsx | 57 ++++++++++ .../components/DroppableList.tsx | 37 ++++++ .../ui/menu-item/components/MenuItem.tsx | 33 ++---- .../components/MenuItemDraggable.tsx | 52 +++++++++ .../menu-item/components/MenuItemNavigate.tsx | 9 +- .../__stories__/MenuItemDraggable.stories.tsx | 107 ++++++++++++++++++ .../components/MenuItemLeftContent.tsx | 6 +- .../components/StyledMenuItemBase.tsx | 17 +++ .../ViewFieldsVisibilityDropdownSection.tsx | 87 +++++--------- 11 files changed, 368 insertions(+), 91 deletions(-) create mode 100644 front/src/modules/ui/draggable-list/__stories__/DraggableItem.stories.tsx create mode 100644 front/src/modules/ui/draggable-list/components/DraggableItem.tsx create mode 100644 front/src/modules/ui/draggable-list/components/DroppableList.tsx create mode 100644 front/src/modules/ui/menu-item/components/MenuItemDraggable.tsx create mode 100644 front/src/modules/ui/menu-item/components/__stories__/MenuItemDraggable.stories.tsx diff --git a/front/src/modules/ui/board/components/BoardColumnMenu.tsx b/front/src/modules/ui/board/components/BoardColumnMenu.tsx index 67a77f0fa..76c805e00 100644 --- a/front/src/modules/ui/board/components/BoardColumnMenu.tsx +++ b/front/src/modules/ui/board/components/BoardColumnMenu.tsx @@ -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; diff --git a/front/src/modules/ui/draggable-list/__stories__/DraggableItem.stories.tsx b/front/src/modules/ui/draggable-list/__stories__/DraggableItem.stories.tsx new file mode 100644 index 000000000..e5e40606b --- /dev/null +++ b/front/src/modules/ui/draggable-list/__stories__/DraggableItem.stories.tsx @@ -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 = { + title: 'ui/draggable-list/DraggableItem', + component: DraggableItem, + decorators: [ + (Story, { parameters }) => ( + } + /> + ), + ComponentDecorator, + ], + parameters: { + droppableId: 'droppable', + onDragEnd: () => console.log('dragged'), + }, + args: { + draggableId: 'draggable-1', + key: 'key-1', + index: 0, + isDragDisabled: false, + itemComponent: ( + <> + + + + ), + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/front/src/modules/ui/draggable-list/components/DraggableItem.tsx b/front/src/modules/ui/draggable-list/components/DraggableItem.tsx new file mode 100644 index 000000000..fc62277f0 --- /dev/null +++ b/front/src/modules/ui/draggable-list/components/DraggableItem.tsx @@ -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 ( + + {(draggableProvided, draggableSnapshot) => { + const draggableStyle = draggableProvided.draggableProps.style; + const isDragged = draggableSnapshot.isDragging; + return ( +
+ {itemComponent} +
+ ); + }} +
+ ); +}; diff --git a/front/src/modules/ui/draggable-list/components/DroppableList.tsx b/front/src/modules/ui/draggable-list/components/DroppableList.tsx new file mode 100644 index 000000000..ed6bdb393 --- /dev/null +++ b/front/src/modules/ui/draggable-list/components/DroppableList.tsx @@ -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 ( + + + + {(provided) => ( +
+ {draggableItems} + {provided.placeholder} +
+ )} +
+
+
+ ); +}; diff --git a/front/src/modules/ui/menu-item/components/MenuItem.tsx b/front/src/modules/ui/menu-item/components/MenuItem.tsx index b687c1825..c3a9117c1 100644 --- a/front/src/modules/ui/menu-item/components/MenuItem.tsx +++ b/front/src/modules/ui/menu-item/components/MenuItem.tsx @@ -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} > - + + +
{showIconButtons && ( diff --git a/front/src/modules/ui/menu-item/components/MenuItemDraggable.tsx b/front/src/modules/ui/menu-item/components/MenuItemDraggable.tsx new file mode 100644 index 000000000..30c0ab30d --- /dev/null +++ b/front/src/modules/ui/menu-item/components/MenuItemDraggable.tsx @@ -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 ( + + +
+ {showIconButtons && ( + + )} +
+
+ ); +}; diff --git a/front/src/modules/ui/menu-item/components/MenuItemNavigate.tsx b/front/src/modules/ui/menu-item/components/MenuItemNavigate.tsx index f01f49223..3973d72f4 100644 --- a/front/src/modules/ui/menu-item/components/MenuItemNavigate.tsx +++ b/front/src/modules/ui/menu-item/components/MenuItemNavigate.tsx @@ -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 ( - + + + ); diff --git a/front/src/modules/ui/menu-item/components/__stories__/MenuItemDraggable.stories.tsx b/front/src/modules/ui/menu-item/components/__stories__/MenuItemDraggable.stories.tsx new file mode 100644 index 000000000..17f987b0b --- /dev/null +++ b/front/src/modules/ui/menu-item/components/__stories__/MenuItemDraggable.stories.tsx @@ -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 = { + title: 'ui/MenuItem/MenuItemDraggable', + component: MenuItemDraggable, +}; + +export default meta; + +type Story = StoryObj; + +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], +}; diff --git a/front/src/modules/ui/menu-item/internals/components/MenuItemLeftContent.tsx b/front/src/modules/ui/menu-item/internals/components/MenuItemLeftContent.tsx index 1e3b3a558..76efeb072 100644 --- a/front/src/modules/ui/menu-item/internals/components/MenuItemLeftContent.tsx +++ b/front/src/modules/ui/menu-item/internals/components/MenuItemLeftContent.tsx @@ -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 ( - {isDraggable && ( + {showGrip && ( theme.spacing(2)}; + transition: opacity ${({ theme }) => theme.animation.duration.instant}s ease; + } + + &:hover { + & .hoverable-buttons { + opacity: 1; + pointer-events: auto; + } + } +`; diff --git a/front/src/modules/ui/view-bar/components/ViewFieldsVisibilityDropdownSection.tsx b/front/src/modules/ui/view-bar/components/ViewFieldsVisibilityDropdownSection.tsx index 84685906f..2a492f30f 100644 --- a/front/src/modules/ui/view-bar/components/ViewFieldsVisibilityDropdownSection.tsx +++ b/front/src/modules/ui/view-bar/components/ViewFieldsVisibilityDropdownSection.tsx @@ -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 = ({ {title} {isDraggable && ( - - - - {(provided) => ( -
- {fields.map((field, index) => ( - + {fields.map((field, index) => ( + - {(draggableProvided) => { - const draggableStyle = - draggableProvided.draggableProps.style; - - return ( -
- -
- ); - }} -
- ))} - {provided.placeholder} -
- )} -
-
-
+ /> + } + /> + ))} + + } + /> )} {!isDraggable && fields.map((field) => (