From 07f50f4d959639e5db853c2e8b4d669b58ae9669 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 7d9a62388d..b5f108a4ce 100644 --- a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss +++ b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss @@ -4,6 +4,8 @@ // into Blueprint and @box/threaded-annotations components, breaking their // styling. This file resets those rules within the v2 adapter boundary. +@import '../../common/variables'; + .bcs-NewActivityFeed { display: flex; flex-direction: column; @@ -52,6 +54,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); + } + } } // Lets inner ellipsis truncation resolve against the sidebar width. diff --git a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx index 7c0d9b0e96..ddfea741ef 100644 --- a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx +++ b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx @@ -361,6 +361,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 0496e01113554d8453f23ec98d52dedecead1246 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 --- i18n/en-US.properties | 150 ++++++++++++++++++ .../activity-feed-v2/ActivityFeedV2.scss | 12 +- .../activity-feed-v2/FeedItemRow.tsx | 4 +- .../__tests__/FeedItemRow.test.tsx | 36 +++++ 4 files changed, 191 insertions(+), 11 deletions(-) diff --git a/i18n/en-US.properties b/i18n/en-US.properties index f6a0cfac0e..17b636cbb8 100644 --- a/i18n/en-US.properties +++ b/i18n/en-US.properties @@ -500,6 +500,10 @@ be.expand = Expand be.externalFolder = External Folder # Label for face skill section in the preview sidebar be.faceSkill = Faces +# Call-to-action text describing what to do to navigate to specified feedback form +be.feedbackCtaText = Click to provide feedback +# Accessible text used to describe the form used for feedback +be.feedbackFormDescription = Beta Feedback Form # Icon title for a Box item of type file be.file = File # File access stats error message @@ -996,6 +1000,8 @@ boxui.base.previousMonth = Previous Month boxui.breadcrumb.breadcrumbLabel = Breadcrumb # Button label for the "more" dropdown menu boxui.categorySelector.label.more = More +# Icon to display more information on the checkbox +boxui.checkboxTooltip.iconInfoText = Info # Button to add classification on an item boxui.classification.add = Add # Title of the card that shows the reason why the AI classification was applied when no date is available. @@ -1426,6 +1432,46 @@ boxui.presence.timeSinceLastModified = Edited {timeAgo} boxui.presence.timeSinceLastPreviewed = Previewed {timeAgo} # Description of the button to toggle the presence overlay with recent activity boxui.presence.toggleButtonLabel = Recent Activity +# Text on the add filter button, on click generates another filter row +boxui.queryBar.addFilterButtonText = + Add Filter +# Text on the apply filter button, on click applies the filters +boxui.queryBar.applyFiltersButtonText = Apply +# Text on the columns button, on click opens a menu which allows users to choose which columns to render +boxui.queryBar.columnsButtonText = Columns +# Text on the columns button, if one or more columns have been hidden then it will display this text +boxui.queryBar.columnsHiddenButtonText = {count, plural, one {1 Column Hidden} other {{count} Columns Hidden}} +# Text on the connector dropdown, on click should open a dropdown showing either AND or OR +boxui.queryBar.connectorAndText = AND +# Text on the connector dropdown, on click should open a dropdown showing either AND or OR +boxui.queryBar.connectorOrText = OR +# Text on the label, the first condition will show WHERE +boxui.queryBar.connectorWhereText = WHERE +# Text on the filters button, on click opens a menu which allows users to filter through the files +boxui.queryBar.filtersButtonText = Modify Filters +# Header text shown in template dropdown +boxui.queryBar.metadataViewTemplateListHeaderTitle = METADATA TEMPLATES +# Text on the filters button, will display a number in front of the filters text indicating how many filters are applied +boxui.queryBar.multipleFiltersButtonText = {number} Filters +# Text on the filters dropdown that is displayed when no filters have been inserted +boxui.queryBar.noFiltersAppliedText = No Filters Applied +# Text on the templates button when templates have been loaded and there are no templates in the enterprise +boxui.queryBar.noTemplatesText = No Templates Available +# Placeholder text on the value button, on click should open a dropdown +boxui.queryBar.selectValuePlaceholderText = Select value +# Text on the templates button, on click opens a menu which allows users to select a metadata templates +boxui.queryBar.templatesButtonText = Select Metadata +# Text on the templates button when templates are still being loaded +boxui.queryBar.templatesLoadingButtonText = Template Name +# Text displayed on the Tooltip for an input field +boxui.queryBar.tooltipEnterValueError = Please Enter a Value +# Text displayed on the Tooltip for an input field of type float +boxui.queryBar.tooltipInvalidFloatError = Please Enter a Decimal Number +# Text displayed on the Tooltip for an input field of type number +boxui.queryBar.tooltipInvalidNumberError = Please Enter an Integer +# Text displayed on the Tooltip for a date field +boxui.queryBar.tooltipSelectDateError = Please Select a Date +# Text displayed on the Tooltip for a value field +boxui.queryBar.tooltipSelectValueError = Please Select a Value # Icon title for a Box item of type bookmark or web-link boxui.quickSearch.bookmark = Bookmark # Icon title for a Box item of type folder that has collaborators @@ -1676,6 +1722,12 @@ boxui.selectField.clearAll = Clear All boxui.selectField.noResults = No Results # Placeholder text shown in the search input boxui.selectField.searchPlaceholder = Search +# Title for "Access Type" menu, in all capital letters +boxui.share.accessType = ACCESS TYPE +# Label for a shared link permission level +boxui.share.canEdit = Can edit +# Label for a shared link permission level +boxui.share.canView = Can view # Text for Co-owner permission level in permissions table boxui.share.coownerLevelText = Co-owner # Text for permissions table Delete column @@ -1686,6 +1738,10 @@ boxui.share.downloadTableHeaderText = Download boxui.share.editTableHeaderText = Edit # Text for Editor permission level in permissions table boxui.share.editorLevelText = Editor +# Field label for shared link recipient list (title-case) +boxui.share.emailSharedLink = Email Shared Link +# Error message when user tries to send shared link as email without entering any recipients +boxui.share.enterAtLeastOneEmail = Enter at least one valid email # Text for permissions table Get Link column boxui.share.getLinkTableHeaderText = Get Link # Label for a Group contact type @@ -1704,10 +1760,70 @@ boxui.share.inviteFileEditorsLabel = Invite people to become editors of this fil boxui.share.inviteePermissionsFieldLabel = Invitee Permissions # Tooltip text a user can use to learn more about collaborator permission options boxui.share.inviteePermissionsLearnMore = Learn More +# Label for "Message" text box to email the shared link (title-case) +boxui.share.message = Message # Placeholder text for message section boxui.share.messageSelectorPlaceholder = Add a message # Text for permissions table Owner column boxui.share.ownerTableHeaderText = Owner +# Description of a company shared link for a file with view and download permissions +boxui.share.peopleInCompanyCanDownloadFile = Anyone in your company with the link can view and download this file. +# Description of a company shared link for a folder with view and download permissions +boxui.share.peopleInCompanyCanDownloadFolder = Anyone in your company with the link can view this folder and download its contents. +# Description of a company shared link for a file with edit permissions (implies view and download permissions as well) +boxui.share.peopleInCompanyCanEditFile = Anyone in your company with the link can edit and download this file. +# Description of a company shared link for a file with view permissions +boxui.share.peopleInCompanyCanViewFile = Anyone in your company with the link can view this file. +# Description of a company shared link for a folder with view permissions +boxui.share.peopleInCompanyCanViewFolder = Anyone in your company with the link can view this folder. +# This string describes the access level of a file or folder, or who can see the item. {enterpriseName} is the company name +boxui.share.peopleInEnterprise = People in {enterpriseName} +# Description of a collaborator-only shared link for a file with no permissions +boxui.share.peopleInItemCanAccessFile = Any collaborator on the file with the link can access this file. +# Description of a collaborator-only shared link for a folder with no permissions +boxui.share.peopleInItemCanAccessFolder = Any collaborator on the folder with the link can access this folder. +# Description of a collaborator-only shared link for a file with download permissions +boxui.share.peopleInItemCanDownloadFile = Any collaborator on this file with the link can download this file. +# Description of a collaborator-only shared link for a folder with download permissions +boxui.share.peopleInItemCanDownloadFolder = Any collaborator on this folder with the link can download this folder. +# Description of a collaborator-only shared link for a file with edit permissions (implies view and download permissions as well) +boxui.share.peopleInItemCanEditFile = Any collaborator on this file with the link can edit this file and download its contents. +# Description of a collaborator-only shared link for a file with view and download permissions +boxui.share.peopleInItemCanPreviewAndDownloadFile = Any collaborator on this file with the link can view this file and download its contents. +# Description of a collaborator-only shared link for a folder with view and download permissions +boxui.share.peopleInItemCanPreviewAndDownloadFolder = Any collaborator on this folder with the link can view this folder and download its contents. +# Description of a collaborator-only shared link for a file with view permissions +boxui.share.peopleInItemCanPreviewFile = Any collaborator on this file with the link can view this file. +# Description of a collaborator-only shared link for a folder with view permissions +boxui.share.peopleInItemCanPreviewFolder = Any collaborator on this folder with the link can view this folder. +# Description of a specific company shared link for a file with view and download permissions. {company} is the company name +boxui.share.peopleInSpecifiedCompanyCanDownloadFile = Anyone in {company} with the link can view and download this file. +# Description of a specific company shared link for a folder with view and download permissions. {company} is the company name +boxui.share.peopleInSpecifiedCompanyCanDownloadFolder = Anyone in {company} with the link can view this folder and download its contents. +# Description of a specific company shared link for a file with edit permissions (implies view and download permissions as well). {company} is the company name +boxui.share.peopleInSpecifiedCompanyCanEditFile = Anyone in {company} with the link can edit and download this file. +# Description of an specific company shared link for a file with view permissions. {company} is the company name +boxui.share.peopleInSpecifiedCompanyCanViewFile = Anyone in {company} with the link can view this file. +# Description of an specific company shared link for a folder with view permissions. {company} is the company name +boxui.share.peopleInSpecifiedCompanyCanViewFolder = Anyone in {company} with the link can view this folder. +# Label for "People in this file" option +boxui.share.peopleInThisFile = People in this file +# Label for "People in this folder" option +boxui.share.peopleInThisFolder = People in this folder +# Label for "People in your company" option +boxui.share.peopleInYourCompany = People in your company +# Description of an open shared link for a file with view and download permissions +boxui.share.peopleWithLinkCanDownloadFile = Anyone with the link can view and download this file. +# Description of an open shared link for a folder with view and download permissions +boxui.share.peopleWithLinkCanDownloadFolder = Anyone with the link can view this folder and download its contents. +# Description of an open shared link for a file with edit permissions (implies view and download permissions as well) +boxui.share.peopleWithLinkCanEditFile = Anyone with the link can edit and download this file. +# Description of an open shared link for a file with view permissions +boxui.share.peopleWithLinkCanViewFile = Anyone with the link can view this file. +# Description of an open shared link for a folder with view permissions +boxui.share.peopleWithLinkCanViewFolder = Anyone with the link can view this folder. +# Label for "People with the link" option +boxui.share.peopleWithTheLink = People with the link # Text for permissions table Permission Levels column boxui.share.permissionLevelsTableHeaderText = Permission Levels # Label for optional personal message to include when inviting collaborators to an item @@ -1726,6 +1842,20 @@ boxui.share.referAFriendBadgeText = REFER boxui.share.referAFriendRewardCenterLinkText = Click Here # Text encouraging users to refer a friend to sign up for Box boxui.share.referAFriendText = Want a free month of Box? Refer your friend! +# Label for option to remove shared link +boxui.share.removeLink = Remove Link +# Description for confirmation modal to remove a shared link +boxui.share.removeLinkConfirmationDescription = This will permanently remove the shared link. If this item is embedded on other sites it will also become inaccessible. Any custom properties, settings and expirations will be removed as well. Do you want to continue? +# Label for confirmation modal to remove a shared link (title-case) +boxui.share.removeLinkConfirmationTitle = Remove Shared Link +# Accessible label for button that loads share settings popup +boxui.share.settingsButtonLabel = Open shared link settings popup +# Tooltip describing when this shared link will expire. {expiration, date, long} is the formatted date +boxui.share.sharedLinkExpirationTooltip = This link will expire on {expiration, date, long} +# Label for field to copy shared link URL (title-case) +boxui.share.sharedLinkLabel = Shared Link +# Title for shared link modal (title-case) +boxui.share.sharedLinkModalTitle = Shared Link for {itemName} # Text to show when the access level of people in company and user can view only boxui.share.sharedLinkSettings.accessLevel.inCompanyView = This content is available to anyone within your company with the link, and can be viewed. # Text to show when the access level of people in company and user can view and download @@ -1788,6 +1918,26 @@ boxui.share.vanityURLEnableText = Publish content broadly with a custom, non-pri boxui.share.viewerLevelText = Viewer # Text for Viewer Uploader permission level in permissions table boxui.share.viewerUploaderLevelText = Viewer Uploader +# Description of permissions granted to users who have access to the shared link +boxui.shareMenu.downloadOnly = Download Only +# Description of permissions granted when inviting a collab to this item +boxui.shareMenu.editAndComment = Edit and Comment +# Label for menu option to get shared link for item (title-case) +boxui.shareMenu.getSharedLink = Get Shared Link +# Label for disabled menu option when user does not have permission to get shared link for item +boxui.shareMenu.insufficientPermissionsMenuOption = Insufficient sharing permissions. Please contact the folder owner. +# Tooltip to show when user does not have permission to invite collaborators to item +boxui.shareMenu.insufficientPermissionsTooltip = You have insufficient permissions to invite collaborators. +# Label for menu option to invite collaborators to item +boxui.shareMenu.inviteCollabs = Invite Collaborators +# Tooltip to show when only owners and co-owners are allowed to invite collaborators to item +boxui.shareMenu.ownerCoownerOnlyTooltip = You have insufficient permissions to invite collaborators. Only the owner and co-owners can invite collaborators. +# Description of permissions granted to users who have access to the shared link +boxui.shareMenu.shortcutOnly = Shortcut Only +# Description of permissions granted to users who have access to the shared link +boxui.shareMenu.viewAndDownload = View and Download +# Description of permissions granted to users who have access to the shared link +boxui.shareMenu.viewOnly = View Only # Aria-label for the dropdown menu that shows actions for selected items boxui.subHeader.bulkItemActionMenuAriaLabel = Bulk actions # Text for metadata button that will open the metadata side panel diff --git a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss index b5f108a4ce..507016a331 100644 --- a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss +++ b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss @@ -4,8 +4,6 @@ // into Blueprint and @box/threaded-annotations components, breaking their // styling. This file resets those rules within the v2 adapter boundary. -@import '../../common/variables'; - .bcs-NewActivityFeed { display: flex; flex-direction: column; @@ -55,17 +53,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'); + }); + }); });