diff --git a/src/elements/content-preview/ContentPreview.js b/src/elements/content-preview/ContentPreview.js index 148ef64157..94388d18e2 100644 --- a/src/elements/content-preview/ContentPreview.js +++ b/src/elements/content-preview/ContentPreview.js @@ -139,12 +139,12 @@ type Props = { token: Token, useHotkeys: boolean, /** - * Optional React element to render instead of Box.Preview. + * Optional render function for custom preview content. * When provided, renders custom preview implementation while preserving * ContentPreview layout (sidebar, navigation, header). - * Box.Preview library will not be loaded when children are provided. + * Box.Preview library will not be loaded when renderCustomPreview is provided. * - * The child element will be cloned with injected props: + * The render function receives a props object with: * - fileId: ID of the file being previewed * - token: Auth token for API calls * - apiHost: Box API endpoint @@ -163,11 +163,13 @@ type Props = { * - Component should be memoized/pure for performance * * @example - * - * - * + * } + * /> */ - children?: React.Node, + renderCustomPreview?: (props: ContentPreviewChildProps) => React.Node, } & ErrorContextProps & WithLoggerProps & WithAnnotationsProps & @@ -889,7 +891,7 @@ class ContentPreview extends React.PureComponent { const { advancedContentInsights, // will be removed once preview package will be updated to utilize feature flip for ACI annotatorState: { activeAnnotationId } = {}, - children, + renderCustomPreview, enableThumbnailsSidebar, features, fileOptions, @@ -903,9 +905,9 @@ class ContentPreview extends React.PureComponent { }: Props = this.props; const { file, selectedVersion, startAt }: State = this.state; - // Early return: Box.Preview initialization not needed when using custom content children. + // Early return: Box.Preview initialization not needed when using custom render function. // Custom content will be rendered directly in the Measure block (see render method) - if (children) { + if (renderCustomPreview) { return; } @@ -1271,12 +1273,12 @@ class ContentPreview extends React.PureComponent { * @return {void} */ onKeyDown = (event: SyntheticKeyboardEvent) => { - const { useHotkeys, children }: Props = this.props; + const { useHotkeys, renderCustomPreview }: Props = this.props; - // Skip ContentPreview hotkeys when custom content children are provided to prevent conflicts. + // Skip ContentPreview hotkeys when custom content is provided to prevent conflicts. // Custom components must implement their own keyboard shortcuts (arrow navigation, etc) // as ContentPreview's default handlers only work with Box.Preview viewer. - if (!useHotkeys || children) { + if (!useHotkeys || renderCustomPreview) { return; } @@ -1536,12 +1538,13 @@ class ContentPreview extends React.PureComponent { {file && ( {({ measureRef: previewRef }) => { - const { children, logger } = this.props; + const { renderCustomPreview, logger } = this.props; return (
- {children ? ( + {renderCustomPreview ? ( { logger={logger} onPreviewError={this.onPreviewError} onPreviewLoad={this.onPreviewLoad} - > - {children} - + /> ) : null}
); diff --git a/src/elements/content-preview/CustomPreviewWrapper.js b/src/elements/content-preview/CustomPreviewWrapper.js index f7b7769160..addc70900f 100644 --- a/src/elements/content-preview/CustomPreviewWrapper.js +++ b/src/elements/content-preview/CustomPreviewWrapper.js @@ -11,15 +11,23 @@ type CustomPreviewOnError = (error: Error | ErrorType | ElementsXhrError) => voi type CustomPreviewOnLoad = (data: Object) => void; /** - * Props that are automatically injected into ContentPreview children. + * Props passed to the renderCustomPreview function. * Import this type to ensure your custom preview component accepts the required props. * * @example * import type { ContentPreviewChildProps } from 'box-ui-elements'; * + * // Define your custom preview component * const MyCustomPreview = ({ fileId, token, apiHost, file, onError, onLoad }: ContentPreviewChildProps) => { * // Your implementation * }; + * + * // Use with ContentPreview + * } + * /> */ export type ContentPreviewChildProps = { fileId: string, @@ -31,7 +39,7 @@ export type ContentPreviewChildProps = { }; type Props = { - children: React.Node, + renderCustomPreview: (props: ContentPreviewChildProps) => React.Node, apiHost: string, file: BoxItem, fileId: string, @@ -43,11 +51,11 @@ type Props = { /** * Wrapper component for custom preview content. - * Clones the child element and injects props (fileId, token, apiHost, file, onError, onLoad). - * Wraps children in ErrorBoundary and transforms errors to ContentPreview error format. + * Calls the render function with props (fileId, token, apiHost, file, onError, onLoad). + * Wraps rendered content in ErrorBoundary and transforms errors to ContentPreview error format. */ function CustomPreviewWrapper({ - children, + renderCustomPreview, apiHost, file, fileId, @@ -93,19 +101,22 @@ function CustomPreviewWrapper({ } }; - // Clone child element and inject props - const childWithProps = React.cloneElement((children: any), { + // Build props object for render function + const childProps: ContentPreviewChildProps = { fileId, token, apiHost, file, onError: handleCustomError, onLoad: onPreviewLoad, - }); + }; + + // Call render function with props and wrap in fragment to ensure it's a valid React.Element + const customContent = <>{renderCustomPreview(childProps)}; return ( - {childWithProps} + {customContent} ); } diff --git a/src/elements/content-preview/__tests__/ContentPreview.test.js b/src/elements/content-preview/__tests__/ContentPreview.test.js index 75e9c1c8ab..0b0ceecbc9 100644 --- a/src/elements/content-preview/__tests__/ContentPreview.test.js +++ b/src/elements/content-preview/__tests__/ContentPreview.test.js @@ -1687,7 +1687,7 @@ describe('elements/content-preview/ContentPreview', () => { }); }); - describe('children (custom preview content)', () => { + describe('renderCustomPreview (custom preview content)', () => { const CustomPreview = () =>
Custom Content
; let onError; let onLoad; @@ -1703,7 +1703,7 @@ describe('elements/content-preview/ContentPreview', () => { token: 'token', fileId: file.id, apiHost: 'https://api.box.com', - children: , + renderCustomPreview: childProps => , onError, onLoad, }; @@ -1722,7 +1722,7 @@ describe('elements/content-preview/ContentPreview', () => { }); describe('loadPreview()', () => { - test('should return early without loading Box.Preview when children is provided', async () => { + test('should return early without loading Box.Preview when renderCustomPreview is provided', async () => { const wrapper = getWrapper(props); wrapper.setState({ file }); const instance = wrapper.instance(); @@ -1735,9 +1735,9 @@ describe('elements/content-preview/ContentPreview', () => { expect(instance.preview).toBeUndefined(); }); - test('should load Box.Preview normally when children is not provided', async () => { + test('should load Box.Preview normally when renderCustomPreview is not provided', async () => { const propsWithoutCustom = { ...props }; - delete propsWithoutCustom.children; + delete propsWithoutCustom.renderCustomPreview; const wrapper = getWrapper(propsWithoutCustom); wrapper.setState({ file }); const instance = wrapper.instance(); @@ -1751,7 +1751,7 @@ describe('elements/content-preview/ContentPreview', () => { }); describe('onKeyDown()', () => { - test('should return early when children is provided', () => { + test('should return early when renderCustomPreview is provided', () => { const wrapper = getWrapper({ ...props, useHotkeys: true }); const instance = wrapper.instance(); const event = { @@ -1766,13 +1766,13 @@ describe('elements/content-preview/ContentPreview', () => { expect(event.stopPropagation).not.toHaveBeenCalled(); }); - test('should not return early due to children when it is not provided', () => { + test('should not return early due to renderCustomPreview when it is not provided', () => { const propsWithoutCustom = { ...props, useHotkeys: true }; - delete propsWithoutCustom.children; + delete propsWithoutCustom.renderCustomPreview; const wrapper = getWrapper(propsWithoutCustom); const instance = wrapper.instance(); - // Spy on getViewer to verify we get past the children check + // Spy on getViewer to verify we get past the renderCustomPreview check const getViewerSpy = jest.spyOn(instance, 'getViewer'); const event = { @@ -1786,15 +1786,15 @@ describe('elements/content-preview/ContentPreview', () => { instance.onKeyDown(event); - // If we got past the children check, getViewer should have been called - // This proves the early return for children didn't trigger + // If we got past the renderCustomPreview check, getViewer should have been called + // This proves the early return for renderCustomPreview didn't trigger expect(getViewerSpy).toHaveBeenCalled(); getViewerSpy.mockRestore(); }); }); describe('render()', () => { - test('should render custom preview content inside .bcpr-content when children is provided', () => { + test('should render custom preview content inside .bcpr-content when renderCustomPreview is provided', () => { const wrapper = getWrapper(props); wrapper.setState({ file }); @@ -1809,9 +1809,9 @@ describe('elements/content-preview/ContentPreview', () => { // Now verify CustomPreviewWrapper is rendered expect(measureContent.find('CustomPreviewWrapper').exists()).toBe(true); - // Verify children are passed to the wrapper + // Verify renderCustomPreview is passed to the wrapper const wrapperInstance = measureContent.find('CustomPreviewWrapper'); - expect(wrapperInstance.prop('children')).toEqual(props.children); + expect(wrapperInstance.prop('renderCustomPreview')).toEqual(props.renderCustomPreview); }); test('should pass correct props to custom preview content', () => { @@ -1832,23 +1832,25 @@ describe('elements/content-preview/ContentPreview', () => { expect(wrapperInstance.prop('token')).toBe(props.token); expect(wrapperInstance.prop('apiHost')).toBe(props.apiHost); expect(wrapperInstance.prop('file')).toBe(file); - expect(wrapperInstance.prop('children')).toEqual(props.children); + expect(wrapperInstance.prop('renderCustomPreview')).toEqual(props.renderCustomPreview); expect(wrapperInstance.prop('onPreviewError')).toBe(instance.onPreviewError); expect(wrapperInstance.prop('onPreviewLoad')).toBe(instance.onPreviewLoad); - // Shallow dive into CustomPreviewWrapper to verify children are cloned with injected props + // Shallow dive into CustomPreviewWrapper to verify render function is called with props const wrapperChildren = wrapperInstance.dive(); const errorBoundary = wrapperChildren.find('ErrorBoundary'); expect(errorBoundary.exists()).toBe(true); - // The cloned child is inside ErrorBoundary - const clonedChild = errorBoundary.prop('children'); - expect(clonedChild.props.fileId).toBe(file.id); - expect(clonedChild.props.token).toBe(props.token); - expect(clonedChild.props.apiHost).toBe(props.apiHost); - expect(clonedChild.props.file).toBe(file); - expect(typeof clonedChild.props.onError).toBe('function'); - expect(typeof clonedChild.props.onLoad).toBe('function'); + // The rendered content is wrapped in a fragment inside ErrorBoundary + const fragment = errorBoundary.prop('children'); + // The actual custom preview content is inside the fragment + const renderedContent = fragment.props.children; + expect(renderedContent.props.fileId).toBe(file.id); + expect(renderedContent.props.token).toBe(props.token); + expect(renderedContent.props.apiHost).toBe(props.apiHost); + expect(renderedContent.props.file).toBe(file); + expect(typeof renderedContent.props.onError).toBe('function'); + expect(typeof renderedContent.props.onLoad).toBe('function'); }); test('should not render custom preview content when file is not loaded', () => { @@ -1868,9 +1870,9 @@ describe('elements/content-preview/ContentPreview', () => { } }); - test('should render normal preview when children is not provided', () => { + test('should render normal preview when renderCustomPreview is not provided', () => { const propsWithoutCustom = { ...props }; - delete propsWithoutCustom.children; + delete propsWithoutCustom.renderCustomPreview; const wrapper = getWrapper(propsWithoutCustom); wrapper.setState({ file });