From d0698ad6f1d1bd93d5c9fee36e68dab3043662f5 Mon Sep 17 00:00:00 2001 From: Alex Thornburg Date: Tue, 17 Mar 2020 20:30:56 -0500 Subject: [PATCH 01/17] Refactor resource list to a functional component + use the 18n hook for internationalization + made item generic + made the item render functions use the items type --- UNRELEASED.md | 3 + src/components/ResourceList/ResourceList.tsx | 902 ++++++++---------- .../ResourceList/tests/ResourceList.test.tsx | 11 +- 3 files changed, 401 insertions(+), 515 deletions(-) diff --git a/UNRELEASED.md b/UNRELEASED.md index abbea975388..badde080833 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -10,6 +10,9 @@ - Added a `fullWidth` prop to `EmptyState` to support full width layout within a content context ([#2992](https://github.com/Shopify/polaris-react/pull/2992)) - Added an `emptyState` prop to `ResourceList` to support in context empty states in list views ([#2569](https://github.com/Shopify/polaris-react/pull/2569)) Set `active` prop of `Popover` to true on keyDown in `ComboBox` to fix `Autocomplete` suggestions not showing when searching and selecting via keyboard ([#3028](https://github.com/Shopify/polaris-react/pull/3028)) +- Added utilities for parsing video duration (https://polaris.shopify.com/components/images-and-icons/video-thumbnail) ([#2725](https://github.com/Shopify/polaris-react/pull/2725)) +- Changed Resource List to a generic functional component (#1995) +- Made the `renderItem` function infer the type of the items prop (#543) ### Bug fixes diff --git a/src/components/ResourceList/ResourceList.tsx b/src/components/ResourceList/ResourceList.tsx index 801d02c8066..5ad59b39bb5 100644 --- a/src/components/ResourceList/ResourceList.tsx +++ b/src/components/ResourceList/ResourceList.tsx @@ -1,4 +1,10 @@ -import React from 'react'; +import React, { + ReactElement, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import debounce from 'lodash/debounce'; import {EnableSelectionMinor} from '@shopify/polaris-icons'; @@ -9,19 +15,17 @@ import {EventListener} from '../EventListener'; import {Sticky} from '../Sticky'; import {Spinner} from '../Spinner'; import { - withAppProvider, - WithAppProviderProps, -} from '../../utilities/with-app-provider'; -import { + CheckableButtonKey, + CheckableButtons, ResourceListContext, ResourceListSelectedItems, SELECT_ALL_ITEMS, - CheckableButtonKey, - CheckableButtons, } from '../../utilities/resource-list'; import {Select, SelectOption} from '../Select'; import {EmptySearchResult} from '../EmptySearchResult'; +import {useI18n} from '../../utilities/i18n'; import {ResourceItem} from '../ResourceItem'; +import {useLazyRef} from '../../utilities/use-lazy-ref'; import { BulkActions, @@ -39,19 +43,9 @@ const SMALL_SCREEN_WIDTH = 458; const SMALL_SPINNER_HEIGHT = 28; const LARGE_SPINNER_HEIGHT = 45; -type Items = any[]; - -interface State { - selectMode: boolean; - loadingPosition: number; - lastSelected: number | null; - smallScreen: boolean; - checkableButtons: CheckableButtons; -} - -export interface ResourceListProps { +export interface ResourceListProps { /** Item data; each item is passed to renderItem */ - items: Items; + items: ItemType[]; filterControl?: React.ReactNode; /** The markup to display when no resources exist yet. Renders when set and items is empty. */ emptyState?: React.ReactNode; @@ -90,82 +84,100 @@ export interface ResourceListProps { onSortChange?(selected: string, id: string): void; /** Callback when selection is changed */ onSelectionChange?(selectedItems: ResourceListSelectedItems): void; - /** Function to render each list item */ - renderItem(item: any, id: string, index: number): React.ReactNode; + /** Function to render each list item */ + renderItem(item: ItemType, id: string, index: number): React.ReactNode; /** Function to customize the unique ID for each item */ - idForItem?(item: any, index: number): string; + idForItem?(item: ItemType, index: number): string; /** Function to resolve the ids of items */ - resolveItemId?(item: any): string; + resolveItemId?(item: ItemType): string; } -type CombinedProps = ResourceListProps & WithAppProviderProps; - -class ResourceListInner extends React.Component { - static Item = ResourceItem; +type ResourceListType = (( + value: ResourceListProps, +) => ReactElement) & { + Item: typeof ResourceItem; // eslint-disable-next-line import/no-deprecated - static FilterControl = FilterControl; + FilterControl: typeof FilterControl; +}; + +export const ResourceList: ResourceListType = function ResourceList({ + items, + filterControl, + emptyState, + emptySearchState, + resourceName: resourceNameProp, + promotedBulkActions, + bulkActions, + selectedItems = [], + selectable, + hasMoreItems, + loading, + showHeader = false, + totalItemsCount, + sortValue, + sortOptions, + alternateTool, + onSortChange, + onSelectionChange, + renderItem, + idForItem = (item: any, index: number) => + Object.prototype.hasOwnProperty.call(item, 'id') + ? item.id + : index.toString(), + resolveItemId, +}: ResourceListProps) { + const i18n = useI18n(); + const [selectMode, setSelectMode] = useState( + Boolean(selectedItems && selectedItems.length > 0), + ); + const [loadingPosition, setLoadingPositionState] = useState(0); + const [lastSelected, setLastSelected] = useState(); + const [smallScreen, setSmallScreen] = useState(isSmallScreen()); + + const [checkableButtons, setCheckableButtons] = useState( + new Map(), + ); + + const defaultResourceName = useLazyRef(() => ({ + singular: i18n.translate('Polaris.ResourceList.defaultItemSingular'), + plural: i18n.translate('Polaris.ResourceList.defaultItemPlural'), + })); + const listRef: React.RefObject = useRef(null); - private defaultResourceName: {singular: string; plural: string}; - private listRef: React.RefObject = React.createRef(); + const handleSelectMode = (selectMode: boolean) => { + setSelectMode(selectMode); + if (!selectMode && onSelectionChange) { + onSelectionChange([]); + } + }; - private handleResize = debounce( + const handleResize = debounce( () => { - const {selectedItems} = this.props; - const {selectMode, smallScreen} = this.state; const newSmallScreen = isSmallScreen(); - if ( selectedItems && selectedItems.length === 0 && selectMode && !newSmallScreen ) { - this.handleSelectMode(false); + handleSelectMode(false); } if (smallScreen !== newSmallScreen) { - this.setState({smallScreen: newSmallScreen}); + setSmallScreen(newSmallScreen); } }, 50, {leading: true, trailing: true, maxWait: 50}, ); - constructor(props: CombinedProps) { - super(props); - - const { - selectedItems, - polaris: {intl}, - } = props; - - this.defaultResourceName = { - singular: intl.translate('Polaris.ResourceList.defaultItemSingular'), - plural: intl.translate('Polaris.ResourceList.defaultItemPlural'), - }; - - // eslint-disable-next-line react/state-in-constructor - this.state = { - selectMode: Boolean(selectedItems && selectedItems.length > 0), - loadingPosition: 0, - lastSelected: null, - smallScreen: isSmallScreen(), - checkableButtons: new Map(), - }; - } - - private selectable() { - const {promotedBulkActions, bulkActions, selectable} = this.props; - - return Boolean( - (promotedBulkActions && promotedBulkActions.length > 0) || - (bulkActions && bulkActions.length > 0) || - selectable, - ); - } + const isSelectable = Boolean( + (promotedBulkActions && promotedBulkActions.length > 0) || + (bulkActions && bulkActions.length > 0) || + selectable, + ); - private bulkSelectState(): boolean | 'indeterminate' { - const {selectedItems, items} = this.props; + const bulkSelectState = (): boolean | 'indeterminate' => { let selectState: boolean | 'indeterminate' = 'indeterminate'; if ( !selectedItems || @@ -179,17 +191,13 @@ class ResourceListInner extends React.Component { selectState = true; } return selectState; - } + }; - private headerTitle() { - const { - resourceName = this.defaultResourceName, - items, - polaris: {intl}, - loading, - totalItemsCount, - } = this.props; + const resourceName = resourceNameProp + ? resourceNameProp + : defaultResourceName.current; + const headerTitle = () => { const itemsCount = items.length; const resource = !loading && @@ -198,64 +206,53 @@ class ResourceListInner extends React.Component { : resourceName.plural; if (loading) { - return intl.translate('Polaris.ResourceList.loading', {resource}); + return i18n.translate('Polaris.ResourceList.loading', {resource}); } else if (totalItemsCount) { - return intl.translate('Polaris.ResourceList.showingTotalCount', { + return i18n.translate('Polaris.ResourceList.showingTotalCount', { itemsCount, totalItemsCount, resource, }); } else { - return intl.translate('Polaris.ResourceList.showing', { + return i18n.translate('Polaris.ResourceList.showing', { itemsCount, resource, }); } - } - - private bulkActionsLabel() { - const { - selectedItems = [], - items, - polaris: {intl}, - } = this.props; + }; + const bulkActionsLabel = () => { const selectedItemsCount = selectedItems === SELECT_ALL_ITEMS ? `${items.length}+` : selectedItems.length; - return intl.translate('Polaris.ResourceList.selected', { + return i18n.translate('Polaris.ResourceList.selected', { selectedItemsCount, }); - } - - private bulkActionsAccessibilityLabel() { - const { - resourceName = this.defaultResourceName, - selectedItems = [], - items, - polaris: {intl}, - } = this.props; + }; + const bulkActionsAccessibilityLabel = () => { const selectedItemsCount = selectedItems.length; const totalItemsCount = items.length; const allSelected = selectedItemsCount === totalItemsCount; if (totalItemsCount === 1 && allSelected) { - return intl.translate( + return i18n.translate( 'Polaris.ResourceList.a11yCheckboxDeselectAllSingle', - {resourceNameSingular: resourceName.singular}, + { + resourceNameSingular: resourceName.singular, + }, ); } else if (totalItemsCount === 1) { - return intl.translate( + return i18n.translate( 'Polaris.ResourceList.a11yCheckboxSelectAllSingle', { resourceNameSingular: resourceName.singular, }, ); } else if (allSelected) { - return intl.translate( + return i18n.translate( 'Polaris.ResourceList.a11yCheckboxDeselectAllMultiple', { itemsLength: items.length, @@ -263,7 +260,7 @@ class ResourceListInner extends React.Component { }, ); } else { - return intl.translate( + return i18n.translate( 'Polaris.ResourceList.a11yCheckboxSelectAllMultiple', { itemsLength: items.length, @@ -271,360 +268,82 @@ class ResourceListInner extends React.Component { }, ); } - } - - private paginatedSelectAllText() { - const { - hasMoreItems, - selectedItems, - items, - resourceName = this.defaultResourceName, - polaris: {intl}, - } = this.props; + }; - if (!this.selectable() || !hasMoreItems) { + const paginatedSelectAllText = () => { + if (!isSelectable || !hasMoreItems) { return; } if (selectedItems === SELECT_ALL_ITEMS) { - return intl.translate('Polaris.ResourceList.allItemsSelected', { + return i18n.translate('Polaris.ResourceList.allItemsSelected', { itemsLength: items.length, resourceNamePlural: resourceName.plural, }); } - } - - private paginatedSelectAllAction() { - const { - hasMoreItems, - selectedItems, - items, - resourceName = this.defaultResourceName, - polaris: {intl}, - } = this.props; + }; - if (!this.selectable() || !hasMoreItems) { + const paginatedSelectAllAction = () => { + if (!isSelectable || !hasMoreItems) { return; } const actionText = selectedItems === SELECT_ALL_ITEMS - ? intl.translate('Polaris.Common.undo') - : intl.translate('Polaris.ResourceList.selectAllItems', { + ? i18n.translate('Polaris.Common.undo') + : i18n.translate('Polaris.ResourceList.selectAllItems', { itemsLength: items.length, resourceNamePlural: resourceName.plural, }); return { content: actionText, - onAction: this.handleSelectAllItemsInStore, + onAction: handleSelectAllItemsInStore, }; - } - - private emptySearchResultText() { - const { - polaris: {intl}, - resourceName = this.defaultResourceName, - } = this.props; - - return { - title: intl.translate('Polaris.ResourceList.emptySearchResultTitle', { - resourceNamePlural: resourceName.plural, - }), - description: intl.translate( - 'Polaris.ResourceList.emptySearchResultDescription', - ), - }; - } - - // eslint-disable-next-line @typescript-eslint/member-ordering - componentDidMount() { - this.forceUpdate(); - if (this.props.loading) { - this.setLoadingPosition(); - } - } - - // eslint-disable-next-line @typescript-eslint/member-ordering - componentDidUpdate({ - loading: prevLoading, - items: prevItems, - selectedItems: prevSelectedItems, - }: ResourceListProps) { - const {selectedItems, loading} = this.props; - - if ( - this.listRef.current && - this.itemsExist() && - !this.itemsExist(prevItems) - ) { - this.forceUpdate(); - } + }; - if (loading && !prevLoading) { - this.setLoadingPosition(); - } + const emptySearchResultText = { + title: i18n.translate('Polaris.ResourceList.emptySearchResultTitle', { + resourceNamePlural: resourceName.plural, + }), + description: i18n.translate( + 'Polaris.ResourceList.emptySearchResultDescription', + ), + }; - if (selectedItems && selectedItems.length > 0 && !this.state.selectMode) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({selectMode: true}); - return; - } + const handleSelectAllItemsInStore = () => { + const newlySelectedItems = + selectedItems === SELECT_ALL_ITEMS + ? getAllItemsOnPage(items, idForItem) + : SELECT_ALL_ITEMS; - if ( - prevSelectedItems && - prevSelectedItems.length > 0 && - (!selectedItems || selectedItems.length === 0) && - !isSmallScreen() - ) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({selectMode: false}); + if (onSelectionChange) { + onSelectionChange(newlySelectedItems); } - } - - // eslint-disable-next-line @typescript-eslint/member-ordering - render() { - const { - items, - promotedBulkActions, - bulkActions, - filterControl, - emptyState, - emptySearchState, - loading, - showHeader = false, - sortOptions, - sortValue, - alternateTool, - selectedItems, - resourceName = this.defaultResourceName, - onSortChange, - polaris: {intl}, - } = this.props; - const {selectMode, loadingPosition, smallScreen} = this.state; - const filterControlMarkup = filterControl ? ( -
{filterControl}
- ) : null; - - const bulkActionsMarkup = this.selectable() ? ( -
- -
- ) : null; - - const sortingSelectMarkup = - sortOptions && sortOptions.length > 0 && !alternateTool ? ( -
- +
+ ) : null; -function defaultIdForItem(item: any, index: number) { - return Object.prototype.hasOwnProperty.call(item, 'id') - ? item.id - : index.toString(); -} + const alternateToolMarkup = + alternateTool && !sortingSelectMarkup ? ( +
{alternateTool}
+ ) : null; -function isSmallScreen() { - return typeof window === 'undefined' - ? false - : window.innerWidth < SMALL_SCREEN_WIDTH; -} + const headerTitleMarkup = ( +
+ {headerTitle()} +
+ ); + + const selectButtonMarkup = isSelectable ? ( +
+ +
+ ) : null; + + const checkableButtonMarkup = isSelectable ? ( +
+ +
+ ) : null; + + const needsHeader = + isSelectable || (sortOptions && sortOptions.length > 0) || alternateTool; + + const headerWrapperOverlay = loading ? ( +
+ ) : null; + + const showEmptyState = emptyState && !itemsExist() && !loading; + + const showEmptySearchState = + !showEmptyState && filterControl && !this.itemsExist() && !loading; + + const headerMarkup = !showEmptyState && + !showEmptySearchState && + (showHeader || needsHeader) && + itemsExist() && ( +
+ + {(isSticky: boolean) => { + const headerClassName = classNames( + styles.HeaderWrapper, + sortOptions && + sortOptions.length > 0 && + !alternateTool && + styles['HeaderWrapper-hasSort'], + alternateTool && styles['HeaderWrapper-hasAlternateTool'], + isSelectable && styles['HeaderWrapper-hasSelect'], + loading && styles['HeaderWrapper-disabled'], + isSelectable && + selectMode && + styles['HeaderWrapper-inSelectMode'], + isSticky && styles['HeaderWrapper-isSticky'], + ); + return ( +
+ + {headerWrapperOverlay} +
+ {headerTitleMarkup} + {checkableButtonMarkup} + {alternateToolMarkup} + {sortingSelectMarkup} + {selectButtonMarkup} +
+ {bulkActionsMarkup} +
+ ); + }} +
+
+ ); + + const emptySearchStateMarkup = showEmptySearchState + ? emptySearchState || ( +
+ +
+ ) + : null; + + const emptyStateMarkup = showEmptyState ? emptyState : null; + + const defaultTopPadding = 8; + const topPadding = loadingPosition > 0 ? loadingPosition : defaultTopPadding; + const spinnerStyle = {paddingTop: `${topPadding}px`}; + + const spinnerSize = items.length < 2 ? 'small' : 'large'; + + const loadingOverlay = loading ? ( + +
+ +
+
+ + ) : null; + + const className = classNames( + styles.ItemWrapper, + loading && styles['ItemWrapper-isLoading'], + ); + const loadingWithoutItemsMarkup = + loading && !itemsExist() ? ( +
+ {loadingOverlay} +
+ ) : null; + + const resourceListClassName = classNames( + styles.ResourceList, + loading && styles.disabledPointerEvents, + selectMode && styles.disableTextSelection, + ); + + const listMarkup = + itemsExist() && !emptySearchStateMarkup && !emptyStateMarkup ? ( +
    + {loadingOverlay} + {items.map(renderItemWithId)} +
+ ) : null; + + const context = { + selectable: isSelectable, + selectedItems, + selectMode, + resourceName, + loading, + onSelectionChange: handleSelectionChange, + registerCheckableButtons: handleCheckableButtonRegistration, + }; + + return ( + +
+ {filterControlMarkup} + {headerMarkup} + {listMarkup} + {emptySearchStateMarkup} + {emptyStateMarkup} + {loadingWithoutItemsMarkup} +
+
+ ); +}; -export const ResourceList = withAppProvider()( - ResourceListInner, -); +ResourceList.Item = ResourceItem; +// eslint-disable-next-line import/no-deprecated +ResourceList.FilterControl = FilterControl; diff --git a/src/components/ResourceList/tests/ResourceList.test.tsx b/src/components/ResourceList/tests/ResourceList.test.tsx index 04e90ca1d1b..39af1a30e2d 100644 --- a/src/components/ResourceList/tests/ResourceList.test.tsx +++ b/src/components/ResourceList/tests/ResourceList.test.tsx @@ -22,8 +22,14 @@ import {BulkActions, CheckableButton} from '../components'; const itemsNoID = [{url: 'item 1'}, {url: 'item 2'}]; const singleItemNoID = [{url: 'item 1'}]; const singleItemWithID = [{id: '1', url: 'item 1'}]; +interface Item { + id: string; + name: string; + url: string; + title: string; +} -const itemsWithID = [ +const itemsWithID: Item[] = [ {id: '5', name: 'item 1', url: 'www.test.com', title: 'title 1'}, {id: '6', name: 'item 2', url: 'www.test.com', title: 'title 2'}, {id: '7', name: 'item 3', url: 'www.test.com', title: 'title 3'}, @@ -89,6 +95,7 @@ describe('', () => { promotedBulkActions={promotedBulkActions} />, ); + expect(resourceList.find(BulkActions).exists()).toBe(true); }); @@ -942,7 +949,7 @@ describe('', () => { expect(deselectAllCheckbox.getDOMNode()).toBe(document.activeElement); }); - it('focuses the plain CheckableButton checkbox when items are selected and the deselect Checkable button the is clicked', () => { + it('focuses the plain CheckableButton checkbox when items are selected and the deselect Checkable button is clicked', () => { const resourceList = mountWithAppProvider( Date: Mon, 20 Apr 2020 18:00:08 -0500 Subject: [PATCH 02/17] Add link to issue number Co-Authored-By: Andrew Musgrave --- UNRELEASED.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UNRELEASED.md b/UNRELEASED.md index badde080833..59580fffe1c 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -11,7 +11,7 @@ - Added an `emptyState` prop to `ResourceList` to support in context empty states in list views ([#2569](https://github.com/Shopify/polaris-react/pull/2569)) Set `active` prop of `Popover` to true on keyDown in `ComboBox` to fix `Autocomplete` suggestions not showing when searching and selecting via keyboard ([#3028](https://github.com/Shopify/polaris-react/pull/3028)) - Added utilities for parsing video duration (https://polaris.shopify.com/components/images-and-icons/video-thumbnail) ([#2725](https://github.com/Shopify/polaris-react/pull/2725)) -- Changed Resource List to a generic functional component (#1995) +- Changed Resource List to a generic functional component ([#2843](https://github.com/Shopify/polaris-react/pull/2843)) - Made the `renderItem` function infer the type of the items prop (#543) ### Bug fixes From bd79c12127a219150e646441f36e0202fbbe96b9 Mon Sep 17 00:00:00 2001 From: Alex Thornburg Date: Mon, 20 Apr 2020 18:00:32 -0500 Subject: [PATCH 03/17] Add link to issue number Co-Authored-By: Andrew Musgrave --- UNRELEASED.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UNRELEASED.md b/UNRELEASED.md index 59580fffe1c..63f0fc26181 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -12,7 +12,7 @@ Set `active` prop of `Popover` to true on keyDown in `ComboBox` to fix `Autocomplete` suggestions not showing when searching and selecting via keyboard ([#3028](https://github.com/Shopify/polaris-react/pull/3028)) - Added utilities for parsing video duration (https://polaris.shopify.com/components/images-and-icons/video-thumbnail) ([#2725](https://github.com/Shopify/polaris-react/pull/2725)) - Changed Resource List to a generic functional component ([#2843](https://github.com/Shopify/polaris-react/pull/2843)) -- Made the `renderItem` function infer the type of the items prop (#543) +- Made the `renderItem` function infer the type of the items prop ([#2843](https://github.com/Shopify/polaris-react/pull/2843)) ### Bug fixes From 8d7ef25e716b41553b34a5327e535efd2748c145 Mon Sep 17 00:00:00 2001 From: athornburg Date: Mon, 20 Apr 2020 18:04:55 -0500 Subject: [PATCH 04/17] Infer the types of items on ResourceList tests --- src/components/ResourceList/tests/ResourceList.test.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/components/ResourceList/tests/ResourceList.test.tsx b/src/components/ResourceList/tests/ResourceList.test.tsx index 39af1a30e2d..da4eed90db2 100644 --- a/src/components/ResourceList/tests/ResourceList.test.tsx +++ b/src/components/ResourceList/tests/ResourceList.test.tsx @@ -22,18 +22,13 @@ import {BulkActions, CheckableButton} from '../components'; const itemsNoID = [{url: 'item 1'}, {url: 'item 2'}]; const singleItemNoID = [{url: 'item 1'}]; const singleItemWithID = [{id: '1', url: 'item 1'}]; -interface Item { - id: string; - name: string; - url: string; - title: string; -} -const itemsWithID: Item[] = [ +const itemsWithID = [ {id: '5', name: 'item 1', url: 'www.test.com', title: 'title 1'}, {id: '6', name: 'item 2', url: 'www.test.com', title: 'title 2'}, {id: '7', name: 'item 3', url: 'www.test.com', title: 'title 3'}, ]; + const allSelectedIDs = ['5', '6', '7']; const promotedBulkActions = [{content: 'action'}, {content: 'action 2'}]; const bulkActions = [{content: 'action 3'}, {content: 'action 4'}]; From 23f5269f66a0aad210ee88c1c834dd7c74cfce2c Mon Sep 17 00:00:00 2001 From: athornburg Date: Mon, 20 Apr 2020 18:13:30 -0500 Subject: [PATCH 05/17] Moved a few functions out of the ResourceList component --- src/components/ResourceList/ResourceList.tsx | 68 +++++++++----------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/src/components/ResourceList/ResourceList.tsx b/src/components/ResourceList/ResourceList.tsx index 5ad59b39bb5..f0e1a8b0016 100644 --- a/src/components/ResourceList/ResourceList.tsx +++ b/src/components/ResourceList/ResourceList.tsx @@ -43,6 +43,21 @@ const SMALL_SCREEN_WIDTH = 458; const SMALL_SPINNER_HEIGHT = 28; const LARGE_SPINNER_HEIGHT = 45; +const getAllItemsOnPage = ( + items: any, + idForItem: (item: any, index: number) => string, +) => { + return items.map((item: any, index: number) => { + return idForItem(item, index); + }); +}; + +const isSmallScreen = () => { + return typeof window === 'undefined' + ? false + : window.innerWidth < SMALL_SCREEN_WIDTH; +}; + export interface ResourceListProps { /** Item data; each item is passed to renderItem */ items: ItemType[]; @@ -322,21 +337,6 @@ export const ResourceList: ResourceListType = function ResourceList({ } }; - function getAllItemsOnPage( - items: any, - idForItem: (item: any, index: number) => string, - ) { - return items.map((item: any, index: number) => { - return idForItem(item, index); - }); - } - - function isSmallScreen() { - return typeof window === 'undefined' - ? false - : window.innerWidth < SMALL_SCREEN_WIDTH; - } - const setLoadingPosition = useCallback(() => { if (listRef.current != null) { if (typeof window === 'undefined') { @@ -363,12 +363,7 @@ export const ResourceList: ResourceListType = function ResourceList({ } }, [listRef, items.length]); - const itemsExist = useCallback( - (itemsParam?: ItemType[]) => { - return (itemsParam || items).length > 0; - }, - [items], - ); + const itemsExist = items.length > 0; useEffect(() => { if (loading) { @@ -588,15 +583,15 @@ export const ResourceList: ResourceListType = function ResourceList({
) : null; - const showEmptyState = emptyState && !itemsExist() && !loading; + const showEmptyState = emptyState && !itemsExist && !loading; const showEmptySearchState = - !showEmptyState && filterControl && !this.itemsExist() && !loading; + !showEmptyState && filterControl && !itemsExist && !loading; const headerMarkup = !showEmptyState && !showEmptySearchState && (showHeader || needsHeader) && - itemsExist() && ( + itemsExist && (
{(isSticky: boolean) => { @@ -663,7 +658,7 @@ export const ResourceList: ResourceListType = function ResourceList({ loading && styles['ItemWrapper-isLoading'], ); const loadingWithoutItemsMarkup = - loading && !itemsExist() ? ( + loading && !itemsExist ? (
{loadingOverlay}
@@ -675,18 +670,17 @@ export const ResourceList: ResourceListType = function ResourceList({ selectMode && styles.disableTextSelection, ); - const listMarkup = - itemsExist() && !emptySearchStateMarkup && !emptyStateMarkup ? ( -
    - {loadingOverlay} - {items.map(renderItemWithId)} -
- ) : null; + const listMarkup = itemsExist ? ( +
    + {loadingOverlay} + {items.map(renderItemWithId)} +
+ ) : null; const context = { selectable: isSelectable, From c2a060adbdd1023fbaed1a3cd0cfc8adcce918dc Mon Sep 17 00:00:00 2001 From: Alex Thornburg Date: Mon, 20 Apr 2020 18:17:43 -0500 Subject: [PATCH 06/17] Move default id for item function definition Co-Authored-By: Andrew Musgrave --- src/components/ResourceList/ResourceList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ResourceList/ResourceList.tsx b/src/components/ResourceList/ResourceList.tsx index f0e1a8b0016..78c4464e6e3 100644 --- a/src/components/ResourceList/ResourceList.tsx +++ b/src/components/ResourceList/ResourceList.tsx @@ -135,7 +135,7 @@ export const ResourceList: ResourceListType = function ResourceList({ onSortChange, onSelectionChange, renderItem, - idForItem = (item: any, index: number) => + idForItem = (item: ItemType, index: number) => Object.prototype.hasOwnProperty.call(item, 'id') ? item.id : index.toString(), From 49be39761b2315bdbf9ca6288fb5316e3f7650c8 Mon Sep 17 00:00:00 2001 From: athornburg Date: Mon, 20 Apr 2020 18:31:09 -0500 Subject: [PATCH 07/17] move default id for item out of the component --- src/components/ResourceList/ResourceList.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/ResourceList/ResourceList.tsx b/src/components/ResourceList/ResourceList.tsx index 78c4464e6e3..f88ebe8f972 100644 --- a/src/components/ResourceList/ResourceList.tsx +++ b/src/components/ResourceList/ResourceList.tsx @@ -58,6 +58,15 @@ const isSmallScreen = () => { : window.innerWidth < SMALL_SCREEN_WIDTH; }; +function defaultIdForItem( + item: ItemType, + index: number, +) { + return Object.prototype.hasOwnProperty.call(item, 'id') + ? item.id + : index.toString(); +} + export interface ResourceListProps { /** Item data; each item is passed to renderItem */ items: ItemType[]; @@ -135,10 +144,7 @@ export const ResourceList: ResourceListType = function ResourceList({ onSortChange, onSelectionChange, renderItem, - idForItem = (item: ItemType, index: number) => - Object.prototype.hasOwnProperty.call(item, 'id') - ? item.id - : index.toString(), + idForItem = defaultIdForItem, resolveItemId, }: ResourceListProps) { const i18n = useI18n(); From 6c7928955d8f61c7a4a3fa6e6262e642de28ca86 Mon Sep 17 00:00:00 2001 From: athornburg Date: Tue, 21 Apr 2020 08:13:31 -0500 Subject: [PATCH 08/17] Add a few more generics to functions that interact on items --- src/components/ResourceList/ResourceList.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/ResourceList/ResourceList.tsx b/src/components/ResourceList/ResourceList.tsx index f88ebe8f972..d57816c06eb 100644 --- a/src/components/ResourceList/ResourceList.tsx +++ b/src/components/ResourceList/ResourceList.tsx @@ -43,14 +43,14 @@ const SMALL_SCREEN_WIDTH = 458; const SMALL_SPINNER_HEIGHT = 28; const LARGE_SPINNER_HEIGHT = 45; -const getAllItemsOnPage = ( - items: any, - idForItem: (item: any, index: number) => string, -) => { - return items.map((item: any, index: number) => { +function getAllItemsOnPage( + items: ItemType[], + idForItem: (item: ItemType, index: number) => string, +) { + return items.map((item: ItemType, index: number) => { return idForItem(item, index); }); -}; +} const isSmallScreen = () => { return typeof window === 'undefined' @@ -386,7 +386,7 @@ export const ResourceList: ResourceListType = function ResourceList({ } }, [selectedItems, selectMode]); - const renderItemWithId = (item: any, index: number) => { + const renderItemWithId = (item: ItemType, index: number) => { const id = idForItem(item, index); return ( @@ -399,7 +399,7 @@ export const ResourceList: ResourceListType = function ResourceList({ const handleMultiSelectionChange = ( lastSelected: number, currentSelected: number, - resolveItemId: (item: any) => string, + resolveItemId: (item: ItemType) => string, ) => { const min = Math.min(lastSelected, currentSelected); const max = Math.max(lastSelected, currentSelected); From f08bad9cda093256cfb828c63ab24305257e9f2c Mon Sep 17 00:00:00 2001 From: amrocha Date: Fri, 8 May 2020 16:06:15 -0700 Subject: [PATCH 09/17] Fix sticky header --- src/components/ResourceList/ResourceList.tsx | 67 ++++++++++---------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/src/components/ResourceList/ResourceList.tsx b/src/components/ResourceList/ResourceList.tsx index d57816c06eb..cebea4fc550 100644 --- a/src/components/ResourceList/ResourceList.tsx +++ b/src/components/ResourceList/ResourceList.tsx @@ -594,43 +594,46 @@ export const ResourceList: ResourceListType = function ResourceList({ const showEmptySearchState = !showEmptyState && filterControl && !itemsExist && !loading; + const StickyHeader = (isSticky: boolean) => { + const headerClassName = classNames( + styles.HeaderWrapper, + sortOptions && + sortOptions.length > 0 && + !alternateTool && + styles['HeaderWrapper-hasSort'], + alternateTool && styles['HeaderWrapper-hasAlternateTool'], + isSelectable && styles['HeaderWrapper-hasSelect'], + loading && styles['HeaderWrapper-disabled'], + isSelectable && selectMode && styles['HeaderWrapper-inSelectMode'], + isSticky && styles['HeaderWrapper-isSticky'], + ); + return ( +
+ + {headerWrapperOverlay} +
+ {headerTitleMarkup} + {checkableButtonMarkup} + {alternateToolMarkup} + {sortingSelectMarkup} + {selectButtonMarkup} +
+ {bulkActionsMarkup} +
+ ); + }; const headerMarkup = !showEmptyState && !showEmptySearchState && (showHeader || needsHeader) && itemsExist && (
- - {(isSticky: boolean) => { - const headerClassName = classNames( - styles.HeaderWrapper, - sortOptions && - sortOptions.length > 0 && - !alternateTool && - styles['HeaderWrapper-hasSort'], - alternateTool && styles['HeaderWrapper-hasAlternateTool'], - isSelectable && styles['HeaderWrapper-hasSelect'], - loading && styles['HeaderWrapper-disabled'], - isSelectable && - selectMode && - styles['HeaderWrapper-inSelectMode'], - isSticky && styles['HeaderWrapper-isSticky'], - ); - return ( -
- - {headerWrapperOverlay} -
- {headerTitleMarkup} - {checkableButtonMarkup} - {alternateToolMarkup} - {sortingSelectMarkup} - {selectButtonMarkup} -
- {bulkActionsMarkup} -
- ); - }} -
+ {listRef.current ? ( + + + + ) : ( + + )}
); From 18681aac97645bc3ec1107f2a307f01e2acaeacb Mon Sep 17 00:00:00 2001 From: athornburg Date: Fri, 8 May 2020 19:22:50 -0500 Subject: [PATCH 10/17] Fix the tests --- src/components/ResourceList/ResourceList.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/ResourceList/ResourceList.tsx b/src/components/ResourceList/ResourceList.tsx index cebea4fc550..11888a360c2 100644 --- a/src/components/ResourceList/ResourceList.tsx +++ b/src/components/ResourceList/ResourceList.tsx @@ -627,13 +627,9 @@ export const ResourceList: ResourceListType = function ResourceList({ (showHeader || needsHeader) && itemsExist && (
- {listRef.current ? ( - - - - ) : ( - - )} + + {StickyHeader(listRef.current !== null)} +
); From 3ab046cc7e121187130d1b08531cf7761ecf38e3 Mon Sep 17 00:00:00 2001 From: athornburg Date: Fri, 8 May 2020 21:19:47 -0500 Subject: [PATCH 11/17] Return StickyHeader function --- src/components/ResourceList/ResourceList.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/ResourceList/ResourceList.tsx b/src/components/ResourceList/ResourceList.tsx index 11888a360c2..9ee3e25e062 100644 --- a/src/components/ResourceList/ResourceList.tsx +++ b/src/components/ResourceList/ResourceList.tsx @@ -627,9 +627,7 @@ export const ResourceList: ResourceListType = function ResourceList({ (showHeader || needsHeader) && itemsExist && (
- - {StickyHeader(listRef.current !== null)} - + {StickyHeader}
); From f519a48c4ee11964f8252abb62be1e9afe670942 Mon Sep 17 00:00:00 2001 From: athornburg Date: Fri, 8 May 2020 21:54:59 -0500 Subject: [PATCH 12/17] Force an update when listRef changes --- src/components/ResourceList/ResourceList.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/ResourceList/ResourceList.tsx b/src/components/ResourceList/ResourceList.tsx index 9ee3e25e062..90a780e7c3c 100644 --- a/src/components/ResourceList/ResourceList.tsx +++ b/src/components/ResourceList/ResourceList.tsx @@ -2,6 +2,7 @@ import React, { ReactElement, useCallback, useEffect, + useReducer, useRef, useState, } from 'react'; @@ -154,6 +155,7 @@ export const ResourceList: ResourceListType = function ResourceList({ const [loadingPosition, setLoadingPositionState] = useState(0); const [lastSelected, setLastSelected] = useState(); const [smallScreen, setSmallScreen] = useState(isSmallScreen()); + const forceUpdate = useReducer((x: number) => x + 1, 0)[1]; const [checkableButtons, setCheckableButtons] = useState( new Map(), @@ -386,6 +388,10 @@ export const ResourceList: ResourceListType = function ResourceList({ } }, [selectedItems, selectMode]); + useEffect(() => { + forceUpdate(0); + }, [items, listRef.current]); + const renderItemWithId = (item: ItemType, index: number) => { const id = idForItem(item, index); @@ -625,7 +631,7 @@ export const ResourceList: ResourceListType = function ResourceList({ const headerMarkup = !showEmptyState && !showEmptySearchState && (showHeader || needsHeader) && - itemsExist && ( + listRef.current && (
{StickyHeader}
From 3c89d94536db751e6536f13470fd94a7b8a71e74 Mon Sep 17 00:00:00 2001 From: athornburg Date: Fri, 8 May 2020 21:59:10 -0500 Subject: [PATCH 13/17] inline the sticky header stuff --- src/components/ResourceList/ResourceList.tsx | 63 ++++++++++---------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/src/components/ResourceList/ResourceList.tsx b/src/components/ResourceList/ResourceList.tsx index 90a780e7c3c..be2ed9eff56 100644 --- a/src/components/ResourceList/ResourceList.tsx +++ b/src/components/ResourceList/ResourceList.tsx @@ -390,7 +390,7 @@ export const ResourceList: ResourceListType = function ResourceList({ useEffect(() => { forceUpdate(0); - }, [items, listRef.current]); + }, [forceUpdate, items]); const renderItemWithId = (item: ItemType, index: number) => { const id = idForItem(item, index); @@ -600,40 +600,43 @@ export const ResourceList: ResourceListType = function ResourceList({ const showEmptySearchState = !showEmptyState && filterControl && !itemsExist && !loading; - const StickyHeader = (isSticky: boolean) => { - const headerClassName = classNames( - styles.HeaderWrapper, - sortOptions && - sortOptions.length > 0 && - !alternateTool && - styles['HeaderWrapper-hasSort'], - alternateTool && styles['HeaderWrapper-hasAlternateTool'], - isSelectable && styles['HeaderWrapper-hasSelect'], - loading && styles['HeaderWrapper-disabled'], - isSelectable && selectMode && styles['HeaderWrapper-inSelectMode'], - isSticky && styles['HeaderWrapper-isSticky'], - ); - return ( -
- - {headerWrapperOverlay} -
- {headerTitleMarkup} - {checkableButtonMarkup} - {alternateToolMarkup} - {sortingSelectMarkup} - {selectButtonMarkup} -
- {bulkActionsMarkup} -
- ); - }; const headerMarkup = !showEmptyState && !showEmptySearchState && (showHeader || needsHeader) && listRef.current && (
- {StickyHeader} + + {(isSticky: boolean) => { + const headerClassName = classNames( + styles.HeaderWrapper, + sortOptions && + sortOptions.length > 0 && + !alternateTool && + styles['HeaderWrapper-hasSort'], + alternateTool && styles['HeaderWrapper-hasAlternateTool'], + isSelectable && styles['HeaderWrapper-hasSelect'], + loading && styles['HeaderWrapper-disabled'], + isSelectable && + selectMode && + styles['HeaderWrapper-inSelectMode'], + isSticky && styles['HeaderWrapper-isSticky'], + ); + return ( +
+ + {headerWrapperOverlay} +
+ {headerTitleMarkup} + {checkableButtonMarkup} + {alternateToolMarkup} + {sortingSelectMarkup} + {selectButtonMarkup} +
+ {bulkActionsMarkup} +
+ ); + }} +
); From ff4329703d67ef257019e2768a155ae24f69feeb Mon Sep 17 00:00:00 2001 From: athornburg Date: Fri, 8 May 2020 22:07:20 -0500 Subject: [PATCH 14/17] small refactor on the force update reducer --- src/components/ResourceList/ResourceList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ResourceList/ResourceList.tsx b/src/components/ResourceList/ResourceList.tsx index be2ed9eff56..8bb73474c56 100644 --- a/src/components/ResourceList/ResourceList.tsx +++ b/src/components/ResourceList/ResourceList.tsx @@ -155,7 +155,7 @@ export const ResourceList: ResourceListType = function ResourceList({ const [loadingPosition, setLoadingPositionState] = useState(0); const [lastSelected, setLastSelected] = useState(); const [smallScreen, setSmallScreen] = useState(isSmallScreen()); - const forceUpdate = useReducer((x: number) => x + 1, 0)[1]; + const forceUpdate = useReducer<(x: number) => number>((x = 0) => x + 1, 0)[1]; const [checkableButtons, setCheckableButtons] = useState( new Map(), @@ -389,7 +389,7 @@ export const ResourceList: ResourceListType = function ResourceList({ }, [selectedItems, selectMode]); useEffect(() => { - forceUpdate(0); + forceUpdate(); }, [forceUpdate, items]); const renderItemWithId = (item: ItemType, index: number) => { From c9ab2b8195f9d922183ec298adfc5923ccb16960 Mon Sep 17 00:00:00 2001 From: athornburg Date: Fri, 8 May 2020 22:28:45 -0500 Subject: [PATCH 15/17] Fixed the types on the force update reducer --- src/components/ResourceList/ResourceList.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/ResourceList/ResourceList.tsx b/src/components/ResourceList/ResourceList.tsx index 8bb73474c56..9068d4d9d7b 100644 --- a/src/components/ResourceList/ResourceList.tsx +++ b/src/components/ResourceList/ResourceList.tsx @@ -155,7 +155,10 @@ export const ResourceList: ResourceListType = function ResourceList({ const [loadingPosition, setLoadingPositionState] = useState(0); const [lastSelected, setLastSelected] = useState(); const [smallScreen, setSmallScreen] = useState(isSmallScreen()); - const forceUpdate = useReducer<(x: number) => number>((x = 0) => x + 1, 0)[1]; + const forceUpdate = useReducer<(x?: number) => number>( + (x = 0) => x + 1, + 0, + )[1]; const [checkableButtons, setCheckableButtons] = useState( new Map(), From 7578ec58598e774e6d071b7202db9a4c832a739f Mon Sep 17 00:00:00 2001 From: athornburg Date: Fri, 8 May 2020 22:44:18 -0500 Subject: [PATCH 16/17] Fix types on force update --- src/components/ResourceList/ResourceList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ResourceList/ResourceList.tsx b/src/components/ResourceList/ResourceList.tsx index 9068d4d9d7b..7f940b67580 100644 --- a/src/components/ResourceList/ResourceList.tsx +++ b/src/components/ResourceList/ResourceList.tsx @@ -155,7 +155,7 @@ export const ResourceList: ResourceListType = function ResourceList({ const [loadingPosition, setLoadingPositionState] = useState(0); const [lastSelected, setLastSelected] = useState(); const [smallScreen, setSmallScreen] = useState(isSmallScreen()); - const forceUpdate = useReducer<(x?: number) => number>( + const forceUpdate: (x?: number) => void = useReducer<(x?: number) => number>( (x = 0) => x + 1, 0, )[1]; From 2128b6451baff7fd8a907cc70b70682bf448c170 Mon Sep 17 00:00:00 2001 From: athornburg Date: Fri, 5 Jun 2020 17:27:17 -0500 Subject: [PATCH 17/17] Fix Unreleased --- UNRELEASED.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/UNRELEASED.md b/UNRELEASED.md index 63f0fc26181..118d7a47f88 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -6,11 +6,6 @@ ### Enhancements -- Added `ReactNode` as an accepted prop type to `primaryAction` on the `Page` component ([#3002](https://github.com/Shopify/polaris-react/pull/3002)) -- Added a `fullWidth` prop to `EmptyState` to support full width layout within a content context ([#2992](https://github.com/Shopify/polaris-react/pull/2992)) -- Added an `emptyState` prop to `ResourceList` to support in context empty states in list views ([#2569](https://github.com/Shopify/polaris-react/pull/2569)) - Set `active` prop of `Popover` to true on keyDown in `ComboBox` to fix `Autocomplete` suggestions not showing when searching and selecting via keyboard ([#3028](https://github.com/Shopify/polaris-react/pull/3028)) -- Added utilities for parsing video duration (https://polaris.shopify.com/components/images-and-icons/video-thumbnail) ([#2725](https://github.com/Shopify/polaris-react/pull/2725)) - Changed Resource List to a generic functional component ([#2843](https://github.com/Shopify/polaris-react/pull/2843)) - Made the `renderItem` function infer the type of the items prop ([#2843](https://github.com/Shopify/polaris-react/pull/2843))