diff --git a/UNRELEASED-V4.md b/UNRELEASED-V4.md index fd20e2469ba..46d76243ed3 100644 --- a/UNRELEASED-V4.md +++ b/UNRELEASED-V4.md @@ -23,5 +23,6 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f ### Code quality - Upgraded the `Banner`, `Card`, and `Modal` components from legacy context API to use createContext ([#786](https://github.com/Shopify/polaris-react/pull/786)) +- Refactored `Frame` and its subcomponents to use the `createContext` API instead of legacy context ([#803](https://github.com/Shopify/polaris-react/pull/803)) ### Deprecations diff --git a/src/components/ContextualSaveBar/ContextualSaveBar.tsx b/src/components/ContextualSaveBar/ContextualSaveBar.tsx index 3c7b86f58c6..2a9e28fb68e 100644 --- a/src/components/ContextualSaveBar/ContextualSaveBar.tsx +++ b/src/components/ContextualSaveBar/ContextualSaveBar.tsx @@ -1,32 +1,38 @@ import * as React from 'react'; +import compose from '@shopify/react-compose'; import isEqual from 'lodash/isEqual'; -import { - ContextualSaveBarProps, - FrameContext, - frameContextTypes, -} from '../Frame'; +import {ContextualSaveBarProps, FrameContext, Consumer} from '../Frame'; +import withContext from '../WithContext'; +import {WithContextTypes} from '../../types'; +import {withAppProvider, WithAppProviderProps} from '../AppProvider'; // The script in the styleguide that generates the Props Explorer data expects // a component's props to be found in the Props interface. This silly workaround // ensures that the Props Explorer table is generated correctly, instead of // crashing if we write `ContextualSaveBar extends React.Component` interface Props extends ContextualSaveBarProps {} +export type ComposedProps = Props & + WithAppProviderProps & + WithContextTypes; -class ContextualSaveBar extends React.PureComponent { - static contextTypes = frameContextTypes; - context: FrameContext; - +class ContextualSaveBar extends React.PureComponent { componentDidMount() { - this.context.frame.setContextualSaveBar(this.props); + const { + props: {polaris, context, ...rest}, + } = this; + context.frame.setContextualSaveBar(rest); } componentWillUnmount() { - this.context.frame.removeContextualSaveBar(); + this.props.context.frame.removeContextualSaveBar(); } - componentDidUpdate(oldProps: Props) { - if (contextualSaveBarHasChanged(this.props, oldProps)) { - this.context.frame.setContextualSaveBar(this.props); + componentDidUpdate(oldProps: ComposedProps) { + const { + props: {polaris, context, ...rest}, + } = this; + if (contextualSaveBarHasChanged(rest, oldProps)) { + context.frame.setContextualSaveBar(rest); } } @@ -50,4 +56,7 @@ function contextualSaveBarHasChanged( ); } -export default ContextualSaveBar; +export default compose( + withContext(Consumer), + withAppProvider(), +)(ContextualSaveBar); diff --git a/src/components/ContextualSaveBar/tests/ContextualSaveBar.test.tsx b/src/components/ContextualSaveBar/tests/ContextualSaveBar.test.tsx index d52e381dedd..d9c277ac159 100644 --- a/src/components/ContextualSaveBar/tests/ContextualSaveBar.test.tsx +++ b/src/components/ContextualSaveBar/tests/ContextualSaveBar.test.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import * as PropTypes from 'prop-types'; import {mountWithAppProvider} from 'test-utilities'; import {noop} from '../../../utilities/other'; import ContextualSaveBar from '../ContextualSaveBar'; +import {Provider, createFrameContext} from '../../Frame'; describe('', () => { const props = { @@ -12,58 +12,87 @@ describe('', () => { }; it('calls the contextual save bar on mount with correct values', () => { - const {frame} = mountWithContext(); - expect(frame.setContextualSaveBar).toHaveBeenCalledWith({ + const mockFrameContext = createFrameContext({ + setContextualSaveBar: jest.fn(), + removeContextualSaveBar: jest.fn(), + }); + + mountWithAppProvider( + + + , + ); + expect(mockFrameContext.frame.setContextualSaveBar).toHaveBeenCalledWith({ ...props, }); }); it('removes the contextual save bar on unmount', () => { - const {contextualSaveBar, frame} = mountWithContext( - , + const mockFrameContext = createFrameContext({ + setContextualSaveBar: jest.fn(), + removeContextualSaveBar: jest.fn(), + }); + + const frame = mountWithAppProvider( + + + , ); - expect(frame.removeContextualSaveBar).not.toHaveBeenCalled(); - contextualSaveBar.unmount(); - expect(frame.removeContextualSaveBar).toHaveBeenCalled(); + expect( + mockFrameContext.frame.removeContextualSaveBar, + ).not.toHaveBeenCalled(); + frame.unmount(); + expect(mockFrameContext.frame.removeContextualSaveBar).toHaveBeenCalled(); }); it('calls the contextual save bar with correct values if its props change after it mounted', () => { - const {frame, contextualSaveBar} = mountWithContext( - , + const mockFrameContext = createFrameContext({ + setContextualSaveBar: jest.fn(), + removeContextualSaveBar: jest.fn(), + }); + + const frame = mountWithAppProvider( + + + , ); - expect(frame.setContextualSaveBar).toHaveBeenCalledTimes(1); const newProps = { saveAction: {content: 'Save', onAction: noop, loading: true}, discardAction: {content: 'Discard', onAction: noop}, message: 'Unsaved changes', }; - contextualSaveBar.setProps({...newProps}); - expect(frame.setContextualSaveBar).toHaveBeenCalledWith({ + frame.setProps({ + children: , + }); + expect(mockFrameContext.frame.setContextualSaveBar).toHaveBeenCalledWith({ ...newProps, }); - expect(frame.setContextualSaveBar).toHaveBeenCalledTimes(2); + expect(mockFrameContext.frame.setContextualSaveBar).toHaveBeenCalledTimes( + 2, + ); }); it('doesnt call the contextual save bar if its props remain unchanged after it mounted', () => { - const {frame, contextualSaveBar} = mountWithContext( - , + const mockFrameContext = createFrameContext({ + setContextualSaveBar: jest.fn(), + removeContextualSaveBar: jest.fn(), + }); + + const frame = mountWithAppProvider( + + + , + ); + + expect(mockFrameContext.frame.setContextualSaveBar).toHaveBeenCalledTimes( + 1, ); - expect(frame.setContextualSaveBar).toHaveBeenCalledTimes(1); const newProps = {...props}; - contextualSaveBar.setProps({...newProps}); - expect(frame.setContextualSaveBar).toHaveBeenCalledTimes(1); + frame.setProps({ + children: , + }); + expect(mockFrameContext.frame.setContextualSaveBar).toHaveBeenCalledTimes( + 1, + ); }); }); - -function mountWithContext(element: React.ReactElement) { - const frame = { - setContextualSaveBar: jest.fn(), - removeContextualSaveBar: jest.fn(), - }; - const contextualSaveBar = mountWithAppProvider(element, { - context: {frame}, - childContextTypes: {frame: PropTypes.any}, - }); - - return {contextualSaveBar, frame}; -} diff --git a/src/components/Frame/Frame.tsx b/src/components/Frame/Frame.tsx index 4e30bc5c38d..281bd06d2f3 100644 --- a/src/components/Frame/Frame.tsx +++ b/src/components/Frame/Frame.tsx @@ -14,10 +14,10 @@ import {setRootProperty} from '../../utilities/setRootProperty'; import { ContextualSaveBarProps, FrameContext, - frameContextTypes, - ToastProps, + ToastID, + ToastPropsWithID, } from './types'; -import {ToastManager, Loading, ContextualSaveBar} from './components'; +import {Provider, ToastManager, Loading, ContextualSaveBar} from './components'; import * as styles from './Frame.scss'; @@ -41,7 +41,7 @@ export interface State { skipFocused?: boolean; globalRibbonHeight: number; loadingStack: number; - toastMessages: (ToastProps & {id: string})[]; + toastMessages: (ToastPropsWithID)[]; showContextualSaveBar: boolean; } @@ -54,8 +54,6 @@ const APP_FRAME_LOADING_BAR = 'AppFrameLoadingBar'; export type CombinedProps = Props & WithAppProviderProps; export class Frame extends React.PureComponent { - static childContextTypes = frameContextTypes; - state: State = { skipFocused: false, globalRibbonHeight: 0, @@ -69,19 +67,6 @@ export class Frame extends React.PureComponent { private globalRibbonContainer: HTMLDivElement | null = null; - getChildContext(): FrameContext { - return { - frame: { - showToast: this.showToast, - hideToast: this.hideToast, - startLoading: this.startLoading, - stopLoading: this.stopLoading, - setContextualSaveBar: this.setContextualSaveBar, - removeContextualSaveBar: this.removeContextualSaveBar, - }, - }; - } - componentDidMount() { this.handleResize(); if (this.props.globalRibbon) { @@ -240,28 +225,30 @@ export class Frame extends React.PureComponent { ) : null; return ( -
- {skipMarkup} - {topBarMarkup} - {contextualSaveBarMarkup} - {loadingMarkup} - {navigationOverlayMarkup} - {navigationMarkup} -
+
-
{children}
-
- - {globalRibbonMarkup} - -
+ {skipMarkup} + {topBarMarkup} + {contextualSaveBarMarkup} + {loadingMarkup} + {navigationOverlayMarkup} + {navigationMarkup} +
+
{children}
+
+ + {globalRibbonMarkup} + + + ); } @@ -289,7 +276,7 @@ export class Frame extends React.PureComponent { } @autobind - private showToast(toast: {id: string} & ToastProps) { + private showToast(toast: ToastPropsWithID) { this.setState(({toastMessages}: State) => { const hasToastById = toastMessages.find(({id}) => id === toast.id) != null; @@ -300,7 +287,7 @@ export class Frame extends React.PureComponent { } @autobind - private hideToast({id}: {id: string}) { + private hideToast({id}: ToastID) { this.setState(({toastMessages}: State) => { return { toastMessages: toastMessages.filter(({id: toastId}) => id !== toastId), @@ -390,6 +377,20 @@ export class Frame extends React.PureComponent { this.handleNavigationDismiss(); } } + + @autobind + get getContext(): FrameContext { + return { + frame: { + showToast: this.showToast, + hideToast: this.hideToast, + startLoading: this.startLoading, + stopLoading: this.stopLoading, + setContextualSaveBar: this.setContextualSaveBar, + removeContextualSaveBar: this.removeContextualSaveBar, + }, + }; + } } const navTransitionClasses = { diff --git a/src/components/Frame/components/Context/Context.tsx b/src/components/Frame/components/Context/Context.tsx new file mode 100644 index 00000000000..dc6d9fdb131 --- /dev/null +++ b/src/components/Frame/components/Context/Context.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import {noop} from '@shopify/javascript-utilities/other'; +import {FrameContext} from '../../types'; + +const defaultContext: FrameContext = { + frame: { + showToast: noop, + hideToast: noop, + setContextualSaveBar: noop, + removeContextualSaveBar: noop, + startLoading: noop, + stopLoading: noop, + }, +}; + +const {Provider, Consumer} = React.createContext(defaultContext); + +export {Provider, Consumer}; diff --git a/src/components/Frame/components/Context/index.ts b/src/components/Frame/components/Context/index.ts new file mode 100644 index 00000000000..e45eea34bd9 --- /dev/null +++ b/src/components/Frame/components/Context/index.ts @@ -0,0 +1 @@ +export {Provider, Consumer} from './Context'; diff --git a/src/components/Frame/components/ToastManager/ToastManager.tsx b/src/components/Frame/components/ToastManager/ToastManager.tsx index ce85733987a..da99c0a0dbc 100644 --- a/src/components/Frame/components/ToastManager/ToastManager.tsx +++ b/src/components/Frame/components/ToastManager/ToastManager.tsx @@ -4,13 +4,13 @@ import {autobind} from '@shopify/javascript-utilities/decorators'; import {classNames} from '@shopify/react-utilities/styles'; import EventListener from '../../../EventListener'; import Portal from '../../../Portal'; -import {ToastProps} from '../../types'; +import {ToastPropsWithID} from '../../types'; import Toast from '../Toast'; import * as styles from './ToastManager.scss'; export interface Props { - toastMessages: (ToastProps & {id: string})[]; + toastMessages: (ToastPropsWithID)[]; } export default class ToastManager extends React.PureComponent { diff --git a/src/components/Frame/components/index.ts b/src/components/Frame/components/index.ts index a02055f399f..f3b40356cea 100644 --- a/src/components/Frame/components/index.ts +++ b/src/components/Frame/components/index.ts @@ -5,3 +5,4 @@ export { } from './ToastManager'; export {default as Loading, Props as LoadingProps} from './Loading'; export {default as ContextualSaveBar} from './ContextualSaveBar'; +export {Provider, Consumer} from './Context'; diff --git a/src/components/Frame/index.ts b/src/components/Frame/index.ts index 4afafa89515..14cbc816d78 100644 --- a/src/components/Frame/index.ts +++ b/src/components/Frame/index.ts @@ -2,13 +2,10 @@ import Frame from './Frame'; export {Props} from './Frame'; -export {DEFAULT_TOAST_DURATION} from './components'; - -export { - ContextualSaveBarProps, - FrameContext, - frameContextTypes, - ToastProps, -} from './types'; +export {DEFAULT_TOAST_DURATION, Provider, Consumer} from './components'; + +export {ContextualSaveBarProps, FrameContext, ToastProps} from './types'; + +export {createFrameContext, CreateFrameContext} from './utilities'; export default Frame; diff --git a/src/components/Frame/types.ts b/src/components/Frame/types.ts index 0be89ca61a8..cc05b6a06d0 100644 --- a/src/components/Frame/types.ts +++ b/src/components/Frame/types.ts @@ -1,8 +1,6 @@ -import * as PropTypes from 'prop-types'; - export interface FrameManager { - showToast(toast: {id: string} & ToastProps): void; - hideToast(toast: {id: string}): void; + showToast(toast: ToastPropsWithID): void; + hideToast(toast: ToastID): void; setContextualSaveBar(props: ContextualSaveBarProps): void; removeContextualSaveBar(): void; startLoading(): void; @@ -13,10 +11,6 @@ export interface FrameContext { frame: FrameManager; } -export const frameContextTypes = { - frame: PropTypes.object, -}; - interface ContextualSaveBarAction { /** A destination to link to */ url?: string; @@ -64,3 +58,9 @@ export interface ToastProps { /** Callback when the dismiss icon is clicked */ onDismiss(): void; } + +export interface ToastID { + id: string; +} + +export type ToastPropsWithID = ToastProps & ToastID; diff --git a/src/components/Frame/utilities/createFrameContext/createFrameContext.ts b/src/components/Frame/utilities/createFrameContext/createFrameContext.ts new file mode 100644 index 00000000000..5c5acfaaece --- /dev/null +++ b/src/components/Frame/utilities/createFrameContext/createFrameContext.ts @@ -0,0 +1,36 @@ +import {noop} from '@shopify/javascript-utilities/other'; +import { + FrameContext, + ContextualSaveBarProps, + ToastID, + ToastPropsWithID, +} from '../../types'; + +export interface CreateFrameContext { + showToast?(toast: ToastPropsWithID): void; + hideToast?(toast: ToastID): void; + setContextualSaveBar?(props: ContextualSaveBarProps): void; + removeContextualSaveBar?(): void; + startLoading?(): void; + stopLoading?(): void; +} + +export default function createFrameContext({ + showToast = noop, + hideToast = noop, + setContextualSaveBar = noop, + removeContextualSaveBar = noop, + startLoading = noop, + stopLoading = noop, +}: CreateFrameContext = {}): FrameContext { + return { + frame: { + showToast, + hideToast, + setContextualSaveBar, + removeContextualSaveBar, + startLoading, + stopLoading, + }, + }; +} diff --git a/src/components/Frame/utilities/createFrameContext/index.ts b/src/components/Frame/utilities/createFrameContext/index.ts new file mode 100644 index 00000000000..2019c9079cf --- /dev/null +++ b/src/components/Frame/utilities/createFrameContext/index.ts @@ -0,0 +1 @@ +export {default, CreateFrameContext} from './createFrameContext'; diff --git a/src/components/Frame/utilities/createFrameContext/tests/createFrameContext.test.ts b/src/components/Frame/utilities/createFrameContext/tests/createFrameContext.test.ts new file mode 100644 index 00000000000..b629f7b34d1 --- /dev/null +++ b/src/components/Frame/utilities/createFrameContext/tests/createFrameContext.test.ts @@ -0,0 +1,110 @@ +import {noop} from '@shopify/javascript-utilities/other'; +import {createFrameContext} from '../..'; + +describe('createFrameContext()', () => { + const defaultFrameContext = { + showToast: noop, + hideToast: noop, + setContextualSaveBar: noop, + removeContextualSaveBar: noop, + startLoading: noop, + stopLoading: noop, + }; + + it('returns the right context without arguments', () => { + const context = createFrameContext(); + const mockContext = { + frame: { + ...defaultFrameContext, + }, + }; + + expect(context).toEqual(mockContext); + }); + + it('returns the right context with showToast', () => { + const mockShowToast = () => {}; + const context = createFrameContext({showToast: mockShowToast}); + const mockContext = { + frame: { + ...defaultFrameContext, + showToast: mockShowToast, + }, + }; + + expect(context).toEqual(mockContext); + }); + + it('returns the right context with hideToast', () => { + const mockHideToast = () => {}; + const context = createFrameContext({hideToast: mockHideToast}); + const mockContext = { + frame: { + ...defaultFrameContext, + hideToast: mockHideToast, + }, + }; + + expect(context).toEqual(mockContext); + }); + + it('returns the right context with setContextualSaveBar', () => { + const mockSetContextualSaveBar = () => {}; + const context = createFrameContext({ + setContextualSaveBar: mockSetContextualSaveBar, + }); + const mockContext = { + frame: { + ...defaultFrameContext, + setContextualSaveBar: mockSetContextualSaveBar, + }, + }; + + expect(context).toEqual(mockContext); + }); + + it('returns the right context with removeContextualSaveBar', () => { + const mockRemoveContextualSaveBar = () => {}; + const context = createFrameContext({ + removeContextualSaveBar: mockRemoveContextualSaveBar, + }); + const mockContext = { + frame: { + ...defaultFrameContext, + removeContextualSaveBar: mockRemoveContextualSaveBar, + }, + }; + + expect(context).toEqual(mockContext); + }); + + it('returns the right context with startLoading', () => { + const mockStartLoading = () => {}; + const context = createFrameContext({ + startLoading: mockStartLoading, + }); + const mockContext = { + frame: { + ...defaultFrameContext, + startLoading: mockStartLoading, + }, + }; + + expect(context).toEqual(mockContext); + }); + + it('returns the right context with stopLoading', () => { + const mockStopLoading = () => {}; + const context = createFrameContext({ + stopLoading: mockStopLoading, + }); + const mockContext = { + frame: { + ...defaultFrameContext, + stopLoading: mockStopLoading, + }, + }; + + expect(context).toEqual(mockContext); + }); +}); diff --git a/src/components/Frame/utilities/index.ts b/src/components/Frame/utilities/index.ts new file mode 100644 index 00000000000..fc0419a0ad2 --- /dev/null +++ b/src/components/Frame/utilities/index.ts @@ -0,0 +1,4 @@ +export { + default as createFrameContext, + CreateFrameContext, +} from './createFrameContext'; diff --git a/src/components/Loading/Loading.tsx b/src/components/Loading/Loading.tsx index b534fadb015..1338e0f08ac 100644 --- a/src/components/Loading/Loading.tsx +++ b/src/components/Loading/Loading.tsx @@ -1,21 +1,27 @@ import * as React from 'react'; +import compose from '@shopify/react-compose'; import {Loading as AppBridgeLoading} from '@shopify/app-bridge/actions'; -import {FrameContext, frameContextTypes} from '../Frame'; +import {FrameContext, Consumer} from '../Frame'; +import withContext from '../WithContext'; +import {WithContextTypes} from '../../types'; import {withAppProvider, WithAppProviderProps} from '../AppProvider'; export interface Props {} -export type ComposedProps = Props & WithAppProviderProps; +export type ComposedProps = Props & + WithAppProviderProps & + WithContextTypes; export class Loading extends React.PureComponent { - static contextTypes = frameContextTypes; - context: FrameContext; private appBridgeLoading: AppBridgeLoading.Loading | undefined; componentDidMount() { - const {appBridge} = this.props.polaris; + const { + polaris: {appBridge}, + context, + } = this.props; if (appBridge == null) { - this.context.frame.startLoading(); + context.frame.startLoading(); } else { this.appBridgeLoading = AppBridgeLoading.create(appBridge); this.appBridgeLoading.dispatch(AppBridgeLoading.Action.START); @@ -23,10 +29,13 @@ export class Loading extends React.PureComponent { } componentWillUnmount() { - const {appBridge} = this.props.polaris; + const { + polaris: {appBridge}, + context, + } = this.props; if (appBridge == null) { - this.context.frame.stopLoading(); + context.frame.stopLoading(); } else if (this.appBridgeLoading != null) { this.appBridgeLoading.dispatch(AppBridgeLoading.Action.STOP); } @@ -37,4 +46,7 @@ export class Loading extends React.PureComponent { } } -export default withAppProvider()(Loading); +export default compose( + withContext(Consumer), + withAppProvider(), +)(Loading); diff --git a/src/components/Loading/tests/Loading.test.tsx b/src/components/Loading/tests/Loading.test.tsx index 2918532c2d3..b585b4b7e4b 100644 --- a/src/components/Loading/tests/Loading.test.tsx +++ b/src/components/Loading/tests/Loading.test.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import {Loading as AppBridgeLoading} from '@shopify/app-bridge/actions'; import * as PropTypes from 'prop-types'; import {mountWithAppProvider} from 'test-utilities'; +import {Provider, createFrameContext} from '../../Frame'; import Loading from '../Loading'; @@ -11,16 +12,31 @@ describe('', () => { }); it('starts loading on mount', () => { - const {frame} = mountWithFrame(); - expect(frame.startLoading).toHaveBeenCalled(); + const mockFrameContext = createFrameContext({ + startLoading: jest.fn(), + }); + + mountWithAppProvider( + + + , + ); + expect(mockFrameContext.frame.startLoading).toHaveBeenCalled(); }); it('stops loading on unmount', () => { - const {loading, frame} = mountWithFrame(); - expect(frame.stopLoading).not.toHaveBeenCalled(); + const mockFrameContext = createFrameContext({ + stopLoading: jest.fn(), + }); + const frame = mountWithAppProvider( + + + , + ); + expect(mockFrameContext.frame.stopLoading).not.toHaveBeenCalled(); - loading.unmount(); - expect(frame.stopLoading).toHaveBeenCalled(); + frame.unmount(); + expect(mockFrameContext.frame.stopLoading).toHaveBeenCalled(); }); describe('with app bridge', () => { @@ -45,16 +61,6 @@ describe('', () => { }); }); -function mountWithFrame(element: React.ReactElement) { - const frame = {startLoading: jest.fn(), stopLoading: jest.fn()}; - const loading = mountWithAppProvider(element, { - context: {frame}, - childContextTypes: {frame: PropTypes.any}, - }); - - return {loading, frame}; -} - function mountWithAppBridge(element: React.ReactElement) { const appBridge = {}; const polaris = {appBridge}; diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx index b67cb2e849f..d80c46e584b 100644 --- a/src/components/Toast/Toast.tsx +++ b/src/components/Toast/Toast.tsx @@ -1,11 +1,14 @@ import * as React from 'react'; +import compose from '@shopify/react-compose'; import {createUniqueIDFactory} from '@shopify/javascript-utilities/other'; import {Flash as AppBridgeToast} from '@shopify/app-bridge/actions'; +import withContext from '../WithContext'; +import {WithContextTypes} from '../../types'; import { DEFAULT_TOAST_DURATION, FrameContext, - frameContextTypes, + Consumer, ToastProps, } from '../Frame'; import {withAppProvider, WithAppProviderProps} from '../AppProvider'; @@ -18,17 +21,18 @@ const createId = createUniqueIDFactory('Toast'); // crashing if we write `ComposedProps = ToastProps & WithAppProviderProps` interface Props extends ToastProps {} -export type ComposedProps = Props & WithAppProviderProps; +export type ComposedProps = Props & + WithAppProviderProps & + WithContextTypes; export class Toast extends React.PureComponent { - static contextTypes = frameContextTypes; context: FrameContext; private id = createId(); private appBridgeToast: AppBridgeToast.Flash | undefined; componentDidMount() { - const {context, id, props} = this; + const {id, props} = this; const { error, content, @@ -38,9 +42,10 @@ export class Toast extends React.PureComponent { const {appBridge} = props.polaris; if (appBridge == null) { + const {polaris, context, ...rest} = props; context.frame.showToast({ id, - ...(props as Props), + ...(rest as Props), }); } else { this.appBridgeToast = AppBridgeToast.create(appBridge, { @@ -56,10 +61,11 @@ export class Toast extends React.PureComponent { } componentWillUnmount() { - const {appBridge} = this.props.polaris; + const {polaris, context} = this.props; + const {appBridge} = polaris; if (appBridge == null) { - this.context.frame.hideToast({id: this.id}); + context.frame.hideToast({id: this.id}); } else if (this.appBridgeToast != null) { this.appBridgeToast.unsubscribe(); } @@ -70,4 +76,7 @@ export class Toast extends React.PureComponent { } } -export default withAppProvider()(Toast); +export default compose( + withContext(Consumer), + withAppProvider(), +)(Toast); diff --git a/src/components/Toast/tests/Toast.test.tsx b/src/components/Toast/tests/Toast.test.tsx index 89b608c618d..12db0fbb8bb 100644 --- a/src/components/Toast/tests/Toast.test.tsx +++ b/src/components/Toast/tests/Toast.test.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import {Flash as AppBridgeToast} from '@shopify/app-bridge/actions'; -import {mountWithAppProvider, createPolarisProps} from 'test-utilities'; +import {mountWithAppProvider} from 'test-utilities'; import {noop} from '../../../utilities/other'; import Toast from '../Toast'; +import {Provider, createFrameContext} from '../../Frame'; describe('', () => { beforeEach(() => { @@ -11,27 +12,40 @@ describe('', () => { }); it('shows the toast with a unique ID on mount', () => { + const mockFrameContext = createFrameContext({ + showToast: jest.fn(), + }); + const props = {content: 'Image uploaded', onDismiss: noop}; - const composedProps = { - ...props, - ...createPolarisProps(), - }; - const {frame} = mountWithContext(); - expect(frame.showToast).toHaveBeenCalledWith({ + mountWithAppProvider( + + + , + ); + + expect(mockFrameContext.frame.showToast).toHaveBeenCalledWith({ id: expect.any(String), - ...composedProps, + ...props, }); }); it('hides the toast based on ID on unmount', () => { - const {toast, frame} = mountWithContext( - , + const mockFrameContext = createFrameContext({ + hideToast: jest.fn(), + }); + + const frame = mountWithAppProvider( + + + , ); - expect(frame.hideToast).not.toHaveBeenCalled(); - toast.unmount(); - const {id} = frame.showToast.mock.calls[0][0]; - expect(frame.hideToast).toHaveBeenCalledWith({id}); + expect(mockFrameContext.frame.hideToast).not.toHaveBeenCalled(); + frame.unmount(); + + const mockHideToast = mockFrameContext.frame.hideToast as jest.Mock; + const {id} = mockHideToast.mock.calls[0][0]; + expect(mockFrameContext.frame.hideToast).toHaveBeenCalledWith({id}); }); describe('with app bridge', () => { @@ -102,16 +116,6 @@ describe('', () => { }); }); -function mountWithContext(element: React.ReactElement) { - const frame = {showToast: jest.fn(), hideToast: jest.fn()}; - const toast = mountWithAppProvider(element, { - context: {frame}, - childContextTypes: {frame: PropTypes.any}, - }); - - return {toast, frame}; -} - function mountWithAppBridge(element: React.ReactElement) { const appBridge = {}; const polaris = {appBridge};