0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Focused reply box when clicking comment button on feed item

ref https://linear.app/tryghost/issue/AP-396

I think it's nicer API to pass in a focused property, rather than an
element ref, but I don't have much experience here, so it might be the
wrong approach!
This commit is contained in:
Fabien O'Carroll 2024-09-19 13:07:51 +07:00 committed by Fabien 'egg' O'Carroll
parent 3e6b3bda8a
commit 403cac4c42
4 changed files with 30 additions and 14 deletions

View file

@ -44,10 +44,10 @@ const Inbox: React.FC<InboxProps> = ({}) => {
return commentsMap.get(id) ?? []; return commentsMap.get(id) ?? [];
}; };
const handleViewContent = (object: ObjectProperties, actor: ActorProperties, comments: Activity[]) => { const handleViewContent = (object: ObjectProperties, actor: ActorProperties, comments: Activity[], focusReply = false) => {
setArticleContent(object); setArticleContent(object);
setArticleActor(actor); setArticleActor(actor);
NiceModal.show(ArticleModal, {object, actor, comments, allComments: commentsMap}); NiceModal.show(ArticleModal, {object, actor, comments, allComments: commentsMap, focusReply});
}; };
function getContentAuthor(activity: Activity) { function getContentAuthor(activity: Activity) {
@ -101,6 +101,12 @@ const Inbox: React.FC<InboxProps> = ({}) => {
layout={layout} layout={layout}
object={activity.object} object={activity.object}
type={activity.type} type={activity.type}
onCommentClick={() => handleViewContent(
activity.object,
getContentAuthor(activity),
getCommentsForObject(activity.object.id),
true
)}
/> />
{index < activities.length - 1 && ( {index < activities.length - 1 && (
<div className="h-px w-full bg-grey-200"></div> <div className="h-px w-full bg-grey-200"></div>

View file

@ -17,6 +17,7 @@ interface ArticleModalProps {
actor: ActorProperties; actor: ActorProperties;
comments: Activity[]; comments: Activity[];
allComments: Map<string, Activity[]>; allComments: Map<string, Activity[]>;
focusReply: boolean;
} }
const ArticleBody: React.FC<{heading: string, image: string|undefined, html: string}> = ({heading, image, html}) => { const ArticleBody: React.FC<{heading: string, image: string|undefined, html: string}> = ({heading, image, html}) => {
@ -74,7 +75,7 @@ const FeedItemDivider: React.FC = () => (
<div className="mx-[-32px] h-px w-[120%] bg-grey-200"></div> <div className="mx-[-32px] h-px w-[120%] bg-grey-200"></div>
); );
const ArticleModal: React.FC<ArticleModalProps> = ({object, actor, comments, allComments}) => { const ArticleModal: React.FC<ArticleModalProps> = ({object, actor, comments, allComments, focusReply}) => {
const MODAL_SIZE_SM = 640; const MODAL_SIZE_SM = 640;
const MODAL_SIZE_LG = 2800; const MODAL_SIZE_LG = 2800;
@ -152,7 +153,7 @@ const ArticleModal: React.FC<ArticleModalProps> = ({object, actor, comments, all
object={object} object={object}
type='Note' type='Note'
/> />
<APReplyBox object={object}/> <APReplyBox focused={focusReply} object={object}/>
<FeedItemDivider /> <FeedItemDivider />
{/* {object.content && <div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.5rem] text-grey-900'></div>} */} {/* {object.content && <div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.5rem] text-grey-900'></div>} */}

View file

@ -205,11 +205,12 @@ interface FeedItemProps {
comments?: Activity[]; comments?: Activity[];
last?: boolean; last?: boolean;
onClick?: () => void; onClick?: () => void;
onCommentClick?: () => void;
} }
const noop = () => {}; const noop = () => {};
const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comments = [], last, onClick = noop}) => { const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comments = [], last, onClick = noop, onCommentClick}) => {
const timestamp = const timestamp =
new Date(object?.published ?? new Date()).toLocaleDateString('default', {year: 'numeric', month: 'short', day: '2-digit'}) + ', ' + new Date(object?.published ?? new Date()).toLocaleTimeString('default', {hour: '2-digit', minute: '2-digit'}); new Date(object?.published ?? new Date()).toLocaleDateString('default', {year: 'numeric', month: 'short', day: '2-digit'}) + ', ' + new Date(object?.published ?? new Date()).toLocaleTimeString('default', {hour: '2-digit', minute: '2-digit'});
@ -292,7 +293,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
</div> </div>
</div> </div>
<Menu items={menuItems} position='end' trigger={UserMenuTrigger}/> <Menu items={menuItems} position='end' trigger={UserMenuTrigger}/>
</div> </div>
<div className={`relative z-10 col-start-2 col-end-3 w-full gap-4`}> <div className={`relative z-10 col-start-2 col-end-3 w-full gap-4`}>
<div className='flex flex-col'> <div className='flex flex-col'>
@ -303,7 +304,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
commentCount={comments.length} commentCount={comments.length}
likeCount={1} likeCount={1}
object={object} object={object}
onCommentClick={onLikeClick} onCommentClick={onCommentClick}
onLikeClick={onLikeClick} onLikeClick={onLikeClick}
/> />
</div> </div>
@ -348,7 +349,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
commentCount={comments.length} commentCount={comments.length}
likeCount={1} likeCount={1}
object={object} object={object}
onCommentClick={onLikeClick} onCommentClick={onCommentClick}
onLikeClick={onLikeClick} onLikeClick={onLikeClick}
/> />
</div> </div>
@ -395,7 +396,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
commentCount={comments.length} commentCount={comments.length}
likeCount={1} likeCount={1}
object={object} object={object}
onCommentClick={onLikeClick} onCommentClick={onCommentClick}
onLikeClick={onLikeClick} onLikeClick={onLikeClick}
/> />
</div> </div>
@ -432,7 +433,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
commentCount={comments.length} commentCount={comments.length}
likeCount={1} likeCount={1}
object={object} object={object}
onCommentClick={onLikeClick} onCommentClick={onCommentClick}
onLikeClick={onLikeClick} onLikeClick={onLikeClick}
/> />
</div> </div>

View file

@ -1,4 +1,4 @@
import React, {HTMLProps, useId, useState} from 'react'; import React, {HTMLProps, useEffect, useId, useRef, useState} from 'react';
import * as FormPrimitive from '@radix-ui/react-form'; import * as FormPrimitive from '@radix-ui/react-form';
import APAvatar from './APAvatar'; import APAvatar from './APAvatar';
@ -10,7 +10,6 @@ import {useReplyMutationForUser, useUserDataForUser} from '../../hooks/useActivi
// import {useFocusContext} from '@tryghost/admin-x-design-system/types/providers/DesignSystemProvider'; // import {useFocusContext} from '@tryghost/admin-x-design-system/types/providers/DesignSystemProvider';
export interface APTextAreaProps extends HTMLProps<HTMLTextAreaElement> { export interface APTextAreaProps extends HTMLProps<HTMLTextAreaElement> {
inputRef?: React.RefObject<HTMLTextAreaElement>;
title?: string; title?: string;
value?: string; value?: string;
rows?: number; rows?: number;
@ -20,10 +19,10 @@ export interface APTextAreaProps extends HTMLProps<HTMLTextAreaElement> {
className?: string; className?: string;
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void; onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
object: ObjectProperties; object: ObjectProperties;
focused: boolean;
} }
const APReplyBox: React.FC<APTextAreaProps> = ({ const APReplyBox: React.FC<APTextAreaProps> = ({
inputRef,
title, title,
value, value,
rows = 1, rows = 1,
@ -32,6 +31,7 @@ const APReplyBox: React.FC<APTextAreaProps> = ({
hint, hint,
className, className,
object, object,
focused,
// onChange, // onChange,
// onFocus, // onFocus,
// onBlur, // onBlur,
@ -43,6 +43,14 @@ const APReplyBox: React.FC<APTextAreaProps> = ({
const {data: user} = useUserDataForUser('index'); const {data: user} = useUserDataForUser('index');
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (textareaRef.current && focused) {
textareaRef.current.focus();
}
}, [focused]);
const styles = clsx( const styles = clsx(
'ap-textarea order-2 w-full resize-none rounded-lg border py-2 pr-3 text-[1.5rem] transition-all dark:text-white', 'ap-textarea order-2 w-full resize-none rounded-lg border py-2 pr-3 text-[1.5rem] transition-all dark:text-white',
error ? 'border-red' : 'border-transparent placeholder:text-grey-500 dark:placeholder:text-grey-800', error ? 'border-red' : 'border-transparent placeholder:text-grey-500 dark:placeholder:text-grey-800',
@ -76,7 +84,7 @@ const APReplyBox: React.FC<APTextAreaProps> = ({
<FormPrimitive.Field name={id} asChild> <FormPrimitive.Field name={id} asChild>
<FormPrimitive.Control asChild> <FormPrimitive.Control asChild>
<textarea <textarea
ref={inputRef} ref={textareaRef}
className={styles} className={styles}
id={id} id={id}
maxLength={maxLength} maxLength={maxLength}