From 9f6475816db64e3333373d7c7775d42742c651b6 Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Sun, 17 Nov 2024 01:21:09 +0300 Subject: [PATCH] CW-instant-reorder Added feedItem keyboard support --- .../ChatComponent/ChatComponent.tsx | 15 ++++++ .../DiscussionFeedCard/DiscussionFeedCard.tsx | 6 --- .../common/components/FeedCard/FeedCard.tsx | 2 +- .../common/components/FeedItem/context.ts | 1 + .../components/FeedLayout/FeedLayout.tsx | 53 ++++++++++++++++++- .../components/FeedLayout/constants/index.ts | 4 ++ src/shared/hooks/index.tsx | 2 + src/shared/hooks/useElementPresence.tsx | 48 +++++++++++++++++ src/shared/hooks/useIsElementFocused.tsx | 24 +++++++++ .../utils/countTextEditorEmojiElements.ts | 1 + 10 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 src/shared/hooks/useElementPresence.tsx create mode 100644 src/shared/hooks/useIsElementFocused.tsx diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index 99b2bec6b4..c7350bd0b1 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -88,6 +88,7 @@ import { } from "./utils"; import styles from "./ChatComponent.module.scss"; import { BaseTextEditorHandles } from "@/shared/ui-kit/TextEditor/BaseTextEditor"; +import { useFeedItemContext } from "../FeedItem"; const BASE_CHAT_INPUT_HEIGHT = 48; const BASE_ORDER_INTERVAL = 1000; @@ -258,6 +259,20 @@ export default function ChatComponent({ parseStringToTextEditorValue(), ); + const { + setIsInputFocused + } = useFeedItemContext(); + + useEffect(() => { + const isEmpty = checkIsTextEditorValueEmpty(message); + if(!isEmpty || message.length > 1) { + setIsInputFocused?.(true); + } else { + setIsInputFocused?.(false); + } + + },[message, setIsInputFocused]) + const emojiCount = useMemo( () => countTextEditorEmojiElements(message), [message], diff --git a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx index 61547c2016..dcd82a373c 100644 --- a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx +++ b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx @@ -218,12 +218,6 @@ function DiscussionFeedCard(props, ref) { const cardTitle = discussion?.title; const commonNotion = outerCommonNotion ?? common?.notion; - // const ownerId = useMemo(() => { - // if(item.userId) { - // return item.userId - // } - // },[item.userId]) - const handleOpenChat = useCallback(() => { if (discussion && !isPreviewMode) { setChatItem({ diff --git a/src/pages/common/components/FeedCard/FeedCard.tsx b/src/pages/common/components/FeedCard/FeedCard.tsx index 1b1625819a..5fac7dcb50 100644 --- a/src/pages/common/components/FeedCard/FeedCard.tsx +++ b/src/pages/common/components/FeedCard/FeedCard.tsx @@ -299,7 +299,7 @@ const FeedCard = (props, ref) => { ]); return ( -
+
{!isPreviewMode &&
{feedItemBaseContent}
}
void; setExpandedFeedItemId?: (feedItemId: string | null) => void; renderFeedItemBaseContent?: (props: FeedItemBaseContentProps) => ReactNode; onFeedItemUpdate?: (item: CommonFeed, isRemoved: boolean) => void; diff --git a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx index 4bfc9ef35b..91f55aafef 100644 --- a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx +++ b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx @@ -13,7 +13,7 @@ import React, { import { useSelector } from "react-redux"; import { useHistory } from "react-router-dom"; import PullToRefresh from "react-simple-pull-to-refresh"; -import { useDeepCompareEffect, useWindowSize } from "react-use"; +import { useDeepCompareEffect, useKey, useWindowSize } from "react-use"; import classNames from "classnames"; import { selectUser } from "@/pages/Auth/store/selectors"; import { useCommonMember } from "@/pages/OldCommon/hooks"; @@ -44,7 +44,7 @@ import { ROUTE_PATHS, } from "@/shared/constants"; import { useRoutesContext } from "@/shared/contexts"; -import { useMemoizedFunction, useQueryParams } from "@/shared/hooks"; +import { useElementPresence, useMemoizedFunction, useQueryParams } from "@/shared/hooks"; import { useGovernanceByCommonId } from "@/shared/hooks/useCases"; import { useDisableOverscroll } from "@/shared/hooks/useDisableOverscroll"; import { useIsTabletView } from "@/shared/hooks/viewport"; @@ -90,6 +90,7 @@ import { import { BATCHES_AMOUNT_TO_PRELOAD, ITEMS_AMOUNT_TO_PRE_LOAD_MESSAGES, + MENTION_TAG_ELEMENT, } from "./constants"; import { useUserForProfile } from "./hooks"; import { @@ -353,6 +354,8 @@ const FeedLayout: ForwardRefRenderFunction = ( const activeFeedItemId = chatItem?.feedItemId || feedItemIdForAutoChatOpen; const sizeKey = `${windowWidth}_${contentWidth}`; + const activeFeedItemIndex = useMemo(() => allFeedItems.findIndex((item) => item.itemId === activeFeedItemId), [activeFeedItemId, allFeedItems]); + const getUserCircleIds = useCallback( (commonId) => { return Object.values( @@ -405,6 +408,50 @@ const FeedLayout: ForwardRefRenderFunction = ( setChatItem(nextChatItem); }, []); + const isMentionOpen = useElementPresence(MENTION_TAG_ELEMENT.key, MENTION_TAG_ELEMENT.value); + const [isInputFocused, setIsInputFocused] = useState(false); + + const handleArrowUp = (event, activeFeedItemIndex, allFeedItems, isMentionOpen, setChatItem) => { + if (!isMentionOpen && !isInputFocused) { + event.preventDefault(); + event.stopPropagation(); + + if (activeFeedItemIndex > 0) { + const nextFeedItemId = allFeedItems[activeFeedItemIndex - 1]?.itemId; + + setChatItem({ feedItemId: nextFeedItemId }); + } + } + }; + + const handleArrowDown = (event, activeFeedItemIndex, allFeedItems, isMentionOpen, setChatItem) => { + if (!isMentionOpen && !isInputFocused) { + event.preventDefault(); + event.stopPropagation(); + + if (activeFeedItemIndex < allFeedItems.length - 1) { + const nextFeedItemId = allFeedItems[activeFeedItemIndex + 1]?.itemId; + + setChatItem({ feedItemId: nextFeedItemId }); + } + } + }; + + // Inside your component + useKey( + "ArrowUp", + (event) => handleArrowUp(event, activeFeedItemIndex, allFeedItems, isMentionOpen, setChatItem), + {}, + [activeFeedItemIndex, allFeedItems, isMentionOpen, isInputFocused] + ); + + useKey( + "ArrowDown", + (event) => handleArrowDown(event, activeFeedItemIndex, allFeedItems, isMentionOpen, setChatItem), + {}, + [activeFeedItemIndex, allFeedItems, isMentionOpen, isInputFocused] + ); + const chatContextValue = useMemo( () => ({ setChatItem: setActiveChatItem, @@ -644,6 +691,7 @@ const FeedLayout: ForwardRefRenderFunction = ( // so we will not have extra re-renders of ALL rendered items const feedItemContextValue = useMemo( () => ({ + setIsInputFocused, setExpandedFeedItemId, renderFeedItemBaseContent, onFeedItemUpdate, @@ -656,6 +704,7 @@ const FeedLayout: ForwardRefRenderFunction = ( onActiveItemDataChange: handleActiveFeedItemDataChange, }), [ + setIsInputFocused, renderFeedItemBaseContent, onFeedItemUpdate, onFeedItemUnfollowed, diff --git a/src/pages/commonFeed/components/FeedLayout/constants/index.ts b/src/pages/commonFeed/components/FeedLayout/constants/index.ts index 68c376375c..b64b9cd957 100644 --- a/src/pages/commonFeed/components/FeedLayout/constants/index.ts +++ b/src/pages/commonFeed/components/FeedLayout/constants/index.ts @@ -1,2 +1,6 @@ export const BATCHES_AMOUNT_TO_PRELOAD = 2; export const ITEMS_AMOUNT_TO_PRE_LOAD_MESSAGES = 10; +export const MENTION_TAG_ELEMENT = { + key: "data-cy", + value: "mentions-portal" +} diff --git a/src/shared/hooks/index.tsx b/src/shared/hooks/index.tsx index 7c78ecdd51..5371485a16 100644 --- a/src/shared/hooks/index.tsx +++ b/src/shared/hooks/index.tsx @@ -33,3 +33,5 @@ export { default as useImageSizeCheck } from "./useImageSizeCheck"; export { default as useLightThemeOnly } from "./useLightThemeOnly"; export * from "./useToggle"; export * from "./useTraceUpdate"; +export * from "./useElementPresence"; +export * from "./useIsElementFocused"; \ No newline at end of file diff --git a/src/shared/hooks/useElementPresence.tsx b/src/shared/hooks/useElementPresence.tsx new file mode 100644 index 0000000000..27eb4f74ce --- /dev/null +++ b/src/shared/hooks/useElementPresence.tsx @@ -0,0 +1,48 @@ +import { useEffect, useState } from "react"; + +export const useElementPresence = (attribute, value) => { + const [elementPresent, setElementPresent] = useState(false); + + useEffect(() => { + const observerCallback = (mutationsList) => { + mutationsList.forEach((mutation) => { + // Check for added nodes + mutation.addedNodes.forEach((node) => { + if ( + node.nodeType === 1 && // Ensure it's an element + node.getAttribute(attribute) === value + ) { + setElementPresent(true); + } + }); + + // Check for removed nodes + mutation.removedNodes.forEach((node) => { + if ( + node.nodeType === 1 && // Ensure it's an element + node.getAttribute(attribute) === value + ) { + setElementPresent(false); + } + }); + }); + }; + + const observer = new MutationObserver(observerCallback); + + // Start observing the entire document + observer.observe(document.body, { childList: true, subtree: true }); + + // Check initially if the element is already present + const initialElement = document.querySelector(`[${attribute}="${value}"]`); + if (initialElement) { + setElementPresent(true); + } + + return () => { + observer.disconnect(); // Cleanup observer on unmount + }; + }, [attribute, value]); + + return elementPresent; +}; \ No newline at end of file diff --git a/src/shared/hooks/useIsElementFocused.tsx b/src/shared/hooks/useIsElementFocused.tsx new file mode 100644 index 0000000000..da6dd02e97 --- /dev/null +++ b/src/shared/hooks/useIsElementFocused.tsx @@ -0,0 +1,24 @@ +import { useEffect, useState } from "react"; + +export const useIsElementFocused = (id: string) => { + const [isFocused, setIsFocused] = useState(false); + + useEffect(() => { + const handleFocusChange = () => { + const element = document.getElementById(id); + setIsFocused(document.activeElement === element); + }; + + // Listen to focus changes globally + document.addEventListener("focusin", handleFocusChange); + document.addEventListener("focusout", handleFocusChange); + + // Cleanup + return () => { + document.removeEventListener("focusin", handleFocusChange); + document.removeEventListener("focusout", handleFocusChange); + }; + }, [id]); + + return isFocused; +}; \ No newline at end of file diff --git a/src/shared/ui-kit/TextEditor/utils/countTextEditorEmojiElements.ts b/src/shared/ui-kit/TextEditor/utils/countTextEditorEmojiElements.ts index f8bdcd9ec8..828decab8a 100644 --- a/src/shared/ui-kit/TextEditor/utils/countTextEditorEmojiElements.ts +++ b/src/shared/ui-kit/TextEditor/utils/countTextEditorEmojiElements.ts @@ -14,6 +14,7 @@ export const countTextEditorEmojiElements = ( let hasText = false; let emojiCount = 0; + editorValue.forEach((element) => { if ( (element as ParagraphElement)?.type === ElementType.Paragraph &&