From 54659e732b48ec161e33a3b24aac3fdcbba2a842 Mon Sep 17 00:00:00 2001 From: Sophia Shoemaker <1317004+mrscobbler@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:23:52 -0700 Subject: [PATCH 1/2] feat(content-sidebar): focused state for threaded-replies in activity-feed-v2 When a comment or annotation is deep-linked via activeFeedEntryId, paint a focused chrome on the matching thread by recoloring the vendor card's existing border + background. The wrapper around ThreadedAnnotation is display: contents by default so layout is untouched for unfocused rows and the vendor's gap/padding contract is preserved. Co-Authored-By: Claude Opus 4.7 --- .../activity-feed-v2/ActivityFeedV2.scss | 16 +++ .../activity-feed-v2/ActivityFeedV2.tsx | 1 + .../activity-feed-v2/FeedItemRow.tsx | 101 ++++++++++-------- 3 files changed, 72 insertions(+), 46 deletions(-) diff --git a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss index 25eb1674dd..c40f146a29 100644 --- a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss +++ b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss @@ -1,6 +1,8 @@ // Resets BUIE .bcs global styles that leak into Blueprint and // @box/threaded-annotations components. +@import '../../common/variables'; + .bcs-NewActivityFeed { position: absolute; inset: 0; @@ -43,6 +45,20 @@ &-mentionEmpty { padding: var(--bp-space-030) var(--bp-space-040); } + + // Wrapper is transparent to layout; serves only as a DOM hook for the + // focused descendant selector below. The visible focus chrome (border, + // background) is painted directly onto the vendor's threaded-annotation + // card so its own border-radius, padding, and position are reused. + // Class name is CSS-modules-hashed; substring match survives bumps. + &-threadRow { + display: contents; + + &.is-focused [class*='threadedAnnotations'] { + border-color: $bdl-box-blue-80; + background-color: rgba($bdl-box-blue, 0.04); + } + } } // Containing block for the absolutely positioned .bcs-NewActivityFeed child, diff --git a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx index 9c0afc2ee1..66a4a91212 100644 --- a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx +++ b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx @@ -417,6 +417,7 @@ const ActivityFeedV2 = ({ {filteredItems.map(item => ( { + const threadRowClassName = classNames('bcs-NewActivityFeed-threadRow', { + 'is-focused': item.id === activeFeedEntryId, + }); + switch (item.type) { case 'comment': { const { permissions } = item; @@ -145,27 +152,28 @@ const FeedItemRow = ({ ? { ...item.annotationTarget, timestamp: formatByTimeFormat(timestampMs, timeFormat, fps) } : item.annotationTarget; return ( - onCommentCopyLink({ id }) : undefined} - onDelete={handleDelete} - onEdit={handleEdit} - onEditError={logEditError} - onPost={buildReplyPost(item.id, FEED_ITEM_TYPE_COMMENT, isDisabled, onReplyCreate)} - onResolve={handleStatusChange('resolved')} - onThreadDelete={() => handleDelete(item.id)} - onUnresolve={handleStatusChange('open')} - resolvedAt={item.resolvedAt} - resolvedBy={item.resolvedBy} - userSelectorProps={userSelectorProps} - /> +
+ onCommentCopyLink({ id }) : undefined} + onDelete={handleDelete} + onEdit={handleEdit} + onEditError={logEditError} + onPost={buildReplyPost(item.id, FEED_ITEM_TYPE_COMMENT, isDisabled, onReplyCreate)} + onResolve={handleStatusChange('resolved')} + onThreadDelete={() => handleDelete(item.id)} + onUnresolve={handleStatusChange('open')} + resolvedAt={item.resolvedAt} + resolvedBy={item.resolvedBy} + userSelectorProps={userSelectorProps} + /> +
); } @@ -209,31 +217,32 @@ const FeedItemRow = ({ } : badgeTarget; return ( - onAnnotationSelect?.(item.annotation)} - onAvatarClick={noop} - onCopyLink={ - onAnnotationCopyLink && fileVersionId - ? () => onAnnotationCopyLink({ annotationId: item.id, fileVersionId }) - : undefined - } - onDelete={handleDelete} - onEdit={handleEdit} - onEditError={logEditError} - onPost={buildReplyPost(item.id, FEED_ITEM_TYPE_ANNOTATION, isDisabled, onReplyCreate)} - onResolve={handleStatusChange('resolved')} - onThreadDelete={() => handleDelete(item.id)} - onUnresolve={handleStatusChange('open')} - resolvedAt={item.resolvedAt} - resolvedBy={item.resolvedBy} - userSelectorProps={userSelectorProps} - /> +
+ onAnnotationSelect?.(item.annotation)} + onAvatarClick={noop} + onCopyLink={ + onAnnotationCopyLink && fileVersionId + ? () => onAnnotationCopyLink({ annotationId: item.id, fileVersionId }) + : undefined + } + onDelete={handleDelete} + onEdit={handleEdit} + onEditError={logEditError} + onPost={buildReplyPost(item.id, FEED_ITEM_TYPE_ANNOTATION, isDisabled, onReplyCreate)} + onResolve={handleStatusChange('resolved')} + onThreadDelete={() => handleDelete(item.id)} + onUnresolve={handleStatusChange('open')} + resolvedAt={item.resolvedAt} + resolvedBy={item.resolvedBy} + userSelectorProps={userSelectorProps} + /> +
); } From 1f47211ea07f5e57fefa40f93c6822422afe9c92 Mon Sep 17 00:00:00 2001 From: Sophia Shoemaker <1317004+mrscobbler@users.noreply.github.com> Date: Mon, 29 Jun 2026 12:16:10 -0700 Subject: [PATCH 2/2] refactor(content-sidebar): address review feedback on threaded-replies focus - Use Blueprint tokens (--bp-box-blue-80, --bp-box-blue-opacity-04) and drop the now-unused variables import - Remove redundant key from the single-root threadRow wrapper divs - Add focus-state tests covering the threadRow wrapper and is-focused toggling Co-Authored-By: Claude Opus 4.8 --- .../activity-feed-v2/ActivityFeedV2.scss | 12 ++----- .../activity-feed-v2/FeedItemRow.tsx | 4 +-- .../__tests__/FeedItemRow.test.tsx | 36 +++++++++++++++++++ 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss index c40f146a29..b9fdbb0a6f 100644 --- a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss +++ b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss @@ -1,8 +1,6 @@ // Resets BUIE .bcs global styles that leak into Blueprint and // @box/threaded-annotations components. -@import '../../common/variables'; - .bcs-NewActivityFeed { position: absolute; inset: 0; @@ -46,17 +44,13 @@ padding: var(--bp-space-030) var(--bp-space-040); } - // Wrapper is transparent to layout; serves only as a DOM hook for the - // focused descendant selector below. The visible focus chrome (border, - // background) is painted directly onto the vendor's threaded-annotation - // card so its own border-radius, padding, and position are reused. - // Class name is CSS-modules-hashed; substring match survives bumps. + // Layout-transparent hook that paints focus chrome onto the vendor card (CSS-modules-hashed, so match a substring). &-threadRow { display: contents; &.is-focused [class*='threadedAnnotations'] { - border-color: $bdl-box-blue-80; - background-color: rgba($bdl-box-blue, 0.04); + border-color: var(--bp-box-blue-80); + background-color: var(--bp-box-blue-opacity-04); } } } diff --git a/src/elements/content-sidebar/activity-feed-v2/FeedItemRow.tsx b/src/elements/content-sidebar/activity-feed-v2/FeedItemRow.tsx index 7df418b8dd..86240d1448 100644 --- a/src/elements/content-sidebar/activity-feed-v2/FeedItemRow.tsx +++ b/src/elements/content-sidebar/activity-feed-v2/FeedItemRow.tsx @@ -152,7 +152,7 @@ const FeedItemRow = ({ ? { ...item.annotationTarget, timestamp: formatByTimeFormat(timestampMs, timeFormat, fps) } : item.annotationTarget; return ( -
+
+
{ }); }); }); + + describe('focus state', () => { + const getThreadRow = () => screen.getByRole('article', { name: 'threaded annotation' }).parentElement; + + test('should wrap a comment thread in a threadRow div without breaking rendering', () => { + render(); + + expect(screen.getByRole('article', { name: 'threaded annotation' })).toBeVisible(); + expect(getThreadRow()).toHaveClass('bcs-NewActivityFeed-threadRow'); + }); + + test('should mark the comment row focused when activeFeedEntryId matches the item id', () => { + render(); + expect(getThreadRow()).toHaveClass('is-focused'); + }); + + test('should not mark the comment row focused when activeFeedEntryId does not match', () => { + render(); + expect(getThreadRow()).not.toHaveClass('is-focused'); + }); + + test('should not mark the comment row focused when activeFeedEntryId is undefined', () => { + render(); + expect(getThreadRow()).not.toHaveClass('is-focused'); + }); + + test('should mark the annotation row focused when activeFeedEntryId matches the item id', () => { + render(); + expect(getThreadRow()).toHaveClass('bcs-NewActivityFeed-threadRow', 'is-focused'); + }); + + test('should not mark the annotation row focused when activeFeedEntryId does not match', () => { + render(); + expect(getThreadRow()).not.toHaveClass('is-focused'); + }); + }); });