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

[ResourceList] Add support for in context empty state #2160

Merged
merged 4 commits into from
Nov 27, 2019

Conversation

chloerice
Copy link
Member

@chloerice chloerice commented Sep 18, 2019

WHY are these changes introduced?

Builds on #1570

Right now our empty states are vastly different from our loading and with-data states. We'd like to make the transition from loading to empty state smoother, as well as make the with-data state more familiar once a feature has been used. For list views, this means having our empty states within the context of the resource list.

WHAT is this pull request doing?

Currently, ResourceList only has an empty state for when there are no results for a filter or search query. This PR adds support for disabling filters and providing markup to render when there are not yet any resources to list. That way the same static content can be rendered in a loading, empty, and with-data state. See the tophatting instructions to view the smooth visual transition between these states using the playground code.

** This is a Foundations Frideations project.

How to 🎩

🖥 Local development instructions
🗒 General tophatting guidelines
📄 Changelog guidelines

View the "Resource list with empty state" example

or

  • git checkout resourcelist-emptystate
  • Copy and paste the playground code below into playground/Playground.tsx
  • yarn dev to run and open Storybook locally
  • Click "Playground" in the left navigation of Storybook and use the page's secondary actions to toggle between loading, empty, and with-data states.
Copy-paste this code in playground/Playground.tsx:
import React, {useState, useCallback} from 'react';
import {
  Page,
  Layout,
  ResourceList,
  ResourceItem,
  EmptyState,
  Card,
  TextStyle,
  Select,
  Filters,
  Thumbnail,
  Tabs,
  SkeletonBodyText,
  Spinner,
} from '../src';

const allItems = [
  {
    id: 341,
    url: 'files/341',
    name: 'Point of sale staff',
    imgSource:
      'https://user-images.githubusercontent.com/29233959/61383074-fc5e6800-a87b-11e9-8e92-487153efd0dc.png',
    fileType: 'png',
  },
  {
    id: 256,
    url: 'files/256',
    name: 'File upload',
    imgSource:
      'https://user-images.githubusercontent.com/18447883/59945230-fa4be980-9434-11e9-9106-a373f0efbe08.png',
    fileType: 'png',
  },
];

const tabs = [
  {
    id: 'all-files',
    content: 'All',
    accessibilityLabel: 'All files',
    panelID: 'all-files-content',
  },
];

export function Playground() {
  const [viewLoadingState, setLoadingState] = useState(false);
  const [viewEmptyState, setEmptyState] = useState(true);
  const [filteredItems, setFilteredItems] = useState([]);
  const [fileType, setFileType] = useState('');
  const [queryValue, setQueryValue] = useState('');

  const toggleLoadingState = useCallback(
    () => setLoadingState((viewLoadingState) => !viewLoadingState),
    [],
  );

  const toggleEmptyState = useCallback(
    () => setEmptyState((viewEmptyState) => !viewEmptyState),
    [],
  );

  const updateFilteredItems = useCallback((items) => {
    setFilteredItems(items);
  }, []);

  const updateFileTypeFilter = useCallback(
    (value) => {
      setFileType(value);
      const itemsToFilter =
        queryValue && filteredItems.length > 0 ? filteredItems : allItems;
      updateFilteredItems(filterByFileType(value, itemsToFilter));
    },
    [filteredItems, queryValue, updateFilteredItems],
  );

  const updateQueryValue = useCallback(
    (value) => {
      setQueryValue(normalizeString(value));
      const itemsToFilter =
        fileType && filteredItems.length > 0 ? filteredItems : allItems;

      updateFilteredItems(
        filterByQueryValue(normalizeString(value), itemsToFilter),
      );
    },
    [fileType, filteredItems, updateFilteredItems],
  );

  const handleRemove = useCallback(() => {
    updateFileTypeFilter('');
  }, [updateFileTypeFilter]);

  const handleQueryClear = useCallback(() => {
    updateQueryValue('');
  }, [updateQueryValue]);

  const handleClearAll = useCallback(() => {
    updateFileTypeFilter('');
    updateQueryValue('');
  }, [updateFileTypeFilter, updateQueryValue]);

  let items = fileType || queryValue ? filteredItems : allItems;

  if (viewEmptyState) {
    items = [];
  }

  const filters = [
    {
      key: 'fileType',
      label: 'File type',
      filter: (
        <Select
          labelHidden
          label="File type"
          value={fileType}
          options={[
            {label: 'JPEG', value: 'jpeg'},
            {label: 'PNG', value: 'png'},
            {label: 'MP4', value: 'mp4'},
          ]}
          onChange={updateFileTypeFilter}
        />
      ),
      shortcut: true,
    },
  ];

  const appliedFilters = fileType
    ? [
        {
          key: 'fileType',
          label: `File type ${fileType.toUpperCase()}`,
          onRemove: handleRemove,
        },
      ]
    : [];

  const filterControl = (
    <Filters
      disabled={viewEmptyState}
      queryValue={queryValue}
      filters={filters}
      appliedFilters={appliedFilters}
      onQueryChange={updateQueryValue}
      onQueryClear={handleQueryClear}
      onClearAll={handleClearAll}
    />
  );

  const emptyStateMarkup = (
    <EmptyState
      heading="Upload a file to get started"
      action={{content: 'Upload files'}}
      image="https://cdn.shopify.com/s/files/1/2376/3301/products/file-upload-empty-state.png"
    >
      <p>
        You can use the Files section to upload images, videos, and other
        documents
      </p>
    </EmptyState>
  );

  const loadingStateMarkup = viewLoadingState ? (
    <Card>
      <SkeletonTabs />

      <div>
        <div style={{padding: '16px', marginTop: '1px'}}>
          <Filters
            disabled={viewEmptyState}
            queryValue={queryValue}
            filters={filters}
            appliedFilters={appliedFilters}
            onQueryChange={updateQueryValue}
            onQueryClear={handleQueryClear}
            onClearAll={handleClearAll}
          />
        </div>
        <div
          style={{
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            minHeight: '160px',
          }}
        >
          <Spinner />
        </div>
      </div>
    </Card>
  ) : null;

  const withDataStateMarkup = !viewLoadingState ? (
    <Card>
      <Tabs selected={0} tabs={tabs}>
        <ResourceList
          showHeader
          hasMoreItems={!viewEmptyState && items.length < allItems.length}
          emptyState={emptyStateMarkup}
          resourceName={{singular: 'file', plural: 'files'}}
          items={!viewEmptyState && items}
          renderItem={renderItem}
          filterControl={filterControl}
        />
      </Tabs>
    </Card>
  ) : null;

  return (
    <Page
      title="Files"
      secondaryActions={[
        {
          content: 'Toggle empty state',
          onAction: toggleEmptyState,
          disabled: viewLoadingState,
        },
        {
          content: 'Toggle loading state',
          onAction: toggleLoadingState,
        },
      ]}
      breadcrumbs={[{content: 'Home', url: '/'}]}
    >
      <Layout>
        <Layout.Section>
          {loadingStateMarkup}
          {withDataStateMarkup}
        </Layout.Section>
      </Layout>
    </Page>
  );
}

function renderItem(item) {
  const {id, url, name, imgSource, altText = '', fileType} = item;
  const media = <Thumbnail alt={altText} source={imgSource} />;
  return (
    <ResourceItem id={id} url={url} media={media}>
      <h3>
        <TextStyle variation="strong">{name}</TextStyle>
      </h3>
      <div>{fileType}</div>
    </ResourceItem>
  );
}

function filterByFileType(fileType, items) {
  return items.filter((item) => item.fileType === fileType);
}

function filterByQueryValue(query, items) {
  return items.filter((item) => {
    return (
      normalizeString(item.name).includes(query) ||
      normalizeString(item.fileType).includes(query) ||
      normalizeString(item.imgSource).includes(query)
    );
  });
}

function normalizeString(string) {
  return string.toLowerCase();
}

function SkeletonTabs() {
  return (
    <div
      style={{
        width: '100%',
        display: 'flex',
        borderBottom: '1px solid #DFE3E8',
        height: '53px',
      }}
    >
      <div
        style={{
          width: '80px',
          padding: '21px 20px',
        }}
      >
        <SkeletonBodyText lines={1} />
      </div>
    </div>
  );
}

🎩 checklist

@chloerice chloerice changed the base branch from master to empty-state-content-context September 18, 2019 15:38
@chloerice chloerice force-pushed the resourcelist-emptystate branch from 8d1392a to 491915e Compare September 18, 2019 15:40
@chloerice chloerice force-pushed the empty-state-content-context branch 4 times, most recently from 0a8e15f to 332db66 Compare September 19, 2019 21:07
@chloerice chloerice force-pushed the empty-state-content-context branch 3 times, most recently from f8d7d7a to bda26c9 Compare October 1, 2019 23:47
@chloerice chloerice force-pushed the resourcelist-emptystate branch 2 times, most recently from e6af687 to 25c002e Compare October 1, 2019 23:58
@github-actions
Copy link
Contributor

github-actions bot commented Oct 2, 2019

💦 Potential splash zone of changes introduced to src/**/*.tsx in this pull request:

No significant changes to src/**/*.tsx were detected.


This comment automatically updates as changes are made to this pull request.
Feedback, troubleshooting: open an issue or reach out on Slack in #polaris-tooling.

@chloerice chloerice force-pushed the resourcelist-emptystate branch 10 times, most recently from 4cd32b8 to 47d7a26 Compare October 2, 2019 02:58
@dpersing
Copy link
Contributor

dpersing commented Oct 2, 2019

@chloerice I think disabling the controls totally makes sense in this context!

The only thing I'm wondering about is a tabindex="-1" on the File type button. It doesn't get focus and is identified as disabled since it is, but the focus ring appears on click. I'm wondering if that tabindex can be removed?

@chloerice
Copy link
Member Author

I'm wondering if that tabindex can be removed?

Definitely 💯 Thanks for catching that!!

@chloerice chloerice force-pushed the resourcelist-emptystate branch from 47d7a26 to a800bd2 Compare October 2, 2019 20:18
@sarahill
Copy link
Contributor

sarahill commented Oct 3, 2019

This looks good to me! Same feedback as Devon on the tab-index. Other than that this looks good 👍

@jessebc
Copy link

jessebc commented Oct 7, 2019

@jessebc What do you think of disabling resource list header content (like the filters) when in an empty state? I've gone ahead and added support for disabling filters.

Agreed that this would be confusing. I think we should either disable them, or remove them completely. Let's start with keeping them disabled and see how that feels when we tophat it.

@chloerice
Copy link
Member Author

chloerice commented Oct 7, 2019

Still need to figure out where that tabindex comes from, will request reviews and move out of draft state once it's been removed 🔥

@chloerice
Copy link
Member Author

@chloerice I think disabling the controls totally makes sense in this context!

The only thing I'm wondering about is a tabindex="-1" on the File type button. It doesn't get focus and is identified as disabled since it is, but the focus ring appears on click. I'm wondering if that tabindex can be removed?

Quick update --- finally figured out that the tabindex="-1" is coming from Popover. Still figuring out how best to only give a tab index to the activator wrapper if the activator is not disabled.

@chloerice chloerice force-pushed the resourcelist-emptystate branch 2 times, most recently from e61ca8c to 800923b Compare November 27, 2019 01:40
@chloerice chloerice force-pushed the resourcelist-emptystate branch from 800923b to 647e551 Compare November 27, 2019 01:43
@chloerice chloerice force-pushed the resourcelist-emptystate branch from 647e551 to 00a265c Compare November 27, 2019 01:44
@chloerice
Copy link
Member Author

@chloerice I think disabling the controls totally makes sense in this context!
The only thing I'm wondering about is a tabindex="-1" on the File type button. It doesn't get focus and is identified as disabled since it is, but the focus ring appears on click. I'm wondering if that tabindex can be removed?

Quick update --- finally figured out that the tabindex="-1" is coming from Popover. Still figuring out how best to only give a tab index to the activator wrapper if the activator is not disabled.

Fixing in #2473!

Copy link
Contributor

@dpersing dpersing left a comment

Choose a reason for hiding this comment

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

🎉

@chloerice chloerice merged commit 96db903 into master Nov 27, 2019
@chloerice chloerice deleted the resourcelist-emptystate branch November 27, 2019 22:24
@dleroux dleroux temporarily deployed to production December 4, 2019 14:42 Inactive
@@ -471,9 +475,14 @@ class ResourceList extends React.Component<CombinedProps, State> {
<div className={styles['HeaderWrapper-overlay']} />
) : null;

const showEmptyState = filterControl && !this.itemsExist() && !loading;
const showNoResults =
filterControl && !this.itemsExist() && hasMoreItems && !loading;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is what might be the issue. hasMoreItems is used to create a link in the bulkActions when there are more results than can be displayed. !this.itemsExist() && hasMoreItems should not ever be true if I'm understanding this correctly.

@chloerice chloerice restored the resourcelist-emptystate branch December 5, 2019 02:30
chloerice added a commit that referenced this pull request Dec 5, 2019
chloerice added a commit that referenced this pull request Dec 5, 2019
chloerice added a commit that referenced this pull request Dec 5, 2019
dleroux added a commit that referenced this pull request Dec 5, 2019
Revert "Merge pull request #2160 from Shopify/resourcelist-emptystate"
chloerice added a commit that referenced this pull request Dec 18, 2019
chloerice added a commit that referenced this pull request Dec 20, 2019
chloerice added a commit that referenced this pull request Jan 8, 2020
chloerice added a commit that referenced this pull request May 13, 2020
chloerice added a commit that referenced this pull request May 13, 2020
chloerice added a commit that referenced this pull request May 14, 2020
* Revert "Revert "Merge pull request #2160 from Shopify/resourcelist-emptystate""

This reverts commit 6ec053f.

* [ResourceList] Refactor empty state logic

* [ResourceList] Adjust tests for emptyState prop

* [CHANGELOG][ResourceList] Add empty state support
@alex-page alex-page deleted the resourcelist-emptystate branch November 10, 2020 00:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants