Polaris-react: [Sticky] - Issue with initial rendering (using SSR) and sticky position

Created on 1 Feb 2019  ·  17Comments  ·  Source: Shopify/polaris-react

Issue summary

The Sticky component seems to be impacting the initial rendering of the Automatic Discount page. The right column of the page is not rendered until you scroll. Then the sticky position is a bit off. I can't reproduce in production but I can on a local branch and master. Removing the Sticky component fixed the issue.

feb-01-2019 15-42-41

Expected behavior

The right column should be properly rendered and the sticky position should not be off.

Actual behavior


The right column is not properly rendered until you scroll in the page and the sticky position is off.

screen shot 2019-02-01 at 3 17 33 pm

Steps to reproduce the problem

  1. Load the React Automatic Discount page on your local env.

Reduced test case

The best way to get your bug fixed is to provide a reduced test case. This CodeSandbox template is a great starting point.

Specifications

  • Are you using the React components? (Y/N): Y
  • Polaris version number: 3.5.0
  • Browser: Chrome 71.0.3578.98
  • Device: Mac OS X
  • Operating System:
Icebox 🐛Bug

All 17 comments

Wondering if it's related to SSR in web?

At first I was thinking this was related to the latest frame changes but I just tested this playground and it's fine:

import * as React from 'react';
import {
  Page,
  AppProvider,
  Toast,
  Navigation,
  ContextualSaveBar,
  TopBar,
  Card,
  ActionList,
  Loading,
  Layout,
  SkeletonBodyText,
  SkeletonDisplayText,
  SkeletonPage,
  FormLayout,
  TextField,
  TextContainer,
  Stack,
  Frame,
  ResourceList,
  FilterType,
  Sticky,
  Modal,
  Button,
} from '@shopify/polaris';
import {autobind} from '@shopify/javascript-utilities/decorators';

import {
  profile,
  logOut,
  home,
  orders,
  products,
  onlineStore,
  circlePlusOutline,
} from '../src/icons';

interface State {
  showToast: boolean;
  isLoading: boolean;
  isDirty: boolean;
  searchActive: boolean;
  searchText: string;
  userMenuOpen: boolean;
  showMobileNavigation: boolean;
  modalActive: boolean;
  nameFieldValue: string;
  emailFieldValue: string;
  storeName: string;
  searchValue?: string;
  filterSearchFocused: boolean;
  selectedItems: string[] | 'All';
  sortValue?: string;
  openModal: boolean;
}

export default class Playground extends React.Component<never, State> {
  defaultState = {
    emailFieldValue: '[email protected]',
    nameFieldValue: 'Ochoa Crafts',
  };

  state: State = {
    showToast: false,
    isLoading: false,
    isDirty: false,
    searchActive: false,
    searchText: '',
    userMenuOpen: false,
    showMobileNavigation: false,
    modalActive: false,
    nameFieldValue: this.defaultState.nameFieldValue,
    emailFieldValue: this.defaultState.emailFieldValue,
    storeName: this.defaultState.nameFieldValue,
    selectedItems: [],
    filterSearchFocused: false,
    openModal: false,
  };

  render() {
    const {
      showToast,
      isLoading,
      isDirty,
      searchActive,
      searchText,
      userMenuOpen,
      showMobileNavigation,
      nameFieldValue,
      emailFieldValue,
      modalActive,
      storeName,
      searchValue,
      openModal,
    } = this.state;

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

    const toastMarkup = showToast ? (
      <Toast
        onDismiss={this.toggleState('showToast')}
        content="Changes saved"
      />
    ) : null;

    const navigationUserMenuMarkup = (
      <Navigation.UserMenu
        actions={[
          {
            id: '123',
            items: [
              {content: 'Your profile', icon: profile},
              {content: 'Log out', icon: logOut},
            ],
          },
          {
            id: '456',
            items: [
              {content: 'Shopify help center'},
              {content: 'Community forums'},
            ],
          },
        ]}
        name="Ellen Ochoa"
        detail={storeName}
        avatarInitials="EO"
      />
    );

    const contextualSaveBarMarkup = isDirty ? (
      <ContextualSaveBar
        message="Unsaved changes"
        saveAction={{
          onAction: this.handleSave,
        }}
        discardAction={{
          onAction: this.handleDiscard,
        }}
      />
    ) : null;

    const userMenuMarkup = (
      <TopBar.UserMenu
        actions={[
          {
            items: [
              {content: 'Your profile', icon: profile},
              {content: 'Log out', icon: logOut},
            ],
          },
          {
            items: [
              {content: 'Shopify help center'},
              {content: 'Community forums'},
            ],
          },
        ]}
        name="Ellen Ochoa"
        detail={storeName}
        initials="EO"
        open={userMenuOpen}
        onToggle={this.toggleState('userMenuOpen')}
      />
    );

    const searchResultsMarkup = (
      <Card>
        <ActionList
          items={[
            {content: 'Shopify help center'},
            {content: 'Community forums'},
          ]}
        />
      </Card>
    );

    const searchFieldMarkup = (
      <TopBar.SearchField
        onChange={this.handleSearchFieldChange}
        value={searchText}
        placeholder="Search"
      />
    );

    const topBarMarkup = (
      <TopBar
        showNavigationToggle={true}
        userMenu={userMenuMarkup}
        searchResultsVisible={searchActive}
        searchField={searchFieldMarkup}
        searchResults={searchResultsMarkup}
        onSearchResultsDismiss={this.handleSearchResultsDismiss}
        onNavigationToggle={this.toggleState('showMobileNavigation')}
      />
    );

    const navigationMarkup = (
      <Navigation location="/" userMenu={navigationUserMenuMarkup}>
        <Navigation.Section
          items={[
            {
              label: 'Home',
              icon: home,
              onClick: this.toggleState('isLoading'),
            },
            {
              label: 'Orders',
              icon: orders,
              onClick: this.toggleState('isLoading'),
            },
            {
              label: 'Products',
              icon: products,
              onClick: this.toggleState('isLoading'),
            },
          ]}
        />
        <Navigation.Section
          title="Sales channels"
          items={[
            {
              label: 'Online Store',
              icon: onlineStore,
              onClick: this.toggleState('isLoading'),
            },
          ]}
          separator
          action={{
            icon: circlePlusOutline,
            accessibilityLabel: 'Add a sales channel',
            onClick: this.toggleState('modalActive'),
          }}
        />
      </Navigation>
    );

    const loadingMarkup = isLoading ? <Loading /> : null;

    const actualPageMarkup = (
      <Page title="Settings">
        <Layout>
          <Layout.Section>
            <Card sectioned>
              <FormLayout>
                <TextField
                  label="Store name"
                  value={nameFieldValue}
                  onChange={this.handleNameFieldChange}
                />
                <TextField
                  type="email"
                  label="Account email"
                  value={emailFieldValue}
                  onChange={this.handleEmailFieldChange}
                />
              </FormLayout>
            </Card>
            <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="Sticky Card" sectioned>
                  <p>Add tags to your order.</p>
                </Card>
              </div>
            </Sticky>
          </Layout.Section>
        </Layout>
      </Page>
    );

    const loadingPageMarkup = (
      <SkeletonPage>
        <Layout>
          <Layout.Section>
            <Card sectioned>
              <TextContainer>
                <SkeletonDisplayText size="small" />
                <SkeletonBodyText lines={9} />
              </TextContainer>
            </Card>
          </Layout.Section>
        </Layout>
      </SkeletonPage>
    );

    const pageMarkup = isLoading ? loadingPageMarkup : actualPageMarkup;

    const modalMarkup = (
      <Modal
        open={modalActive}
        onClose={this.toggleState('modalActive')}
        title="Add sales channel"
        primaryAction={{
          content: 'Add Instagram',
          onAction: this.toggleState('modalActive'),
        }}
      >
        <Modal.Section>
          <Stack wrap={false}>
            <Stack.Item>
              <img
                style={{
                  minWidth: '4rem',
                  maxWidth: '4rem',
                  height: '4rem',
                }}
                src="https://cdn.shopify.com/s/files/applications/e47f922e057de8e341967bbd31444fdb.png?1521134959"
                alt="Instagram logo"
              />
            </Stack.Item>
            <Stack.Item fill>
              <p>
                Sell your products directly on Instagram by tagging products in
                your posts, to create a seamless shopping experience for your
                customers.
              </p>
            </Stack.Item>
          </Stack>
        </Modal.Section>
      </Modal>
    );

    const theme = {
      colors: {
        topBar: {
          background: '#108043',
        },
      },
      logo: {
        width: 104,
        topBarSource:
          'https://cdn.shopify.com/shopify-marketing_assets/static/shopify-full-color-white.svg',
        contextualSaveBarSource:
          'https://cdn.shopify.com/shopify-marketing_assets/static/shopify-full-color-black.svg',
      },
    };

    return (
      <AppProvider theme={theme}>
        <Frame
          topBar={topBarMarkup}
          navigation={navigationMarkup}
          showMobileNavigation={showMobileNavigation}
          onNavigationDismiss={this.toggleState('showMobileNavigation')}
        >
          {contextualSaveBarMarkup}
          {loadingMarkup}
          {pageMarkup}
          {toastMarkup}
          {modalMarkup}
        </Frame>
      </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.');
  }

  toggleState = (key: string) => {
    return () => {
      this.setState((prevState) => ({[key]: !prevState[key]}));
    };
  };

  handleSearchFieldChange = (value: string) => {
    this.setState({searchText: value});
    if (value.length > 0) {
      this.setState({searchActive: true});
    } else {
      this.setState({searchActive: false});
    }
  };

  handleSearchResultsDismiss = () => {
    this.setState(() => {
      return {
        searchActive: false,
        searchText: '',
      };
    });
  };

  handleEmailFieldChange = (emailFieldValue: string) => {
    this.setState({emailFieldValue});
    if (emailFieldValue != '') {
      this.setState({isDirty: true});
    }
  };

  handleNameFieldChange = (nameFieldValue: string) => {
    this.setState({nameFieldValue});
    if (nameFieldValue != '') {
      this.setState({isDirty: true});
    }
  };

  handleSave = () => {
    this.defaultState.nameFieldValue = this.state.nameFieldValue;
    this.defaultState.emailFieldValue = this.state.emailFieldValue;

    this.setState({
      isDirty: false,
      showToast: true,
      storeName: this.defaultState.nameFieldValue,
    });
  };

  handleDiscard = () => {
    this.setState({
      emailFieldValue: this.defaultState.emailFieldValue,
      nameFieldValue: this.defaultState.nameFieldValue,
      isDirty: false,
    });
  };
}

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,
  },
];

Removing SSR seems to have fixed the issue. Wondering why we are only getting the error now?

So we'll need to prevent rendering of Sticky on the server.

The offset is still there even when disabling SSR

screen shot 2019-02-01 at 4 02 39 pm

🤔 This also works in the playground

Also affected by this 😢

Specifically, I'm affected by the bug where the position is wrong, and my sticky container is being overlapped by the TopBar.

Disabling SSR doesn't help either.

This is preventing the new <CollectionProductsSendTo /> from shipping:

gif

@dleroux @maxariss
So we have two issues:

  • Initial rendering is not properly done when rendering on the server
  • Top sticky position is not properly computed anymore (when rendered on the server or not)

I'm not convinced that ssr is the culprit because when we start scrolling the calculations should fix themselves with the exception of the top bar. The tobBarOffset is calculated on ComponentDidMount of the app provider so this should happen on the client only. In Quilt the react-html package adds display: none to the body, so when we query the top bar clientHeight on mount clientHeight will return 0. Not sure why this wasn't an issue before, maybe it was always in prod but we never saw it because internal routes weren't enabled.

In any case, switching display: none to visibility: hidden seems to fix the issue. Not sure what the repercussions would be but this also works to hide the css flicker. @lemonmade I see you recently touched this code. Do you see potential issues form switching display to visibility?

@dleroux @lemonmade Are either of you working on this issue? It's blocking the release of a feature we had planned to bug hunt two days ago

@dleroux I don't see a problem with it, no. If that is the source of this issue, it only affects dev, and might not actually be fixed in dev anyways because of how we inject the styles into the head after the JS loads (I am not positive that it will still break, but in dev it certainly corrects when you do anything that forces the sticky manager to recompute the position). Running the prod build will tell you for sure whether this is a development-only issue or not.

From what I see, production does not seem to be impacted.

I could reproduce the issue on a local branch and I could not reproduce anymore in production once it 🚢 'ed

Other than investigating i have not done additional work. It will need a change in Quilt. I will see if i can make time soon but if someone else has time go for it. @loic-d?

@dleroux Are you suggesting not to render the component on the server?

No. The react-html package in ‘Quilt’ sets the body style to ‘display: none’ while the page loads. We need to change that to ‘visibility: hidden’. Then we need a version bump and update web.

That will solve the top bar height to be registered in the ‘Sticky Manager’. I can’t reproduce your second issue but I have a feeling it’s related as well.

This issue has been inactive for 180 days and labeled with Icebox. It will be closed in 7 days if there is no further activity.

Was this page helpful?
0 / 5 - 0 ratings