From c03216c8f40e713e4784dfeb1a728fcfa952f860 Mon Sep 17 00:00:00 2001 From: jfox-box Date: Wed, 22 Jul 2026 20:23:38 -0700 Subject: [PATCH 1/4] feat(elements): Enable Blueprint animations Animations will be turned on by default in all elements. Opt out with `animationsEnabled={false}`. --- .storybook/preview.tsx | 26 +++- .../withBlueprintAnimations.test.tsx | 123 ++++++++++++++++++ .../common/withBlueprintAnimations.tsx | 68 ++++++++++ .../content-explorer/ContentExplorer.tsx | 11 +- src/elements/content-picker/ContentPicker.js | 4 +- .../content-preview/ContentPreview.js | 3 + .../content-sharing/ContentSharing.js | 6 +- .../content-sharing/ContentSharingV2.tsx | 4 +- .../content-sidebar/ContentSidebar.js | 3 + .../content-uploader/ContentUploader.tsx | 3 +- src/features/presence/PresenceAvatarList.tsx | 3 +- .../__snapshots__/Presence.test.js.snap | 10 +- 12 files changed, 249 insertions(+), 15 deletions(-) create mode 100644 src/elements/common/__tests__/withBlueprintAnimations.test.tsx create mode 100644 src/elements/common/withBlueprintAnimations.tsx diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index fe4a25ae15..be843724bb 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,10 +1,15 @@ +import React from 'react'; +import { BlueprintProvider, useNoopTreatment } from '@box/blueprint-web'; import { boxLanguages } from '@box/languages'; import { addWindowFocusTracking } from '@react-aria/interactions'; +import isChromatic from 'chromatic/isChromatic'; import { initialize, mswLoader } from 'msw-storybook-addon'; +import type { Decorator, Preview } from '@storybook/react-webpack5'; import '../src/styles/variables'; import '../src/styles/base.scss'; +import { AnimationsEnabledContext } from '../src/elements/common/withBlueprintAnimations'; import { reactIntl } from './reactIntl'; // Constants @@ -24,7 +29,26 @@ initialize({ }, }); -const preview = { +const withBlueprintAnimations: Decorator = Story => + isChromatic() ? ( + + + + ) : ( + + + + ); + +const preview: Preview = { + decorators: [withBlueprintAnimations], + 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..1e44e4ad61 --- /dev/null +++ b/src/elements/common/__tests__/withBlueprintAnimations.test.tsx @@ -0,0 +1,123 @@ +import * as React from 'react'; +import { render } from '../../../test-utils/testing-library'; +import { + AnimationsEnabledContext, + 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 animationsEnabled is false', () => { + const { getByTestId } = renderComponent({ animationsEnabled: 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 animationsEnabled is true', () => { + const { getByTestId } = renderComponent({ animationsEnabled: 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({ + animationsEnabled: { + 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({ + animationsEnabled: { + 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 animationsEnabled to the wrapped component', () => { + const DomFacingComponent = ({ value, ...rest }: TestComponentProps & Record) => ( +
+ {value} +
+ ); + const WrappedDomFacing = withBlueprintAnimations(DomFacingComponent); + + const { getByTestId } = render(); + + expect(getByTestId('dom-facing')).not.toHaveAttribute('animationsEnabled'); + expect(getByTestId('dom-facing')).toHaveAttribute('data-extra', 'keep'); + expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase1', 'false'); + }); + + test('should force animations off when AnimationsEnabledContext is false', () => { + const { getByTestId } = render( + + + , + ); + + expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase1', 'false'); + expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase2', 'false'); + }); +}); diff --git a/src/elements/common/withBlueprintAnimations.tsx b/src/elements/common/withBlueprintAnimations.tsx new file mode 100644 index 0000000000..272b87b196 --- /dev/null +++ b/src/elements/common/withBlueprintAnimations.tsx @@ -0,0 +1,68 @@ +import React, { useContext } from 'react'; +import { BlueprintProvider, useNoopTreatment } from '@box/blueprint-web'; + +export type BlueprintAnimationPhases = { + animationsPhase1Enabled?: boolean; + animationsPhase2Enabled?: boolean; +}; + +export type BlueprintAnimationsProps = { + /** + * Blueprint component animations for this element. + * - omitted / `true`: all phases on (default) + * - `false`: all phases off + * - object: per-phase overrides (omitted keys default on) + */ + animationsEnabled?: boolean | BlueprintAnimationPhases; +}; + +/** + * Optional gate for Storybook/Chromatic. `null` means no override (use prop / default). + * Chromatic sets this to `false` so Element HOCs cannot reinstate animations in VRTs. + */ +export const AnimationsEnabledContext = React.createContext(null); + +export function resolveAnimationPhases( + value: boolean | BlueprintAnimationPhases | undefined, +): Required { + 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 contextAnimationsEnabled = useContext(AnimationsEnabledContext); + const { animationsEnabled, ...rest } = props; + const animationPhases = + contextAnimationsEnabled === null + ? resolveAnimationPhases(animationsEnabled) + : resolveAnimationPhases(contextAnimationsEnabled); + + 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..8d13a77888 100644 --- a/src/elements/content-sharing/ContentSharing.js +++ b/src/elements/content-sharing/ContentSharing.js @@ -16,9 +16,11 @@ import { isFeatureEnabled } from '../common/feature-checking'; import Internationalize from '../common/Internationalize'; import Providers from '../common/Providers'; // $FlowFixMe +import { withBlueprintAnimations } from '../common/withBlueprintAnimations'; +// $FlowFixMe import { withBlueprintModernization } from '../common/withBlueprintModernization'; // $FlowFixMe -import ContentSharingV2 from './ContentSharingV2'; +import { ContentSharingV2Component as ContentSharingV2 } from './ContentSharingV2'; import SharingModal from './SharingModal'; import type { ElementsXhrError } from '../../common/types/api'; @@ -177,4 +179,4 @@ function ContentSharing({ ); } -export default withBlueprintModernization(ContentSharing); +export default withBlueprintAnimations(withBlueprintModernization(ContentSharing)); diff --git a/src/elements/content-sharing/ContentSharingV2.tsx b/src/elements/content-sharing/ContentSharingV2.tsx index a3fa0bc61b..7957352405 100644 --- a/src/elements/content-sharing/ContentSharingV2.tsx +++ b/src/elements/content-sharing/ContentSharingV2.tsx @@ -15,6 +15,7 @@ import type { } from '@box/unified-share-modal'; import API from '../../api'; +import { withBlueprintAnimations } from '../common/withBlueprintAnimations'; import { withBlueprintModernization } from '../common/withBlueprintModernization'; import { fetchAvatars, fetchCollaborators, fetchCurrentUser, fetchItem } from './apis'; import { CONTENT_SHARING_ERRORS } from './constants'; @@ -300,4 +301,5 @@ function ContentSharingV2({ ); } -export default withBlueprintModernization(ContentSharingV2); +export { ContentSharingV2 as ContentSharingV2Component }; +export default withBlueprintAnimations(withBlueprintModernization(ContentSharingV2)); 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 }; diff --git a/src/features/presence/PresenceAvatarList.tsx b/src/features/presence/PresenceAvatarList.tsx index c7359546b1..a8fbdf427e 100644 --- a/src/features/presence/PresenceAvatarList.tsx +++ b/src/features/presence/PresenceAvatarList.tsx @@ -6,6 +6,7 @@ import { Tooltip as BPTooltip, TooltipProvider as BPTooltipProvider } from '@box import PresenceAvatar from './PresenceAvatar'; import PresenceAvatarTooltipContent from './PresenceAvatarTooltipContent'; import Tooltip, { TooltipPosition } from '../../components/tooltip'; +import { withBlueprintAnimations } from '../../elements/common/withBlueprintAnimations'; import { withBlueprintModernization } from '../../elements/common/withBlueprintModernization'; import './PresenceAvatarList.scss'; @@ -146,4 +147,4 @@ function PresenceAvatarList(props: Props, ref: React.Ref): JSX.E export { PresenceAvatarList as PresenceAvatarListComponent }; -export default withBlueprintModernization(React.forwardRef(PresenceAvatarList)); +export default withBlueprintAnimations(withBlueprintModernization(React.forwardRef(PresenceAvatarList))); diff --git a/src/features/presence/__tests__/__snapshots__/Presence.test.js.snap b/src/features/presence/__tests__/__snapshots__/Presence.test.js.snap index 2ff3a3deaf..22ebe027ce 100644 --- a/src/features/presence/__tests__/__snapshots__/Presence.test.js.snap +++ b/src/features/presence/__tests__/__snapshots__/Presence.test.js.snap @@ -17,7 +17,7 @@ exports[`features/presence/Presence render() Tests for presence autofly - GROWTH portaledClasses={[]} position="bottom-left" > - - - - - Date: Thu, 23 Jul 2026 13:38:24 -0700 Subject: [PATCH 2/4] fix: Enable for Chromatic and remove wrapper from PresenceAvatarList --- .storybook/preview.tsx | 29 +++++++------------ .../withBlueprintAnimations.test.tsx | 22 +------------- .../common/withBlueprintAnimations.tsx | 15 ++-------- src/features/presence/PresenceAvatarList.tsx | 3 +- .../__snapshots__/Presence.test.js.snap | 10 +++---- 5 files changed, 20 insertions(+), 59 deletions(-) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index be843724bb..4b4fb61d59 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -2,14 +2,12 @@ import React from 'react'; import { BlueprintProvider, useNoopTreatment } from '@box/blueprint-web'; import { boxLanguages } from '@box/languages'; import { addWindowFocusTracking } from '@react-aria/interactions'; -import isChromatic from 'chromatic/isChromatic'; import { initialize, mswLoader } from 'msw-storybook-addon'; import type { Decorator, Preview } from '@storybook/react-webpack5'; import '../src/styles/variables'; import '../src/styles/base.scss'; -import { AnimationsEnabledContext } from '../src/elements/common/withBlueprintAnimations'; import { reactIntl } from './reactIntl'; // Constants @@ -29,22 +27,17 @@ initialize({ }, }); -const withBlueprintAnimations: Decorator = Story => - isChromatic() ? ( - - - - ) : ( - - - - ); +const withBlueprintAnimations: Decorator = Story => ( + + + +); const preview: Preview = { decorators: [withBlueprintAnimations], diff --git a/src/elements/common/__tests__/withBlueprintAnimations.test.tsx b/src/elements/common/__tests__/withBlueprintAnimations.test.tsx index 1e44e4ad61..1d33560ddf 100644 --- a/src/elements/common/__tests__/withBlueprintAnimations.test.tsx +++ b/src/elements/common/__tests__/withBlueprintAnimations.test.tsx @@ -1,10 +1,6 @@ import * as React from 'react'; import { render } from '../../../test-utils/testing-library'; -import { - AnimationsEnabledContext, - type BlueprintAnimationsProps, - withBlueprintAnimations, -} from '../withBlueprintAnimations'; +import { type BlueprintAnimationsProps, withBlueprintAnimations } from '../withBlueprintAnimations'; jest.mock('@box/blueprint-web', () => { const ReactMock = jest.requireActual('react'); @@ -104,20 +100,4 @@ describe('src/elements/common/withBlueprintAnimations', () => { expect(getByTestId('dom-facing')).toHaveAttribute('data-extra', 'keep'); expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase1', 'false'); }); - - test('should force animations off when AnimationsEnabledContext is false', () => { - const { getByTestId } = render( - - - , - ); - - expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase1', 'false'); - expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase2', 'false'); - }); }); diff --git a/src/elements/common/withBlueprintAnimations.tsx b/src/elements/common/withBlueprintAnimations.tsx index 272b87b196..dd88fcf712 100644 --- a/src/elements/common/withBlueprintAnimations.tsx +++ b/src/elements/common/withBlueprintAnimations.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React from 'react'; import { BlueprintProvider, useNoopTreatment } from '@box/blueprint-web'; export type BlueprintAnimationPhases = { @@ -16,12 +16,6 @@ export type BlueprintAnimationsProps = { animationsEnabled?: boolean | BlueprintAnimationPhases; }; -/** - * Optional gate for Storybook/Chromatic. `null` means no override (use prop / default). - * Chromatic sets this to `false` so Element HOCs cannot reinstate animations in VRTs. - */ -export const AnimationsEnabledContext = React.createContext(null); - export function resolveAnimationPhases( value: boolean | BlueprintAnimationPhases | undefined, ): Required { @@ -49,17 +43,12 @@ export function withBlueprintAnimations

( Component: React.ComponentType

, ): React.FC

{ return function WithBlueprintAnimations(props: P & BlueprintAnimationsProps) { - const contextAnimationsEnabled = useContext(AnimationsEnabledContext); const { animationsEnabled, ...rest } = props; - const animationPhases = - contextAnimationsEnabled === null - ? resolveAnimationPhases(animationsEnabled) - : resolveAnimationPhases(contextAnimationsEnabled); return ( diff --git a/src/features/presence/PresenceAvatarList.tsx b/src/features/presence/PresenceAvatarList.tsx index a8fbdf427e..c7359546b1 100644 --- a/src/features/presence/PresenceAvatarList.tsx +++ b/src/features/presence/PresenceAvatarList.tsx @@ -6,7 +6,6 @@ import { Tooltip as BPTooltip, TooltipProvider as BPTooltipProvider } from '@box import PresenceAvatar from './PresenceAvatar'; import PresenceAvatarTooltipContent from './PresenceAvatarTooltipContent'; import Tooltip, { TooltipPosition } from '../../components/tooltip'; -import { withBlueprintAnimations } from '../../elements/common/withBlueprintAnimations'; import { withBlueprintModernization } from '../../elements/common/withBlueprintModernization'; import './PresenceAvatarList.scss'; @@ -147,4 +146,4 @@ function PresenceAvatarList(props: Props, ref: React.Ref): JSX.E export { PresenceAvatarList as PresenceAvatarListComponent }; -export default withBlueprintAnimations(withBlueprintModernization(React.forwardRef(PresenceAvatarList))); +export default withBlueprintModernization(React.forwardRef(PresenceAvatarList)); diff --git a/src/features/presence/__tests__/__snapshots__/Presence.test.js.snap b/src/features/presence/__tests__/__snapshots__/Presence.test.js.snap index 22ebe027ce..2ff3a3deaf 100644 --- a/src/features/presence/__tests__/__snapshots__/Presence.test.js.snap +++ b/src/features/presence/__tests__/__snapshots__/Presence.test.js.snap @@ -17,7 +17,7 @@ exports[`features/presence/Presence render() Tests for presence autofly - GROWTH portaledClasses={[]} position="bottom-left" > - - - - - Date: Thu, 23 Jul 2026 14:09:36 -0700 Subject: [PATCH 3/4] fix: chromatic test --- .../stories/tests/ContentSharingV2-visual.stories.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/elements/content-sharing/stories/tests/ContentSharingV2-visual.stories.tsx b/src/elements/content-sharing/stories/tests/ContentSharingV2-visual.stories.tsx index cd877b3bc9..c19e5baf6a 100644 --- a/src/elements/content-sharing/stories/tests/ContentSharingV2-visual.stories.tsx +++ b/src/elements/content-sharing/stories/tests/ContentSharingV2-visual.stories.tsx @@ -18,7 +18,10 @@ export const withModernization = { expect(button).toBeInTheDocument(); await userEvent.click(button); - expect(screen.getByRole('heading', { name: 'Share ‘Box Development Guide.pdf’' })).toBeVisible(); + // Wait for modal to finish animation before checking visibility + await waitFor(() => { + 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(); }, From a248a373d919b3988568d1e354f5925c44005309 Mon Sep 17 00:00:00 2001 From: jfox-box Date: Thu, 23 Jul 2026 15:41:05 -0700 Subject: [PATCH 4/4] fix: rename to enableBlueprintAnimations --- .storybook/preview.tsx | 4 ++-- .../withBlueprintAnimations.test.tsx | 20 ++++++++++--------- .../common/withBlueprintAnimations.tsx | 20 ++++++++----------- .../content-sharing/ContentSharing.js | 16 +++++++++------ 4 files changed, 31 insertions(+), 29 deletions(-) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 4b4fb61d59..9972327f8b 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -27,7 +27,7 @@ initialize({ }, }); -const withBlueprintAnimations: Decorator = Story => ( +const withStorybookBlueprintAnimations: Decorator = Story => ( ( ); const preview: Preview = { - decorators: [withBlueprintAnimations], + decorators: [withStorybookBlueprintAnimations], parameters: { chromatic: { diff --git a/src/elements/common/__tests__/withBlueprintAnimations.test.tsx b/src/elements/common/__tests__/withBlueprintAnimations.test.tsx index 1d33560ddf..1bb9857758 100644 --- a/src/elements/common/__tests__/withBlueprintAnimations.test.tsx +++ b/src/elements/common/__tests__/withBlueprintAnimations.test.tsx @@ -43,15 +43,15 @@ describe('src/elements/common/withBlueprintAnimations', () => { expect(getByTestId('blueprint-animations-provider')).toHaveAttribute('data-animations-phase2', 'true'); }); - test('should opt out when animationsEnabled is false', () => { - const { getByTestId } = renderComponent({ animationsEnabled: false }); + 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 animationsEnabled is true', () => { - const { getByTestId } = renderComponent({ animationsEnabled: true }); + 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'); @@ -59,7 +59,7 @@ describe('src/elements/common/withBlueprintAnimations', () => { test('should honor per-phase overrides matching Blueprint configurationOverrides keys', () => { const { getByTestId } = renderComponent({ - animationsEnabled: { + enableBlueprintAnimations: { animationsPhase1Enabled: true, animationsPhase2Enabled: false, }, @@ -71,7 +71,7 @@ describe('src/elements/common/withBlueprintAnimations', () => { test('should default omitted phase keys to on when an object is passed', () => { const { getByTestId } = renderComponent({ - animationsEnabled: { + enableBlueprintAnimations: { animationsPhase2Enabled: false, }, }); @@ -86,7 +86,7 @@ describe('src/elements/common/withBlueprintAnimations', () => { expect(getByTestId('test-component')).toHaveTextContent('Test test-value'); }); - test('should not forward animationsEnabled to the wrapped component', () => { + test('should not forward enableBlueprintAnimations to the wrapped component', () => { const DomFacingComponent = ({ value, ...rest }: TestComponentProps & Record) => (

{value} @@ -94,9 +94,11 @@ describe('src/elements/common/withBlueprintAnimations', () => { ); const WrappedDomFacing = withBlueprintAnimations(DomFacingComponent); - const { getByTestId } = render(); + const { getByTestId } = render( + , + ); - expect(getByTestId('dom-facing')).not.toHaveAttribute('animationsEnabled'); + 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 index dd88fcf712..c8af1146c0 100644 --- a/src/elements/common/withBlueprintAnimations.tsx +++ b/src/elements/common/withBlueprintAnimations.tsx @@ -1,24 +1,20 @@ import React from 'react'; import { BlueprintProvider, useNoopTreatment } from '@box/blueprint-web'; - -export type BlueprintAnimationPhases = { - animationsPhase1Enabled?: boolean; - animationsPhase2Enabled?: boolean; -}; +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 (omitted keys default on) + * - object: per-phase overrides from Blueprint `configurationOverrides` (omitted keys default on) */ - animationsEnabled?: boolean | BlueprintAnimationPhases; + enableBlueprintAnimations?: boolean | BlueprintConfigurationOverrides; }; -export function resolveAnimationPhases( - value: boolean | BlueprintAnimationPhases | undefined, -): Required { +export function getBlueprintAnimationOverrides( + value: boolean | BlueprintConfigurationOverrides | undefined, +): Pick { if (value === false) { return { animationsPhase1Enabled: false, @@ -43,12 +39,12 @@ export function withBlueprintAnimations

( Component: React.ComponentType

, ): React.FC

{ return function WithBlueprintAnimations(props: P & BlueprintAnimationsProps) { - const { animationsEnabled, ...rest } = props; + const { enableBlueprintAnimations, ...rest } = props; return ( diff --git a/src/elements/content-sharing/ContentSharing.js b/src/elements/content-sharing/ContentSharing.js index 8d13a77888..2a89d3f4e5 100644 --- a/src/elements/content-sharing/ContentSharing.js +++ b/src/elements/content-sharing/ContentSharing.js @@ -16,11 +16,7 @@ import { isFeatureEnabled } from '../common/feature-checking'; import Internationalize from '../common/Internationalize'; import Providers from '../common/Providers'; // $FlowFixMe -import { withBlueprintAnimations } from '../common/withBlueprintAnimations'; -// $FlowFixMe -import { withBlueprintModernization } from '../common/withBlueprintModernization'; -// $FlowFixMe -import { ContentSharingV2Component as ContentSharingV2 } from './ContentSharingV2'; +import ContentSharingV2 from './ContentSharingV2'; import SharingModal from './SharingModal'; import type { ElementsXhrError } from '../../common/types/api'; @@ -53,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 */ @@ -94,6 +94,8 @@ function ContentSharing({ config, customButton, displayInModal, + enableBlueprintAnimations, + enableModernizedComponents, features = {}, hasProviders = true, itemID, @@ -143,6 +145,8 @@ function ContentSharing({