From da35728fe186e1132f89427260a67d317da6e01a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Tib=C3=BArcio?= Date: Wed, 27 Nov 2024 13:32:55 -0500 Subject: [PATCH 1/4] feat: add overlay actions wrapper component --- .../__shared__/ActionsOverlay/index.tsx | 208 ++++++++++++++++++ .../__shared__/ActionsOverlay/styled.tsx | 27 +++ .../__shared__/ActionsOverlay/types.ts | 36 +++ 3 files changed, 271 insertions(+) create mode 100644 packages/components/modules/__shared__/ActionsOverlay/index.tsx create mode 100644 packages/components/modules/__shared__/ActionsOverlay/styled.tsx create mode 100644 packages/components/modules/__shared__/ActionsOverlay/types.ts diff --git a/packages/components/modules/__shared__/ActionsOverlay/index.tsx b/packages/components/modules/__shared__/ActionsOverlay/index.tsx new file mode 100644 index 00000000..6f79c5f4 --- /dev/null +++ b/packages/components/modules/__shared__/ActionsOverlay/index.tsx @@ -0,0 +1,208 @@ +import { forwardRef, useState } from 'react' + +import { + ConfirmDialog, + SwipeableDrawer as DefaultSwipeableDrawer, + IconButton, + TrashCanIcon, +} from '@baseapp-frontend/design-system' + +import { LoadingButton } from '@mui/lab' +import { Box, BoxProps, Divider, Typography } from '@mui/material' +import { LongPressCallbackReason, useLongPress } from 'use-long-press' + +import { ActionOverlayContainer, IconButtonContentContainer } from './styled' +import { ActionOverlayProps, LongPressHandler } from './types' + +const ActionsOverlay = forwardRef( + ( + { + ContainerProps = {}, + children, + title, + isDeletingItem, + handleDeleteItem, + enableDelete, + actions: options, + SwipeableDrawerProps, + SwipeableDrawer = DefaultSwipeableDrawer, + offsetRight = 0, + offsetTop = 0, + }, + ref, + ) => { + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) + const [isHoveringItem, setIsHoveringItem] = useState(false) + const [longPressHandler, setLongPressHandler] = useState({ + isLongPressingItem: false, + shouldOpenItemOptions: false, + }) + + const longPressHandlers = useLongPress( + (e: any) => { + e.stopPropagation() + setLongPressHandler({ isLongPressingItem: true, shouldOpenItemOptions: true }) + }, + { + onCancel: (e, { reason }) => { + // This is a workaround to prevent the comment options's drawer from closing when the user clicks on the drawer's content. + // Ideally, we would call setLongPressHandler({ isLongPressingComment: false }) on `onFinished` instead of `onCancel`. + // But, on mobile google chrome devices, the long press click is being wrongly propagated to the backdrop and closing the comment options's drawer right after it opens. + const className = (e?.target as HTMLElement)?.className || '' + const classNameString = typeof className === 'string' ? className : '' + const isClickOnBackdrop = classNameString.includes('MuiBackdrop') + if (reason === LongPressCallbackReason.CancelledByRelease && isClickOnBackdrop) { + setLongPressHandler((prevState) => ({ ...prevState, isLongPressingItem: false })) + } + }, + cancelOutsideElement: true, + threshold: 400, + }, + ) + const handleLongPressItemOptionsClose = () => { + setLongPressHandler({ isLongPressingItem: false, shouldOpenItemOptions: false }) + } + + const handleDeleteDialogOpen = () => { + setIsDeleteDialogOpen(true) + } + + const deviceHasHover = window.matchMedia('(hover: hover)').matches + + const onDeleteItemClick = () => { + setIsDeleteDialogOpen(false) + handleDeleteItem?.() + } + + const renderDeleteDialog = () => ( + + Delete + + } + onClose={() => setIsDeleteDialogOpen(false)} + open={isDeleteDialogOpen} + /> + ) + + const renderActionsOverlay = () => { + if (!deviceHasHover) { + const handleDrawerClose = () => { + if (!longPressHandler.isLongPressingItem) { + handleLongPressItemOptionsClose() + } + } + + return ( + <> + {renderDeleteDialog()} + +
+ {options?.map(({ label, icon, onClick, disabled, hasPermission }) => { + if (!hasPermission) return null + + return ( + + + + {icon} + + {label} + + + ) + })} + {enableDelete && ( + <> + + + + + + + + {`Delete ${title}`} + + + + + )} +
+
+ + ) + } + + if (deviceHasHover && isHoveringItem) { + return ( + <> + {renderDeleteDialog()} + + {enableDelete && ( + + + + )} + {options?.map(({ label, icon, onClick, disabled, hasPermission }) => { + if (!hasPermission) return null + + return ( + + {icon} + + ) + })} + + + ) + } + return renderDeleteDialog() + } + + return ( + setIsHoveringItem(true)} + onMouseLeave={() => setIsHoveringItem(false)} + {...longPressHandlers()} + {...ContainerProps} + sx={{ position: 'relative' }} + > + {renderActionsOverlay()} + {children} + + ) + }, +) + +export default ActionsOverlay diff --git a/packages/components/modules/__shared__/ActionsOverlay/styled.tsx b/packages/components/modules/__shared__/ActionsOverlay/styled.tsx new file mode 100644 index 00000000..6066e036 --- /dev/null +++ b/packages/components/modules/__shared__/ActionsOverlay/styled.tsx @@ -0,0 +1,27 @@ +import { Box } from '@mui/material' +import { styled } from '@mui/material/styles' + +import { ActionOverlayContainerProps } from './types' + +export const ActionOverlayContainer = styled(Box)( + ({ theme, offsetTop = 0, offsetRight = 0 }) => ({ + backgroundColor: theme.palette.background.default, + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.spacing(1), + display: 'flex', + gap: theme.spacing(1), + padding: theme.spacing(0.75, 1), + position: 'absolute', + right: 12 - offsetRight, + top: -12 - offsetTop, + zIndex: 1, + }), +) + +export const IconButtonContentContainer = styled(Box)(({ theme }) => ({ + alignItems: 'center', + display: 'grid', + gridTemplateColumns: 'minmax(max-content, 24px) 1fr', + gap: theme.spacing(1), + alignSelf: 'center', +})) diff --git a/packages/components/modules/__shared__/ActionsOverlay/types.ts b/packages/components/modules/__shared__/ActionsOverlay/types.ts new file mode 100644 index 00000000..b122b110 --- /dev/null +++ b/packages/components/modules/__shared__/ActionsOverlay/types.ts @@ -0,0 +1,36 @@ +import { FC } from 'react' + +import type { SwipeableDrawerProps } from '@baseapp-frontend/design-system' + +import { BoxProps } from '@mui/material' + +export type OverlayAction = { + label: string + icon: JSX.Element + onClick: () => void + disabled: boolean + hasPermission?: boolean | null + closeOnClick?: boolean +} + +export type LongPressHandler = { + isLongPressingItem: boolean + shouldOpenItemOptions: boolean +} + +export interface ActionOverlayProps extends BoxProps { + ContainerProps?: BoxProps + enableDelete?: boolean + isDeletingItem?: boolean + handleDeleteItem?: () => void + actions: OverlayAction[] + SwipeableDrawer?: FC + SwipeableDrawerProps?: Partial + offsetTop?: number + offsetRight?: number +} + +export interface ActionOverlayContainerProps extends BoxProps { + offsetTop?: number + offsetRight?: number +} From abde09b13d21104d4c4e4252eae8cf8e3f44c682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Tib=C3=BArcio?= Date: Thu, 28 Nov 2024 08:00:02 -0500 Subject: [PATCH 2/4] feat: add overlay actions to ChatRoomCard --- .../ChatRoomsList/ChatRoomItem/index.tsx | 132 +++++++++++------- 1 file changed, 80 insertions(+), 52 deletions(-) diff --git a/packages/components/modules/messages/ChatRoomsList/ChatRoomItem/index.tsx b/packages/components/modules/messages/ChatRoomsList/ChatRoomItem/index.tsx index 66a4046d..cb6674ee 100644 --- a/packages/components/modules/messages/ChatRoomsList/ChatRoomItem/index.tsx +++ b/packages/components/modules/messages/ChatRoomsList/ChatRoomItem/index.tsx @@ -1,11 +1,12 @@ -import { FC, SyntheticEvent } from 'react' +import { FC, SyntheticEvent, useRef } from 'react' -import { AvatarWithPlaceholder } from '@baseapp-frontend/design-system' +import { AvatarWithPlaceholder, LinkIcon, PenEditIcon } from '@baseapp-frontend/design-system' -import { Box, Badge as DefaultBadge, Typography } from '@mui/material' +import { Box, BoxProps, Badge as DefaultBadge, Typography } from '@mui/material' import { useFragment } from 'react-relay' import { RoomFragment$key } from '../../../../__generated__/RoomFragment.graphql' +import ActionsOverlay from '../../../__shared__/ActionsOverlay' import { useCurrentProfile } from '../../../profiles' import { MINIMUM_AMOUNT_OF_PARTICIPANTS_TO_SHOW_ROOM_TITLE } from '../../constants' import { RoomFragment } from '../../graphql/queries/Room' @@ -27,6 +28,8 @@ const ChatRoomItem: FC = ({ if (handleClick) handleClick() } + const chatCardRef = useRef(null) + const { profile } = useCurrentProfile() const roomData = { @@ -51,57 +54,82 @@ const ChatRoomItem: FC = ({ const showBadge = room.unreadMessagesCount && room.unreadMessagesCount > 0 return ( - , + label: 'Share Comment', + onClick: () => console.log('Share'), + hasPermission: true, + }, + { + disabled: false, + icon: , + label: 'Edit Comment', + onClick: () => console.log('Edit'), + hasPermission: true, + }, + ]} + enableDelete + handleDeleteItem={() => console.log('Delete')} + isDeletingItem={false} + ref={chatCardRef} > - - - {roomData.title} - - {lastMessage && lastMessageTime && ( - <> - - {formatDate(lastMessageTime)} - - - - {lastMessage} - - - )} + + + + {roomData.title} + + {lastMessage && lastMessageTime && ( + <> + + {formatDate(lastMessageTime)} + + + + {lastMessage} + + + )} + - - - + + + ) } From a78a76cc7a1f52bf075a2f87d88f3eebe51561eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Tib=C3=BArcio?= Date: Thu, 28 Nov 2024 08:25:22 -0500 Subject: [PATCH 3/4] feat: add overlay actions to CommentItem --- .../__storybook__/ActionsOverlay.mdx | 64 ++++++ .../ActionsOverlayOnButton/index.tsx | 19 ++ .../ActionsOverlay/__storybook__/stories.tsx | 38 ++++ .../__shared__/ActionsOverlay/index.tsx | 196 ++++++++++-------- .../__shared__/ActionsOverlay/styled.tsx | 8 +- .../__shared__/ActionsOverlay/types.ts | 11 +- .../CommentItem/CommentOptions/index.tsx | 160 -------------- .../CommentItem/CommentOptions/styled.tsx | 23 -- .../CommentItem/CommentOptions/types.ts | 18 -- .../modules/comments/CommentItem/index.tsx | 130 +++++------- .../modules/comments/CommentItem/types.ts | 4 +- .../CommentItem/useCommentOptions/index.tsx | 15 +- .../CommentItem/useCommentOptions/types.ts | 1 - .../Comments/__tests__/Comments.cy.tsx | 4 +- .../ChatRoomsList/ChatRoomItem/index.tsx | 20 +- .../components/icons/ArchiveIcon/index.tsx | 20 ++ .../components/icons/UnreadIcon/index.tsx | 48 +++++ .../design-system/components/icons/index.ts | 12 +- 18 files changed, 388 insertions(+), 403 deletions(-) create mode 100644 packages/components/modules/__shared__/ActionsOverlay/__storybook__/ActionsOverlay.mdx create mode 100644 packages/components/modules/__shared__/ActionsOverlay/__storybook__/ActionsOverlayOnButton/index.tsx create mode 100644 packages/components/modules/__shared__/ActionsOverlay/__storybook__/stories.tsx delete mode 100644 packages/components/modules/comments/CommentItem/CommentOptions/index.tsx delete mode 100644 packages/components/modules/comments/CommentItem/CommentOptions/styled.tsx delete mode 100644 packages/components/modules/comments/CommentItem/CommentOptions/types.ts create mode 100644 packages/design-system/components/icons/ArchiveIcon/index.tsx create mode 100644 packages/design-system/components/icons/UnreadIcon/index.tsx diff --git a/packages/components/modules/__shared__/ActionsOverlay/__storybook__/ActionsOverlay.mdx b/packages/components/modules/__shared__/ActionsOverlay/__storybook__/ActionsOverlay.mdx new file mode 100644 index 00000000..899122c7 --- /dev/null +++ b/packages/components/modules/__shared__/ActionsOverlay/__storybook__/ActionsOverlay.mdx @@ -0,0 +1,64 @@ +import { Meta } from '@storybook/addon-docs' + + + +# Component Documentation + +## ActionsOverlay + +- **Purpose**: The `ActionsOverlay` component wraps around any other component and provides it with a tooltip/swippable drawer that receives a list of custom actions the user wished to assign to the wrapped component. +- **Expected Behavior**: In mobile view, when long press on the child component, a swippable drawer will appear, containing all the actions (in the form of icon/label pair) the user configured. In web view, the actions appear on hover of the child element, and are displayed in a tooltip containing only the icons of the configured actions. + +## Use Cases + +- **Current Usage**: This component is currently used within `CommentItem` and `ChatRoomItem`. +- **Potential Usage**: Can de used for any other component that requires additional actions other than the base action provided by that component, such as posts, list items, etc. + +## Props + +- **actions** (OverlayAction[]): The list of actions desired for the child component. Note that to implement a delete action, the component provides a enabler, loading and click handler props specifically for that action (see props below). +- **title** (string): Title for the child component (currently only used on the delete dialog). +- **children** (ReactNode): The child component to be wrapped. +- **enableDelete** (boolean): Enables the delete action inside the tooltip/swippable drawer. +- **isDeletingItem** (boolean): Mutation loading state for the delete action. +- **handleDeleteItem** (function): Callback function to handle deletion. +- **offsetTop** (number): Number to offset the top positioning of the default tooltip position (only affects tooltip). +- **offsetRight** (number): Number to offset the right positioning of the default tooltip position (only affects tooltip). +- **ContainerProps** (BoxProps): Props for the parent `Box` component that wraps the child component. +- **SwipeableDrawer** (`FC`): `SwipeableDrawer` component. Defaults to current MUI component. +- **SwipeableDrawerProps** (`Partial`): Props extension for the parent `Box` that wraps the child component. + +## Example Usage + +```javascript +import React, { RefAttributes } from 'react' + +import { Button } from '@mui/material' + +import ActionsOverlay from '../' +import { ActionOverlayProps } from '../types' + +export const DefaultActionsOverlay = ( + props: Omit & RefAttributes, +) => { + const pageRef = React.useRef(null) + return ( + {}} + actions={[ + { + label: 'Archive', + icon: , + onClick: () => {}, + hasPermission: true, + closeOnClick: true, + }, + ]}, + ref={pageRef} + > + + + ) +} diff --git a/packages/components/modules/__shared__/ActionsOverlay/__storybook__/ActionsOverlayOnButton/index.tsx b/packages/components/modules/__shared__/ActionsOverlay/__storybook__/ActionsOverlayOnButton/index.tsx new file mode 100644 index 00000000..5560a6e6 --- /dev/null +++ b/packages/components/modules/__shared__/ActionsOverlay/__storybook__/ActionsOverlayOnButton/index.tsx @@ -0,0 +1,19 @@ +import React, { RefAttributes } from 'react' + +import { Button } from '@mui/material' + +import ActionsOverlay from '../..' +import { ActionOverlayProps } from '../../types' + +const ActionsOverlayOnButton = ( + props: Omit & RefAttributes, +) => { + const pageRef = React.useRef(null) + return ( + + + + ) +} + +export default ActionsOverlayOnButton diff --git a/packages/components/modules/__shared__/ActionsOverlay/__storybook__/stories.tsx b/packages/components/modules/__shared__/ActionsOverlay/__storybook__/stories.tsx new file mode 100644 index 00000000..d4179b6d --- /dev/null +++ b/packages/components/modules/__shared__/ActionsOverlay/__storybook__/stories.tsx @@ -0,0 +1,38 @@ +import { ArchiveIcon } from '@baseapp-frontend/design-system' + +import type { Meta, StoryObj } from '@storybook/react' + +import ActionsOverlay from '..' +import ActionsOverlayOnButton from './ActionsOverlayOnButton' + +const meta: Meta = { + title: '@baseapp-frontend | components/Shared/ActionsOverlay', + component: ActionsOverlayOnButton, +} + +export default meta + +type Story = StoryObj + +export const DefaultActionsOverlay: Story = { + name: 'ActionsOverlay', + args: { + title: 'Button', + enableDelete: true, + handleDeleteItem: () => {}, + offsetRight: 0, + offsetTop: 0, + ContainerProps: { + sx: { width: '100px' }, + }, + actions: [ + { + label: 'Archive', + icon: , + onClick: () => {}, + hasPermission: true, + closeOnClick: true, + }, + ], + }, +} diff --git a/packages/components/modules/__shared__/ActionsOverlay/index.tsx b/packages/components/modules/__shared__/ActionsOverlay/index.tsx index 6f79c5f4..f7fae2e4 100644 --- a/packages/components/modules/__shared__/ActionsOverlay/index.tsx +++ b/packages/components/modules/__shared__/ActionsOverlay/index.tsx @@ -8,26 +8,26 @@ import { } from '@baseapp-frontend/design-system' import { LoadingButton } from '@mui/lab' -import { Box, BoxProps, Divider, Typography } from '@mui/material' +import { Box, Divider, Typography } from '@mui/material' import { LongPressCallbackReason, useLongPress } from 'use-long-press' import { ActionOverlayContainer, IconButtonContentContainer } from './styled' import { ActionOverlayProps, LongPressHandler } from './types' -const ActionsOverlay = forwardRef( +const ActionsOverlay = forwardRef( ( { - ContainerProps = {}, + actions = [], children, - title, - isDeletingItem, - handleDeleteItem, - enableDelete, - actions: options, - SwipeableDrawerProps, - SwipeableDrawer = DefaultSwipeableDrawer, - offsetRight = 0, + title = 'Item', + enableDelete = false, + isDeletingItem = false, + handleDeleteItem = () => {}, offsetTop = 0, + offsetRight = 0, + ContainerProps = {}, + SwipeableDrawerProps = {}, + SwipeableDrawer = DefaultSwipeableDrawer, }, ref, ) => { @@ -39,7 +39,7 @@ const ActionsOverlay = forwardRef( }) const longPressHandlers = useLongPress( - (e: any) => { + (e: React.MouseEvent | React.TouchEvent) => { e.stopPropagation() setLongPressHandler({ isLongPressingItem: true, shouldOpenItemOptions: true }) }, @@ -67,11 +67,13 @@ const ActionsOverlay = forwardRef( setIsDeleteDialogOpen(true) } - const deviceHasHover = window.matchMedia('(hover: hover)').matches + const deviceHasHover = + typeof window !== 'undefined' && window.matchMedia('(hover: hover)').matches const onDeleteItemClick = () => { setIsDeleteDialogOpen(false) handleDeleteItem?.() + handleLongPressItemOptionsClose() } const renderDeleteDialog = () => ( @@ -102,91 +104,106 @@ const ActionsOverlay = forwardRef( } return ( - <> - {renderDeleteDialog()} - -
- {options?.map(({ label, icon, onClick, disabled, hasPermission }) => { - if (!hasPermission) return null - - return ( - - - - {icon} - - {label} - - - ) - })} - {enableDelete && ( - <> - - - - - - - - {`Delete ${title}`} - - - - - )} -
-
- + + + {actions?.map(({ label, icon, onClick, disabled, hasPermission, closeOnClick }) => { + if (!hasPermission) return null + + const handleClick = () => { + onClick?.() + if (closeOnClick) { + handleLongPressItemOptionsClose() + } + } + + return ( + + + + {icon} + + {label} + + + ) + })} + {enableDelete && ( + <> + + + + + + + + {`Delete ${title}`} + + + + + )} + + ) } if (deviceHasHover && isHoveringItem) { return ( - <> - {renderDeleteDialog()} - - {enableDelete && ( + + {enableDelete && ( + + + + )} + {actions?.map(({ label, icon, onClick, disabled, hasPermission, closeOnClick }) => { + if (!hasPermission) return null + + const handleClick = () => { + onClick?.() + if (closeOnClick) { + handleLongPressItemOptionsClose() + } + } + + return ( - + {icon} - )} - {options?.map(({ label, icon, onClick, disabled, hasPermission }) => { - if (!hasPermission) return null - - return ( - - {icon} - - ) - })} - - + ) + })} + ) } - return renderDeleteDialog() + return
} return ( @@ -196,8 +213,9 @@ const ActionsOverlay = forwardRef( onMouseLeave={() => setIsHoveringItem(false)} {...longPressHandlers()} {...ContainerProps} - sx={{ position: 'relative' }} + sx={{ position: 'relative', maxWidth: 'max-content' }} > + {renderDeleteDialog()} {renderActionsOverlay()} {children} diff --git a/packages/components/modules/__shared__/ActionsOverlay/styled.tsx b/packages/components/modules/__shared__/ActionsOverlay/styled.tsx index 6066e036..c927d45f 100644 --- a/packages/components/modules/__shared__/ActionsOverlay/styled.tsx +++ b/packages/components/modules/__shared__/ActionsOverlay/styled.tsx @@ -12,9 +12,15 @@ export const ActionOverlayContainer = styled(Box)( gap: theme.spacing(1), padding: theme.spacing(0.75, 1), position: 'absolute', + role: 'menu', + 'aria-label': 'Action options', right: 12 - offsetRight, top: -12 - offsetTop, - zIndex: 1, + zIndex: theme.zIndex.tooltip, + transition: theme.transitions.create(['opacity', 'visibility'], { + duration: theme.transitions.duration.shorter, + easing: theme.transitions.easing.easeInOut, + }), }), ) diff --git a/packages/components/modules/__shared__/ActionsOverlay/types.ts b/packages/components/modules/__shared__/ActionsOverlay/types.ts index b122b110..36e8feb0 100644 --- a/packages/components/modules/__shared__/ActionsOverlay/types.ts +++ b/packages/components/modules/__shared__/ActionsOverlay/types.ts @@ -8,7 +8,7 @@ export type OverlayAction = { label: string icon: JSX.Element onClick: () => void - disabled: boolean + disabled?: boolean hasPermission?: boolean | null closeOnClick?: boolean } @@ -18,16 +18,15 @@ export type LongPressHandler = { shouldOpenItemOptions: boolean } -export interface ActionOverlayProps extends BoxProps { - ContainerProps?: BoxProps +export interface ActionOverlayProps extends ActionOverlayContainerProps { + actions: OverlayAction[] + title: string enableDelete?: boolean isDeletingItem?: boolean handleDeleteItem?: () => void - actions: OverlayAction[] + ContainerProps?: Partial SwipeableDrawer?: FC SwipeableDrawerProps?: Partial - offsetTop?: number - offsetRight?: number } export interface ActionOverlayContainerProps extends BoxProps { diff --git a/packages/components/modules/comments/CommentItem/CommentOptions/index.tsx b/packages/components/modules/comments/CommentItem/CommentOptions/index.tsx deleted file mode 100644 index 4e2591cc..00000000 --- a/packages/components/modules/comments/CommentItem/CommentOptions/index.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { FC, useState } from 'react' - -import { - ConfirmDialog, - SwipeableDrawer as DefaultSwipeableDrawer, - IconButton, - TrashCanIcon, -} from '@baseapp-frontend/design-system' - -import { LoadingButton } from '@mui/lab' -import { Box, Divider, Typography } from '@mui/material' - -import { useCommentReply } from '../../context' -import useCommentDeleteMutation from '../../graphql/mutations/CommentDelete' -import { Container, IconButtonContentContainer } from './styled' -import { CommentOptionsProps } from './types' - -const CommentOptions: FC = ({ - comment, - longPressHandler: { isLongPressingComment, shouldOpenCommentOptions }, - onLongPressLeave, - isHoveringComment, - onDelete, - enableDelete, - options, - SwipeableDrawerProps, - SwipeableDrawer = DefaultSwipeableDrawer, -}) => { - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) - - const { resetCommentReply } = useCommentReply() - - const [deleteComment, isDeletingComment] = useCommentDeleteMutation() - const handleDeleteComment = () => { - deleteComment({ variables: { id: comment.id } }) - onLongPressLeave() - resetCommentReply() - onDelete?.() - } - - const handleDeleteDialogOpen = () => { - setIsDeleteDialogOpen(true) - } - - const deviceHasHover = window.matchMedia('(hover: hover)').matches - - const isCommentDeletionAllowed = enableDelete && comment.canDelete - - const renderDeleteDialog = () => ( - - Delete - - } - onClose={() => setIsDeleteDialogOpen(false)} - open={isDeleteDialogOpen} - /> - ) - - if (!deviceHasHover) { - const handleDrawerClose = () => { - if (!isLongPressingComment) { - onLongPressLeave() - } - } - - return ( - <> - {renderDeleteDialog()} - -
- {options?.map(({ label, icon, onClick, disabled, hasPermission }) => { - if (!hasPermission) return null - - return ( - - - - {icon} - - {label} - - - ) - })} - {isCommentDeletionAllowed && ( - <> - - - - - - - - Delete Comment - - - - - )} -
-
- - ) - } - - if (deviceHasHover && isHoveringComment) { - return ( - <> - {renderDeleteDialog()} - - {isCommentDeletionAllowed && ( - - - - )} - {options?.map(({ label, icon, onClick, disabled, hasPermission }) => { - if (!hasPermission) return null - - return ( - - {icon} - - ) - })} - - - ) - } - - return renderDeleteDialog() -} - -export default CommentOptions diff --git a/packages/components/modules/comments/CommentItem/CommentOptions/styled.tsx b/packages/components/modules/comments/CommentItem/CommentOptions/styled.tsx deleted file mode 100644 index 09971592..00000000 --- a/packages/components/modules/comments/CommentItem/CommentOptions/styled.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Box } from '@mui/material' -import { styled } from '@mui/material/styles' - -export const Container = styled(Box)(({ theme }) => ({ - backgroundColor: theme.palette.background.default, - border: `1px solid ${theme.palette.divider}`, - borderRadius: theme.spacing(1), - display: 'flex', - gap: theme.spacing(1), - padding: theme.spacing(0.75, 1), - position: 'absolute', - right: '12px', - top: '-12px', - zIndex: 1, -})) - -export const IconButtonContentContainer = styled(Box)(({ theme }) => ({ - alignItems: 'center', - display: 'grid', - gridTemplateColumns: 'minmax(max-content, 24px) 1fr', - gap: theme.spacing(1), - alignSelf: 'center', -})) diff --git a/packages/components/modules/comments/CommentItem/CommentOptions/types.ts b/packages/components/modules/comments/CommentItem/CommentOptions/types.ts deleted file mode 100644 index 941925b2..00000000 --- a/packages/components/modules/comments/CommentItem/CommentOptions/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { FC } from 'react' - -import type { SwipeableDrawerProps } from '@baseapp-frontend/design-system' - -import type { CommentItem_comment$data } from '../../../../__generated__/CommentItem_comment.graphql' -import type { CommentOption, LongPressHandler } from '../types' - -export interface CommentOptionsProps { - comment: CommentItem_comment$data - longPressHandler: LongPressHandler - onLongPressLeave: () => void - isHoveringComment: boolean - enableDelete: boolean - onDelete?: () => void - options?: CommentOption[] - SwipeableDrawer?: FC - SwipeableDrawerProps?: Partial -} diff --git a/packages/components/modules/comments/CommentItem/index.tsx b/packages/components/modules/comments/CommentItem/index.tsx index e71c7090..7b4f7b69 100644 --- a/packages/components/modules/comments/CommentItem/index.tsx +++ b/packages/components/modules/comments/CommentItem/index.tsx @@ -6,21 +6,21 @@ import { AvatarWithPlaceholder } from '@baseapp-frontend/design-system' import { Typography } from '@mui/material' import { useRefetchableFragment } from 'react-relay' -import { LongPressCallbackReason, useLongPress } from 'use-long-press' import { CommentItemRefetchQuery } from '../../../__generated__/CommentItemRefetchQuery.graphql' import { CommentItem_comment$key } from '../../../__generated__/CommentItem_comment.graphql' +import ActionsOverlay from '../../__shared__/ActionsOverlay' import DefaultTimestamp from '../../__shared__/Timestamp' import DefaultCommentUpdate from '../CommentUpdate' import { useCommentReply } from '../context' +import useCommentDeleteMutation from '../graphql/mutations/CommentDelete' import { CommentItemFragmentQuery } from '../graphql/queries/CommentItem' -import CommentOptions from './CommentOptions' import DefaultCommentPinnedBadge from './CommentPinnedBadge' import DefaultCommentReactionButton from './CommentReactionButton' import DefaultCommentReplyButton from './CommentReplyButton' import CommentsReplies from './CommentsReplies' import { CommentContainer, CommentContainerWrapper } from './styled' -import { CommentItemProps, LongPressHandler } from './types' +import { CommentItemProps } from './types' import useCommentOptions from './useCommentOptions' const CommentItem: FC = ({ @@ -31,7 +31,7 @@ const CommentItem: FC = ({ onReplyClick, CommentUpdateProps, CommentsRepliesProps, - CommentOptionsProps, + ActionOverlayProps, enableDelete = true, Timestamp = DefaultTimestamp, CommentUpdate = DefaultCommentUpdate, @@ -44,49 +44,27 @@ const CommentItem: FC = ({ CommentItem_comment$key >(CommentItemFragmentQuery, commentRef) const { setCommentReply } = useCommentReply() + const commentItemRef = useRef(null) - const [isHoveringComment, setIsHoveringComment] = useState(false) const [isEditMode, setIsEditMode] = useState(false) - const [isRepliesExpanded, setIsReplyRepliesExpanded] = useState(false) - - const [longPressHandler, setLongPressHandler] = useState({ - isLongPressingComment: false, - shouldOpenCommentOptions: false, - }) - - const longPressHandlers = useLongPress( - () => setLongPressHandler({ isLongPressingComment: true, shouldOpenCommentOptions: true }), - { - onCancel: (e, { reason }) => { - // This is a workaround to prevent the comment options's drawer from closing when the user clicks on the drawer's content. - // Ideally, we would call setLongPressHandler({ isLongPressingComment: false }) on `onFinished` instead of `onCancel`. - // But, on mobile google chrome devices, the long press click is being wrongly propagated to the backdrop and closing the comment options's drawer right after it opens. - const className = (e?.target as HTMLElement)?.className || '' - const classNameString = typeof className === 'string' ? className : '' - const isClickOnBackdrop = classNameString.includes('MuiBackdrop') - if (reason === LongPressCallbackReason.CancelledByRelease && isClickOnBackdrop) { - setLongPressHandler((prevState) => ({ ...prevState, isLongPressingComment: false })) - } - }, - cancelOutsideElement: true, - threshold: 400, - }, - ) - const handleLongPressCommentOptionsClose = () => { - setLongPressHandler({ isLongPressingComment: false, shouldOpenCommentOptions: false }) - } + const [isReplyExpanded, setIsReplyExpanded] = useState(false) const defaultCommentOptions = useCommentOptions({ comment, - onLongPressLeave: handleLongPressCommentOptionsClose, onEdit: () => setIsEditMode(true), }) + const { actions = defaultCommentOptions, ...restOfActionOverlayProps } = ActionOverlayProps ?? {} + const [isLoadingReplies, startLoadingReplies] = useTransition() + const { resetCommentReply } = useCommentReply() + + const [deleteComment, isDeletingComment] = useCommentDeleteMutation() + const showReplies = () => { - if (isRepliesExpanded) return + if (isReplyExpanded) return startLoadingReplies(() => { refetchCommentItem( @@ -97,7 +75,7 @@ const CommentItem: FC = ({ if (error) { console.error(error) } else { - setIsReplyRepliesExpanded(true) + setIsReplyExpanded(true) } }, }, @@ -143,57 +121,55 @@ const CommentItem: FC = ({ return null } - const { options = defaultCommentOptions, ...restOfCommentOptionsProps } = - CommentOptionsProps ?? {} + const handleDeleteComment = () => { + deleteComment({ variables: { id: comment.id } }) + resetCommentReply() + } return (
- setIsHoveringComment(true)} - onMouseLeave={() => setIsHoveringComment(false)} - {...longPressHandlers()} > - - -
-
-
- {comment.user?.fullName} - + + +
+
+
+ {comment.user?.fullName} + +
+ {renderCommentContent()}
- {renderCommentContent()} -
-
-
- - +
+
+ + +
+
-
-
-
+ + - {isRepliesExpanded && !isLoadingReplies && ( + {isReplyExpanded && !isLoadingReplies && ( void enableDelete?: boolean + ActionOverlayProps?: Partial CommentUpdate?: FC CommentUpdateProps?: Partial CommentsRepliesProps?: Partial - CommentOptionsProps?: Partial CommentReactionButton?: FC CommentReplyButton?: FC CommentPinnedBadge?: FC diff --git a/packages/components/modules/comments/CommentItem/useCommentOptions/index.tsx b/packages/components/modules/comments/CommentItem/useCommentOptions/index.tsx index 43468e76..a108b30a 100644 --- a/packages/components/modules/comments/CommentItem/useCommentOptions/index.tsx +++ b/packages/components/modules/comments/CommentItem/useCommentOptions/index.tsx @@ -1,22 +1,16 @@ import { LinkIcon, PenEditIcon, PinIcon } from '@baseapp-frontend/design-system' +import { OverlayAction } from '../../../__shared__/ActionsOverlay/types' import useCommentPinMutation from '../../graphql/mutations/CommentPin' -import { CommentOption } from '../types' import { UseCommentOptionsParams } from './types' -const useCommentOptions = ({ - comment, - onLongPressLeave, - onEdit, -}: UseCommentOptionsParams): CommentOption[] => { +const useCommentOptions = ({ comment, onEdit }: UseCommentOptionsParams): OverlayAction[] => { const [pinComment, isPinningComment] = useCommentPinMutation() const handlePinComment = () => { pinComment({ variables: { id: comment!.id } }) - onLongPressLeave() } const handleEditComment = () => { - onLongPressLeave() onEdit() } @@ -25,8 +19,9 @@ const useCommentOptions = ({ disabled: true, icon: , label: 'Share Comment', - onClick: onLongPressLeave, + onClick: () => {}, hasPermission: true, + closeOnClick: true, }, { disabled: isPinningComment, @@ -34,6 +29,7 @@ const useCommentOptions = ({ label: `${comment?.isPinned ? 'Unpin' : 'Pin'} Comment`, onClick: handlePinComment, hasPermission: comment?.canPin, + closeOnClick: true, }, { disabled: false, @@ -41,6 +37,7 @@ const useCommentOptions = ({ label: 'Edit Comment', onClick: handleEditComment, hasPermission: comment?.canChange, + closeOnClick: true, }, ] } diff --git a/packages/components/modules/comments/CommentItem/useCommentOptions/types.ts b/packages/components/modules/comments/CommentItem/useCommentOptions/types.ts index 046fa848..f39645ca 100644 --- a/packages/components/modules/comments/CommentItem/useCommentOptions/types.ts +++ b/packages/components/modules/comments/CommentItem/useCommentOptions/types.ts @@ -2,6 +2,5 @@ import { CommentItem_comment$data } from '../../../../__generated__/CommentItem_ export interface UseCommentOptionsParams { comment?: CommentItem_comment$data - onLongPressLeave: () => void onEdit: () => void } diff --git a/packages/components/modules/comments/Comments/__tests__/Comments.cy.tsx b/packages/components/modules/comments/Comments/__tests__/Comments.cy.tsx index 198096a2..5b9a2bbe 100644 --- a/packages/components/modules/comments/Comments/__tests__/Comments.cy.tsx +++ b/packages/components/modules/comments/Comments/__tests__/Comments.cy.tsx @@ -146,7 +146,7 @@ describe('Comments', () => { cy.step('Delete a comment') cy.findByText('This is another reply').click() - cy.findAllByRole('button', { name: /delete comment/i }) + cy.findAllByRole('button', { name: /delete item/i }) .last() .click() cy.findByText('Delete Comment?').should('exist') @@ -157,7 +157,7 @@ describe('Comments', () => { cy.findByText('This is another reply').should('exist') cy.step('Confirm comment deletion') - cy.findAllByRole('button', { name: /delete comment/i }) + cy.findAllByRole('button', { name: /delete item/i }) .last() .click() cy.findByRole('button', { name: /delete/i }) diff --git a/packages/components/modules/messages/ChatRoomsList/ChatRoomItem/index.tsx b/packages/components/modules/messages/ChatRoomsList/ChatRoomItem/index.tsx index cb6674ee..442a526b 100644 --- a/packages/components/modules/messages/ChatRoomsList/ChatRoomItem/index.tsx +++ b/packages/components/modules/messages/ChatRoomsList/ChatRoomItem/index.tsx @@ -1,8 +1,8 @@ import { FC, SyntheticEvent, useRef } from 'react' -import { AvatarWithPlaceholder, LinkIcon, PenEditIcon } from '@baseapp-frontend/design-system' +import { ArchiveIcon, AvatarWithPlaceholder, UnreadIcon } from '@baseapp-frontend/design-system' -import { Box, BoxProps, Badge as DefaultBadge, Typography } from '@mui/material' +import { Box, Badge as DefaultBadge, Typography } from '@mui/material' import { useFragment } from 'react-relay' import { RoomFragment$key } from '../../../../__generated__/RoomFragment.graphql' @@ -28,7 +28,7 @@ const ChatRoomItem: FC = ({ if (handleClick) handleClick() } - const chatCardRef = useRef(null) + const chatCardRef = useRef(null) const { profile } = useCurrentProfile() @@ -60,21 +60,21 @@ const ChatRoomItem: FC = ({ actions={[ { disabled: true, - icon: , - label: 'Share Comment', - onClick: () => console.log('Share'), + icon: , + label: 'Archive Chat', + onClick: () => {}, hasPermission: true, }, { disabled: false, - icon: , - label: 'Edit Comment', - onClick: () => console.log('Edit'), + icon: , + label: 'Mark as Unread', + onClick: () => {}, hasPermission: true, }, ]} enableDelete - handleDeleteItem={() => console.log('Delete')} + handleDeleteItem={() => {}} isDeletingItem={false} ref={chatCardRef} > diff --git a/packages/design-system/components/icons/ArchiveIcon/index.tsx b/packages/design-system/components/icons/ArchiveIcon/index.tsx new file mode 100644 index 00000000..75c87ea6 --- /dev/null +++ b/packages/design-system/components/icons/ArchiveIcon/index.tsx @@ -0,0 +1,20 @@ +import { FC } from 'react' + +import { SvgIcon, SvgIconProps } from '@mui/material' + +const ArchiveIcon: FC = ({ sx, ...props }) => ( + + + + + + +) + +export default ArchiveIcon diff --git a/packages/design-system/components/icons/UnreadIcon/index.tsx b/packages/design-system/components/icons/UnreadIcon/index.tsx new file mode 100644 index 00000000..0fc54181 --- /dev/null +++ b/packages/design-system/components/icons/UnreadIcon/index.tsx @@ -0,0 +1,48 @@ +import { FC } from 'react' + +import { SvgIcon, SvgIconProps } from '@mui/material' + +const UnreadIcon: FC = ({ sx, ...props }) => ( + + + + + + + + + + +) + +export default UnreadIcon diff --git a/packages/design-system/components/icons/index.ts b/packages/design-system/components/icons/index.ts index 99e922be..c21851a1 100644 --- a/packages/design-system/components/icons/index.ts +++ b/packages/design-system/components/icons/index.ts @@ -1,19 +1,21 @@ export { default as AddIcon } from './AddIcon' +export { default as ArchiveIcon } from './ArchiveIcon' export { default as AttachmentIcon } from './AttachmentIcon' export { default as BaseAppLogoCondensed } from './BaseAppLogoCondensed' export { default as CheckMarkIcon } from './CheckMarkIcon' +export { default as ChevronIcon } from './ChevronIcon' export { default as CloseIcon } from './CloseIcon' export { default as CommentReplyIcon } from './CommentReplyIcon' export { default as FavoriteIcon } from './FavoriteIcon' export { default as FavoriteSelectedIcon } from './FavoriteSelectedIcon' export { default as LinkIcon } from './LinkIcon' export { default as MentionIcon } from './MentionIcon' -export { default as PenEditIcon } from './PenEditIcon' -export { default as PinIcon } from './PinIcon' -export { default as SendMessageIcon } from './SendMessageIcon' -export { default as TrashCanIcon } from './TrashCanIcon' export { default as MenuIcon } from './MenuIcon' -export { default as ChevronIcon } from './ChevronIcon' export { default as NotificationBellIcon } from './NotificationBellIcon' export { default as OutlinedEditIcon } from './OutlinedEditIcon' +export { default as PenEditIcon } from './PenEditIcon' +export { default as PinIcon } from './PinIcon' +export { default as SendMessageIcon } from './SendMessageIcon' export { default as ShareIcon } from './ShareIcon' +export { default as TrashCanIcon } from './TrashCanIcon' +export { default as UnreadIcon } from './UnreadIcon' From f2d8eb696a6f52282b59b3e58a7e8f7c40c2032d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Tib=C3=BArcio?= Date: Fri, 29 Nov 2024 10:24:17 -0500 Subject: [PATCH 4/4] chore: versioning --- packages/components/CHANGELOG.md | 8 ++++++++ packages/components/package.json | 2 +- packages/design-system/CHANGELOG.md | 6 ++++++ packages/design-system/package.json | 2 +- packages/wagtail/CHANGELOG.md | 7 +++++++ packages/wagtail/package.json | 2 +- 6 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 6b7ccb60..66d8ebdc 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,5 +1,13 @@ # @baseapp-frontend/components +## 0.0.21 + +### Patch Changes + +- Removed CommentOptions from CommentItem component and refactored into ActionsOverlay. Applied ActionsOverlay to CommentItem and ChatRoomItem components. +- Updated dependencies + - @baseapp-frontend/design-system@0.0.22 + ## 0.0.20 ### Patch Changes diff --git a/packages/components/package.json b/packages/components/package.json index 2e8f17f3..dc505352 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,7 +1,7 @@ { "name": "@baseapp-frontend/components", "description": "BaseApp components modules such as comments, notifications, messages, and more.", - "version": "0.0.20", + "version": "0.0.21", "main": "./index.ts", "types": "dist/index.d.ts", "sideEffects": false, diff --git a/packages/design-system/CHANGELOG.md b/packages/design-system/CHANGELOG.md index cc81a5f5..1c484cae 100644 --- a/packages/design-system/CHANGELOG.md +++ b/packages/design-system/CHANGELOG.md @@ -1,5 +1,11 @@ # @baseapp-frontend/design-system +## 0.0.22 + +### Patch Changes + +- Added Archive and Unread icons + ## 0.0.21 ### Patch Changes diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 4b21e0cc..a7d25fc7 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -1,7 +1,7 @@ { "name": "@baseapp-frontend/design-system", "description": "Design System components and configurations.", - "version": "0.0.21", + "version": "0.0.22", "main": "./index.ts", "types": "dist/index.d.ts", "sideEffects": false, diff --git a/packages/wagtail/CHANGELOG.md b/packages/wagtail/CHANGELOG.md index 456b9b93..febf2c05 100644 --- a/packages/wagtail/CHANGELOG.md +++ b/packages/wagtail/CHANGELOG.md @@ -1,5 +1,12 @@ # @baseapp-frontend/wagtail +## 1.0.4 + +### Patch Changes + +- Updated dependencies + - @baseapp-frontend/design-system@0.0.22 + ## 1.0.3 ### Patch Changes diff --git a/packages/wagtail/package.json b/packages/wagtail/package.json index 92f3c9c1..6f854fec 100644 --- a/packages/wagtail/package.json +++ b/packages/wagtail/package.json @@ -1,7 +1,7 @@ { "name": "@baseapp-frontend/wagtail", "description": "BaseApp Wagtail", - "version": "1.0.3", + "version": "1.0.4", "main": "./index.ts", "types": "dist/index.d.ts", "sideEffects": false,