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
1 change: 1 addition & 0 deletions UNRELEASED.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

- Truncated long sort options in `ResourceList` ([#2957](https://github.com/Shopify/polaris-react/pull/2957)
- Updated type restrictions for `Pagination` to allow its `label` prop to accept `React.ReactNode` instead of `string` ([#2972](https://github.com/Shopify/polaris-react/pull/2972))
- Added an `emptySearchState` prop to `ResourceList` to enable the customization of the empty search state ([#2971](https://github.com/Shopify/polaris-react/pull/2971))

### Bug fixes

Expand Down
120 changes: 116 additions & 4 deletions src/components/ResourceList/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -506,10 +506,6 @@ function ResourceListWithFilteringExample() {
(value) => setTaggedWith(value),
[],
);
const handleQueryValueChange = useCallback(
(value) => setQueryValue(value),
[],
);
const handleTaggedWithRemove = useCallback(() => setTaggedWith(null), []);
const handleQueryValueRemove = useCallback(() => setQueryValue(null), []);
const handleClearAll = useCallback(() => {
Expand Down Expand Up @@ -553,6 +549,121 @@ function ResourceListWithFilteringExample() {
},
];

const appliedFilters = !isEmpty(taggedWith)
? [
{
key: 'taggedWith',
label: disambiguateLabel('taggedWith', taggedWith),
onRemove: handleTaggedWithRemove,
},
]
: [];

const filterControl = (
<Filters
queryValue={queryValue}
filters={filters}
appliedFilters={appliedFilters}
onQueryChange={setQueryValue}
onQueryClear={handleQueryValueRemove}
onClearAll={handleClearAll}
>
<div style={{paddingLeft: '8px'}}>
<Button onClick={() => console.log('New filter saved')}>Save</Button>
</div>
</Filters>
);

return (
<Card>
<ResourceList
resourceName={resourceName}
items={items}
renderItem={renderItem}
filterControl={filterControl}
/>
</Card>
);

function renderItem(item) {
const {id, url, name, location} = item;
const media = <Avatar customer size="medium" name={name} />;

return (
<ResourceItem id={id} url={url} media={media}>
<h3>
<TextStyle variation="strong">{name}</TextStyle>
</h3>
<div>{location}</div>
</ResourceItem>
);
}

function disambiguateLabel(key, value) {
switch (key) {
case 'taggedWith':
return `Tagged with ${value}`;
default:
return value;
}
}

function isEmpty(value) {
if (Array.isArray(value)) {
return value.length === 0;
} else {
return value === '' || value == null;
}
}
}
```

### Resource list with a custom empty search result state

Allows merchants to narrow the resource list to a subset of the original items. If the filters or search applied return no results, then display a custom empty search state.

```jsx
function ResourceListWithFilteringExample() {
const [taggedWith, setTaggedWith] = useState('VIP');
const [queryValue, setQueryValue] = useState(null);
const [items, setItems] = useState([]);

const handleTaggedWithChange = useCallback(
(value) => setTaggedWith(value),
[],
);
const handleQueryValueChange = useCallback((value) => {
setQueryValue(value);
setItems([]);
}, []);
const handleTaggedWithRemove = useCallback(() => setTaggedWith(null), []);
const handleQueryValueRemove = useCallback(() => setQueryValue(null), []);
const handleClearAll = useCallback(() => {
handleTaggedWithRemove();
handleQueryValueRemove();
}, [handleQueryValueRemove, handleTaggedWithRemove]);

const resourceName = {
singular: 'customer',
plural: 'customers',
};

const filters = [
{
key: 'taggedWith',
label: 'Tagged with',
filter: (
<TextField
label="Tagged with"
value={taggedWith}
onChange={handleTaggedWithChange}
labelHidden
/>
),
shortcut: true,
},
];

const appliedFilters = !isEmpty(taggedWith)
? [
{
Expand Down Expand Up @@ -585,6 +696,7 @@ function ResourceListWithFilteringExample() {
items={items}
renderItem={renderItem}
filterControl={filterControl}
emptySearchState={<div>This is a custom empty state</div>}
/>
</Card>
);
Expand Down
25 changes: 17 additions & 8 deletions src/components/ResourceList/ResourceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ export interface ResourceListProps {
idForItem?(item: any, index: number): string;
/** Function to resolve an id from a item */
resolveItemId?(item: any): string;
/** React node to display when `filterControl` is set and no results are returned on search or filter of the list. */
emptySearchState?: React.ReactNode;
}

type CombinedProps = ResourceListProps & WithAppProviderProps;
Expand Down Expand Up @@ -391,6 +393,7 @@ class ResourceListInner extends React.Component<CombinedProps, State> {
selectedItems,
resourceName = this.defaultResourceName,
onSortChange,
emptySearchState,
polaris: {intl},
} = this.props;
const {selectMode, loadingPosition, smallScreen} = this.state;
Expand Down Expand Up @@ -476,9 +479,10 @@ class ResourceListInner extends React.Component<CombinedProps, State> {
<div className={styles['HeaderWrapper-overlay']} />
) : null;

const showEmptyState = filterControl && !this.itemsExist() && !loading;
const showEmptySearchState =
filterControl && !this.itemsExist() && !loading;

const headerMarkup = !showEmptyState &&
const headerMarkup = !showEmptySearchState &&
(showHeader || needsHeader) &&
this.listRef.current && (
<div className={styles.HeaderOuterWrapper}>
Expand Down Expand Up @@ -517,11 +521,16 @@ class ResourceListInner extends React.Component<CombinedProps, State> {
</div>
);

const emptyStateMarkup = showEmptyState ? (
<div className={styles.EmptySearchResultWrapper}>
<EmptySearchResult {...this.emptySearchResultText()} withIllustration />
</div>
) : null;
const emptySearchStateMarkup = showEmptySearchState
? emptySearchState || (
<div className={styles.EmptySearchResultWrapper}>
<EmptySearchResult
{...this.emptySearchResultText()}
withIllustration
/>
</div>
)
: null;

const defaultTopPadding = 8;
const topPadding =
Expand Down Expand Up @@ -567,7 +576,7 @@ class ResourceListInner extends React.Component<CombinedProps, State> {
{items.map(this.renderItem)}
</ul>
) : (
emptyStateMarkup
emptySearchStateMarkup
);

const context = {
Expand Down
16 changes: 16 additions & 0 deletions src/components/ResourceList/tests/ResourceList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,22 @@ describe('<ResourceList />', () => {
);
expect(resourceList.find(EmptySearchResult).exists()).toBe(false);
});

it('renders the provided markup when emptySearchState is set', () => {
const resourceList = mountWithAppProvider(
<ResourceList
items={[]}
renderItem={renderItem}
filterControl={<div>fake filterControl</div>}
emptySearchState={
<div id="emptySearchState">Alternate empty state</div>
}
/>,
);

expect(resourceList.find(EmptySearchResult).exists()).toBe(false);
expect(resourceList.find('div#emptySearchState').exists()).toBe(true);
});
});

describe('Sorting', () => {
Expand Down
1 change: 0 additions & 1 deletion src/components/Select/Select.scss
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ $stacking-order: (
border-radius: var(--p-border-radius-base);
background-color: var(--p-surface);
@include focus-ring($border-width: rem(1px));

// 'position' needs to sit below focus-ring since it will be overwritten
// with relative when the focus ring style is 'base'
// stylelint-disable-next-line order/properties-order
Expand Down