diff --git a/.prettierignore b/.prettierignore
index b43bf86b50..1604bd4b3f 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1 +1,2 @@
README.md
+flow-typed/*
diff --git a/flow-typed/npm/react-intl_v2.x.x.js b/flow-typed/npm/react-intl_v2.x.x.js
index bb739aac5a..e778581a15 100644
--- a/flow-typed/npm/react-intl_v2.x.x.js
+++ b/flow-typed/npm/react-intl_v2.x.x.js
@@ -260,4 +260,5 @@ declare module "react-intl" {
> {}
declare type IntlShape = $npm$ReactIntl$IntlShape;
declare type MessageDescriptor = $npm$ReactIntl$MessageDescriptor;
+ declare function useIntl(): $npm$ReactIntl$IntlShape;
}
diff --git a/src/elements/content-explorer/PreviewDialog.js b/src/elements/content-explorer/PreviewDialog.js.flow
similarity index 71%
rename from src/elements/content-explorer/PreviewDialog.js
rename to src/elements/content-explorer/PreviewDialog.js.flow
index 34daacdccf..46cf48ac52 100644
--- a/src/elements/content-explorer/PreviewDialog.js
+++ b/src/elements/content-explorer/PreviewDialog.js.flow
@@ -5,35 +5,36 @@
*/
import * as React from 'react';
+import { useIntl } from 'react-intl';
import Modal from 'react-modal';
-import { injectIntl } from 'react-intl';
-import type { IntlShape } from 'react-intl';
import cloneDeep from 'lodash/cloneDeep';
-import messages from '../common/messages';
-import ContentPreview from '../content-preview';
+
+import ContentPreview, { ContentPreviewProps } from '../content-preview';
import { TYPE_FILE, CLASS_MODAL_CONTENT_FULL_BLEED, CLASS_MODAL_OVERLAY, CLASS_MODAL } from '../../constants';
import type { Token, BoxItem, Collection } from '../../common/types/core';
import type APICache from '../../utils/Cache';
-type Props = {
+import messages from '../common/messages';
+
+import './PreviewDialog.scss';
+
+type PreviewDialogProps = {
apiHost: string,
- appElement: HTMLElement,
appHost: string,
cache: APICache,
canDownload: boolean,
contentPreviewProps: ContentPreviewProps,
currentCollection: Collection,
- intl: IntlShape,
isOpen: boolean,
isTouch: boolean,
item: BoxItem,
- onCancel: Function,
- onDownload: Function,
- onPreview: Function,
+ onCancel: any,
+ onDownload: any,
+ onPreview: any,
parentElement: HTMLElement,
previewLibraryVersion: string,
- requestInterceptor?: Function,
- responseInterceptor?: Function,
+ responseInterceptor?: any,
+ requestInterceptor?: any,
sharedLink?: string,
sharedLinkPassword?: string,
staticHost: string,
@@ -42,32 +43,35 @@ type Props = {
};
const PreviewDialog = ({
- item,
- isOpen,
- parentElement,
- appElement,
- token,
- cache,
- currentCollection,
- canDownload,
- onCancel,
- onPreview,
- onDownload,
- apiHost,
- appHost,
- staticHost,
- staticPath,
- previewLibraryVersion,
- sharedLink,
- sharedLinkPassword,
- contentPreviewProps,
- requestInterceptor,
- responseInterceptor,
- intl,
-}: Props) => {
+ appElement,
+ apiHost,
+ appHost,
+ cache,
+ canDownload,
+ contentPreviewProps,
+ currentCollection,
+ isOpen,
+ item,
+ onCancel,
+ onDownload,
+ onPreview,
+ parentElement,
+ previewLibraryVersion,
+ requestInterceptor,
+ responseInterceptor,
+ sharedLink,
+ sharedLinkPassword,
+ staticHost,
+ staticPath,
+ token,
+}: PreviewDialogProps) => {
+ const { formatMessage } = useIntl();
const { items }: Collection = currentCollection;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const onLoad = (data: any): void => {
- onPreview(cloneDeep(data));
+ if (onPreview) {
+ onPreview(cloneDeep(data));
+ }
};
if (!item || !items) {
@@ -82,34 +86,33 @@ const PreviewDialog = ({
portalClassName={`${CLASS_MODAL} be-modal-preview`}
className={CLASS_MODAL_CONTENT_FULL_BLEED}
overlayClassName={CLASS_MODAL_OVERLAY}
- contentLabel={intl.formatMessage(messages.preview)}
+ contentLabel={formatMessage(messages.preview)}
onRequestClose={onCancel}
appElement={appElement}
>
);
};
-
-export default injectIntl(PreviewDialog);
+export default PreviewDialog;
diff --git a/src/elements/content-explorer/PreviewDialog.tsx b/src/elements/content-explorer/PreviewDialog.tsx
new file mode 100644
index 0000000000..c01b60302d
--- /dev/null
+++ b/src/elements/content-explorer/PreviewDialog.tsx
@@ -0,0 +1,113 @@
+import * as React from 'react';
+import { useIntl } from 'react-intl';
+import Modal from 'react-modal';
+import cloneDeep from 'lodash/cloneDeep';
+
+import ContentPreview, { ContentPreviewProps } from '../content-preview';
+import { TYPE_FILE, CLASS_MODAL_CONTENT_FULL_BLEED, CLASS_MODAL_OVERLAY, CLASS_MODAL } from '../../constants';
+import type { Token, BoxItem, Collection } from '../../common/types/core';
+import type APICache from '../../utils/Cache';
+
+import messages from '../common/messages';
+
+export interface PreviewDialogProps {
+ appElement: HTMLElement;
+ apiHost: string;
+ appHost: string;
+ cache: APICache;
+ canDownload: boolean;
+ contentPreviewProps: ContentPreviewProps;
+ currentCollection: Collection;
+ isOpen: boolean;
+ isTouch: boolean;
+ item: BoxItem;
+ onCancel: () => void;
+ onDownload: () => void;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ onPreview: (data: any) => void;
+ parentElement: HTMLElement;
+ previewLibraryVersion: string;
+ requestInterceptor?: () => void;
+ responseInterceptor?: () => void;
+ sharedLink?: string;
+ sharedLinkPassword?: string;
+ staticHost: string;
+ staticPath: string;
+ token: Token;
+}
+
+const PreviewDialog = ({
+ appElement,
+ apiHost,
+ appHost,
+ cache,
+ canDownload,
+ contentPreviewProps,
+ currentCollection,
+ isOpen,
+ item,
+ onCancel,
+ onDownload,
+ onPreview,
+ parentElement,
+ previewLibraryVersion,
+ requestInterceptor,
+ responseInterceptor,
+ sharedLink,
+ sharedLinkPassword,
+ staticHost,
+ staticPath,
+ token,
+}: PreviewDialogProps) => {
+ const { formatMessage } = useIntl();
+ const { items }: Collection = currentCollection;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const onLoad = (data: any): void => {
+ if (onPreview) {
+ onPreview(cloneDeep(data));
+ }
+ };
+
+ if (!item || !items) {
+ return null;
+ }
+
+ const files: BoxItem[] = items.filter(({ type }) => type === TYPE_FILE);
+ return (
+ parentElement}
+ portalClassName={`${CLASS_MODAL} be-modal-preview`}
+ className={CLASS_MODAL_CONTENT_FULL_BLEED}
+ overlayClassName={CLASS_MODAL_OVERLAY}
+ contentLabel={formatMessage(messages.preview)}
+ onRequestClose={onCancel}
+ appElement={appElement}
+ >
+
+
+ );
+};
+
+export default PreviewDialog;
diff --git a/src/elements/content-explorer/__tests__/PreviewDialog.test.tsx b/src/elements/content-explorer/__tests__/PreviewDialog.test.tsx
new file mode 100644
index 0000000000..a93e3f0298
--- /dev/null
+++ b/src/elements/content-explorer/__tests__/PreviewDialog.test.tsx
@@ -0,0 +1,80 @@
+import * as React from 'react';
+import userEvent from '@testing-library/user-event';
+import { render, screen, waitFor } from '../../../test-utils/testing-library';
+
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+import APICache from '../../../utils/Cache';
+
+import PreviewDialog, { PreviewDialogProps } from '../PreviewDialog';
+
+jest.mock('react-modal', () => {
+ return jest.fn(({ children }) =>
{children}
);
+});
+
+describe('elements/content-explorer/PreviewDialog', () => {
+ const defaultProps = {
+ appElement: document.body,
+ apiHost: 'https://api.box.com',
+ appHost: 'https://app.box.com',
+ cache: new APICache(),
+ canDownload: true,
+ contentPreviewProps: {},
+ currentCollection: { items: [{ id: '1', type: 'file' }] },
+ isOpen: true,
+ isTouch: false,
+ item: { id: '1', type: 'file' },
+ onCancel: jest.fn(),
+ onDownload: jest.fn(),
+ onPreview: jest.fn(),
+ parentElement: document.createElement('div'),
+ previewLibraryVersion: '1.0.0',
+ staticHost: 'https://static.box.com',
+ staticPath: '/static',
+ token: 'token',
+ };
+
+ const renderComponent = (props?: Partial) =>
+ render();
+
+ test('renders', async () => {
+ renderComponent();
+ expect(await screen.findByLabelText('Preview')).toBeInTheDocument();
+ expect(await screen.findByRole('button', { name: 'Close' }));
+ });
+
+ test('calls onCancel when modal is closed', async () => {
+ renderComponent();
+ const closeButton = screen.getByRole('button', { name: 'Close' });
+ await userEvent.click(closeButton);
+ expect(defaultProps.onCancel).toHaveBeenCalled();
+ });
+
+ test('does not render when item is null', () => {
+ const props = { item: null };
+ const { container } = renderComponent(props);
+ expect(container.firstChild).toBeNull();
+ });
+
+ test('does not render when items are null', () => {
+ const props = { currentCollection: { items: null } };
+ const { container } = renderComponent(props);
+ expect(container.firstChild).toBeNull();
+ });
+
+ test('calls onPreview with cloned data on load', () => {
+ const data = { id: '1', type: 'file' };
+ renderComponent();
+ waitFor(() => {
+ expect(defaultProps.onPreview).toHaveBeenCalledWith(expect.objectContaining(data));
+ });
+ });
+
+ test('does not call onPreview with cloned data on load', () => {
+ const data = { id: '1', type: 'file' };
+ renderComponent({ onPreview: null });
+ waitFor(() => {
+ expect(defaultProps.onPreview).not.toHaveBeenCalledWith(expect.objectContaining(data));
+ });
+ });
+});
diff --git a/src/elements/content-explorer/stories/PreviewDialog.stories.tsx b/src/elements/content-explorer/stories/PreviewDialog.stories.tsx
new file mode 100644
index 0000000000..fcbb0fe705
--- /dev/null
+++ b/src/elements/content-explorer/stories/PreviewDialog.stories.tsx
@@ -0,0 +1,83 @@
+import * as React from 'react';
+import { useArgs } from '@storybook/preview-api';
+import { Button } from '@box/blueprint-web';
+
+import { addRootElement } from '../../../utils/storybook';
+
+import PreviewDialog from '../PreviewDialog';
+
+// need to import this into the story because it's usually in ContentExplorer
+import '../../common/modal.scss';
+
+export const previewDialog = {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ render: (args: any) => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const [, setArgs] = useArgs();
+
+ const handleOpenModal = () => setArgs({ isOpen: true });
+
+ const handleCloseModal = () => {
+ setArgs({ isOpen: false });
+ };
+
+ const { appElement, rootElement } = addRootElement();
+
+ return (
+
+
+
+
+
+ );
+ },
+};
+
+export default {
+ title: 'Elements/ContentExplorer',
+ component: PreviewDialog,
+ args: {
+ isLoading: false,
+ isOpen: false,
+ token: global.TOKEN,
+ },
+};
diff --git a/src/elements/content-explorer/stories/tests/PreviewDialog-visual.stories.tsx b/src/elements/content-explorer/stories/tests/PreviewDialog-visual.stories.tsx
new file mode 100644
index 0000000000..6c96a6c41e
--- /dev/null
+++ b/src/elements/content-explorer/stories/tests/PreviewDialog-visual.stories.tsx
@@ -0,0 +1,88 @@
+import * as React from 'react';
+import { useArgs } from '@storybook/preview-api';
+import { Button } from '@box/blueprint-web';
+import { userEvent, within } from '@storybook/test';
+
+import { addRootElement } from '../../../../utils/storybook';
+
+import PreviewDialog from '../../PreviewDialog';
+
+// need to import this into the story because it's usually in ContentExplorer
+import '../../../common/modal.scss';
+
+export const basic = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const button = canvas.getByRole('button', { name: 'Launch PreviewDialog' });
+ await userEvent.click(button);
+ expect(canvas.getByText('Book Sample.pdf')).toBeInTheDocument();
+ },
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ render: (args: any) => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const [, setArgs] = useArgs();
+
+ const handleOpenModal = () => setArgs({ isOpen: true });
+
+ const handleCloseModal = () => {
+ setArgs({ isOpen: false });
+ };
+
+ const { appElement, rootElement } = addRootElement();
+
+ return (
+
+
+
+
+
+ );
+ },
+};
+
+export default {
+ title: 'Elements/ContentExplorer/tests/PreviewDialog/visual',
+ component: PreviewDialog,
+ args: {
+ token: global.TOKEN,
+ },
+};