diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index fe4a25ae15..9972327f8b 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,6 +1,9 @@ +import React from 'react'; +import { BlueprintProvider, useNoopTreatment } from '@box/blueprint-web'; import { boxLanguages } from '@box/languages'; import { addWindowFocusTracking } from '@react-aria/interactions'; import { initialize, mswLoader } from 'msw-storybook-addon'; +import type { Decorator, Preview } from '@storybook/react-webpack5'; import '../src/styles/variables'; import '../src/styles/base.scss'; @@ -24,7 +27,21 @@ initialize({ }, }); -const preview = { +const withStorybookBlueprintAnimations: Decorator = Story => ( + + + +); + +const preview: Preview = { + decorators: [withStorybookBlueprintAnimations], + parameters: { chromatic: { cropToViewport: true, diff --git a/src/elements/common/__tests__/withBlueprintAnimations.test.tsx b/src/elements/common/__tests__/withBlueprintAnimations.test.tsx new file mode 100644 index 0000000000..1bb9857758 --- /dev/null +++ b/src/elements/common/__tests__/withBlueprintAnimations.test.tsx @@ -0,0 +1,105 @@ +import * as React from 'react'; +import { render } from '../../../test-utils/testing-library'; +import { type BlueprintAnimationsProps, withBlueprintAnimations } from '../withBlueprintAnimations'; + +jest.mock('@box/blueprint-web', () => { + const ReactMock = jest.requireActual('react'); + + return { + BlueprintProvider: ({ children, configurationOverrides }) => + ReactMock.createElement( + 'div', + { + 'data-testid': 'blueprint-animations-provider', + 'data-animations-phase1': String(configurationOverrides?.animationsPhase1Enabled), + 'data-animations-phase2': String(configurationOverrides?.animationsPhase2Enabled), + }, + children, + ), + useNoopTreatment: () => 'control', + TooltipProvider: ({ children }) => + ReactMock.createElement('div', { 'data-testid': 'tooltip-provider' }, children), + }; +}); + +type TestComponentProps = { + value?: string; +} & BlueprintAnimationsProps; + +describe('src/elements/common/withBlueprintAnimations', () => { + const TestComponent = ({ value }: TestComponentProps) => ( +
{`Test ${value || 'default'}`}
+ ); + + const WrappedComponent = withBlueprintAnimations(TestComponent); + + const renderComponent = (props?: TestComponentProps) => render(); + + test('should enable Blueprint animations by default', () => { + const { getByTestId } = renderComponent(); + + expect(getByTestId('test-component')).toHaveTextContent('Test default'); + expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase1', 'true'); + expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase2', 'true'); + }); + + test('should opt out when enableBlueprintAnimations is false', () => { + const { getByTestId } = renderComponent({ enableBlueprintAnimations: false }); + + expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase1', 'false'); + expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase2', 'false'); + }); + + test('should enable all phases when enableBlueprintAnimations is true', () => { + const { getByTestId } = renderComponent({ enableBlueprintAnimations: true }); + + expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase1', 'true'); + expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase2', 'true'); + }); + + test('should honor per-phase overrides matching Blueprint configurationOverrides keys', () => { + const { getByTestId } = renderComponent({ + enableBlueprintAnimations: { + animationsPhase1Enabled: true, + animationsPhase2Enabled: false, + }, + }); + + expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase1', 'true'); + expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase2', 'false'); + }); + + test('should default omitted phase keys to on when an object is passed', () => { + const { getByTestId } = renderComponent({ + enableBlueprintAnimations: { + animationsPhase2Enabled: false, + }, + }); + + expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase1', 'true'); + expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase2', 'false'); + }); + + test('should pass props to wrapped component', () => { + const { getByTestId } = renderComponent({ value: 'test-value' }); + + expect(getByTestId('test-component')).toHaveTextContent('Test test-value'); + }); + + test('should not forward enableBlueprintAnimations to the wrapped component', () => { + const DomFacingComponent = ({ value, ...rest }: TestComponentProps & Record) => ( +
+ {value} +
+ ); + const WrappedDomFacing = withBlueprintAnimations(DomFacingComponent); + + const { getByTestId } = render( + , + ); + + expect(getByTestId('dom-facing')).not.toHaveAttribute('enableBlueprintAnimations'); + expect(getByTestId('dom-facing')).toHaveAttribute('data-extra', 'keep'); + expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase1', 'false'); + }); +}); diff --git a/src/elements/common/withBlueprintAnimations.tsx b/src/elements/common/withBlueprintAnimations.tsx new file mode 100644 index 0000000000..c8af1146c0 --- /dev/null +++ b/src/elements/common/withBlueprintAnimations.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { BlueprintProvider, useNoopTreatment } from '@box/blueprint-web'; +import type { BlueprintConfigurationOverrides } from '@box/blueprint-web'; + +export type BlueprintAnimationsProps = { + /** + * Blueprint component animations for this element. + * - omitted / `true`: all phases on (default) + * - `false`: all phases off + * - object: per-phase overrides from Blueprint `configurationOverrides` (omitted keys default on) + */ + enableBlueprintAnimations?: boolean | BlueprintConfigurationOverrides; +}; + +export function getBlueprintAnimationOverrides( + value: boolean | BlueprintConfigurationOverrides | undefined, +): Pick { + if (value === false) { + return { + animationsPhase1Enabled: false, + animationsPhase2Enabled: false, + }; + } + + if (value === true || value == null) { + return { + animationsPhase1Enabled: true, + animationsPhase2Enabled: true, + }; + } + + return { + animationsPhase1Enabled: value.animationsPhase1Enabled !== false, + animationsPhase2Enabled: value.animationsPhase2Enabled !== false, + }; +} + +export function withBlueprintAnimations

( + Component: React.ComponentType

, +): React.FC

{ + return function WithBlueprintAnimations(props: P & BlueprintAnimationsProps) { + const { enableBlueprintAnimations, ...rest } = props; + + return ( + + + + ); + }; +} diff --git a/src/elements/content-explorer/ContentExplorer.tsx b/src/elements/content-explorer/ContentExplorer.tsx index 0a470fe0a0..809df69a95 100644 --- a/src/elements/content-explorer/ContentExplorer.tsx +++ b/src/elements/content-explorer/ContentExplorer.tsx @@ -103,6 +103,7 @@ import '../common/fonts.scss'; import '../common/base.scss'; import '../common/modal.scss'; import './ContentExplorer.scss'; +import { withBlueprintAnimations } from '../common/withBlueprintAnimations'; import { withBlueprintModernization } from '../common/withBlueprintModernization'; import Providers from '../common/Providers'; @@ -2100,6 +2101,10 @@ class ContentExplorer extends Component { } export { ContentExplorer as ContentExplorerComponent }; -export default flow([makeResponsive, withFeatureConsumer, withFeatureProvider, withBlueprintModernization])( - ContentExplorer, -); +export default flow([ + makeResponsive, + withFeatureConsumer, + withFeatureProvider, + withBlueprintModernization, + withBlueprintAnimations, +])(ContentExplorer); diff --git a/src/elements/content-picker/ContentPicker.js b/src/elements/content-picker/ContentPicker.js index 62e687630c..971c6e10a0 100644 --- a/src/elements/content-picker/ContentPicker.js +++ b/src/elements/content-picker/ContentPicker.js @@ -20,6 +20,8 @@ import CreateFolderDialog from '../common/create-folder-dialog'; import Internationalize from '../common/Internationalize'; import makeResponsive from '../common/makeResponsive'; // $FlowFixMe +import { withBlueprintAnimations } from '../common/withBlueprintAnimations'; +// $FlowFixMe import { withBlueprintModernization } from '../common/withBlueprintModernization'; // $FlowFixMe TypeScript file import ThemingStyles from '../common/theming'; @@ -1350,4 +1352,4 @@ class ContentPicker extends Component { } export { ContentPicker as ContentPickerComponent }; -export default flow([makeResponsive, withBlueprintModernization])(ContentPicker); +export default flow([makeResponsive, withBlueprintModernization, withBlueprintAnimations])(ContentPicker); diff --git a/src/elements/content-preview/ContentPreview.js b/src/elements/content-preview/ContentPreview.js index 3f6a6e8ec9..7157a5103d 100644 --- a/src/elements/content-preview/ContentPreview.js +++ b/src/elements/content-preview/ContentPreview.js @@ -40,6 +40,8 @@ import { mark } from '../../utils/performance'; import { convertTimestampToSeconds } from '../../utils/timestamp'; import { isFeatureEnabled, withFeatureConsumer, withFeatureProvider } from '../common/feature-checking'; // $FlowFixMe +import { withBlueprintAnimations } from '../common/withBlueprintAnimations'; +// $FlowFixMe import { withBlueprintModernization } from '../common/withBlueprintModernization'; import { EVENT_JS_READY } from '../common/logger/constants'; import ReloadNotification from './ReloadNotification'; @@ -1763,6 +1765,7 @@ export default flow([ withFeatureConsumer, withFeatureProvider, withBlueprintModernization, + withBlueprintAnimations, withLogger(ORIGIN_CONTENT_PREVIEW), withErrorBoundary(ORIGIN_CONTENT_PREVIEW), ])(ContentPreview); diff --git a/src/elements/content-sharing/ContentSharing.js b/src/elements/content-sharing/ContentSharing.js index 72a503a43a..2a89d3f4e5 100644 --- a/src/elements/content-sharing/ContentSharing.js +++ b/src/elements/content-sharing/ContentSharing.js @@ -16,8 +16,6 @@ import { isFeatureEnabled } from '../common/feature-checking'; import Internationalize from '../common/Internationalize'; import Providers from '../common/Providers'; // $FlowFixMe -import { withBlueprintModernization } from '../common/withBlueprintModernization'; -// $FlowFixMe import ContentSharingV2 from './ContentSharingV2'; import SharingModal from './SharingModal'; @@ -51,6 +49,10 @@ type ContentSharingProps = { * the modal will appear on page load. See ContentSharing.stories.js for examples. */ displayInModal: boolean, + /** Forwarded to ContentSharingV2 Blueprint HOCs when contentSharingV2 is enabled */ + enableBlueprintAnimations?: boolean | Object, + /** Forwarded to ContentSharingV2 Blueprint HOCs when contentSharingV2 is enabled */ + enableModernizedComponents?: boolean, /** features - Features for the element */ features?: FeatureConfig, /** hasProviders - Whether the element has providers for USM already */ @@ -92,6 +94,8 @@ function ContentSharing({ config, customButton, displayInModal, + enableBlueprintAnimations, + enableModernizedComponents, features = {}, hasProviders = true, itemID, @@ -141,6 +145,8 @@ function ContentSharing({ { + expect(screen.getByRole('heading', { name: 'Share ‘Box Development Guide.pdf’' })).toBeVisible(); + }); expect(screen.getByRole('combobox', { name: 'Invite People' })).toBeVisible(); expect(screen.getByRole('switch', { name: 'Shared link' })).toBeVisible(); }, diff --git a/src/elements/content-sidebar/ContentSidebar.js b/src/elements/content-sidebar/ContentSidebar.js index 29e272b605..f3bd54ed4c 100644 --- a/src/elements/content-sidebar/ContentSidebar.js +++ b/src/elements/content-sidebar/ContentSidebar.js @@ -27,6 +27,8 @@ import { mark } from '../../utils/performance'; import { SIDEBAR_FIELDS_TO_FETCH, SIDEBAR_FIELDS_TO_FETCH_ARCHIVE } from '../../utils/fields'; import { withErrorBoundary } from '../common/error-boundary'; // $FlowFixMe +import { withBlueprintAnimations } from '../common/withBlueprintAnimations'; +// $FlowFixMe import { withBlueprintModernization } from '../common/withBlueprintModernization'; import { isFeatureEnabled as isFeatureEnabledInContext, @@ -474,6 +476,7 @@ export default flow([ withFeatureConsumer, withFeatureProvider, withBlueprintModernization, + withBlueprintAnimations, withLogger(ORIGIN_CONTENT_SIDEBAR), withErrorBoundary(ORIGIN_CONTENT_SIDEBAR), ])(ContentSidebar); diff --git a/src/elements/content-uploader/ContentUploader.tsx b/src/elements/content-uploader/ContentUploader.tsx index bb7f84ae05..8630d3be05 100644 --- a/src/elements/content-uploader/ContentUploader.tsx +++ b/src/elements/content-uploader/ContentUploader.tsx @@ -21,6 +21,7 @@ import API from '../../api'; import Browser from '../../utils/Browser'; import Internationalize from '../common/Internationalize'; import makeResponsive from '../common/makeResponsive'; +import { withBlueprintAnimations } from '../common/withBlueprintAnimations'; import { withBlueprintModernization } from '../common/withBlueprintModernization'; import ThemingStyles, { Theme } from '../common/theming'; import FolderUpload from '../../api/uploads/FolderUpload'; @@ -1982,5 +1983,5 @@ class ContentUploader extends Component { } } -export default flow([makeResponsive, withBlueprintModernization])(ContentUploader); +export default flow([makeResponsive, withBlueprintModernization, withBlueprintAnimations])(ContentUploader); export { ContentUploader as ContentUploaderComponent, CHUNKED_UPLOAD_MIN_SIZE_BYTES };