diff --git a/UNRELEASED.md b/UNRELEASED.md index 5910393f12d..c767c3dbb16 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -18,3 +18,5 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f ### Dependency upgrades ### 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)) diff --git a/src/components/Banner/Banner.tsx b/src/components/Banner/Banner.tsx index a90a92e2be7..faa0f831fd7 100644 --- a/src/components/Banner/Banner.tsx +++ b/src/components/Banner/Banner.tsx @@ -1,17 +1,21 @@ import * as React from 'react'; import {classNames, variationName} from '@shopify/react-utilities/styles'; +import compose from '@shopify/react-compose'; import { Action, DisableableAction, LoadableAction, - contentContextTypes, + WithContextTypes, } from '../../types'; import Button, {buttonFrom} from '../Button'; import Heading from '../Heading'; import ButtonGroup from '../ButtonGroup'; import UnstyledLink from '../UnstyledLink'; import Icon, {Props as IconProps} from '../Icon'; +import {Consumer, WithinContentContext} from '../WithinContentContext'; +import withContext from '../WithContext'; +import {withAppProvider, WithAppProviderProps} from '../AppProvider'; import * as styles from './Banner.scss'; @@ -40,9 +44,9 @@ export interface Props { onDismiss?(): void; } -export default class Banner extends React.PureComponent { - static contextTypes = contentContextTypes; +export type CombinedProps = Props & WithContextTypes; +export class Banner extends React.PureComponent { render() { const { icon, @@ -52,8 +56,8 @@ export default class Banner extends React.PureComponent { children, status, onDismiss, + context: {withinContentContainer}, } = this.props; - const {withinContentContainer} = this.context; let color: IconProps['color']; let defaultIcon: IconProps['source']; @@ -82,7 +86,6 @@ export default class Banner extends React.PureComponent { color = 'inkLighter'; defaultIcon = fallbackIcon; } - const className = classNames( styles.Banner, status && styles[variationName('status', status)], @@ -198,3 +201,8 @@ function secondaryActionFrom(action: Action) { ); } + +export default compose( + withContext(Consumer), + withAppProvider(), +)(Banner); diff --git a/src/components/Banner/tests/Banner.test.tsx b/src/components/Banner/tests/Banner.test.tsx index 7e6c9a19e71..f2516cd3a3b 100644 --- a/src/components/Banner/tests/Banner.test.tsx +++ b/src/components/Banner/tests/Banner.test.tsx @@ -8,6 +8,7 @@ import fallbackIcon from '../icons/flag.svg'; import warningIcon from '../icons/circle-alert.svg'; import criticalIcon from '../icons/circle-barred.svg'; import infoIcon from '../icons/circle-information.svg'; +import {Provider} from '../../WithinContentContext'; describe('', () => { it('renders a title', () => { @@ -95,14 +96,15 @@ describe('', () => { }; const bannerWithContentContext = mountWithAppProvider( - - Some content - , - {context: mockContext}, + + + Some content + + , ); it('renders a slim button with contentContext', () => { diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx index b76f5c2d4d4..a000c8360c4 100644 --- a/src/components/Card/Card.tsx +++ b/src/components/Card/Card.tsx @@ -1,9 +1,11 @@ import * as React from 'react'; import {classNames} from '@shopify/react-utilities/styles'; +import {autobind} from '@shopify/javascript-utilities/decorators'; -import {Action, DisableableAction, contentContextTypes} from '../../types'; +import {Action, DisableableAction} from '../../types'; import {buttonFrom} from '../Button'; import ButtonGroup from '../ButtonGroup'; +import {Provider, WithinContentContext} from '../WithinContentContext'; import {Header, Section} from './components'; import * as styles from './Card.scss'; @@ -28,13 +30,6 @@ export interface Props { export default class Card extends React.PureComponent { static Section = Section; static Header = Header; - static childContextTypes = contentContextTypes; - - getChildContext() { - return { - withinContentContainer: true, - }; - } render() { const { @@ -74,11 +69,20 @@ export default class Card extends React.PureComponent { ) : null; return ( -
- {headerMarkup} - {content} - {footerMarkup} -
+ +
+ {headerMarkup} + {content} + {footerMarkup} +
+
); } + + @autobind + get getContext(): WithinContentContext { + return { + withinContentContainer: true, + }; + } } diff --git a/src/components/Card/tests/Card.test.tsx b/src/components/Card/tests/Card.test.tsx index af83c2ba332..9c4fe2c9cad 100644 --- a/src/components/Card/tests/Card.test.tsx +++ b/src/components/Card/tests/Card.test.tsx @@ -1,25 +1,28 @@ import * as React from 'react'; import {mountWithAppProvider} from 'test-utilities'; import {Card, Badge} from 'components'; -import {contentContextTypes} from '../../../types'; + +import {Consumer, WithinContentContext} from '../../WithinContentContext'; describe('', () => { - it('has a child with contentContext', () => { - const Child: React.SFC<{}> = (_props, context) => - context.withinContentContainer ?
: null; - Child.contextTypes = contentContextTypes; + it('has a child with prop withinContentContainer set to true', () => { + function TestComponent(_: WithinContentContext) { + return null; + } - const containedChild = mountWithAppProvider( + const component = mountWithAppProvider( - + + {(props) => { + return ; + }} + , ); - const div = containedChild - .find(Child) - .find('div') - .first(); - expect(div.exists()).toBe(true); + expect(component.find(TestComponent).prop('withinContentContainer')).toBe( + true, + ); }); it('has a header tag when the title is a string', () => { diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 15d4e3e8c5a..662f01bcf1c 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -8,8 +8,8 @@ import {focusFirstFocusableNode} from '@shopify/javascript-utilities/focus'; import {createUniqueIDFactory} from '@shopify/javascript-utilities/other'; import {wrapWithComponent} from '@shopify/react-utilities'; import {Modal as AppBridgeModal} from '@shopify/app-bridge/actions'; +import {Provider, WithinContentContext} from '../WithinContentContext'; -import {contentContextTypes} from '../../types'; import {transformActions} from '../../utilities/app-bridge-transformers'; import {withAppProvider, WithAppProviderProps} from '../AppProvider'; @@ -91,8 +91,6 @@ const APP_BRIDGE_PROPS: (keyof Props)[] = [ ]; export class Modal extends React.Component { - static childContextTypes = contentContextTypes; - static Dialog = Dialog; static Section = Section; focusReturnPointNode: HTMLElement; @@ -107,12 +105,6 @@ export class Modal extends React.Component { | AppBridgeModal.ModalIframe | undefined; - getChildContext() { - return { - withinContentContainer: true, - }; - } - componentDidMount() { if (this.props.polaris.appBridge == null) { return; @@ -290,12 +282,14 @@ export class Modal extends React.Component { const animated = !instant; return ( - - - {dialog} - - {backdrop} - + + + + {dialog} + + {backdrop} + + ); } @@ -370,6 +364,13 @@ export class Modal extends React.Component { }, }; } + + @autobind + get getContext(): WithinContentContext { + return { + withinContentContainer: true, + }; + } } function isIframeModal( diff --git a/src/components/Modal/tests/Modal.test.tsx b/src/components/Modal/tests/Modal.test.tsx index 3bb597fdabc..7430fe9a4c2 100644 --- a/src/components/Modal/tests/Modal.test.tsx +++ b/src/components/Modal/tests/Modal.test.tsx @@ -4,10 +4,11 @@ import {noop} from '@shopify/javascript-utilities/other'; import {animationFrame} from '@shopify/jest-dom-mocks'; import {findByTestID, trigger, mountWithAppProvider} from 'test-utilities'; import {Badge, Spinner, Portal, Scrollable} from 'components'; -import {contentContextTypes} from '../../../types'; import {Footer, Dialog} from '../components'; import Modal from '../Modal'; +import {Consumer, WithinContentContext} from '../../WithinContentContext'; + jest.mock('../../../utilities/app-bridge-transformers', () => ({ ...require.requireActual('../../../utilities/app-bridge-transformers'), transformActions: jest.fn((...args) => args), @@ -23,21 +24,23 @@ describe('', () => { }); it('has a child with contentContext', () => { - const Child: React.SFC<{}> = (_props, context) => - context.withinContentContainer ?
: null; - Child.contextTypes = contentContextTypes; - - const containedChild = mountWithAppProvider( - - + function TestComponent(_: WithinContentContext) { + return null; + } + + const component = mountWithAppProvider( + + + {(props) => { + return ; + }} + , ); - const div = containedChild - .find(Child) - .find('div') - .first(); - expect(div.exists()).toBe(true); + expect(component.find(TestComponent).prop('withinContentContainer')).toBe( + true, + ); }); describe('src', () => { diff --git a/src/components/WithinContentContext/WithinContentContext.tsx b/src/components/WithinContentContext/WithinContentContext.tsx new file mode 100644 index 00000000000..cb04b3a414f --- /dev/null +++ b/src/components/WithinContentContext/WithinContentContext.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; + +export interface WithinContentContext { + withinContentContainer: boolean; +} + +const {Provider, Consumer} = React.createContext({ + withinContentContainer: false, +}); + +export {Provider, Consumer}; diff --git a/src/components/WithinContentContext/index.ts b/src/components/WithinContentContext/index.ts new file mode 100644 index 00000000000..ab8ed75bb31 --- /dev/null +++ b/src/components/WithinContentContext/index.ts @@ -0,0 +1 @@ +export {Provider, Consumer, WithinContentContext} from './WithinContentContext'; diff --git a/src/types.ts b/src/types.ts index 01555928621..7ac6f38f335 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,3 @@ -import * as PropTypes from 'prop-types'; -import {ValidationMap} from 'react'; // eslint-disable-next-line shopify/strict-component-boundaries import {Props as IconProps} from './components/Icon'; @@ -235,10 +233,6 @@ export enum Key { SingleQuote = 222, } -export const contentContextTypes: ValidationMap = { - withinContentContainer: PropTypes.bool, -}; - export interface WithContextTypes { context: IJ; }