Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AppProvider] Update context #1424

Merged
merged 5 commits into from
May 15, 2019
Merged

Conversation

AndrewMusgrave
Copy link
Member

@AndrewMusgrave AndrewMusgrave commented May 7, 2019

WHY are these changes introduced?

Sorry for the large pr, a lot of these changes needed to happen at the same time 😅 I tried to keep comments to a minimum since a lot of the stuff repeats.

There was previously an issue with our context types, this required a lot of changes (app bridge, theme, and stickyManager)

A lot of tests needed to change with our new utils. You'll no longer be able to use api's like setState and prop on your mounted component which will promote stronger more valuable tests. Example below

// Bad
const component = mount(<C color="red" />)
expect(c.prop('color')).toBe("red")

// Good
const component = mount(<C color="red" />)
expect(c.find(X).prop('color')).toBe("red")

We no longer have namespaces on Polaris and frame context since we don't have to worry about collisions. This is because we're receiving context from a consumer.

WHAT is this pull request doing?

  • Moving AppProvider and associated components to the new context api
  • Removing polaris, frame, theme provider namespace on contexts
  • Updating context utils
  • Delete withSticky and moving some logic into withAppProvider
  • Fix types / tests
  • Create/update testing utils to work well with the new context

🎩

A lot of the changes are related to tests and types

  • the currently failing test is related to tsconfig change. (I wonder why it wasn't failing locally? 🤔)

A lovely sticky playground from Dan L.

Copy-paste this code in playground/Playground.tsx:
/* eslint-disable */

import * as React from 'react';
import {
  Page,
  ResourceList,
  Card,
  FilterType,
  AppProvider,
  Layout,
  Sticky,
  Modal,
  Button,
} from '@shopify/polaris';
import {autobind} from '@shopify/javascript-utilities/decorators';

interface State {
  searchValue?: string;
  filterSearchFocused: boolean;
  selectedItems: string[] | 'All';
  sortValue?: string;
  openModal: boolean;
}

export default class Playground extends React.Component<never, State> {
  state: State = {
    selectedItems: [],
    filterSearchFocused: false,
    openModal: false,
  };

  render() {
    const {searchValue, openModal} = this.state;

    const resourceName = {
      singular: 'Product',
      plural: 'Products',
    };

    return (
      <AppProvider>
        <Page title="Playground">
          <Layout>
            <Layout.Section>
              <Card>
                <Button onClick={() => this.setState({openModal: true})}>
                  Open Modal
                </Button>
                <ResourceList
                  items={items}
                  renderItem={handleRenderItem}
                  hasMoreItems
                  filterControl={
                    <ResourceList.FilterControl
                      searchValue={searchValue}
                      onSearchChange={this.handleSearchChange}
                      additionalAction={{
                        content: 'Save',
                      }}
                    />
                  }
                  selectedItems={this.state.selectedItems}
                  onSelectionChange={this.handleSelectionChange}
                  promotedBulkActions={[
                    {
                      content: 'Really long text on button 1',
                      onAction: this.bulkActionOne,
                    },
                    {
                      content: 'long text button 2',
                      disabled: true,
                      url: 'http://www.google.com',
                    },
                  ]}
                  bulkActions={[
                    {
                      content: 'button 3',
                      onAction: this.bulkActionThree,
                    },
                    {
                      content: 'button 4',
                      onAction: this.bulkActionFour,
                    },
                    {
                      content: 'button 5',
                      onAction: this.bulkActionFive,
                      disabled: true,
                    },
                  ]}
                  sortValue={this.state.sortValue}
                  sortOptions={mockSortOptions}
                  onSortChange={this.handleSortChange}
                />
                <Modal
                  title="Resource List in card"
                  open={openModal}
                  onClose={() => this.setState({openModal: false})}
                >
                  <ResourceList
                    resourceName={resourceName}
                    items={items}
                    renderItem={handleRenderItem}
                    hasMoreItems
                    filterControl={
                      <ResourceList.FilterControl
                        searchValue={searchValue}
                        onSearchChange={this.handleSearchChange}
                        additionalAction={{
                          content: 'Save',
                        }}
                        filters={mockFilters}
                      />
                    }
                    onSelectionChange={this.handleSelectionChange}
                    sortValue={this.state.sortValue}
                    sortOptions={mockSortOptions}
                    onSortChange={this.handleSortChange}
                  />
                </Modal>
              </Card>
            </Layout.Section>
            <Layout.Section secondary>
              <Sticky offset disableWhenStacked>
                <div style={{paddingBottom: '20px'}}>
                  <Card title="Tags" sectioned>
                    <p>Add tags to your order.</p>
                  </Card>
                </div>
              </Sticky>
              <Card title="Tags" sectioned>
                <p>Add tags to your order.</p>
              </Card>
              <Card title="Tags" sectioned>
                <p>Add tags to your order.</p>
              </Card>
            </Layout.Section>
          </Layout>
        </Page>
      </AppProvider>
    );
  }

  @autobind
  private handleSelectionChange(selectedItems: string[]) {
    this.setState({selectedItems});
  }

  @autobind
  private handleSearchChange(searchValue: string) {
    this.setState({searchValue});
  }

  @autobind
  private handleSortChange(sortValue: string) {
    this.setState({sortValue});
  }

  @autobind
  private bulkActionOne() {
    console.log('Clicked on bulk action one.');
  }

  @autobind
  private bulkActionThree() {
    console.log('Clicked on bulk action three.');
  }

  @autobind
  private bulkActionFour() {
    console.log('Clicked on bulk action four.');
  }

  @autobind
  private bulkActionFive() {
    console.log('Clicked on bulk action five.');
  }
}

function handleRenderItem(item: any, id: any) {
  return (
    <ResourceList.Item
      id={id}
      url={item.url}
      shortcutActions={item.actions}
      accessibilityLabel={`View details for ${item.title}`}
    >
      <div>Item {id}</div>
      <div>{item.title}</div>
    </ResourceList.Item>
  );
}

const items: any[] = [
  {
    onClick: true,
    url: 'https://www.google.com',
    actions: [{content: 'View listing', url: 'http://www.facebook.com'}],
    title: 'Has url, onClick, and actions',
  },
  {
    onClick: false,
    url: 'https://www.google.com',
    actions: [{content: 'View listing', url: 'http://www.facebook.com'}],
    title: 'Has url, and actions',
  },
  {
    onClick: true,
    actions: [{content: 'View listing', url: 'http://www.facebook.com'}],
    title: 'Has onClick, and actions',
  },
  {
    onClick: false,
    url: 'https://www.google.com',
    title: 'Has url',
  },
  {
    onClick: true,
    title: 'Has onClick',
  },
  {
    onClick: true,
    url: 'https://www.google.com',
    actions: [{content: 'View listing', url: 'http://www.facebook.com'}],
    title: 'Has url, onClick, and actions',
  },
  {
    onClick: false,
    url: 'https://www.google.com',
    actions: [{content: 'View listing', url: 'http://www.facebook.com'}],
    title: 'Has url, and actions',
  },
  {
    onClick: true,
    actions: [{content: 'View listing', url: 'http://www.facebook.com'}],
    title: 'Has onClick, and actions',
  },
  {
    onClick: false,
    url: 'https://www.google.com',
    title: 'Has url',
  },
  {
    onClick: true,
    title: 'Has onClick',
  },
  {
    onClick: true,
    url: 'https://www.google.com',
    actions: [{content: 'View listing', url: 'http://www.facebook.com'}],
    title: 'Has url, onClick, and actions',
  },
  {
    onClick: false,
    url: 'https://www.google.com',
    actions: [{content: 'View listing', url: 'http://www.facebook.com'}],
    title: 'Has url, and actions',
  },
  {
    onClick: true,
    actions: [{content: 'View listing', url: 'http://www.facebook.com'}],
    title: 'Has onClick, and actions',
  },
  {
    onClick: false,
    url: 'https://www.google.com',
    title: 'Has url',
  },
  {
    onClick: true,
    title: 'Has onClick',
  },
];

const mockFilters: any[] = [
  {
    key: 'filterKey1',
    label: 'Product type',
    operatorText: 'is',
    type: FilterType.Select,
    options: [
      'Bundle',
      {
        value: 'electronic_value',
        label: 'Electronic',
        disabled: true,
      },
      {
        value: 'beauty_value',
        label: 'Beauty',
      },
    ],
  },
  {
    key: 'filterKey2',
    label: 'Tagged with',
    type: FilterType.TextField,
  },
];

const mockSortOptions = [
  'Product title (A-Z)',
  {
    value: 'PRODUCT_TITLE_DESC',
    label: 'Product title (Z-A)',
  },
  {
    value: 'EXTRA',
    label: 'Disabled Option',
    disabled: true,
  },
];

/* eslint-enable */

Notes

  • this has a lot of changes so I'll create a changelog entry in a follow-up pr

@BPScott BPScott temporarily deployed to polaris-react-pr-1424 May 7, 2019 21:03 Inactive

interface State {
polaris: Context;
context: Context;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing this on the local state to be clearer about what it is

@@ -31,18 +29,9 @@ export interface AppProviderProps {
}

export interface Context {
polaris: {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the polaris namespace now since we consume it from a render prop rather than context types. We won't have to worry about collisions now

@@ -14,7 +14,7 @@ export interface StickyItem {
/** Placeholder element */
placeHolderNode: HTMLElement;
/** Element outlining the fixed position boundaries */
boundingElement: HTMLElement | null;
boundingElement?: HTMLElement | null;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a type issue and boundElement could be undefined

@@ -5,7 +5,7 @@ export {
PrimitiveReplacementDictionary,
ComplexReplacementDictionary,
} from './Intl';
export {default as withSticky, StickyManager} from './withSticky';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withSticky was deleted

@@ -13,7 +14,11 @@ describe('withAppProvider', () => {
const WrappedComponent = withAppProvider<any>()(Child);

const fn = () => {
mount(<WrappedComponent />);
mount(
<AppProviderContext.Provider value={{} as any}>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapping the component in a provider so withAppProvider can consume it

appBridge: ClientApplication<{}>;
subscribe(callback: () => void): void;
unsubscribe(callback: () => void): void;
theme?: ThemeContext;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a type issue and theme / appBridge could always be undefined

? merge(WrappedComponent.contextTypes, polarisAppProviderContextTypes)
: polarisAppProviderContextTypes;
export interface Options {
inScrollable?: boolean;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withScrollable | inScrollable? Any opinions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withinScrollable?

@@ -1,55 +0,0 @@
import * as React from 'react';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A smaller more compact version was included in withAppProvider so we can cut down on individual HoC

@@ -388,21 +388,6 @@ describe('<ComboBox/>', () => {
});

describe('keypress events', () => {
it('selects the first option when the down arrow is pressed', () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test doesn't do what's expected, the test below covers it.

@@ -430,7 +415,7 @@ describe('<ComboBox/>', () => {
);

comboBox.simulate('focus');
expect(comboBox.state('popoverActive')).toBe(true);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should never expect on state (component internals)

@@ -29,11 +29,4 @@ describe('<DisplayText />', () => {
);
expect(displayText.find('p')).toHaveLength(1);
});

it('renders the specified size', () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't expect on the base components props since it doesn't provide value. This test should be handled with visual regression like percy

@@ -30,32 +30,6 @@ describe('<Layout />', () => {
expect(section.find(MyComponent).exists()).toBe(true);
});

describe('<Layout.Section />', () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't expect on the base components props since it doesn't provide value. These tests should be handled by percy

@@ -46,13 +46,16 @@ export class Page extends React.PureComponent<ComposedProps, never> {
private titlebar: AppBridgeTitleBar.TitleBar | undefined;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the correct appBridge type from context

@@ -36,7 +67,10 @@ describe('<PositionedOverlay />', () => {
const positionedOverlay = mountWithAppProvider(
<PositionedOverlay {...mockProps} preferredAlignment="left" />,
);
expect(positionedOverlay.prop('preferredAlignment')).toBe('left');

expect(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed this test and the test below but we should be relying on visual regression for these

@@ -30,8 +30,14 @@ export default class Sticky extends React.Component<Props, State> {
private stickyNode: HTMLElement | null = null;

componentDidMount() {
const {stickyManager} = this.context.polaris;
const {boundingElement, offset, disableWhenStacked} = this.props;
const {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The types were originally incorrect and weren't compatible with registerStickyItem so I added defaults and an early return @dleroux

@@ -27,13 +24,7 @@ export function findByTestID(root: ReactWrapper<any, any>, id: string) {
return wrapper.length > 0 && wrapper.prop('testID') === id;
}

const rootResult = root.findWhere(hasTestID).first();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated this to be compatible with our new testing util

@@ -82,98 +73,85 @@ export function trigger(wrapper: AnyWrapper, keypath: string, ...args: any[]) {
return returnValue;
}

/**
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed a few functions below that are no longer in use

}) as any;
interface MountWithAppProviderOptions {
context?: {
frame?: DeepPartial<FrameContext>;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DeepPartial is used so we can only merge one branch of an object. For example, updating appBridge from undefined to X but leaving the rest of polaris intact.

): AppContextReactWrapper<P, any> {
const {context: ctx = {}} = options;

const polarisDefault = createPolarisContext();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a default and merging it with provided if it exists

// We need this wrapping div because Enzyme expects a single node, not an array.
nodeJSX = <div>{nodeJSX}</div>;
}
const context: AppContext = {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating our context to pass too AppContextReactWrapper

options?: ShallowRendererProps,
): ShallowWrapper<P, any> {
return shallow(node, mergeAppProviderOptions(options)).dive(options);
this.app = app;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can now access context, for example

const x = mountWithAppProvider(<Component />)
console.log(x.app.polaris.intl.translate(...)

// Unfortunately, this is how we have to type this at the moment.
// There is currently a proposal to support variadic kinds.
// https://github.com/Microsoft/TypeScript/issues/5453
function merge<TSource1, TSource2>(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we needed a stronger type for merge

tsconfig.json Outdated
@@ -8,7 +8,7 @@
"resolveJsonModule": true,
"declaration": true,
"declarationDir": "types",
"jsx": "react-native",
"jsx": "react",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this change I'm getting ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor in tests. If we don't want to make this change could we create a tsconfig for testing or provide some sort of transform to sewing-kit?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yo! Back in the day we had a thing in our sewing-kit config that force changed the jsx compilation in jest.

It got took out because it didn't seem to be needed. I don't know what in your update stuff has made its return required but it's easy to bring back

296dc34#diff-1afd2752a047671ab9dea43845beb75a

@BPScott BPScott temporarily deployed to polaris-react-pr-1424 May 7, 2019 21:43 Inactive
@BPScott BPScott temporarily deployed to polaris-react-pr-1424 May 7, 2019 21:51 Inactive
return mountWithAppProvider(
<ThemeProvider value={context}>{node}</ThemeProvider>,
);
function mergeThemeProviderContext(providedThemeContext: ThemeProviderContext) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be overkill, it just saves writing {context: themeProvider: {}}} every time

@@ -2,13 +2,10 @@ import * as React from 'react';
import {noop} from '@shopify/javascript-utilities/other';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using mountWithAppProvider now in this file

} from '../components/AppProvider';
// eslint-disable-next-line shopify/strict-component-boundaries
import {Provider as FrameProvider, FrameContext} from '../components/Frame';
// eslint-disable-next-line shopify/strict-component-boundaries
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we export the return value from React.createContext we should be able to import this from '../components'

@AndrewMusgrave
Copy link
Member Author

This should be ready for review 🤞

@AndrewMusgrave AndrewMusgrave added the 🤖Skip Changelog Causes CI to ignore changelog update check. label May 8, 2019
@AndrewMusgrave AndrewMusgrave changed the title [WIP][AppProvider] Update context [AppProvider] Update context May 8, 2019
@AndrewMusgrave AndrewMusgrave marked this pull request as ready for review May 8, 2019 19:10
@danrosenthal
Copy link
Contributor

danrosenthal commented May 8, 2019

Nice one! 🙏

@BPScott BPScott temporarily deployed to polaris-react-pr-1424 May 8, 2019 20:21 Inactive
Copy link
Contributor

@danrosenthal danrosenthal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nicely annotated PR. Thanks for putting in the effort to explain.

I've mostly asked a bunch of questions here to help improve mine and others' understanding of what's going on.

I feel comfortable with merging this once CI is 🍏, especially since it is merging into a feature branch.

We'll want to think about a sane strategy for tophatting the v4 branch with all these changes in place. That probably looks like doing some pre-releasing and testing directly in web.

src/components/AppProvider/Context.tsx Outdated Show resolved Hide resolved
? merge(WrappedComponent.contextTypes, polarisAppProviderContextTypes)
: polarisAppProviderContextTypes;
export interface Options {
inScrollable?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withinScrollable?

src/components/ScrollLock/ScrollLock.tsx Show resolved Hide resolved
@@ -10,13 +10,18 @@ describe('ScrollLock', () => {
showScrollLock: true,
};

setScollLockFalse = () => {
this.setState({showScrollLock: false});
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we want to avoid setting state in tests?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing state within a component through user interactions is ok, however, we want to avoid explicitly setting state with the enzyme API. However, in this case, we may be able to simplify the test by unmounting the wrapper, I'll give that a try.

src/components/Modal/Modal.tsx Show resolved Hide resolved
src/components/Tabs/tests/Tabs.test.tsx Outdated Show resolved Hide resolved
src/components/TopBar/tests/TopBar.test.tsx Show resolved Hide resolved
@BPScott BPScott temporarily deployed to polaris-react-pr-1424 May 9, 2019 22:07 Inactive
Add types to test until

Remove polaris context namespace

Remove frame namespace

Export createContext result

Small quaility change

Rename context wrapper

Update themeprovider namespace

Add type

Add theme context to util

Update type name for app provider context

Update test utility and revert tsconfig changee
@BPScott BPScott temporarily deployed to polaris-react-pr-1424 May 10, 2019 22:34 Inactive
Copy link
Member

@BPScott BPScott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good.

Nothing jumps out at me, though I'm not very familiar with the AppProvider

Convert ScrollTo

Remove dropzone context from index

Convert navigation

Convert combobox

Convert withincontext

Update frame context

Update resourcelist context

Update themeprovider context

Update appprovider context

Touchups

changelog

Update appprovider context type

Rename file
@BPScott BPScott temporarily deployed to polaris-react-pr-1424 May 14, 2019 14:43 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🤖Skip Changelog Causes CI to ignore changelog update check.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants