diff --git a/front/src/modules/activities/components/ActivityComments.tsx b/front/src/modules/activities/components/ActivityComments.tsx index 059df97816..c88c5a1be8 100644 --- a/front/src/modules/activities/components/ActivityComments.tsx +++ b/front/src/modules/activities/components/ActivityComments.tsx @@ -4,7 +4,10 @@ import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { currentUserState } from '@/auth/states/currentUserState'; -import { AutosizeTextInput } from '@/ui/input/autosize-text/components/AutosizeTextInput'; +import { + AutosizeTextInput, + AutosizeTextInputVariant, +} from '@/ui/input/autosize-text/components/AutosizeTextInput'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { Activity, useCreateCommentMutation } from '~/generated/graphql'; import { isNonEmptyString } from '~/utils/isNonEmptyString'; @@ -17,6 +20,7 @@ type OwnProps = { activity: Pick & { comments: Array; }; + scrollableContainerRef: React.RefObject; }; const StyledThreadItemListContainer = styled.div` @@ -31,17 +35,20 @@ const StyledThreadItemListContainer = styled.div` justify-content: flex-start; padding: ${({ theme }) => theme.spacing(8)}; + padding-bottom: ${({ theme }) => theme.spacing(32)}; padding-left: ${({ theme }) => theme.spacing(12)}; width: 100%; `; const StyledCommentActionBar = styled.div` + background: ${({ theme }) => theme.background.primary}; border-top: 1px solid ${({ theme }) => theme.border.color.light}; + bottom: 0; display: flex; padding: 16px 24px 16px 48px; + position: absolute; width: calc( - ${({ theme }) => (useIsMobile() ? '100%' : theme.rightDrawerWidth)} - 48px - - 24px + ${({ theme }) => (useIsMobile() ? '100%' : theme.rightDrawerWidth)} - 72px ); `; @@ -52,7 +59,10 @@ const StyledThreadCommentTitle = styled.div` text-transform: uppercase; `; -export function ActivityComments({ activity }: OwnProps) { +export function ActivityComments({ + activity, + scrollableContainerRef, +}: OwnProps) { const [createCommentMutation] = useCreateCommentMutation(); const currentUser = useRecoilValue(currentUserState); @@ -74,6 +84,21 @@ export function ActivityComments({ activity }: OwnProps) { createdAt: new Date().toISOString(), }, refetchQueries: [getOperationName(GET_ACTIVITY) ?? ''], + onCompleted: () => { + setTimeout(() => { + handleFocus(); + }, 100); + }, + awaitRefetchQueries: true, + }); + } + + function handleFocus() { + const scrollableContainer = scrollableContainerRef.current; + + scrollableContainer?.scrollTo({ + top: scrollableContainer.scrollHeight, + behavior: 'smooth', }); } @@ -91,7 +116,14 @@ export function ActivityComments({ activity }: OwnProps) { )} - {currentUser && } + {currentUser && ( + 0 ? 'Reply...' : undefined} + /> + )} ); diff --git a/front/src/modules/activities/components/ActivityEditor.tsx b/front/src/modules/activities/components/ActivityEditor.tsx index dcf575792f..9b0234abbc 100644 --- a/front/src/modules/activities/components/ActivityEditor.tsx +++ b/front/src/modules/activities/components/ActivityEditor.tsx @@ -1,4 +1,5 @@ -import React, { useCallback, useState } from 'react'; + +import React, { useCallback, useRef, useState } from 'react'; import { useApolloClient } from '@apollo/client'; import { getOperationName } from '@apollo/client/utilities'; import styled from '@emotion/styled'; @@ -92,6 +93,7 @@ export function ActivityEditor({ const [completedAt, setCompletedAt] = useState( activity.completedAt ?? '', ); + const containerRef = useRef(null); const [updateActivityMutation] = useUpdateActivityMutation(); @@ -166,7 +168,7 @@ export function ActivityEditor({ } return ( - + @@ -219,6 +221,7 @@ export function ActivityEditor({ id: activity.id, comments: activity.comments ?? [], }} + scrollableContainerRef={containerRef} /> )} diff --git a/front/src/modules/activities/right-drawer/components/RightDrawerActivity.tsx b/front/src/modules/activities/right-drawer/components/RightDrawerActivity.tsx index 3ee556fd37..90f36afd14 100644 --- a/front/src/modules/activities/right-drawer/components/RightDrawerActivity.tsx +++ b/front/src/modules/activities/right-drawer/components/RightDrawerActivity.tsx @@ -13,6 +13,7 @@ const StyledContainer = styled.div` height: 100%; justify-content: space-between; overflow-y: auto; + position: relative; `; type OwnProps = { diff --git a/front/src/modules/ui/input/autosize-text/components/AutosizeTextInput.tsx b/front/src/modules/ui/input/autosize-text/components/AutosizeTextInput.tsx index ef46196ad2..8a7b04ca73 100644 --- a/front/src/modules/ui/input/autosize-text/components/AutosizeTextInput.tsx +++ b/front/src/modules/ui/input/autosize-text/components/AutosizeTextInput.tsx @@ -4,25 +4,45 @@ import { HotkeysEvent } from 'react-hotkeys-hook/dist/types'; import TextareaAutosize from 'react-textarea-autosize'; import styled from '@emotion/styled'; +import { Button } from '@/ui/button/components/Button'; import { RoundedIconButton } from '@/ui/button/components/RoundedIconButton'; import { IconArrowRight } from '@/ui/icon/index'; const MAX_ROWS = 5; +export enum AutosizeTextInputVariant { + Icon = 'icon', + Button = 'button', +} + type OwnProps = { onValidate?: (text: string) => void; minRows?: number; placeholder?: string; + onFocus?: () => void; + variant?: AutosizeTextInputVariant; + buttonTitle?: string; }; const StyledContainer = styled.div` - display: flex; - width: 100%; `; -const StyledTextArea = styled(TextareaAutosize)` - background: ${({ theme }) => theme.background.tertiary}; +const StyledInputContainer = styled.div` + display: flex; + position: relative; + width: 100%; +`; + +type StyledTextAreaProps = { + variant: AutosizeTextInputVariant; +}; + +const StyledTextArea = styled(TextareaAutosize)` + background: ${({ theme, variant }) => + variant === AutosizeTextInputVariant.Button + ? 'transparent' + : theme.background.tertiary}; border: none; border-radius: 5px; color: ${({ theme }) => theme.font.color.primary}; @@ -31,9 +51,6 @@ const StyledTextArea = styled(TextareaAutosize)` font-weight: ${({ theme }) => theme.font.weight.regular}; line-height: 16px; overflow: auto; - padding: 8px; - resize: none; - width: 100%; &:focus { border: none; @@ -44,6 +61,10 @@ const StyledTextArea = styled(TextareaAutosize)` color: ${({ theme }) => theme.font.color.light}; font-weight: ${({ theme }) => theme.font.weight.regular}; } + padding: ${({ variant }) => + variant === AutosizeTextInputVariant.Button ? '8px 0' : '8px'}; + resize: none; + width: 100%; `; // TODO: this messes with the layout, fix it @@ -51,19 +72,55 @@ const StyledBottomRightRoundedIconButton = styled.div` height: 0; position: relative; right: 26px; - top: calc(100% - 26.5px); + top: 6px; width: 0px; `; +const StyledSendButton = styled(Button)` + margin-left: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledWordCounter = styled.div` + color: ${({ theme }) => theme.font.color.light}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + line-height: 150%; + width: 100%; +`; + +type StyledBottomContainerProps = { + isTextAreaHidden: boolean; +}; + +const StyledBottomContainer = styled.div` + align-items: center; + display: flex; + justify-content: space-between; + margin-top: ${({ theme, isTextAreaHidden }) => + isTextAreaHidden ? 0 : theme.spacing(4)}; +`; + +const StyledCommentText = styled.div` + cursor: text; + padding-bottom: ${({ theme }) => theme.spacing(1)}; + padding-top: ${({ theme }) => theme.spacing(1)}; +`; + export function AutosizeTextInput({ placeholder, onValidate, minRows = 1, + onFocus, + variant = AutosizeTextInputVariant.Icon, + buttonTitle, }: OwnProps) { const [isFocused, setIsFocused] = useState(false); + const [isHidden, setIsHidden] = useState( + variant === AutosizeTextInputVariant.Button, + ); const [text, setText] = useState(''); const isSendButtonDisabled = !text; + const words = text.split(/\s|\n/).filter((word) => word).length; useHotkeys( ['shift+enter', 'enter'], @@ -120,22 +177,57 @@ export function AutosizeTextInput({ return ( <> - setIsFocused(true)} - onBlur={() => setIsFocused(false)} - /> - - } - disabled={isSendButtonDisabled} - /> - + + {!isHidden && ( + { + onFocus?.(); + setIsFocused(true); + }} + onBlur={() => setIsFocused(false)} + variant={variant} + /> + )} + {variant === AutosizeTextInputVariant.Icon && ( + + } + disabled={isSendButtonDisabled} + /> + + )} + + + {variant === AutosizeTextInputVariant.Button && ( + + + {isHidden ? ( + { + setIsHidden(false); + onFocus?.(); + }} + > + Write a comment + + ) : ( + `${words} word${words === 1 ? '' : 's'}` + )} + + + + )} ); diff --git a/front/src/modules/ui/input/autosize-text/components/__stories__/AutosizeTextInput.stories.tsx b/front/src/modules/ui/input/autosize-text/components/__stories__/AutosizeTextInput.stories.tsx index ff828721a8..25a4bde895 100644 --- a/front/src/modules/ui/input/autosize-text/components/__stories__/AutosizeTextInput.stories.tsx +++ b/front/src/modules/ui/input/autosize-text/components/__stories__/AutosizeTextInput.stories.tsx @@ -1,8 +1,12 @@ import type { Meta, StoryObj } from '@storybook/react'; +import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; -import { AutosizeTextInput } from '../AutosizeTextInput'; +import { + AutosizeTextInput, + AutosizeTextInputVariant, +} from '../AutosizeTextInput'; const meta: Meta = { title: 'UI/Input/AutosizeTextInput', @@ -14,3 +18,30 @@ export default meta; type Story = StoryObj; export const Default: Story = {}; + +export const ButtonVariant: Story = { + args: { variant: AutosizeTextInputVariant.Button }, +}; + +export const Catalog: Story = { + parameters: { + catalog: { + dimensions: [ + { + name: 'variants', + values: Object.values(AutosizeTextInputVariant), + props: (variant: AutosizeTextInputVariant) => ({ variant }), + labels: (variant: AutosizeTextInputVariant) => + `variant -> ${variant}`, + }, + { + name: 'minRows', + values: [1, 4], + props: (minRows: number) => ({ minRows }), + labels: (minRows: number) => `minRows -> ${minRows}`, + }, + ], + }, + }, + decorators: [CatalogDecorator], +};