⚡️(frontend) improve Comments feature
Improve the comments feature to reduce annoyance: - gives focus on input when opening comment threads - hide comment button when mobile view - improve contrast of overline commented text - remove thread if last comment deleted - scroll to bottom thread when adding new comment
This commit is contained in:
@@ -6,12 +6,16 @@ and this project adheres to
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- ⚡️(frontend) export html #1669
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- ♿(frontend) improve accessibility:
|
- ♿(frontend) improve accessibility:
|
||||||
- ♿(frontend) add skip to content button for keyboard accessibility #1624
|
- ♿(frontend) add skip to content button for keyboard accessibility #1624
|
||||||
- ♿(frontend) fix toggle panel button a11y labels #1634
|
- ♿(frontend) fix toggle panel button a11y labels #1634
|
||||||
- ⚡️(frontend) Enhance/html copy to download #1669
|
- ⚡️(frontend) improve Comments feature #1687
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { createDoc, getOtherBrowserName, verifyDocName } from './utils-common';
|
import {
|
||||||
|
closeHeaderMenu,
|
||||||
|
createDoc,
|
||||||
|
getOtherBrowserName,
|
||||||
|
verifyDocName,
|
||||||
|
} from './utils-common';
|
||||||
import { writeInEditor } from './utils-editor';
|
import { writeInEditor } from './utils-editor';
|
||||||
import {
|
import {
|
||||||
addNewMember,
|
addNewMember,
|
||||||
@@ -116,8 +121,7 @@ test.describe('Doc Comments', () => {
|
|||||||
await createDoc(page, 'comment-interaction', browserName, 1);
|
await createDoc(page, 'comment-interaction', browserName, 1);
|
||||||
|
|
||||||
// Checks add react reaction
|
// Checks add react reaction
|
||||||
const editor = page.locator('.ProseMirror');
|
const editor = await writeInEditor({ page, text: 'Hello' });
|
||||||
await editor.locator('.bn-block-outer').last().fill('Hello World');
|
|
||||||
await editor.getByText('Hello').selectText();
|
await editor.getByText('Hello').selectText();
|
||||||
await page.getByRole('button', { name: 'Comment' }).click();
|
await page.getByRole('button', { name: 'Comment' }).click();
|
||||||
|
|
||||||
@@ -181,6 +185,28 @@ test.describe('Doc Comments', () => {
|
|||||||
'background-color',
|
'background-color',
|
||||||
'rgba(0, 0, 0, 0)',
|
'rgba(0, 0, 0, 0)',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* Delete the last comment remove the thread */
|
||||||
|
await editor.getByText('Hello').selectText();
|
||||||
|
await page.getByRole('button', { name: 'Comment' }).click();
|
||||||
|
|
||||||
|
await thread.getByRole('paragraph').first().fill('This is a new comment');
|
||||||
|
await thread.locator('[data-test="save"]').click();
|
||||||
|
|
||||||
|
await expect(editor.getByText('Hello')).toHaveCSS(
|
||||||
|
'background-color',
|
||||||
|
'rgba(237, 180, 0, 0.4)',
|
||||||
|
);
|
||||||
|
await editor.getByText('Hello').click();
|
||||||
|
|
||||||
|
await thread.getByText('This is a new comment').first().hover();
|
||||||
|
await thread.locator('[data-test="moreactions"]').first().click();
|
||||||
|
await thread.getByRole('menuitem', { name: 'Delete comment' }).click();
|
||||||
|
|
||||||
|
await expect(editor.getByText('Hello')).toHaveCSS(
|
||||||
|
'background-color',
|
||||||
|
'rgba(0, 0, 0, 0)',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it checks the comments abilities', async ({ page, browserName }) => {
|
test('it checks the comments abilities', async ({ page, browserName }) => {
|
||||||
@@ -293,3 +319,27 @@ test.describe('Doc Comments', () => {
|
|||||||
await cleanup();
|
await cleanup();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Doc Comments mobile', () => {
|
||||||
|
test.use({ viewport: { width: 500, height: 1200 } });
|
||||||
|
|
||||||
|
test('Comments are not visible on mobile', async ({ page, browserName }) => {
|
||||||
|
const [title] = await createDoc(
|
||||||
|
page,
|
||||||
|
'comment-mobile',
|
||||||
|
browserName,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
await closeHeaderMenu(page);
|
||||||
|
|
||||||
|
await verifyDocName(page, title);
|
||||||
|
|
||||||
|
// Checks add react reaction
|
||||||
|
const editor = await writeInEditor({ page, text: 'Hello' });
|
||||||
|
await editor.getByText('Hello').selectText();
|
||||||
|
await expect(page.getByRole('button', { name: 'Comment' })).toBeHidden();
|
||||||
|
await expect(page.getByRole('button', { name: 'Paragraph' })).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* This file is adapted from BlockNote's AddCommentButton component
|
||||||
|
* https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx
|
||||||
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useBlockNoteEditor,
|
useBlockNoteEditor,
|
||||||
useComponentsContext,
|
useComponentsContext,
|
||||||
@@ -10,6 +15,7 @@ import { css } from 'styled-components';
|
|||||||
import { Box, Icon } from '@/components';
|
import { Box, Icon } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import { useDocStore } from '@/features/docs/doc-management';
|
import { useDocStore } from '@/features/docs/doc-management';
|
||||||
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DocsBlockSchema,
|
DocsBlockSchema,
|
||||||
@@ -22,6 +28,7 @@ export const CommentToolbarButton = () => {
|
|||||||
const { currentDoc } = useDocStore();
|
const { currentDoc } = useDocStore();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||||
|
const { isDesktop } = useResponsiveStore();
|
||||||
|
|
||||||
const editor = useBlockNoteEditor<
|
const editor = useBlockNoteEditor<
|
||||||
DocsBlockSchema,
|
DocsBlockSchema,
|
||||||
@@ -35,7 +42,18 @@ export const CommentToolbarButton = () => {
|
|||||||
return !!selectedBlocks.find((block) => block.content !== undefined);
|
return !!selectedBlocks.find((block) => block.content !== undefined);
|
||||||
}, [selectedBlocks]);
|
}, [selectedBlocks]);
|
||||||
|
|
||||||
|
const focusOnInputThread = () => {
|
||||||
|
// Use setTimeout to ensure the DOM has been updated with the new comment
|
||||||
|
setTimeout(() => {
|
||||||
|
const threadElement = document.querySelector<HTMLElement>(
|
||||||
|
'.bn-thread .bn-editor',
|
||||||
|
);
|
||||||
|
threadElement?.focus();
|
||||||
|
}, 400);
|
||||||
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
!isDesktop ||
|
||||||
!show ||
|
!show ||
|
||||||
!editor.isEditable ||
|
!editor.isEditable ||
|
||||||
!Components ||
|
!Components ||
|
||||||
@@ -51,6 +69,7 @@ export const CommentToolbarButton = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.comments?.startPendingComment();
|
editor.comments?.startPendingComment();
|
||||||
editor.formattingToolbar.closeMenu();
|
editor.formattingToolbar.closeMenu();
|
||||||
|
focusOnInputThread();
|
||||||
}}
|
}}
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
data-test="comment-toolbar-button"
|
data-test="comment-toolbar-button"
|
||||||
|
|||||||
@@ -117,6 +117,21 @@ export class DocsThreadStore extends ThreadStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls to the bottom of a thread modal
|
||||||
|
* @param threadId
|
||||||
|
*/
|
||||||
|
private scrollToBottomOfThread() {
|
||||||
|
// Use setTimeout to ensure the DOM has been updated with the new comment
|
||||||
|
setTimeout(() => {
|
||||||
|
const threadElement = document.querySelector('.bn-thread');
|
||||||
|
threadElement?.scrollBy({
|
||||||
|
top: threadElement.scrollHeight,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies all subscribers about the current thread state
|
* Notifies all subscribers about the current thread state
|
||||||
*/
|
*/
|
||||||
@@ -345,6 +360,10 @@ export class DocsThreadStore extends ThreadStore {
|
|||||||
await this.refreshThread(threadId);
|
await this.refreshThread(threadId);
|
||||||
}
|
}
|
||||||
this.ping(threadId);
|
this.ping(threadId);
|
||||||
|
|
||||||
|
// Auto-scroll to bottom of thread after adding comment
|
||||||
|
this.scrollToBottomOfThread();
|
||||||
|
|
||||||
return serverCommentToClientComment(comment);
|
return serverCommentToClientComment(comment);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -405,10 +424,20 @@ export class DocsThreadStore extends ThreadStore {
|
|||||||
// Optimistically remove the comment locally if we have the thread
|
// Optimistically remove the comment locally if we have the thread
|
||||||
const existing = this.threads.get(threadId);
|
const existing = this.threads.get(threadId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
const updatedComments = existing.comments.filter(
|
||||||
|
(c) => c.id !== commentId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// If this was the last comment, delete the thread
|
||||||
|
if (updatedComments.length === 0) {
|
||||||
|
await this.deleteThread({ threadId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const updated: ClientThreadData = {
|
const updated: ClientThreadData = {
|
||||||
...existing,
|
...existing,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
comments: existing.comments.filter((c) => c.id !== commentId),
|
comments: updatedComments,
|
||||||
};
|
};
|
||||||
this.upsertClientThreadData(updated);
|
this.upsertClientThreadData(updated);
|
||||||
this.notifySubscribers();
|
this.notifySubscribers();
|
||||||
@@ -419,10 +448,6 @@ export class DocsThreadStore extends ThreadStore {
|
|||||||
this.ping(threadId);
|
this.ping(threadId);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* UI not implemented
|
|
||||||
* @param _options
|
|
||||||
*/
|
|
||||||
public deleteThread = async (_options: { threadId: string }) => {
|
public deleteThread = async (_options: { threadId: string }) => {
|
||||||
const response = await fetchAPI(
|
const response = await fetchAPI(
|
||||||
`documents/${this.docId}/threads/${_options.threadId}/`,
|
`documents/${this.docId}/threads/${_options.threadId}/`,
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ export const cssComments = (
|
|||||||
background: ${canSeeComment ? '#EDB40066' : 'transparent'};
|
background: ${canSeeComment ? '#EDB40066' : 'transparent'};
|
||||||
color: var(--c--globals--colors--gray-700);
|
color: var(--c--globals--colors--gray-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-show-selection] {
|
||||||
|
color: HighlightText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
em-emoji-picker {
|
em-emoji-picker {
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import { css } from 'styled-components';
|
|||||||
|
|
||||||
import { Box, Text } from '@/components';
|
import { Box, Text } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import { Doc, useDocUtils, useTrans } from '@/docs/doc-management';
|
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
import ChildDocument from '../assets/child-document.svg';
|
import ChildDocument from '../assets/child-document.svg';
|
||||||
import PinnedDocumentIcon from '../assets/pinned-document.svg';
|
import PinnedDocumentIcon from '../assets/pinned-document.svg';
|
||||||
import SimpleFileIcon from '../assets/simple-document.svg';
|
import SimpleFileIcon from '../assets/simple-document.svg';
|
||||||
|
import { useDocUtils, useTrans } from '../hooks';
|
||||||
|
import { Doc } from '../types';
|
||||||
|
|
||||||
const ItemTextCss = css`
|
const ItemTextCss = css`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
Reference in New Issue
Block a user