diff --git a/package-lock.json b/package-lock.json index 057ccb17acc0b..efd5ed04bed52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4473,6 +4473,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", "dependencies": { "@emotion/memoize": "^0.8.1" } @@ -4480,7 +4481,8 @@ "node_modules/@emotion/is-prop-valid/node_modules/@emotion/memoize": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" }, "node_modules/@emotion/jest": { "version": "11.7.1", @@ -51177,6 +51179,7 @@ "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "7.25.7", + "@floating-ui/react-dom": "2.0.8", "@wordpress/a11y": "*", "@wordpress/api-fetch": "*", "@wordpress/blob": "*", @@ -51233,6 +51236,19 @@ "react-dom": "^18.0.0" } }, + "packages/editor/node_modules/@floating-ui/react-dom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.6.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "packages/element": { "name": "@wordpress/element", "version": "6.14.0", @@ -51856,6 +51872,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", @@ -51878,6 +51895,7 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "license": "MIT", "engines": { "node": "*" }, @@ -51889,7 +51907,8 @@ "packages/postcss-plugins-preset/node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" }, "packages/postcss-themes": { "name": "@wordpress/postcss-themes", diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index 21b9f4bca7fc3..2288acc1e0355 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -50,6 +50,8 @@ import useBlockDisplayTitle from './components/block-title/use-block-display-tit import TabbedSidebar from './components/tabbed-sidebar'; import CommentIconSlotFill from './components/collab/block-comment-icon-slot'; import CommentIconToolbarSlotFill from './components/collab/block-comment-icon-toolbar-slot'; +import { useBlockElementRef } from './components/block-list/use-block-props/use-block-refs'; + /** * Private @wordpress/block-editor APIs. */ @@ -97,4 +99,5 @@ lock( privateApis, { sectionRootClientIdKey, CommentIconSlotFill, CommentIconToolbarSlotFill, + useBlockElementRef, } ); diff --git a/packages/editor/package.json b/packages/editor/package.json index b19e2fb3dab71..442d32b08e7d7 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -34,6 +34,7 @@ ], "dependencies": { "@babel/runtime": "7.25.7", + "@floating-ui/react-dom": "2.0.8", "@wordpress/a11y": "*", "@wordpress/api-fetch": "*", "@wordpress/blob": "*", diff --git a/packages/editor/src/components/collab-sidebar/comments.js b/packages/editor/src/components/collab-sidebar/comments.js index d492d233956cb..b166eb0fae2fe 100644 --- a/packages/editor/src/components/collab-sidebar/comments.js +++ b/packages/editor/src/components/collab-sidebar/comments.js @@ -2,11 +2,16 @@ * External dependencies */ import clsx from 'clsx'; +import { + useFloating, + autoUpdate, + offset as offsetMiddleware, +} from '@floating-ui/react-dom'; /** * WordPress dependencies */ -import { useState, RawHTML } from '@wordpress/element'; +import { useState, RawHTML, useRef, useEffect } from '@wordpress/element'; import { __experimentalHStack as HStack, __experimentalVStack as VStack, @@ -18,13 +23,19 @@ import { import { Icon, check, published, moreVertical } from '@wordpress/icons'; import { __, _x, sprintf } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; -import { store as blockEditorStore } from '@wordpress/block-editor'; +import { + store as blockEditorStore, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; /** * Internal dependencies */ import CommentAuthorInfo from './comment-author-info'; import CommentForm from './comment-form'; +import { unlock } from '../../lock-unlock'; + +const { useBlockElementRef } = unlock( blockEditorPrivateApis ); /** * Renders the Comments component. @@ -37,6 +48,7 @@ import CommentForm from './comment-form'; * @param {Function} props.onCommentResolve - The function to mark a comment as resolved. * @param {boolean} props.showCommentBoard - Whether to show the comment board. * @param {Function} props.setShowCommentBoard - The function to set the comment board visibility. + * @param {boolean} props.canvasSidebar - Whether is this canvas sidebar or not. * @return {React.ReactNode} The rendered Comments component. */ export function Comments( { @@ -47,7 +59,19 @@ export function Comments( { onCommentResolve, showCommentBoard, setShowCommentBoard, + canvasSidebar, } ) { + const [ heights, setHeights ] = useState( {} ); + + const updateHeight = ( id, newHeight ) => { + setHeights( ( prev ) => { + if ( prev[ id ] !== newHeight ) { + return { ...prev, [ id ]: newHeight }; + } + return prev; + } ); + }; + const { blockCommentId } = useSelect( ( select ) => { const { getBlockAttributes, getSelectedBlockClientId } = select( blockEditorStore ); @@ -64,11 +88,20 @@ export function Comments( { showCommentBoard && blockCommentId ? blockCommentId : null ); + // Object to store offsets for each board. + const offsetsRef = useRef( {} ); + + const updateOffsets = ( id, offset ) => { + offsetsRef.current[ id ] = offset; + }; + const clearThreadFocus = () => { setFocusThread( null ); setShowCommentBoard( false ); }; + const ParentWrapper = canvasSidebar ? ThreadWrapper : VStack; + return ( <> { @@ -89,9 +122,12 @@ export function Comments( { } { Array.isArray( threads ) && threads.length > 0 && - threads.map( ( thread ) => ( - ( + setFocusThread( thread.id ) } + offsetsRef={ offsetsRef } + updateOffsets={ updateOffsets } + previousThreadId={ threads[ index - 1 ]?.id } + updateHeight={ updateHeight } + heights={ heights } > - + ) ) } ); @@ -357,3 +396,81 @@ const CommentBoard = ( { thread, onResolve, onEdit, onDelete, status } ) => { ); }; + +const ThreadWrapper = ( { + children, + thread, + className, + onClick, + offsetsRef, + updateOffsets, + previousThreadId, + updateHeight, + heights, +} ) => { + const blockRef = useRef(); + useBlockElementRef( thread.clientId, blockRef ); + + const selectedBlockElementRect = blockRef.current?.getBoundingClientRect(); + + const initialOffsetTop = selectedBlockElementRect?.top; + + const previousOffset = previousThreadId + ? offsetsRef.current[ previousThreadId ] + : 0; + + const previousBoardHeight = heights[ previousThreadId ]; + + const calculateOffset = () => { + if ( + previousOffset && + initialOffsetTop < previousOffset + previousBoardHeight + ) { + return previousOffset - initialOffsetTop + previousBoardHeight + 20; + } + return 0; + }; + + const { y, refs } = useFloating( { + placement: 'right-start', + middleware: [ + offsetMiddleware( { + crossAxis: calculateOffset(), + } ), + ], + whileElementsMounted: autoUpdate, + } ); + + useEffect( () => { + if ( blockRef.current ) { + refs.setReference( blockRef.current ); // Bind reference element + } + }, [ blockRef, refs ] ); + + useEffect( () => { + if ( y !== null && y !== 0 ) { + updateOffsets( thread.id, y, refs.floating?.current?.clientHeight ); // Pass the offset to the parent + } + }, [ y, updateOffsets ] ); + + useEffect( () => { + if ( refs.floating?.current ) { + const newHeight = refs.floating?.current.scrollHeight; + updateHeight( thread.id, newHeight ); + } + }, [ thread.id, updateHeight ] ); + + return ( + + { children } + + ); +}; diff --git a/packages/editor/src/components/collab-sidebar/index.js b/packages/editor/src/components/collab-sidebar/index.js index c53ee9e7e919c..d58a3f766d9df 100644 --- a/packages/editor/src/components/collab-sidebar/index.js +++ b/packages/editor/src/components/collab-sidebar/index.js @@ -56,6 +56,7 @@ function CollabSidebarContent( { setShowCommentBoard, styles, comments, + canvasSidebar = false, } ) { const { createNotice } = useDispatch( noticesStore ); const { saveEntityRecord, deleteEntityRecord } = useDispatch( coreStore ); @@ -203,7 +204,6 @@ function CollabSidebarContent( { setShowCommentBoard={ setShowCommentBoard } /> ); @@ -296,19 +297,23 @@ export default function CollabSidebar() { return { resultComments: [], sortedThreads: [] }; } + const blockCommentIds = getCommentIdsFromBlocks( blocks ); + const blockCommentIdMap = new Map( + blockCommentIds.map( ( item ) => [ item.commentID, item ] ) + ); + const updatedResult = result.map( ( item ) => ( { ...item, reply: [ ...item.reply ].reverse(), + clientId: blockCommentIdMap.get( item.id )?.clientID, } ) ); - const blockCommentIds = getCommentIdsFromBlocks( blocks ); - const threadIdMap = new Map( updatedResult.map( ( thread ) => [ thread.id, thread ] ) ); const sortedComments = blockCommentIds - .map( ( id ) => threadIdMap.get( id ) ) + .map( ( item ) => threadIdMap.get( item.commentID ) ) .filter( ( thread ) => thread !== undefined ); return { resultComments: updatedResult, sortedThreads: sortedComments }; @@ -367,6 +372,7 @@ export default function CollabSidebar() { styles={ { backgroundColor, } } + canvasSidebar /> diff --git a/packages/editor/src/components/collab-sidebar/style.scss b/packages/editor/src/components/collab-sidebar/style.scss index 29fcc80779f68..585b5e4580746 100644 --- a/packages/editor/src/components/collab-sidebar/style.scss +++ b/packages/editor/src/components/collab-sidebar/style.scss @@ -8,11 +8,19 @@ .editor-collab-sidebar { height: 100%; + overflow: hidden; + + .editor-collab-sidebar-panel { + &__thread { + position: absolute; + width: calc(100% - 32px); + } + } } .editor-collab-sidebar-panel { padding: $grid-unit-20; - height: 100%; + height: 100vw; &__thread { position: relative; diff --git a/packages/editor/src/components/collab-sidebar/utils.js b/packages/editor/src/components/collab-sidebar/utils.js index 51345392098ff..020ab14790eef 100644 --- a/packages/editor/src/components/collab-sidebar/utils.js +++ b/packages/editor/src/components/collab-sidebar/utils.js @@ -27,7 +27,10 @@ export function getCommentIdsFromBlocks( blocks ) { block.attributes.blockCommentId && ! commentIds.includes( block.attributes.blockCommentId ) ) { - commentIds.push( block.attributes.blockCommentId ); + commentIds.push( { + clientID: block.clientId, + commentID: block.attributes.blockCommentId, + } ); } // Recursively check inner blocks