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:
gitstart-twenty 2023-08-10 02:19:35 +08:00 committed by GitHub
parent fbac345164
commit 1f4df67a89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 191 additions and 32 deletions

View File

@ -4,7 +4,10 @@ import { useRecoilValue } from 'recoil';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { currentUserState } from '@/auth/states/currentUserState'; 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 { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { Activity, useCreateCommentMutation } from '~/generated/graphql'; import { Activity, useCreateCommentMutation } from '~/generated/graphql';
import { isNonEmptyString } from '~/utils/isNonEmptyString'; import { isNonEmptyString } from '~/utils/isNonEmptyString';
@ -17,6 +20,7 @@ type OwnProps = {
activity: Pick<Activity, 'id'> & { activity: Pick<Activity, 'id'> & {
comments: Array<CommentForDrawer>; comments: Array<CommentForDrawer>;
}; };
scrollableContainerRef: React.RefObject<HTMLDivElement>;
}; };
const StyledThreadItemListContainer = styled.div` const StyledThreadItemListContainer = styled.div`
@ -31,17 +35,20 @@ const StyledThreadItemListContainer = styled.div`
justify-content: flex-start; justify-content: flex-start;
padding: ${({ theme }) => theme.spacing(8)}; padding: ${({ theme }) => theme.spacing(8)};
padding-bottom: ${({ theme }) => theme.spacing(32)};
padding-left: ${({ theme }) => theme.spacing(12)}; padding-left: ${({ theme }) => theme.spacing(12)};
width: 100%; width: 100%;
`; `;
const StyledCommentActionBar = styled.div` const StyledCommentActionBar = styled.div`
background: ${({ theme }) => theme.background.primary};
border-top: 1px solid ${({ theme }) => theme.border.color.light}; border-top: 1px solid ${({ theme }) => theme.border.color.light};
bottom: 0;
display: flex; display: flex;
padding: 16px 24px 16px 48px; padding: 16px 24px 16px 48px;
position: absolute;
width: calc( width: calc(
${({ theme }) => (useIsMobile() ? '100%' : theme.rightDrawerWidth)} - 48px - ${({ theme }) => (useIsMobile() ? '100%' : theme.rightDrawerWidth)} - 72px
24px
); );
`; `;
@ -52,7 +59,10 @@ const StyledThreadCommentTitle = styled.div`
text-transform: uppercase; text-transform: uppercase;
`; `;
export function ActivityComments({ activity }: OwnProps) { export function ActivityComments({
activity,
scrollableContainerRef,
}: OwnProps) {
const [createCommentMutation] = useCreateCommentMutation(); const [createCommentMutation] = useCreateCommentMutation();
const currentUser = useRecoilValue(currentUserState); const currentUser = useRecoilValue(currentUserState);
@ -74,6 +84,21 @@ export function ActivityComments({ activity }: OwnProps) {
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
}, },
refetchQueries: [getOperationName(GET_ACTIVITY) ?? ''], 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> <StyledCommentActionBar>
{currentUser && <AutosizeTextInput onValidate={handleSendComment} />} {currentUser && (
<AutosizeTextInput
onValidate={handleSendComment}
onFocus={handleFocus}
variant={AutosizeTextInputVariant.Button}
placeholder={activity?.comments.length > 0 ? 'Reply...' : undefined}
/>
)}
</StyledCommentActionBar> </StyledCommentActionBar>
</> </>
); );

View File

@ -1,4 +1,5 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities'; import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
@ -92,6 +93,7 @@ export function ActivityEditor({
const [completedAt, setCompletedAt] = useState<string | null>( const [completedAt, setCompletedAt] = useState<string | null>(
activity.completedAt ?? '', activity.completedAt ?? '',
); );
const containerRef = useRef<HTMLDivElement>(null);
const [updateActivityMutation] = useUpdateActivityMutation(); const [updateActivityMutation] = useUpdateActivityMutation();
@ -166,7 +168,7 @@ export function ActivityEditor({
} }
return ( return (
<StyledContainer> <StyledContainer ref={containerRef}>
<StyledUpperPartContainer> <StyledUpperPartContainer>
<StyledTopContainer> <StyledTopContainer>
<ActivityTypeDropdown activity={activity} /> <ActivityTypeDropdown activity={activity} />
@ -219,6 +221,7 @@ export function ActivityEditor({
id: activity.id, id: activity.id,
comments: activity.comments ?? [], comments: activity.comments ?? [],
}} }}
scrollableContainerRef={containerRef}
/> />
)} )}
</StyledContainer> </StyledContainer>

View File

@ -13,6 +13,7 @@ const StyledContainer = styled.div`
height: 100%; height: 100%;
justify-content: space-between; justify-content: space-between;
overflow-y: auto; overflow-y: auto;
position: relative;
`; `;
type OwnProps = { type OwnProps = {

View File

@ -4,25 +4,45 @@ import { HotkeysEvent } from 'react-hotkeys-hook/dist/types';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Button } from '@/ui/button/components/Button';
import { RoundedIconButton } from '@/ui/button/components/RoundedIconButton'; import { RoundedIconButton } from '@/ui/button/components/RoundedIconButton';
import { IconArrowRight } from '@/ui/icon/index'; import { IconArrowRight } from '@/ui/icon/index';
const MAX_ROWS = 5; const MAX_ROWS = 5;
export enum AutosizeTextInputVariant {
Icon = 'icon',
Button = 'button',
}
type OwnProps = { type OwnProps = {
onValidate?: (text: string) => void; onValidate?: (text: string) => void;
minRows?: number; minRows?: number;
placeholder?: string; placeholder?: string;
onFocus?: () => void;
variant?: AutosizeTextInputVariant;
buttonTitle?: string;
}; };
const StyledContainer = styled.div` const StyledContainer = styled.div`
display: flex;
width: 100%; width: 100%;
`; `;
const StyledTextArea = styled(TextareaAutosize)` const StyledInputContainer = styled.div`
background: ${({ theme }) => theme.background.tertiary}; 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: none;
border-radius: 5px; border-radius: 5px;
color: ${({ theme }) => theme.font.color.primary}; color: ${({ theme }) => theme.font.color.primary};
@ -31,9 +51,6 @@ const StyledTextArea = styled(TextareaAutosize)`
font-weight: ${({ theme }) => theme.font.weight.regular}; font-weight: ${({ theme }) => theme.font.weight.regular};
line-height: 16px; line-height: 16px;
overflow: auto; overflow: auto;
padding: 8px;
resize: none;
width: 100%;
&:focus { &:focus {
border: none; border: none;
@ -44,6 +61,10 @@ const StyledTextArea = styled(TextareaAutosize)`
color: ${({ theme }) => theme.font.color.light}; color: ${({ theme }) => theme.font.color.light};
font-weight: ${({ theme }) => theme.font.weight.regular}; 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 // TODO: this messes with the layout, fix it
@ -51,19 +72,55 @@ const StyledBottomRightRoundedIconButton = styled.div`
height: 0; height: 0;
position: relative; position: relative;
right: 26px; right: 26px;
top: calc(100% - 26.5px); top: 6px;
width: 0px; 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({ export function AutosizeTextInput({
placeholder, placeholder,
onValidate, onValidate,
minRows = 1, minRows = 1,
onFocus,
variant = AutosizeTextInputVariant.Icon,
buttonTitle,
}: OwnProps) { }: OwnProps) {
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
const [isHidden, setIsHidden] = useState(
variant === AutosizeTextInputVariant.Button,
);
const [text, setText] = useState(''); const [text, setText] = useState('');
const isSendButtonDisabled = !text; const isSendButtonDisabled = !text;
const words = text.split(/\s|\n/).filter((word) => word).length;
useHotkeys( useHotkeys(
['shift+enter', 'enter'], ['shift+enter', 'enter'],
@ -120,22 +177,57 @@ export function AutosizeTextInput({
return ( return (
<> <>
<StyledContainer> <StyledContainer>
<StyledTextArea <StyledInputContainer>
placeholder={placeholder || 'Write a comment'} {!isHidden && (
maxRows={MAX_ROWS} <StyledTextArea
minRows={computedMinRows} autoFocus={variant === AutosizeTextInputVariant.Button}
onChange={handleInputChange} placeholder={placeholder ?? 'Write a comment'}
value={text} maxRows={MAX_ROWS}
onFocus={() => setIsFocused(true)} minRows={computedMinRows}
onBlur={() => setIsFocused(false)} onChange={handleInputChange}
/> value={text}
<StyledBottomRightRoundedIconButton> onFocus={() => {
<RoundedIconButton onFocus?.();
onClick={handleOnClickSendButton} setIsFocused(true);
icon={<IconArrowRight size={15} />} }}
disabled={isSendButtonDisabled} onBlur={() => setIsFocused(false)}
/> variant={variant}
</StyledBottomRightRoundedIconButton> />
)}
{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> </StyledContainer>
</> </>
); );

View File

@ -1,8 +1,12 @@
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { AutosizeTextInput } from '../AutosizeTextInput'; import {
AutosizeTextInput,
AutosizeTextInputVariant,
} from '../AutosizeTextInput';
const meta: Meta<typeof AutosizeTextInput> = { const meta: Meta<typeof AutosizeTextInput> = {
title: 'UI/Input/AutosizeTextInput', title: 'UI/Input/AutosizeTextInput',
@ -14,3 +18,30 @@ export default meta;
type Story = StoryObj<typeof AutosizeTextInput>; type Story = StoryObj<typeof AutosizeTextInput>;
export const Default: Story = {}; 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],
};