From 85ba2f09fb97bb7379c28b145e0eb786f20c8db3 Mon Sep 17 00:00:00 2001 From: zhirongwang Date: Tue, 1 Sep 2026 22:30:19 -0700 Subject: [PATCH 1/3] feat(content-preview): add middle panel and version-reload control Allow hosts to inject a compared pane between the viewer and sidebar without remounting the current viewer when the selected version changes. Co-authored-by: Cursor --- .../content-preview/ContentPreview.js | 19 +++++++- .../content-preview/ContentPreview.scss | 12 ++++- .../__tests__/ContentPreview.test.js | 44 +++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/elements/content-preview/ContentPreview.js b/src/elements/content-preview/ContentPreview.js index 51d15e5109..700be5398e 100644 --- a/src/elements/content-preview/ContentPreview.js +++ b/src/elements/content-preview/ContentPreview.js @@ -135,6 +135,8 @@ type Props = { hasHeader?: boolean, hasProviders?: boolean, hideSidebar?: boolean, + // When true, selectedVersion changes do not reload this viewer (host owns version display). + disableVersionChangeReload?: boolean, isLarge: boolean, isVeryLarge?: boolean, language: string, @@ -144,6 +146,8 @@ type Props = { logoUrl?: string, measureRef: Function, messages?: StringMap, + // Rendered between the current viewer and the sidebar inside .bcpr-body. + middlePanel?: React.Node, onAnnotator: Function, onAnnotatorEvent: Function, onBeforeNavigate?: (targetFileId: string) => boolean | Promise, @@ -341,6 +345,7 @@ class ContentPreview extends React.PureComponent { enableBoundingBoxHighlights: false, hasHeader: false, hideSidebar: false, + disableVersionChangeReload: false, language: DEFAULT_LOCALE, loadingIndicatorDelayMs: 0, onAnnotator: noop, @@ -649,6 +654,7 @@ class ContentPreview extends React.PureComponent { * @return {boolean} */ shouldLoadPreview(prevState: State): boolean { + const { disableVersionChangeReload } = this.props; const { file, selectedVersion }: State = this.state; const { file: prevFile, selectedVersion: prevSelectedVersion }: State = prevState; const prevSelectedVersionId = getProp(prevSelectedVersion, 'id'); @@ -665,6 +671,10 @@ class ContentPreview extends React.PureComponent { } if (selectedVersionId !== prevSelectedVersionId) { + if (disableVersionChangeReload) { + return false; + } + const isPreviousCurrent = fileVersionId === prevSelectedVersionId || !prevSelectedVersionId; const isSelectedCurrent = fileVersionId === selectedVersionId || !selectedVersionId; @@ -1671,6 +1681,7 @@ class ContentPreview extends React.PureComponent { hasHeader, hasProviders, hideSidebar, + middlePanel, history, isLarge, isVeryLarge, @@ -1761,7 +1772,12 @@ class ContentPreview extends React.PureComponent { selectedVersion={selectedVersion} /> )} -
+
{ onNavigateRight={this.navigateRight} />
+ {middlePanel} {file && !hideSidebar && ( { expect(instance.shouldLoadPreview({ file })).toBe(true); expect(instance.previewLibraryLoaded).toBe(true); }); + + test('should return false on selectedVersion change when disableVersionChangeReload is true', () => { + wrapper.setProps({ disableVersionChangeReload: true }); + expect(instance.shouldLoadPreview({ selectedVersion: { id: '12345' } })).toBe(false); + }); + + test('should still return true on selectedVersion change when disableVersionChangeReload is omitted', () => { + expect(instance.shouldLoadPreview({ selectedVersion: { id: '12345' } })).toBe(true); + }); }); describe('canDownload()', () => { @@ -2566,6 +2575,41 @@ describe('elements/content-preview/ContentPreview', () => { }); }); + describe('middlePanel prop', () => { + const middlePanel =
compared pane
; + + test('should render middlePanel between the viewer and sidebar', () => { + const wrapper = getWrapper({ + fileId: '123', + middlePanel, + }); + wrapper.setState({ + currentFileId: '123', + file: { id: '123', name: 'test.pdf' }, + }); + + const bodyDiv = wrapper.find('.bcpr-body'); + expect(bodyDiv.hasClass('bcpr-body--with-middle-panel')).toBe(true); + expect(bodyDiv.children().length).toBe(3); + expect(bodyDiv.children().at(0).hasClass('bcpr-container')).toBe(true); + expect(bodyDiv.children().at(1).hasClass('middle-panel-content')).toBe(true); + }); + + test('should not add the middle-panel body class when middlePanel is omitted', () => { + const wrapper = getWrapper({ + fileId: '123', + }); + wrapper.setState({ + currentFileId: '123', + file: { id: '123', name: 'test.pdf' }, + }); + + const bodyDiv = wrapper.find('.bcpr-body'); + expect(bodyDiv.hasClass('bcpr-body--with-middle-panel')).toBe(false); + expect(bodyDiv.children().length).toBe(2); + }); + }); + describe('npm preview load path (useNpmBoxContentPreview)', () => { const createPreviewModule = () => ({ Preview: function Preview() { From 11af76606f3937b78b61660fdfb30e7dd4252414 Mon Sep 17 00:00:00 2001 From: zhirongwang Date: Tue, 1 Sep 2026 23:00:20 -0700 Subject: [PATCH 2/3] feat(content-preview): rename slot to comparedPanel and hide file nav When comparedPanel is passed, skip collection chevrons and arrow-key file switching so comparison mode cannot change the current file. Co-authored-by: Cursor --- .../content-preview/ContentPreview.js | 50 ++++++++----- .../content-preview/ContentPreview.scss | 4 +- .../__tests__/ContentPreview.test.js | 74 +++++++++++++++++-- 3 files changed, 101 insertions(+), 27 deletions(-) diff --git a/src/elements/content-preview/ContentPreview.js b/src/elements/content-preview/ContentPreview.js index 700be5398e..b13f30bd29 100644 --- a/src/elements/content-preview/ContentPreview.js +++ b/src/elements/content-preview/ContentPreview.js @@ -146,8 +146,7 @@ type Props = { logoUrl?: string, measureRef: Function, messages?: StringMap, - // Rendered between the current viewer and the sidebar inside .bcpr-body. - middlePanel?: React.Node, + comparedPanel?: React.Node, onAnnotator: Function, onAnnotatorEvent: Function, onBeforeNavigate?: (targetFileId: string) => boolean | Promise, @@ -1028,6 +1027,8 @@ class ContentPreview extends React.PureComponent { enableBoundingBoxHighlights, features, fileOptions, + comparedPanel, + disableVersionChangeReload, onAnnotatorEvent, onAnnotator, onContentInsightsEventReport, @@ -1385,6 +1386,10 @@ class ContentPreview extends React.PureComponent { * @return {void} */ navigateLeft = () => { + if (this.props.comparedPanel !== undefined) { + return; + } + const currentIndex = this.getFileIndex(); const newIndex = currentIndex === 0 ? 0 : currentIndex - 1; if (newIndex !== currentIndex) { @@ -1399,7 +1404,11 @@ class ContentPreview extends React.PureComponent { * @return {void} */ navigateRight = () => { - const { collection }: Props = this.props; + const { collection, comparedPanel }: Props = this.props; + if (comparedPanel !== undefined) { + return; + } + const currentIndex = this.getFileIndex(); const newIndex = currentIndex === collection.length - 1 ? collection.length - 1 : currentIndex + 1; if (newIndex !== currentIndex) { @@ -1475,7 +1484,7 @@ class ContentPreview extends React.PureComponent { * @return {void} */ onKeyDown = (event: SyntheticKeyboardEvent) => { - const { useHotkeys, renderCustomPreview }: Props = this.props; + const { comparedPanel, useHotkeys, renderCustomPreview }: Props = this.props; // Skip ContentPreview hotkeys when custom content is provided to prevent conflicts. // Custom components must implement their own keyboard shortcuts (arrow navigation, etc) @@ -1501,12 +1510,16 @@ class ContentPreview extends React.PureComponent { if (!consumed) { switch (key) { case 'ArrowLeft': - this.navigateLeft(); - consumed = true; + if (comparedPanel === undefined) { + this.navigateLeft(); + consumed = true; + } break; case 'ArrowRight': - this.navigateRight(); - consumed = true; + if (comparedPanel === undefined) { + this.navigateRight(); + consumed = true; + } break; default: // no-op @@ -1681,7 +1694,7 @@ class ContentPreview extends React.PureComponent { hasHeader, hasProviders, hideSidebar, - middlePanel, + comparedPanel, history, isLarge, isVeryLarge, @@ -1740,6 +1753,7 @@ class ContentPreview extends React.PureComponent { const currentVersionId = getProp(file, 'file_version.id'); const selectedVersionId = getProp(selectedVersion, 'id', currentVersionId); const onHeaderClose = currentVersionId === selectedVersionId ? onClose : this.updateVersionToCurrent; + const isComparedPanelMode = comparedPanel !== undefined; /* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable jsx-a11y/no-noninteractive-tabindex */ @@ -1774,7 +1788,7 @@ class ContentPreview extends React.PureComponent { )}
@@ -1813,14 +1827,16 @@ class ContentPreview extends React.PureComponent { isLoading={isLoading} isLoadingDeferred={isLoadingDeferred} /> - + {!isComparedPanelMode && ( + + )}
- {middlePanel} + {comparedPanel} {file && !hideSidebar && ( { }); }); - describe('middlePanel prop', () => { - const middlePanel =
compared pane
; + describe('comparedPanel prop', () => { + const collection = ['123', '456', '789']; + const comparedPanel =
compared pane
; - test('should render middlePanel between the viewer and sidebar', () => { + test('should render comparedPanel between the viewer and sidebar', () => { const wrapper = getWrapper({ fileId: '123', - middlePanel, + comparedPanel, }); wrapper.setState({ currentFileId: '123', @@ -2589,13 +2590,13 @@ describe('elements/content-preview/ContentPreview', () => { }); const bodyDiv = wrapper.find('.bcpr-body'); - expect(bodyDiv.hasClass('bcpr-body--with-middle-panel')).toBe(true); + expect(bodyDiv.hasClass('bcpr-body--with-compared-panel')).toBe(true); expect(bodyDiv.children().length).toBe(3); expect(bodyDiv.children().at(0).hasClass('bcpr-container')).toBe(true); - expect(bodyDiv.children().at(1).hasClass('middle-panel-content')).toBe(true); + expect(bodyDiv.children().at(1).hasClass('compared-panel-content')).toBe(true); }); - test('should not add the middle-panel body class when middlePanel is omitted', () => { + test('should not add the compared-panel body class when comparedPanel is omitted', () => { const wrapper = getWrapper({ fileId: '123', }); @@ -2605,9 +2606,66 @@ describe('elements/content-preview/ContentPreview', () => { }); const bodyDiv = wrapper.find('.bcpr-body'); - expect(bodyDiv.hasClass('bcpr-body--with-middle-panel')).toBe(false); + expect(bodyDiv.hasClass('bcpr-body--with-compared-panel')).toBe(false); expect(bodyDiv.children().length).toBe(2); }); + + test('should not render PreviewNavigation when comparedPanel is provided', () => { + const wrapper = getWrapper({ + fileId: '456', + collection, + comparedPanel, + }); + wrapper.setState({ + currentFileId: '456', + file: { id: '456', name: 'test.pdf' }, + }); + + expect(wrapper.find('PreviewNavigation').exists()).toBe(false); + }); + + test('should not render PreviewNavigation when comparedPanel is null', () => { + const wrapper = getWrapper({ + fileId: '456', + collection, + comparedPanel: null, + }); + wrapper.setState({ + currentFileId: '456', + file: { id: '456', name: 'test.pdf' }, + }); + + expect(wrapper.find('PreviewNavigation').exists()).toBe(false); + }); + + test('should render PreviewNavigation when comparedPanel is omitted', () => { + const wrapper = getWrapper({ + fileId: '456', + collection, + }); + wrapper.setState({ + currentFileId: '456', + file: { id: '456', name: 'test.pdf' }, + }); + + expect(wrapper.find('PreviewNavigation').exists()).toBe(true); + }); + + test('should not navigate when comparedPanel is provided', () => { + const wrapper = getWrapper({ + fileId: '456', + collection, + comparedPanel, + }); + wrapper.setState({ currentFileId: '456' }); + const instance = wrapper.instance(); + instance.navigateToIndex = jest.fn(); + + instance.navigateLeft(); + instance.navigateRight(); + + expect(instance.navigateToIndex).not.toHaveBeenCalled(); + }); }); describe('npm preview load path (useNpmBoxContentPreview)', () => { From 57d38fbc8a27c1aaaff284a1e0765c694413984b Mon Sep 17 00:00:00 2001 From: zhirongwang Date: Tue, 1 Sep 2026 23:05:53 -0700 Subject: [PATCH 3/3] feat(content-preview): drop disableVersionChangeReload Keep default version-change reloads on the current viewer. Comparison uses comparedPanel instead of suppressing reload on this instance. Co-authored-by: Cursor --- src/elements/content-preview/ContentPreview.js | 9 --------- .../content-preview/__tests__/ContentPreview.test.js | 9 --------- 2 files changed, 18 deletions(-) diff --git a/src/elements/content-preview/ContentPreview.js b/src/elements/content-preview/ContentPreview.js index b13f30bd29..9e6ade8101 100644 --- a/src/elements/content-preview/ContentPreview.js +++ b/src/elements/content-preview/ContentPreview.js @@ -135,8 +135,6 @@ type Props = { hasHeader?: boolean, hasProviders?: boolean, hideSidebar?: boolean, - // When true, selectedVersion changes do not reload this viewer (host owns version display). - disableVersionChangeReload?: boolean, isLarge: boolean, isVeryLarge?: boolean, language: string, @@ -344,7 +342,6 @@ class ContentPreview extends React.PureComponent { enableBoundingBoxHighlights: false, hasHeader: false, hideSidebar: false, - disableVersionChangeReload: false, language: DEFAULT_LOCALE, loadingIndicatorDelayMs: 0, onAnnotator: noop, @@ -653,7 +650,6 @@ class ContentPreview extends React.PureComponent { * @return {boolean} */ shouldLoadPreview(prevState: State): boolean { - const { disableVersionChangeReload } = this.props; const { file, selectedVersion }: State = this.state; const { file: prevFile, selectedVersion: prevSelectedVersion }: State = prevState; const prevSelectedVersionId = getProp(prevSelectedVersion, 'id'); @@ -670,10 +666,6 @@ class ContentPreview extends React.PureComponent { } if (selectedVersionId !== prevSelectedVersionId) { - if (disableVersionChangeReload) { - return false; - } - const isPreviousCurrent = fileVersionId === prevSelectedVersionId || !prevSelectedVersionId; const isSelectedCurrent = fileVersionId === selectedVersionId || !selectedVersionId; @@ -1028,7 +1020,6 @@ class ContentPreview extends React.PureComponent { features, fileOptions, comparedPanel, - disableVersionChangeReload, onAnnotatorEvent, onAnnotator, onContentInsightsEventReport, diff --git a/src/elements/content-preview/__tests__/ContentPreview.test.js b/src/elements/content-preview/__tests__/ContentPreview.test.js index 9d297c6893..c19a96ab1c 100644 --- a/src/elements/content-preview/__tests__/ContentPreview.test.js +++ b/src/elements/content-preview/__tests__/ContentPreview.test.js @@ -223,15 +223,6 @@ describe('elements/content-preview/ContentPreview', () => { expect(instance.shouldLoadPreview({ file })).toBe(true); expect(instance.previewLibraryLoaded).toBe(true); }); - - test('should return false on selectedVersion change when disableVersionChangeReload is true', () => { - wrapper.setProps({ disableVersionChangeReload: true }); - expect(instance.shouldLoadPreview({ selectedVersion: { id: '12345' } })).toBe(false); - }); - - test('should still return true on selectedVersion change when disableVersionChangeReload is omitted', () => { - expect(instance.shouldLoadPreview({ selectedVersion: { id: '12345' } })).toBe(true); - }); }); describe('canDownload()', () => {