Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions UNRELEASED.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
18 changes: 13 additions & 5 deletions src/components/Banner/Banner.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -40,9 +44,9 @@ export interface Props {
onDismiss?(): void;
}

export default class Banner extends React.PureComponent<Props, never> {
static contextTypes = contentContextTypes;
export type CombinedProps = Props & WithContextTypes<WithinContentContext>;

export class Banner extends React.PureComponent<CombinedProps, never> {
render() {
const {
icon,
Expand All @@ -52,8 +56,8 @@ export default class Banner extends React.PureComponent<Props, never> {
children,
status,
onDismiss,
context: {withinContentContainer},
} = this.props;
const {withinContentContainer} = this.context;

let color: IconProps['color'];
let defaultIcon: IconProps['source'];
Expand Down Expand Up @@ -82,7 +86,6 @@ export default class Banner extends React.PureComponent<Props, never> {
color = 'inkLighter';
defaultIcon = fallbackIcon;
}

const className = classNames(
styles.Banner,
status && styles[variationName('status', status)],
Expand Down Expand Up @@ -198,3 +201,8 @@ function secondaryActionFrom(action: Action) {
</button>
);
}

export default compose<Props>(
withContext<Props, WithAppProviderProps, WithinContentContext>(Consumer),
withAppProvider(),
)(Banner);
18 changes: 10 additions & 8 deletions src/components/Banner/tests/Banner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('<Banner />', () => {
it('renders a title', () => {
Expand Down Expand Up @@ -95,14 +96,15 @@ describe('<Banner />', () => {
};

const bannerWithContentContext = mountWithAppProvider(
<Banner
action={{
content: 'Primary action',
}}
>
Some content
</Banner>,
{context: mockContext},
<Provider value={mockContext}>
<Banner
action={{
content: 'Primary action',
}}
>
Some content
</Banner>
</Provider>,
);

it('renders a slim button with contentContext', () => {
Expand Down
30 changes: 17 additions & 13 deletions src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -28,13 +30,6 @@ export interface Props {
export default class Card extends React.PureComponent<Props, never> {
static Section = Section;
static Header = Header;
static childContextTypes = contentContextTypes;

getChildContext() {
return {
withinContentContainer: true,
};
}

render() {
const {
Expand Down Expand Up @@ -74,11 +69,20 @@ export default class Card extends React.PureComponent<Props, never> {
) : null;

return (
<div className={className}>
{headerMarkup}
{content}
{footerMarkup}
</div>
<Provider value={this.getContext}>
<div className={className}>
{headerMarkup}
{content}
{footerMarkup}
</div>
</Provider>
);
}

@autobind
get getContext(): WithinContentContext {
return {
withinContentContainer: true,
};
}
}
27 changes: 15 additions & 12 deletions src/components/Card/tests/Card.test.tsx
Original file line number Diff line number Diff line change
@@ -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('<Card />', () => {
it('has a child with contentContext', () => {
const Child: React.SFC<{}> = (_props, context) =>
context.withinContentContainer ? <div /> : 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(
<Card>
<Child />
<Consumer>
{(props) => {
return <TestComponent {...props} />;
}}
</Consumer>
</Card>,
);

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', () => {
Expand Down
31 changes: 16 additions & 15 deletions src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -91,8 +91,6 @@ const APP_BRIDGE_PROPS: (keyof Props)[] = [
];

export class Modal extends React.Component<CombinedProps, State> {
static childContextTypes = contentContextTypes;

static Dialog = Dialog;
static Section = Section;
focusReturnPointNode: HTMLElement;
Expand All @@ -107,12 +105,6 @@ export class Modal extends React.Component<CombinedProps, State> {
| AppBridgeModal.ModalIframe
| undefined;

getChildContext() {
return {
withinContentContainer: true,
};
}

componentDidMount() {
if (this.props.polaris.appBridge == null) {
return;
Expand Down Expand Up @@ -290,12 +282,14 @@ export class Modal extends React.Component<CombinedProps, State> {
const animated = !instant;

return (
<Portal idPrefix="modal">
<TransitionGroup appear={animated} enter={animated} exit={animated}>
{dialog}
</TransitionGroup>
{backdrop}
</Portal>
<Provider value={this.getContext}>
<Portal idPrefix="modal">
<TransitionGroup appear={animated} enter={animated} exit={animated}>
{dialog}
</TransitionGroup>
{backdrop}
</Portal>
</Provider>
);
}

Expand Down Expand Up @@ -370,6 +364,13 @@ export class Modal extends React.Component<CombinedProps, State> {
},
};
}

@autobind
get getContext(): WithinContentContext {
return {
withinContentContainer: true,
};
}
}

function isIframeModal(
Expand Down
29 changes: 16 additions & 13 deletions src/components/Modal/tests/Modal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -23,21 +24,23 @@ describe('<Modal>', () => {
});

it('has a child with contentContext', () => {
const Child: React.SFC<{}> = (_props, context) =>
context.withinContentContainer ? <div /> : null;
Child.contextTypes = contentContextTypes;

const containedChild = mountWithAppProvider(
<Modal open onClose={jest.fn()}>
<Child />
function TestComponent(_: WithinContentContext) {
return null;
}

const component = mountWithAppProvider(
<Modal onClose={jest.fn()} open>
<Consumer>
{(props) => {
return <TestComponent {...props} />;
}}
</Consumer>
</Modal>,
);

const div = containedChild
.find(Child)
.find('div')
.first();
expect(div.exists()).toBe(true);
expect(component.find(TestComponent).prop('withinContentContainer')).toBe(
true,
);
});

describe('src', () => {
Expand Down
11 changes: 11 additions & 0 deletions src/components/WithinContentContext/WithinContentContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as React from 'react';

export interface WithinContentContext {
withinContentContainer: boolean;
}

const {Provider, Consumer} = React.createContext<WithinContentContext>({
withinContentContainer: false,
});

export {Provider, Consumer};
1 change: 1 addition & 0 deletions src/components/WithinContentContext/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {Provider, Consumer, WithinContentContext} from './WithinContentContext';
6 changes: 0 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -235,10 +233,6 @@ export enum Key {
SingleQuote = 222,
}

export const contentContextTypes: ValidationMap<any> = {
withinContentContainer: PropTypes.bool,
};

export interface WithContextTypes<IJ> {
context: IJ;
}