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
3 changes: 2 additions & 1 deletion UNRELEASED.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f

- Moved icons to a separate npm package ([#686](https://github.com/Shopify/polaris-react/pull/686))
- Added `oneHalf` and `oneThird` props to `Layout` component ([#724](https://github.com/Shopify/polaris-react/pull/724))
- Added `helpText` prop to ActionList items ([#777](https://github.com/Shopify/polaris-react/pull/777))
- Added `helpText` prop to `ActionList` items ([#777](https://github.com/Shopify/polaris-react/pull/777))
- Updated `Page` header layout so actions take up less room on small screens ([#707](https://github.com/Shopify/polaris-react/pull/707))
- Added `alternateTool` prop to `ResourceList` component ([#812](https://github.com/Shopify/polaris-react/pull/812))

### Bug fixes

Expand Down
60 changes: 60 additions & 0 deletions src/components/ResourceList/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,66 @@ class ResourceListExample extends React.Component {
}
```

### Resource list with alternate tool

Allows merchants to add an alternate tool in the current sort option location when sort may not be the most relevant action for the current list.

```jsx
class ResourceListExample extends React.Component {
renderItem = (item) => {
const {id, url, name, location} = item;
const media = <Avatar customer size="medium" name={name} />;

return (
<ResourceList.Item
id={id}
url={url}
media={media}
accessibilityLabel={`View details for ${name}`}
>
<h3>
<TextStyle variation="strong">{name}</TextStyle>
</h3>
<div>{location}</div>
</ResourceList.Item>
);
};

render() {
const resourceName = {
singular: 'Customer',
plural: 'Customers',
};

const items = [
{
id: 341,
url: 'customers/341',
name: 'Mae Jemison',
location: 'Decatur, USA',
},
{
id: 256,
url: 'customers/256',
name: 'Ellen Ochoa',
location: 'Los Angeles, USA',
},
];

return (
<Card>
<ResourceList
items={items}
renderItem={this.renderItem}
resourceName={resourceName}
alternateTool={<Button>Email customers</Button>}
/>
</Card>
);
}
}
```

### Resource list with filtering

Allows merchants to narrow the resource list to a subset of the original items. See the [filter control subcomponent](#subcomponent-filter-control) and the [filtering section of the case study](#study-filtering) for implementation details.
Expand Down
8 changes: 6 additions & 2 deletions src/components/ResourceList/ResourceList.scss
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ $item-wrapper-loading-height: rem(64px);
}
}

.SortWrapper {
.SortWrapper,
.AlternateToolWrapper {
position: relative;
display: flex;
flex: 1;
Expand All @@ -109,6 +110,7 @@ $item-wrapper-loading-height: rem(64px);
margin-left: spacing();

// stylelint-disable-next-line selector-max-class
.HeaderWrapper-hasAlternateTool.HeaderWrapper-hasSelect &,
.HeaderWrapper-hasSort.HeaderWrapper-hasSelect & {
padding-right: 0;
}
Expand All @@ -131,17 +133,19 @@ $item-wrapper-loading-height: rem(64px);
padding-left: spacing(extra-tight);
align-self: center;

.HeaderWrapper-hasAlternateTool &,
.HeaderWrapper-hasSort & {
display: none;
}

@include breakpoint-after(resource-list(breakpoint-small)) {
// stylelint-disable-next-line selector-max-class
.HeaderWrapper-hasSelect &,
.HeaderWrapper-hasAlternateTool.HeaderWrapper-hasSelect &,
.HeaderWrapper-hasSort.HeaderWrapper-hasSelect & {
display: none;
}

.HeaderWrapper-hasAlternateTool &,
.HeaderWrapper-hasSort & {
display: block;
}
Expand Down
17 changes: 15 additions & 2 deletions src/components/ResourceList/ResourceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export interface Props {
sortValue?: string;
/** Collection of sort options to choose from */
sortOptions?: Option[];
/** ReactNode to display instead of the sort control */
alternateTool?: React.ReactNode;
/** Callback when sort option is changed */
onSortChange?(selected: string, id: string): void;
/** Callback when selection is changed */
Expand Down Expand Up @@ -342,6 +344,7 @@ export class ResourceList extends React.Component<CombinedProps, State> {
showHeader = false,
sortOptions,
sortValue,
alternateTool,
onSortChange,
polaris: {intl},
} = this.props;
Expand Down Expand Up @@ -379,7 +382,7 @@ export class ResourceList extends React.Component<CombinedProps, State> {
);

const sortingSelectMarkup =
sortOptions && sortOptions.length > 0 ? (
sortOptions && sortOptions.length > 0 && !alternateTool ? (
<div className={styles.SortWrapper}>
{sortingLabelMarkup}
<Select
Expand All @@ -393,6 +396,11 @@ export class ResourceList extends React.Component<CombinedProps, State> {
</div>
) : null;

const alternateToolMarkup =
alternateTool && !sortingSelectMarkup ? (
<div className={styles.AlternateToolWrapper}>{alternateTool}</div>
) : null;

const headerTitleMarkup = (
<div className={styles.HeaderTitleWrapper} testID="headerTitleWrapper">
{this.headerTitle}
Expand Down Expand Up @@ -425,7 +433,9 @@ export class ResourceList extends React.Component<CombinedProps, State> {
) : null;

const needsHeader =
this.selectable || (sortOptions && sortOptions.length > 0);
this.selectable ||
(sortOptions && sortOptions.length > 0) ||
alternateTool;

const headerWrapperOverlay = loading ? (
<div className={styles['HeaderWrapper-overlay']} />
Expand All @@ -440,7 +450,9 @@ export class ResourceList extends React.Component<CombinedProps, State> {
styles.HeaderWrapper,
sortOptions &&
sortOptions.length > 0 &&
!alternateTool &&
styles['HeaderWrapper-hasSort'],
alternateTool && styles['HeaderWrapper-hasAlternateTool'],
this.selectable && styles['HeaderWrapper-hasSelect'],
loading && styles['HeaderWrapper-disabled'],
this.selectable &&
Expand All @@ -454,6 +466,7 @@ export class ResourceList extends React.Component<CombinedProps, State> {
<div className={styles.HeaderContentWrapper}>
{headerTitleMarkup}
{checkableButtonMarkup}
{alternateToolMarkup}
{sortingSelectMarkup}
{selectButtonMarkup}
</div>
Expand Down
110 changes: 110 additions & 0 deletions src/components/ResourceList/tests/ResourceList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const sortOptions = [
},
];

const alternateTool = <div id="AlternateTool">Alternate Tool</div>;

describe('<ResourceList />', () => {
describe('renderItem', () => {
it('renders list items', () => {
Expand Down Expand Up @@ -335,6 +337,19 @@ describe('<ResourceList />', () => {
);
});

it('renders when an alternateTool is provided', () => {
const resourceList = mountWithAppProvider(
<ResourceList
alternateTool={alternateTool}
items={itemsWithID}
renderItem={renderItem}
/>,
);
expect(findByTestID(resourceList, 'ResourceList-Header').exists()).toBe(
true,
);
});

it('renders when bulkActions are given', () => {
const resourceList = mountWithAppProvider(
<ResourceList
Expand Down Expand Up @@ -461,6 +476,101 @@ describe('<ResourceList />', () => {
expect(resourceList.find(Select).exists()).toBe(true);
});

it('does not render a sort select if an alternateTool is provided', () => {
const resourceList = mountWithAppProvider(
<ResourceList
items={itemsWithID}
renderItem={renderItem}
sortOptions={sortOptions}
alternateTool={alternateTool}
/>,
);
expect(resourceList.find(Select).exists()).toBe(false);
});

describe('sortOptions', () => {
it('passes a sortOptions to the Select options', () => {
const resourceList = mountWithAppProvider(
<ResourceList
items={itemsWithID}
sortOptions={sortOptions}
renderItem={renderItem}
/>,
);
expect(resourceList.find(Select).props()).toHaveProperty(
'options',
sortOptions,
);
});
});

describe('sortValue', () => {
it('passes a sortValue to the Select value', () => {
const onSortChange = jest.fn();
const resourceList = mountWithAppProvider(
<ResourceList
items={itemsWithID}
sortOptions={sortOptions}
sortValue="sortValue"
onSortChange={onSortChange}
renderItem={renderItem}
/>,
);
expect(resourceList.find(Select).props()).toHaveProperty(
'value',
'sortValue',
);
});
});

describe('onSortChange', () => {
it('calls onSortChange when the Sort Select changes', () => {
const onSortChange = jest.fn();
const resourceList = mountWithAppProvider(
<ResourceList
items={itemsWithID}
onSortChange={onSortChange}
sortOptions={sortOptions}
renderItem={renderItem}
/>,
);
trigger(resourceList.find(Select), 'onChange', 'PRODUCT_TITLE_DESC');
expect(onSortChange).toHaveBeenCalledWith('PRODUCT_TITLE_DESC');
});
});
});

describe('Alternate Tool', () => {
it('does not render if an alternateTool is not provided', () => {
const resourceList = mountWithAppProvider(
<ResourceList items={itemsWithID} renderItem={renderItem} />,
);
expect(resourceList.find('#AlternateTool').exists()).toBe(false);
});

it('renders if an alternateTool is provided', () => {
const resourceList = mountWithAppProvider(
<ResourceList
items={itemsWithID}
renderItem={renderItem}
alternateTool={alternateTool}
/>,
);
expect(resourceList.find('#AlternateTool').exists()).toBe(true);
});

it('renders even if sortOptions are provided', () => {
const resourceList = mountWithAppProvider(
<ResourceList
items={itemsWithID}
renderItem={renderItem}
sortOptions={sortOptions}
alternateTool={alternateTool}
/>,
);
expect(resourceList.find('#AlternateTool').exists()).toBe(true);
});

describe('sortOptions', () => {
it('passes a sortOptions to the Select options', () => {
const resourceList = mountWithAppProvider(
Expand Down