Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c621921
feat(ContentPreview): add customPreviewContent prop for extensibility
ahorowitz123 Feb 6, 2026
52746a3
fix(ContentPreview): address deep review findings for customPreviewCo…
ahorowitz123 Feb 9, 2026
6e04e94
fix(ContentPreview): fix test failures for deep review changes
ahorowitz123 Feb 9, 2026
62da5d8
fix: resolve Flow and ESLint errors in customPreviewContent
ahorowitz123 Feb 23, 2026
489ead2
refactor: remove try-catch around onLoad callback
ahorowitz123 Feb 24, 2026
028da57
refactor: extract custom preview logic into CustomPreviewWrapper comp…
ahorowitz123 Feb 24, 2026
c498ce7
fix: align customPreviewContent onError signature with implementation
ahorowitz123 Feb 24, 2026
73dfd7a
fix: remove redundant runtime validation that rejects memoized compon…
ahorowitz123 Feb 24, 2026
be18567
refactor: change customPreviewContent from prop to children pattern
ahorowitz123 Feb 24, 2026
9b0dd3e
refactor: remove validation and export ContentPreviewChildProps type
ahorowitz123 Feb 24, 2026
5e3d889
fix: handle children prop transitions and simplify asset loading
ahorowitz123 Feb 24, 2026
b16e9b9
test: enhance assertions to verify CustomPreviewWrapper internals
ahorowitz123 Feb 24, 2026
fdc0cd9
chore: remove accidentally committed temp files
ahorowitz123 Feb 24, 2026
ccf4117
refactor: restore original endLoadingSession order
ahorowitz123 Feb 24, 2026
11f44ab
refactor: remove children prop transition handling
ahorowitz123 Feb 24, 2026
84f2a6c
Merge branch 'master' into feature/custom-preview-content
mergify[bot] Feb 25, 2026
8eb4a3b
Merge branch 'master' into feature/custom-preview-content
mergify[bot] Feb 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/common/types/logging.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type ElementsLoadMetricData = {
};

type LoggerProps = {
logError?: (error: Error, errorCode: string, context?: Object) => void,
onPreviewMetric: (data: Object) => void,
onReadyMetric: (data: ElementsLoadMetricData) => void,
};
Expand Down
74 changes: 69 additions & 5 deletions src/elements/content-preview/ContentPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { isInputElement, focus } from '../../utils/dom';
import { getTypedFileId } from '../../utils/file';
import { withAnnotations, withAnnotatorContext } from '../common/annotator-context';
import { withErrorBoundary } from '../common/error-boundary';
import CustomPreviewWrapper, { type ContentPreviewChildProps } from './CustomPreviewWrapper';
import { withLogger } from '../common/logger';
import { PREVIEW_FIELDS_TO_FETCH } from '../../utils/fields';
import { mark } from '../../utils/performance';
Expand Down Expand Up @@ -137,6 +138,36 @@ type Props = {
theme?: Theme,
token: Token,
useHotkeys: boolean,
/**
* Optional React element to render instead of Box.Preview.
* When provided, renders custom preview implementation while preserving
* ContentPreview layout (sidebar, navigation, header).
* Box.Preview library will not be loaded when children are provided.
*
* The child element will be cloned with injected props:
* - fileId: ID of the file being previewed
* - token: Auth token for API calls
* - apiHost: Box API endpoint
* - file: Current file object with full metadata
* - onError: Optional callback for preview failures - call when content fails to load
* Pass error object with optional 'code' property for error categorization
* - onLoad: Optional callback for successful load - call when content is ready
*
* Expected behavior:
* - Component should call onLoad() when content is successfully rendered
* - Component should call onError(error) on failures, where error can be:
* - Error instance with optional 'code' property
* - Object with 'code' and 'message' properties
* - Component should handle its own loading states and error display
* - Component should handle its own keyboard shortcuts (ContentPreview hotkeys are disabled)
* - Component should be memoized/pure for performance
*
* @example
* <ContentPreview fileId="123" token={token}>
* <MarkdownEditor />
* </ContentPreview>
*/
children?: React.Node,
} & ErrorContextProps &
WithLoggerProps &
WithAnnotationsProps &
Expand Down Expand Up @@ -395,6 +426,8 @@ class ContentPreview extends React.PureComponent<Props, State> {
* @return {void}
*/
componentDidMount(): void {
// Always load Box.Preview library assets
// Even when children are provided, we need assets ready for transitions
this.loadStylesheet();
this.loadScript();

Expand Down Expand Up @@ -856,6 +889,7 @@ class ContentPreview extends React.PureComponent<Props, State> {
const {
advancedContentInsights, // will be removed once preview package will be updated to utilize feature flip for ACI
annotatorState: { activeAnnotationId } = {},
children,
enableThumbnailsSidebar,
features,
fileOptions,
Expand All @@ -868,6 +902,13 @@ class ContentPreview extends React.PureComponent<Props, State> {
...rest
}: Props = this.props;
const { file, selectedVersion, startAt }: State = this.state;

// Early return: Box.Preview initialization not needed when using custom content children.
// Custom content will be rendered directly in the Measure block (see render method)
if (children) {
return;
}

this.previewLibraryLoaded = this.isPreviewLibraryLoaded();

if (!this.previewLibraryLoaded || !file || !tokenOrTokenFunction) {
Expand Down Expand Up @@ -1230,8 +1271,12 @@ class ContentPreview extends React.PureComponent<Props, State> {
* @return {void}
*/
onKeyDown = (event: SyntheticKeyboardEvent<HTMLElement>) => {
const { useHotkeys }: Props = this.props;
if (!useHotkeys) {
const { useHotkeys, children }: Props = this.props;

// Skip ContentPreview hotkeys when custom content children are 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) {
return;
}

Expand Down Expand Up @@ -1490,9 +1535,27 @@ class ContentPreview extends React.PureComponent<Props, State> {
>
{file && (
<Measure bounds onResize={this.onResize}>
{({ measureRef: previewRef }) => (
<div ref={previewRef} className="bcpr-content" />
)}
{({ measureRef: previewRef }) => {
const { children, logger } = this.props;

return (
<div ref={previewRef} className="bcpr-content">
{children ? (
<CustomPreviewWrapper
fileId={currentFileId}
token={token}
apiHost={apiHost}
file={file}
logger={logger}
onPreviewError={this.onPreviewError}
onPreviewLoad={this.onPreviewLoad}
>
{children}
</CustomPreviewWrapper>
) : null}
</div>
);
}}
</Measure>
)}
<PreviewMask
Expand Down Expand Up @@ -1548,6 +1611,7 @@ class ContentPreview extends React.PureComponent<Props, State> {
}

export type ContentPreviewProps = Props;
export type { ContentPreviewChildProps };
export { ContentPreview as ContentPreviewComponent };
export default flow([
makeResponsive,
Expand Down
113 changes: 113 additions & 0 deletions src/elements/content-preview/CustomPreviewWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// @flow
import * as React from 'react';
import ErrorBoundary from '../common/error-boundary';
import { ORIGIN_CONTENT_PREVIEW } from '../../constants';
import type { Token, BoxItem } from '../../common/types/core';
import type { ErrorType } from '../common/flowTypes';
import type { ElementsXhrError } from '../../common/types/api';
import type { LoggerProps } from '../../common/types/logging';

type CustomPreviewOnError = (error: Error | ErrorType | ElementsXhrError) => void;
type CustomPreviewOnLoad = (data: Object) => void;

/**
* Props that are automatically injected into ContentPreview children.
* Import this type to ensure your custom preview component accepts the required props.
*
* @example
* import type { ContentPreviewChildProps } from 'box-ui-elements';
*
* const MyCustomPreview = ({ fileId, token, apiHost, file, onError, onLoad }: ContentPreviewChildProps) => {
* // Your implementation
* };
*/
export type ContentPreviewChildProps = {
fileId: string,
token: Token,
apiHost: string,
file: BoxItem,
onError: CustomPreviewOnError,
onLoad: CustomPreviewOnLoad,
};

type Props = {
children: React.Node,
apiHost: string,
file: BoxItem,
fileId: string,
logger?: LoggerProps,
onPreviewError: (errorData: { error: ErrorType }) => void,
onPreviewLoad: CustomPreviewOnLoad,
token: Token,
};

/**
* 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.
*/
function CustomPreviewWrapper({
children,
apiHost,
file,
fileId,
logger,
onPreviewError,
onPreviewLoad,
token,
}: Props): React.Node {
// Create wrapper for onError to transform to PreviewLibraryError signature
const handleCustomError: CustomPreviewOnError = (customError: ErrorType | ElementsXhrError) => {
// Extract error code
const errorCodeValue =
customError && typeof customError === 'object' && 'code' in customError
? customError.code
: 'error_custom_preview';

// Extract error message
let errorMessageValue: string;
if (customError instanceof Error) {
errorMessageValue = customError.message;
} else if (customError && typeof customError === 'object' && 'message' in customError) {
errorMessageValue = customError.message || 'Unknown error';
} else {
errorMessageValue = String(customError);
}

const errorObj: ErrorType = {
code: errorCodeValue,
message: errorMessageValue,
};
onPreviewError({ error: errorObj });
};

// Error boundary handler for render errors
const handleRenderError = (elementsError: { code: string, message: string }) => {
const logError = logger?.logError;
if (logError) {
logError(new Error(elementsError.message), 'CUSTOM_PREVIEW_RENDER_ERROR', {
fileId,
fileName: file.name,
errorCode: elementsError.code,
});
}
};

// Clone child element and inject props
const childWithProps = React.cloneElement((children: any), {
fileId,
token,
apiHost,
file,
onError: handleCustomError,
onLoad: onPreviewLoad,
});

return (
<ErrorBoundary errorOrigin={ORIGIN_CONTENT_PREVIEW} onError={handleRenderError}>
{childWithProps}
</ErrorBoundary>
);
}

export default CustomPreviewWrapper;
Loading