mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-18 17:12:53 +03:00
chore: Improve design of comment bar in notes (#1102)
* Improve design of comment bar in notes Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Add autoFocus Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Add requested changes Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Add requested changes Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Align the text area Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Use ref instead of getElementById Co-authored-by: v1b3m <vibenjamin6@gmail.com> --------- Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
parent
fbac345164
commit
1f4df67a89
@ -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<Activity, 'id'> & {
|
||||
comments: Array<CommentForDrawer>;
|
||||
};
|
||||
scrollableContainerRef: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
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) {
|
||||
)}
|
||||
|
||||
<StyledCommentActionBar>
|
||||
{currentUser && <AutosizeTextInput onValidate={handleSendComment} />}
|
||||
{currentUser && (
|
||||
<AutosizeTextInput
|
||||
onValidate={handleSendComment}
|
||||
onFocus={handleFocus}
|
||||
variant={AutosizeTextInputVariant.Button}
|
||||
placeholder={activity?.comments.length > 0 ? 'Reply...' : undefined}
|
||||
/>
|
||||
)}
|
||||
</StyledCommentActionBar>
|
||||
</>
|
||||
);
|
||||
|
@ -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<string | null>(
|
||||
activity.completedAt ?? '',
|
||||
);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [updateActivityMutation] = useUpdateActivityMutation();
|
||||
|
||||
@ -166,7 +168,7 @@ export function ActivityEditor({
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledContainer ref={containerRef}>
|
||||
<StyledUpperPartContainer>
|
||||
<StyledTopContainer>
|
||||
<ActivityTypeDropdown activity={activity} />
|
||||
@ -219,6 +221,7 @@ export function ActivityEditor({
|
||||
id: activity.id,
|
||||
comments: activity.comments ?? [],
|
||||
}}
|
||||
scrollableContainerRef={containerRef}
|
||||
/>
|
||||
)}
|
||||
</StyledContainer>
|
||||
|
@ -13,6 +13,7 @@ const StyledContainer = styled.div`
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
|
@ -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)<StyledTextAreaProps>`
|
||||
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<StyledBottomContainerProps>`
|
||||
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 (
|
||||
<>
|
||||
<StyledContainer>
|
||||
<StyledTextArea
|
||||
placeholder={placeholder || 'Write a comment'}
|
||||
maxRows={MAX_ROWS}
|
||||
minRows={computedMinRows}
|
||||
onChange={handleInputChange}
|
||||
value={text}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
/>
|
||||
<StyledBottomRightRoundedIconButton>
|
||||
<RoundedIconButton
|
||||
onClick={handleOnClickSendButton}
|
||||
icon={<IconArrowRight size={15} />}
|
||||
disabled={isSendButtonDisabled}
|
||||
/>
|
||||
</StyledBottomRightRoundedIconButton>
|
||||
<StyledInputContainer>
|
||||
{!isHidden && (
|
||||
<StyledTextArea
|
||||
autoFocus={variant === AutosizeTextInputVariant.Button}
|
||||
placeholder={placeholder ?? 'Write a comment'}
|
||||
maxRows={MAX_ROWS}
|
||||
minRows={computedMinRows}
|
||||
onChange={handleInputChange}
|
||||
value={text}
|
||||
onFocus={() => {
|
||||
onFocus?.();
|
||||
setIsFocused(true);
|
||||
}}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
variant={variant}
|
||||
/>
|
||||
)}
|
||||
{variant === AutosizeTextInputVariant.Icon && (
|
||||
<StyledBottomRightRoundedIconButton>
|
||||
<RoundedIconButton
|
||||
onClick={handleOnClickSendButton}
|
||||
icon={<IconArrowRight size={15} />}
|
||||
disabled={isSendButtonDisabled}
|
||||
/>
|
||||
</StyledBottomRightRoundedIconButton>
|
||||
)}
|
||||
</StyledInputContainer>
|
||||
|
||||
{variant === AutosizeTextInputVariant.Button && (
|
||||
<StyledBottomContainer isTextAreaHidden={isHidden}>
|
||||
<StyledWordCounter>
|
||||
{isHidden ? (
|
||||
<StyledCommentText
|
||||
onClick={() => {
|
||||
setIsHidden(false);
|
||||
onFocus?.();
|
||||
}}
|
||||
>
|
||||
Write a comment
|
||||
</StyledCommentText>
|
||||
) : (
|
||||
`${words} word${words === 1 ? '' : 's'}`
|
||||
)}
|
||||
</StyledWordCounter>
|
||||
<StyledSendButton
|
||||
title={buttonTitle ?? 'Comment'}
|
||||
disabled={isSendButtonDisabled}
|
||||
onClick={handleOnClickSendButton}
|
||||
/>
|
||||
</StyledBottomContainer>
|
||||
)}
|
||||
</StyledContainer>
|
||||
</>
|
||||
);
|
||||
|
@ -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<typeof AutosizeTextInput> = {
|
||||
title: 'UI/Input/AutosizeTextInput',
|
||||
@ -14,3 +18,30 @@ export default meta;
|
||||
type Story = StoryObj<typeof AutosizeTextInput>;
|
||||
|
||||
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],
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user