From b72244378ba8c9c28e6771480dfa703b0d8ede3b Mon Sep 17 00:00:00 2001 From: Hector Vergara Date: Wed, 13 Oct 2021 15:46:49 -0300 Subject: [PATCH 1/6] improvements: isPagination/isSearchable --- docs/package.json | 1 + docs/src/components/Layout/index.tsx | 2 + .../pages/ui/primitives/collection/react.mdx | 24 ++++- packages/react/__tests__/exports.ts | 1 - .../src/primitives/Collection/Collection.tsx | 87 +++++++++++++++++-- .../react/src/primitives/Collection/utils.ts | 28 ++++++ .../primitives/Pagination/usePagination.ts | 8 +- .../primitives/SearchField/SearchField.tsx | 35 +++++--- .../react/src/primitives/types/collection.ts | 55 +++++++----- .../react/src/primitives/types/pagination.ts | 2 +- .../react/src/primitives/types/searchField.ts | 5 ++ yarn.lock | 5 ++ 12 files changed, 207 insertions(+), 46 deletions(-) create mode 100644 packages/react/src/primitives/Collection/utils.ts diff --git a/docs/package.json b/docs/package.json index 6fbfe5a771f..aba16742629 100644 --- a/docs/package.json +++ b/docs/package.json @@ -22,6 +22,7 @@ "aws-amplify-react": "latest", "aws-amplify": "latest", "classnames": "^2.3.1", + "countries-list": "^2.6.1", "gray-matter": "^4.0.3", "lodash": "^4.17.21", "mdx-prism": "^0.3.3", diff --git a/docs/src/components/Layout/index.tsx b/docs/src/components/Layout/index.tsx index a5c9da549e4..7668efb4ff1 100644 --- a/docs/src/components/Layout/index.tsx +++ b/docs/src/components/Layout/index.tsx @@ -181,7 +181,9 @@ export default function Page({ } // Dynamically set the Menu URLs to include ?platform=${filterKey} + directory.ui = directory.ui || {}; directory.ui.items = {}; + groupedPages.forEach(([folder, pages]) => { if (!folder) { return; diff --git a/docs/src/pages/ui/primitives/collection/react.mdx b/docs/src/pages/ui/primitives/collection/react.mdx index 128c9bba04f..27cc96460ea 100644 --- a/docs/src/pages/ui/primitives/collection/react.mdx +++ b/docs/src/pages/ui/primitives/collection/react.mdx @@ -1,4 +1,12 @@ -import { Card, Collection, View, Text, Heading } from '@aws-amplify/ui-react'; +import { + Button, + Card, + Collection, + View, + Text, + Heading, +} from '@aws-amplify/ui-react'; +import { countries } from 'countries-list'; import { CollectionDemo, ListCollectionExample } from './demo'; import { Example } from '@/components/Example'; @@ -74,6 +82,20 @@ const visitNewZealand = [ +## Paginated collection + +A Collection can be paginated, using the special `isPaginated` property. + +```jsx + + {(country) => ( + + )} + +``` + ### Collection types Collection `type` options include `list`, `grid`, and `table`. diff --git a/packages/react/__tests__/exports.ts b/packages/react/__tests__/exports.ts index 12c35c466cc..5227fe9fc5d 100644 --- a/packages/react/__tests__/exports.ts +++ b/packages/react/__tests__/exports.ts @@ -18,7 +18,6 @@ describe('@aws-amplify/ui-react', () => { "Card", "CheckboxField", "Collection", - "CollectionTypeMap", "ComponentClassNames", "ComponentPropsToStylePropsMap", "ComponentPropsToStylePropsMapKeys", diff --git a/packages/react/src/primitives/Collection/Collection.tsx b/packages/react/src/primitives/Collection/Collection.tsx index d3d6811dd96..e6e0d3ccd5d 100644 --- a/packages/react/src/primitives/Collection/Collection.tsx +++ b/packages/react/src/primitives/Collection/Collection.tsx @@ -1,18 +1,89 @@ -import * as React from 'react'; -import { CollectionProps } from '../types'; +import debounce from 'lodash/debounce'; +import { useCallback, useState } from 'react'; import { Flex } from '../Flex'; +import { Grid } from '../Grid'; +import { Pagination, usePagination } from '../Pagination'; +import { SearchField } from '../SearchField'; +import { strHasLength } from '../shared/utils'; +import { + CollectionProps, + GridCollectionProps, + ListCollectionProps, +} from '../types'; +import { getItemsAtPage, itemHasText } from './utils'; -export const Collection = ({ +const DEFAULT_PAGE_SIZE = 10; +const TYPEAHEAD_DELAY_MS = 300; + +const ListCollection = ({ + children, items, + ...rest +}: ListCollectionProps) => ( + {Array.isArray(items) && items.map(children)} +); + +const GridCollection = ({ children, - className, - direction = 'column', + items, + ...rest +}: GridCollectionProps) => ( + {Array.isArray(items) && items.map(children)} +); + +export const Collection = ({ + isSearchable, + isPaginated, + items, + itemsPerPage = DEFAULT_PAGE_SIZE, + searchFilter = itemHasText, type = 'list', ...rest -}: CollectionProps): JSX.Element => { +}: CollectionProps): JSX.Element => { + const [searchText, setSearchText] = useState(); + + const onSearch = useCallback(debounce(setSearchText, TYPEAHEAD_DELAY_MS), [ + setSearchText, + ]); + + // Filter items by text + if (isSearchable && strHasLength(searchText)) { + items = items.filter((item) => searchFilter(item, searchText)); + } + + // Pagination + const pagination = usePagination({ + totalPages: Math.floor(items.length / itemsPerPage), + }); + + if (isPaginated) { + items = getItemsAtPage(items, pagination.currentPage, itemsPerPage); + } + return ( - - {Array.isArray(items) && items.map(children)} + + {isSearchable && ( + + onSearch(e.target.value)} + onClear={() => setSearchText('')} + /> + + )} + + {type === 'list' ? ( + + ) : type === 'grid' ? ( + + ) : null} + + {isPaginated && ( + + + + )} ); }; diff --git a/packages/react/src/primitives/Collection/utils.ts b/packages/react/src/primitives/Collection/utils.ts new file mode 100644 index 00000000000..7e0dbb0e208 --- /dev/null +++ b/packages/react/src/primitives/Collection/utils.ts @@ -0,0 +1,28 @@ +import { strHasLength } from '../shared/utils'; + +/** + * Slice a collection based on page index (starting at 1) + */ +export const getItemsAtPage = ( + items: T[], + page: number, + itemsPerPage: number +) => { + const startIndex = (page - 1) * itemsPerPage; + return items.slice(startIndex, startIndex + itemsPerPage); +}; + +/** + * Recursively find a keyword within an object (case insensitive) + */ +export const itemHasText = (item: unknown, text: string): boolean => { + if (strHasLength(item)) { + return item.toLowerCase().includes(text.toLowerCase()); + } + + if (typeof item === 'object') { + return Object.values(item).some((subItem) => itemHasText(subItem, text)); + } + + return false; +}; diff --git a/packages/react/src/primitives/Pagination/usePagination.ts b/packages/react/src/primitives/Pagination/usePagination.ts index 6a6c9ac90a3..585b66dd824 100644 --- a/packages/react/src/primitives/Pagination/usePagination.ts +++ b/packages/react/src/primitives/Pagination/usePagination.ts @@ -1,11 +1,11 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { UsePaginationProps, UsePaginationResult } from '../types/pagination'; export const usePagination = ( props: UsePaginationProps ): UsePaginationResult => { - let { currentPage: initialPage, totalPages, siblingCount = 1 } = props; + let { currentPage: initialPage = 1, totalPages, siblingCount = 1 } = props; // The current page should not be less than 1 initialPage = Math.max(initialPage, 1); @@ -13,9 +13,11 @@ export const usePagination = ( siblingCount = Math.max(siblingCount, 1); // The total pages should be always greater than current page totalPages = Math.max(initialPage, totalPages); - const [currentPage, setCurrentPage] = useState(initialPage); + // Reset current page if initialPage or totalPages changes + useEffect(() => setCurrentPage(initialPage), [initialPage, totalPages]); + const onNext = useCallback(() => { if (currentPage < totalPages) { setCurrentPage(currentPage + 1); diff --git a/packages/react/src/primitives/SearchField/SearchField.tsx b/packages/react/src/primitives/SearchField/SearchField.tsx index 77aa5fe7dc5..69cc58781af 100644 --- a/packages/react/src/primitives/SearchField/SearchField.tsx +++ b/packages/react/src/primitives/SearchField/SearchField.tsx @@ -5,21 +5,30 @@ import { ComponentClassNames } from '../shared/constants'; import { TextField } from '../TextField'; import { FieldClearButton } from '../Field'; import { SearchFieldButton } from './SearchFieldButton'; -import { strHasLength } from '../shared/utils'; +import { isFunction, strHasLength } from '../shared/utils'; import { SearchFieldProps, InputProps, ButtonProps } from '../types'; const ESCAPE_KEY = 'Escape'; const ENTER_KEY = 'Enter'; const DEFAULT_KEYS = [ESCAPE_KEY, ENTER_KEY]; -export const useSearchField = (onSubmit: SearchFieldProps['onSubmit']) => { +export const useSearchField = ({ + onSubmit, + onClear, +}: Partial) => { const [value, setValue] = React.useState(''); - const clearValue = React.useCallback(() => setValue(''), [setValue]); + const onClearHandler = React.useCallback(() => { + setValue(''); + + if (isFunction(onClear)) { + onClear(); + } + }, [setValue, onClear]); const onSubmitHandler = React.useCallback( (value: string) => { - if (onSubmit) { + if (isFunction(onSubmit)) { onSubmit(value); } }, @@ -33,14 +42,14 @@ export const useSearchField = (onSubmit: SearchFieldProps['onSubmit']) => { if (DEFAULT_KEYS.includes(key)) { event.preventDefault(); } + if (key === ESCAPE_KEY) { - clearValue(); - } - if (key === ENTER_KEY) { + onClearHandler(); + } else if (key === ENTER_KEY) { onSubmitHandler(value); } }, - [value, clearValue, onSubmitHandler] + [value, onClearHandler, onSubmitHandler] ); const onInput: InputProps['onInput'] = React.useCallback( @@ -56,7 +65,7 @@ export const useSearchField = (onSubmit: SearchFieldProps['onSubmit']) => { return { value, - clearValue, + onClearHandler, onKeyDown, onInput, onClick, @@ -70,11 +79,13 @@ export const SearchField: React.FC = ({ label, name = 'q', onSubmit, + onClear, size, ...rest }) => { - const { value, clearValue, onInput, onKeyDown, onClick } = - useSearchField(onSubmit); + const { value, onClearHandler, onInput, onKeyDown, onClick } = useSearchField( + { onSubmit, onClear } + ); return ( = ({ innerEndComponent={ - extends BaseStyleProps { - /* +export interface CollectionWrapperProps extends BaseStyleProps { + /** * Collection type. This will be used to determine collection wrapper component. * @default 'list' */ type?: CollectionType; + /** + * Enable pagination for collection items + */ + isPaginated?: boolean; + + /** + * Page size (when pagination is enabled) + */ + itemsPerPage?: number; + + /** + * Enable collection filtering + */ + isSearchable?: boolean; + + /** + * Custom search filter (when search is enabled) + */ + searchFilter?: (item: unknown, searchText: string) => boolean; +} + +export interface CollectionBaseProps { /* * Data source. Items to be repeated over the collection. */ - items: Array; + items: Array; /* * The component to be repeated * Same interface as Array.prototype.map */ - children: (item: CollectionItemType, index: number) => JSX.Element; + children: (item: Item, index: number) => JSX.Element; } -// @TODO Add GridCollectionProps and TableCollectionProps -export type ListCollectionProps = - CollectionBaseProps & FlexProps & { type: 'list' }; +// @TODO Add TableCollectionProps +export type ListCollectionProps = FlexProps & CollectionBaseProps; +export type GridCollectionProps = GridProps & CollectionBaseProps; -export type CollectionProps = - ListCollectionProps; +export type CollectionProps = CollectionWrapperProps & + ( + | ({ type: 'list' } & ListCollectionProps) + | ({ type: 'grid' } & GridCollectionProps) + ); diff --git a/packages/react/src/primitives/types/pagination.ts b/packages/react/src/primitives/types/pagination.ts index e8a5fe340e7..038310a4418 100644 --- a/packages/react/src/primitives/types/pagination.ts +++ b/packages/react/src/primitives/types/pagination.ts @@ -9,7 +9,7 @@ interface BasePaginationProps { /** * Index of the current page. (starting from 1) */ - currentPage: number; + currentPage?: number; /** * Total number of available pages. diff --git a/packages/react/src/primitives/types/searchField.ts b/packages/react/src/primitives/types/searchField.ts index 01744c330c7..6cab6bef4ba 100644 --- a/packages/react/src/primitives/types/searchField.ts +++ b/packages/react/src/primitives/types/searchField.ts @@ -7,6 +7,11 @@ export interface SearchFieldProps extends TextFieldProps { */ onSubmit?: (value: string) => void; + /** + * Triggered when search field is cleared + */ + onClear?: () => void; + /** * Visually hide label * @default true diff --git a/yarn.lock b/yarn.lock index c58679c25a5..4699fe10a30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8671,6 +8671,11 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: path-type "^4.0.0" yaml "^1.10.0" +countries-list@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/countries-list/-/countries-list-2.6.1.tgz#d479757ac873b1e596ccea0a925962d20396c0cb" + integrity sha512-jXM1Nv3U56dPQ1DsUSsEaGmLHburo4fnB7m+1yhWDUVvx5gXCd1ok/y3gXCjXzhqyawG+igcPYcAl4qjkvopaQ== + create-ecdh@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" From c9850dc61c01f179cedddd2ffab41020c14938e1 Mon Sep 17 00:00:00 2001 From: Hector Vergara Date: Fri, 15 Oct 2021 03:13:39 -0300 Subject: [PATCH 2/6] Add search placeholder property --- .../src/primitives/Collection/Collection.tsx | 24 ++++++++++++++----- .../react/src/primitives/types/collection.ts | 5 ++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/react/src/primitives/Collection/Collection.tsx b/packages/react/src/primitives/Collection/Collection.tsx index e6e0d3ccd5d..9a3674fc0d4 100644 --- a/packages/react/src/primitives/Collection/Collection.tsx +++ b/packages/react/src/primitives/Collection/Collection.tsx @@ -37,6 +37,7 @@ export const Collection = ({ items, itemsPerPage = DEFAULT_PAGE_SIZE, searchFilter = itemHasText, + searchPlaceholder, type = 'list', ...rest }: CollectionProps): JSX.Element => { @@ -46,6 +47,9 @@ export const Collection = ({ setSearchText, ]); + // Make sure that items are iterable + items = Array.isArray(items) ? items : []; + // Filter items by text if (isSearchable && strHasLength(searchText)) { items = items.filter((item) => searchFilter(item, searchText)); @@ -60,24 +64,32 @@ export const Collection = ({ items = getItemsAtPage(items, pagination.currentPage, itemsPerPage); } + const collection = + type === 'list' ? ( + + ) : type === 'grid' ? ( + + ) : null; + + if (!isSearchable && !isPaginated) { + return collection; + } + return ( {isSearchable && ( - + onSearch(e.target.value)} onClear={() => setSearchText('')} /> )} - {type === 'list' ? ( - - ) : type === 'grid' ? ( - - ) : null} + {collection} {isPaginated && ( diff --git a/packages/react/src/primitives/types/collection.ts b/packages/react/src/primitives/types/collection.ts index 7971e36d9c6..0fab124900c 100644 --- a/packages/react/src/primitives/types/collection.ts +++ b/packages/react/src/primitives/types/collection.ts @@ -30,6 +30,11 @@ export interface CollectionWrapperProps extends BaseStyleProps { * Custom search filter (when search is enabled) */ searchFilter?: (item: unknown, searchText: string) => boolean; + + /** + * Search field placeholder + */ + searchPlaceholder?: string; } export interface CollectionBaseProps { From ab5bca1d73ec62bbd34bbce16e6173795ae6175a Mon Sep 17 00:00:00 2001 From: Hector Vergara Date: Fri, 15 Oct 2021 03:14:03 -0300 Subject: [PATCH 3/6] Add documentation & tests --- .../pages/ui/primitives/collection/react.mdx | 97 +++++++++++++++---- .../Collection/__tests__/Collection.test.tsx | 46 ++++++++- .../src/primitives/Pagination/Pagination.tsx | 2 + .../primitives/SearchField/SearchField.tsx | 4 +- yarn.lock | 15 --- 5 files changed, 126 insertions(+), 38 deletions(-) diff --git a/docs/src/pages/ui/primitives/collection/react.mdx b/docs/src/pages/ui/primitives/collection/react.mdx index 27cc96460ea..df164bd9197 100644 --- a/docs/src/pages/ui/primitives/collection/react.mdx +++ b/docs/src/pages/ui/primitives/collection/react.mdx @@ -2,6 +2,7 @@ import { Button, Card, Collection, + Flex, View, Text, Heading, @@ -10,7 +11,7 @@ import { countries } from 'countries-list'; import { CollectionDemo, ListCollectionExample } from './demo'; import { Example } from '@/components/Example'; -A Collection wraps Flex and Grid components, and provides a way to display items in a collection from a data source. They come in `paginated` and `infiniteScrolling` versions. Both versions need paginated data. +A Collection wraps Flex and Grid components, and provides a way to display items in a collection from a data source. ## Demo @@ -82,25 +83,11 @@ const visitNewZealand = [ -## Paginated collection - -A Collection can be paginated, using the special `isPaginated` property. - -```jsx - - {(country) => ( - - )} - -``` - -### Collection types +## Collection types Collection `type` options include `list`, `grid`, and `table`. -**List collection type** +### List The `list` collection type can be customized with any of following Flex props: `alignItems`, `alignContent`, `direction`, `gap`, `justifyContent`, `wrap`. @@ -155,10 +142,78 @@ The `list` collection type can be customized with any of following Flex props: ` -#### Grid collection type +## Pagination + +A Collection can be paginated, adding a special `isPaginated` property. Change the page size passing a `itemsPerPage` property (default: 10). + +```jsx + + {(country) => ( + + )} + +``` + + + ({ name, emoji }))} + isPaginated + itemsPerPage={12} + > + {(country) => ( + + )} + + + +## Search + +Collections can also be filtered, adding a `isSearchable` property. Pass a custom `searchFilter` function to enhance your search experience. -TBD +```jsx +const startsWith = (country, keyword) => country.name.startsWith(keyword) -#### Table collection type + + {(country) => ( + + )} + +``` -TBD + + ({ name, emoji }))} + isSearchable + isPaginated + itemsPerPage={9} + searchPlaceholder="Type to search..." + searchFilter={(country, keyword) => + country.name.toLowerCase().startsWith(keyword.toLowerCase()) + } + > + {(country) => ( + + )} + + diff --git a/packages/react/src/primitives/Collection/__tests__/Collection.test.tsx b/packages/react/src/primitives/Collection/__tests__/Collection.test.tsx index 23f90beb4b6..777a5c50493 100644 --- a/packages/react/src/primitives/Collection/__tests__/Collection.test.tsx +++ b/packages/react/src/primitives/Collection/__tests__/Collection.test.tsx @@ -3,6 +3,7 @@ import { kebabCase } from 'lodash'; import { Collection } from '../Collection'; import { ComponentPropsToStylePropsMap } from '../../types'; +import { ComponentClassNames } from '../../shared/constants'; const emojis = [ { @@ -22,9 +23,52 @@ const emojis = [ describe('Collection component', () => { const testList = 'testList'; + it('should render a Search box when isSearchable is true', async () => { + render( + + {(item, index) => ( +
+ {item.emoji} +
+ )} +
+ ); + + const searchField = await screen.findByRole('searchbox'); + expect(searchField).not.toBe(undefined); + }); + + it('should render pagination when isPaginated is true', async () => { + render( + + {(item, index) => ( +
+ {item.emoji} +
+ )} +
+ ); + + const navigation = await screen.findByRole('navigation'); + + expect(navigation.classList).toContain(ComponentClassNames.Pagination); + expect(navigation).not.toBe(undefined); + }); + it('should render Flex when rendering list collection', async () => { render( - + {(item, index) => (
{item.emoji} diff --git a/packages/react/src/primitives/Pagination/Pagination.tsx b/packages/react/src/primitives/Pagination/Pagination.tsx index c220dcf3f04..2192f4fd4f1 100644 --- a/packages/react/src/primitives/Pagination/Pagination.tsx +++ b/packages/react/src/primitives/Pagination/Pagination.tsx @@ -16,6 +16,7 @@ export const Pagination: React.FC = (props) => { onNext, onPrevious, onChange, + role = 'navigation', ...rest } = props; @@ -31,6 +32,7 @@ export const Pagination: React.FC = (props) => { return ( diff --git a/packages/react/src/primitives/SearchField/SearchField.tsx b/packages/react/src/primitives/SearchField/SearchField.tsx index 69cc58781af..e8e81c9f305 100644 --- a/packages/react/src/primitives/SearchField/SearchField.tsx +++ b/packages/react/src/primitives/SearchField/SearchField.tsx @@ -77,7 +77,8 @@ export const SearchField: React.FC = ({ className, labelHidden = true, label, - name = 'q', + name, + role = 'searchbox', onSubmit, onClear, size, @@ -106,6 +107,7 @@ export const SearchField: React.FC = ({ onInput={onInput} onKeyDown={onKeyDown} outerEndComponent={} + role={role} size={size} value={value} {...rest} diff --git a/yarn.lock b/yarn.lock index 4699fe10a30..566b22b4d82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -413,14 +413,6 @@ qrcode "^1.4.4" uuid "^8.2.0" -"@aws-amplify/ui-components@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@aws-amplify/ui-components/-/ui-components-1.9.0.tgz#c1b72a071ac75e6bf650aece0983f7243b826279" - integrity sha512-fkYBaM3WDSJOGmpROlgzg638D19ai/3Z4CQ4Cbm/HKNsooDRyq3yXY3Y/2L/+4OMCnt9NTQ9DjnOjbP8wFHQxw== - dependencies: - qrcode "^1.4.4" - uuid "^8.2.0" - "@aws-amplify/ui-react-v1@npm:@aws-amplify/ui-react": version "1.2.19" resolved "https://registry.yarnpkg.com/@aws-amplify/ui-react/-/ui-react-1.2.19.tgz#8721fb33151522faf2a49aaf1ff641c809260927" @@ -435,13 +427,6 @@ dependencies: "@aws-amplify/ui-components" "1.7.2" -"@aws-amplify/ui-react@^1.2.5": - version "1.2.20" - resolved "https://registry.yarnpkg.com/@aws-amplify/ui-react/-/ui-react-1.2.20.tgz#913433348cb54dd836d236bd011617f869831d0a" - integrity sha512-R2znqKXc/kPwzUUIIOaQnM7PIo7fu1dupz6r+t7eUlhkWa8pPSYS0UantQWEyZi7yGh0rkhPRcmZQ7baqlkJhw== - dependencies: - "@aws-amplify/ui-components" "1.9.0" - "@aws-amplify/ui@2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@aws-amplify/ui/-/ui-2.0.3.tgz#7a88a416942aedbc6a6ea9850a2c98693c80340a" From 5560f866852082f016d387889fb1cce8b105a732 Mon Sep 17 00:00:00 2001 From: Hector Vergara Date: Fri, 15 Oct 2021 03:15:42 -0300 Subject: [PATCH 4/6] Restore docs layout file --- docs/src/components/Layout/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/src/components/Layout/index.tsx b/docs/src/components/Layout/index.tsx index 7668efb4ff1..a5c9da549e4 100644 --- a/docs/src/components/Layout/index.tsx +++ b/docs/src/components/Layout/index.tsx @@ -181,9 +181,7 @@ export default function Page({ } // Dynamically set the Menu URLs to include ?platform=${filterKey} - directory.ui = directory.ui || {}; directory.ui.items = {}; - groupedPages.forEach(([folder, pages]) => { if (!folder) { return; From 603e1e30738400c7481f27a9f215b1f74d4200ca Mon Sep 17 00:00:00 2001 From: Hector Vergara Date: Fri, 15 Oct 2021 17:20:54 -0300 Subject: [PATCH 5/6] Add missing collection search Flex style props --- packages/react/src/primitives/Collection/Collection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/primitives/Collection/Collection.tsx b/packages/react/src/primitives/Collection/Collection.tsx index 9a3674fc0d4..30a55f23cc0 100644 --- a/packages/react/src/primitives/Collection/Collection.tsx +++ b/packages/react/src/primitives/Collection/Collection.tsx @@ -78,7 +78,7 @@ export const Collection = ({ return ( {isSearchable && ( - + Date: Tue, 26 Oct 2021 23:52:27 -0300 Subject: [PATCH 6/6] Address PR comments --- .../pages/ui/primitives/collection/react.mdx | 2 +- .../src/primitives/Collection/Collection.tsx | 41 +++++++----- .../Collection/__tests__/Collection.test.tsx | 66 +++++++++++++------ .../Collection/__tests__/utils.test.ts | 61 +++++++++++++++++ .../react/src/primitives/Collection/utils.ts | 4 ++ .../src/primitives/Pagination/Pagination.tsx | 2 - .../primitives/SearchField/SearchField.tsx | 4 +- .../react/src/primitives/shared/constants.ts | 4 ++ packages/react/src/primitives/shared/i18n.ts | 3 + .../src/theme/css/component/collection.scss | 12 ++++ packages/ui/src/theme/css/styles.scss | 1 + 11 files changed, 159 insertions(+), 41 deletions(-) create mode 100644 packages/react/src/primitives/Collection/__tests__/utils.test.ts create mode 100644 packages/ui/src/theme/css/component/collection.scss diff --git a/docs/src/pages/ui/primitives/collection/react.mdx b/docs/src/pages/ui/primitives/collection/react.mdx index df164bd9197..321811d7132 100644 --- a/docs/src/pages/ui/primitives/collection/react.mdx +++ b/docs/src/pages/ui/primitives/collection/react.mdx @@ -175,7 +175,7 @@ A Collection can be paginated, adding a special `isPaginated` property. Change t ## Search -Collections can also be filtered, adding a `isSearchable` property. Pass a custom `searchFilter` function to enhance your search experience. +Collections can also be filtered, adding a `isSearchable` property. Pass a custom `searchFilter` function to enhance your search experience (default search filter looks for any string-like property inside of items) ```jsx const startsWith = (country, keyword) => country.name.startsWith(keyword) diff --git a/packages/react/src/primitives/Collection/Collection.tsx b/packages/react/src/primitives/Collection/Collection.tsx index 14fcc7a4cac..2314c475bab 100644 --- a/packages/react/src/primitives/Collection/Collection.tsx +++ b/packages/react/src/primitives/Collection/Collection.tsx @@ -1,9 +1,12 @@ +import classNames from 'classnames'; import debounce from 'lodash/debounce'; import { useCallback, useState } from 'react'; import { Flex } from '../Flex'; import { Grid } from '../Grid'; import { Pagination, usePagination } from '../Pagination'; import { SearchField } from '../SearchField'; +import { ComponentClassNames } from '../shared/constants'; +import { SharedText } from '../shared/i18n'; import { strHasLength } from '../shared/utils'; import { CollectionProps, @@ -35,6 +38,7 @@ const GridCollection = ({ ); export const Collection = ({ + className, isSearchable, isPaginated, items, @@ -42,6 +46,7 @@ export const Collection = ({ searchFilter = itemHasText, searchPlaceholder, type = 'list', + testId, ...rest }: CollectionProps): JSX.Element => { const [searchText, setSearchText] = useState(); @@ -69,36 +74,42 @@ export const Collection = ({ const collection = type === 'list' ? ( - + ) : type === 'grid' ? ( - + ) : null; - if (!isSearchable && !isPaginated) { - return collection; - } - return ( - - {isSearchable && ( - + + {isSearchable ? ( + onSearch(e.target.value)} onClear={() => setSearchText('')} /> - )} + ) : null} {collection} - {isPaginated && ( - + {isPaginated ? ( + - )} + ) : null} ); }; diff --git a/packages/react/src/primitives/Collection/__tests__/Collection.test.tsx b/packages/react/src/primitives/Collection/__tests__/Collection.test.tsx index 777a5c50493..07661143a49 100644 --- a/packages/react/src/primitives/Collection/__tests__/Collection.test.tsx +++ b/packages/react/src/primitives/Collection/__tests__/Collection.test.tsx @@ -20,12 +20,23 @@ const emojis = [ }, ]; +const getElementByClassName = ( + element: HTMLElement, + className: string +) => element.querySelector(`.${className}`); + describe('Collection component', () => { const testList = 'testList'; it('should render a Search box when isSearchable is true', async () => { render( - + {(item, index) => (
{item.emoji} @@ -34,8 +45,13 @@ describe('Collection component', () => { ); - const searchField = await screen.findByRole('searchbox'); - expect(searchField).not.toBe(undefined); + const collection = await screen.findByTestId('testList'); + const search = getElementByClassName( + collection, + ComponentClassNames.CollectionSearch + ); + + expect(search).not.toBe(null); }); it('should render pagination when isPaginated is true', async () => { @@ -55,13 +71,16 @@ describe('Collection component', () => { ); - const navigation = await screen.findByRole('navigation'); + const collection = await screen.findByTestId(testList); + const pagination = getElementByClassName( + collection, + ComponentClassNames.CollectionPagination + ); - expect(navigation.classList).toContain(ComponentClassNames.Pagination); - expect(navigation).not.toBe(undefined); + expect(pagination).not.toBe(null); }); - it('should render Flex when rendering list collection', async () => { + it('should render Flex when rendering list collection items', async () => { render( { )} ); - const collection = (await screen.findByTestId(testList)) as HTMLDivElement; + + const collection = await screen.findByTestId(testList); + const items = getElementByClassName( + collection, + ComponentClassNames.CollectionItems + ); + expect( - collection.style.getPropertyValue( + items.style.getPropertyValue( kebabCase(ComponentPropsToStylePropsMap.direction) ) ).toBe('column'); - expect(collection.children[0].getAttribute('aria-label')).toBe('LOL'); + + expect(items.children[0].getAttribute('aria-label')).toBe('LOL'); }); it('should not throw when items is not an array', () => { @@ -114,15 +140,9 @@ describe('Collection component', () => { expect(collection.classList.contains('custom-collection')).toBe(true); }); - it('can render any arbitrary data-* attribute', async () => { + it('can render arbitrary attributes to items container', async () => { render( - + {(item, index) => (
{item.emoji} @@ -130,7 +150,13 @@ describe('Collection component', () => { )} ); - const view = await screen.findByTestId(testList); - expect(view.dataset['demo']).toBe('true'); + + const collection = await screen.findByTestId(testList); + const items = getElementByClassName( + collection, + ComponentClassNames.CollectionItems + ); + + expect(items.dataset['demo']).toBe('true'); }); }); diff --git a/packages/react/src/primitives/Collection/__tests__/utils.test.ts b/packages/react/src/primitives/Collection/__tests__/utils.test.ts new file mode 100644 index 00000000000..95e9774437c --- /dev/null +++ b/packages/react/src/primitives/Collection/__tests__/utils.test.ts @@ -0,0 +1,61 @@ +import { getItemsAtPage, itemHasText } from '../utils'; + +describe('getItemsAtPage', () => { + const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + + it('should return expected items', () => { + expect(getItemsAtPage(items, 2, 3)).toStrictEqual([4, 5, 6]); + expect(getItemsAtPage(items, 5, 1)).toStrictEqual([5]); + expect(getItemsAtPage(items, 3, 3)).toStrictEqual([7, 8, 9]); + }); + + it('should return no items if page is lower than 1 ', () => { + expect(getItemsAtPage(items, 0, 3)).toHaveLength(0); + expect(getItemsAtPage(items, -3, 3)).toHaveLength(0); + expect(getItemsAtPage(items, -10, 3)).toHaveLength(0); + }); + + it('should return no items if page is greater than available pages', () => { + expect(getItemsAtPage(items, 5, 3)).toHaveLength(0); + expect(getItemsAtPage(items, 10, 3)).toHaveLength(0); + expect(getItemsAtPage(items, 30, 3)).toHaveLength(0); + }); +}); + +describe('itemsHasText', () => { + it("should return false if item doesn't match provided text", () => { + const result = itemHasText('hello world', 'not found'); + expect(result).toBe(false); + }); + + it('should return true if item matches provided text', () => { + const result = itemHasText('hello world', 'o wo'); + expect(result).toBe(true); + }); + + it('should return true if any of item properties matches provided text', () => { + const obj = { + very: { + nested: { + property: 'this is a secret', + }, + }, + }; + + const result = itemHasText(obj, 'secret'); + expect(result).toBe(true); + }); + + it('should return false if no item properties matches provided text', () => { + const obj = { + very: { + nested: { + property: 'this is a secret', + }, + }, + }; + + const result = itemHasText(obj, 'not found'); + expect(result).toBe(false); + }); +}); diff --git a/packages/react/src/primitives/Collection/utils.ts b/packages/react/src/primitives/Collection/utils.ts index 7e0dbb0e208..23377c5a51d 100644 --- a/packages/react/src/primitives/Collection/utils.ts +++ b/packages/react/src/primitives/Collection/utils.ts @@ -8,6 +8,10 @@ export const getItemsAtPage = ( page: number, itemsPerPage: number ) => { + if (page < 1 || itemsPerPage < 1) { + return []; + } + const startIndex = (page - 1) * itemsPerPage; return items.slice(startIndex, startIndex + itemsPerPage); }; diff --git a/packages/react/src/primitives/Pagination/Pagination.tsx b/packages/react/src/primitives/Pagination/Pagination.tsx index e185135b1bb..5333943a1cf 100644 --- a/packages/react/src/primitives/Pagination/Pagination.tsx +++ b/packages/react/src/primitives/Pagination/Pagination.tsx @@ -14,7 +14,6 @@ export const Pagination: Primitive = ({ onNext, onPrevious, onChange, - role = 'navigation', ...rest }) => { const paginationItems = usePaginationItems( @@ -29,7 +28,6 @@ export const Pagination: Primitive = ({ return ( diff --git a/packages/react/src/primitives/SearchField/SearchField.tsx b/packages/react/src/primitives/SearchField/SearchField.tsx index e8a9bb28660..a228dcfffdb 100644 --- a/packages/react/src/primitives/SearchField/SearchField.tsx +++ b/packages/react/src/primitives/SearchField/SearchField.tsx @@ -77,8 +77,7 @@ export const SearchField: Primitive = ({ className, labelHidden = true, label, - name, - role = 'searchbox', + name = 'q', onSubmit, onClear, size, @@ -108,7 +107,6 @@ export const SearchField: Primitive = ({ onInput={onInput} onKeyDown={onKeyDown} outerEndComponent={} - role={role} size={size} value={value} {...rest} diff --git a/packages/react/src/primitives/shared/constants.ts b/packages/react/src/primitives/shared/constants.ts index 59b714fe32f..fcf19a78b89 100644 --- a/packages/react/src/primitives/shared/constants.ts +++ b/packages/react/src/primitives/shared/constants.ts @@ -11,6 +11,10 @@ export enum ComponentClassNames { CheckboxInput = 'amplify-checkbox__input', CheckboxLabel = 'amplify-checkbox__label', CheckboxField = 'amplify-checkboxfield', + Collection = 'amplify-collection', + CollectionItems = 'amplify-collection-items', + CollectionSearch = 'amplify-collection-search', + CollectionPagination = 'amplify-collection-pagination', CountryCodeSelect = 'amplify-countrycodeselect', Divider = 'amplify-divider', Field = 'amplify-field', diff --git a/packages/react/src/primitives/shared/i18n.ts b/packages/react/src/primitives/shared/i18n.ts index db571690e28..77093ebd819 100644 --- a/packages/react/src/primitives/shared/i18n.ts +++ b/packages/react/src/primitives/shared/i18n.ts @@ -24,4 +24,7 @@ export const SharedText = { DecreaseTo: 'Decrease to', }, }, + Collection: { + SearchFieldLabel: 'Search', + }, }; diff --git a/packages/ui/src/theme/css/component/collection.scss b/packages/ui/src/theme/css/component/collection.scss new file mode 100644 index 00000000000..a49d1ba2924 --- /dev/null +++ b/packages/ui/src/theme/css/component/collection.scss @@ -0,0 +1,12 @@ +.amplify-collection { + flex-direction: column; +} + +.amplify-collection-search { + flex-direction: row; + justify-content: center; +} + +.amplify-collection-pagination { + justify-content: center; +} diff --git a/packages/ui/src/theme/css/styles.scss b/packages/ui/src/theme/css/styles.scss index bf44f4b81b6..9f32019c2c0 100644 --- a/packages/ui/src/theme/css/styles.scss +++ b/packages/ui/src/theme/css/styles.scss @@ -50,6 +50,7 @@ html { @import './component/alert.scss'; @import './component/authenticator.scss'; @import './component/card.scss'; +@import './component/collection.scss'; @import './component/checkbox.scss'; @import './component/checkboxField.scss'; @import './component/divider.scss';