From 3c50dab5dc4aef277422fbb9992dc3fdf622b2b0 Mon Sep 17 00:00:00 2001 From: Justin Conabree Date: Mon, 19 Apr 2021 13:17:24 -0400 Subject: [PATCH 01/15] Adding FilterSidebar component for desktop, refactor category and search layout and styles for sidebar, keep filter accordion open if selected or in first 3 (#3115) --- .../i18n/fr_FR.json | 1 + .../lib/talons/FilterModal/useFilterBlock.js | 10 +- .../lib/talons/FilterModal/useFilterModal.js | 6 ++ packages/venia-ui/i18n/en_US.json | 1 + .../lib/RootComponents/Category/category.css | 42 ++++++++ .../Category/categoryContent.js | 34 +++--- .../FilterModal/FilterList/filterItem.js | 15 ++- .../FilterModal/FilterList/filterList.js | 13 ++- .../lib/components/FilterModal/filterBlock.js | 25 +++-- .../FilterSidebar/filterSidebar.css | 37 +++++++ .../components/FilterSidebar/filterSidebar.js | 102 ++++++++++++++++++ .../lib/components/FilterSidebar/index.js | 1 + .../components/ProductSort/productSort.css | 14 +++ .../lib/components/ProductSort/productSort.js | 19 +++- .../lib/components/SearchPage/searchPage.css | 66 +++++++++++- .../lib/components/SearchPage/searchPage.js | 43 +++++--- 16 files changed, 378 insertions(+), 51 deletions(-) create mode 100644 packages/venia-ui/lib/components/FilterSidebar/filterSidebar.css create mode 100644 packages/venia-ui/lib/components/FilterSidebar/filterSidebar.js create mode 100644 packages/venia-ui/lib/components/FilterSidebar/index.js diff --git a/packages/extensions/venia-sample-language-packs/i18n/fr_FR.json b/packages/extensions/venia-sample-language-packs/i18n/fr_FR.json index d376546018..8b63176fda 100644 --- a/packages/extensions/venia-sample-language-packs/i18n/fr_FR.json +++ b/packages/extensions/venia-sample-language-packs/i18n/fr_FR.json @@ -280,6 +280,7 @@ "productListing.loading": "Récupération du panier ...", "productOptions.selectedLabel": "Choisie {label}:", "productQuantity.label": "quantité de produit", + "productSort.sortByButton": "Trier par", "productSort.sortButton": "Trier", "postcode.label": "ZIP / Code Postal", "quantity.buttonDecrement": "Diminuer la quantité", diff --git a/packages/peregrine/lib/talons/FilterModal/useFilterBlock.js b/packages/peregrine/lib/talons/FilterModal/useFilterBlock.js index fcca17b8ea..cfa8ed21bb 100644 --- a/packages/peregrine/lib/talons/FilterModal/useFilterBlock.js +++ b/packages/peregrine/lib/talons/FilterModal/useFilterBlock.js @@ -1,7 +1,11 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useState, useEffect } from 'react'; -export const useFilterBlock = () => { - const [isExpanded, setExpanded] = useState(false); +export const useFilterBlock = (hasSelectedElements = false, initiallyOpen = false) => { + const [isExpanded, setExpanded] = useState(hasSelectedElements || initiallyOpen); + + useEffect(() => { + setExpanded(hasSelectedElements || initiallyOpen); + }, [hasSelectedElements, initiallyOpen]); const handleClick = useCallback(() => { setExpanded(value => !value); diff --git a/packages/peregrine/lib/talons/FilterModal/useFilterModal.js b/packages/peregrine/lib/talons/FilterModal/useFilterModal.js index 71b839ecf2..e92d250994 100644 --- a/packages/peregrine/lib/talons/FilterModal/useFilterModal.js +++ b/packages/peregrine/lib/talons/FilterModal/useFilterModal.js @@ -140,6 +140,12 @@ export const useFilterModal = props => { prevDrawer.current = drawer; }, [drawer, filterApi, filterItems, filterKeys, search]); + useEffect(() => { + const nextState = getStateFromSearch(search, filterKeys, filterItems); + + filterApi.setItems(nextState); + }, [filterApi, filterItems, filterKeys, search]); + const handleApply = useCallback(() => { setIsApplying(true); closeDrawer(); diff --git a/packages/venia-ui/i18n/en_US.json b/packages/venia-ui/i18n/en_US.json index a6923ee25c..d1e29186eb 100644 --- a/packages/venia-ui/i18n/en_US.json +++ b/packages/venia-ui/i18n/en_US.json @@ -305,6 +305,7 @@ "productListing.loading": "Fetching Cart...", "productOptions.selectedLabel": "Selected {label}:", "productQuantity.label": "product's quantity", + "productSort.sortByButton": "Sort by", "productSort.sortButton": "Sort", "quantity.buttonDecrement": "Decrease Quantity", "quantity.buttonIncrement": "Increase Quantity", diff --git a/packages/venia-ui/lib/RootComponents/Category/category.css b/packages/venia-ui/lib/RootComponents/Category/category.css index d589a6f023..fa1f501cea 100644 --- a/packages/venia-ui/lib/RootComponents/Category/category.css +++ b/packages/venia-ui/lib/RootComponents/Category/category.css @@ -1,3 +1,7 @@ +:root { + --category-sidebar-width: 325px; +} + .root { padding: 1rem; } @@ -47,3 +51,41 @@ composes: root_lowPriority from '../../components/Button/button.css'; min-width: 6.25rem; } + +.sidebar { + display: none; +} + +@media (min-width: 1024px) { + .root { + display: flex; + flex-wrap: wrap; + } + + .categoryHeader { + width: 100%; + } + + .headerButtons { + justify-content: flex-end; + } + + .filterButton { + display: none; + } + + .sortContainer { + display: none; + } + + .sidebar { + display: flex; + align-self: flex-start; + width: var(--category-sidebar-width); + padding-top: 3rem; + } + + .categoryContent { + flex-grow: 1; + } +} diff --git a/packages/venia-ui/lib/RootComponents/Category/categoryContent.js b/packages/venia-ui/lib/RootComponents/Category/categoryContent.js index 4efca02f0f..33b3a340a2 100644 --- a/packages/venia-ui/lib/RootComponents/Category/categoryContent.js +++ b/packages/venia-ui/lib/RootComponents/Category/categoryContent.js @@ -15,6 +15,7 @@ import defaultClasses from './category.css'; import NoProductsFound from './NoProductsFound'; const FilterModal = React.lazy(() => import('../../components/FilterModal')); +const FilterSidebar = React.lazy(() => import('../../components/FilterSidebar')); const CategoryContent = props => { const { categoryId, data, pageControl, sortProps, pageSize } = props; @@ -30,7 +31,6 @@ const CategoryContent = props => { categoryName, categoryDescription, filters, - handleLoadFilters, handleOpenFilters, items, totalPagesFromData @@ -43,8 +43,6 @@ const CategoryContent = props => { priority={'low'} classes={{ root_lowPriority: classes.filterButton }} onClick={handleOpenFilters} - onFocus={handleLoadFilters} - onMouseOver={handleLoadFilters} type="button" > { // (hover, focus, click), simply add the talon's `loadFilters` prop as // part of the conditional here. const modal = filters ? : null; + const sidebar = filters ? : null; const categoryDescriptionElement = categoryDescription ? ( @@ -102,17 +101,26 @@ const CategoryContent = props => { {categoryName}
-

-
{categoryName}
-

- {categoryDescriptionElement} -
- {maybeFilterButtons} - {maybeSortButton} +
+

+
{categoryName}
+

+ {categoryDescriptionElement} +
+
+ {sidebar} +
+
+
+ {maybeFilterButtons} + {maybeSortButton} +
+ {maybeSortContainer} +
+ {content} +
+ {modal}
- {maybeSortContainer} - {content} - {modal}
); diff --git a/packages/venia-ui/lib/components/FilterModal/FilterList/filterItem.js b/packages/venia-ui/lib/components/FilterModal/FilterList/filterItem.js index da5a5efab9..fe7d4d4e40 100644 --- a/packages/venia-ui/lib/components/FilterModal/FilterList/filterItem.js +++ b/packages/venia-ui/lib/components/FilterModal/FilterList/filterItem.js @@ -5,7 +5,7 @@ import setValidator from '@magento/peregrine/lib/validators/set'; import FilterDefault from './filterDefault'; const FilterItem = props => { - const { filterApi, filterState, group, item } = props; + const { filterApi, filterState, group, item, handleApply } = props; const { toggleItem } = filterApi; const { title, value } = item; const isSelected = filterState && filterState.has(item); @@ -21,6 +21,10 @@ const FilterItem = props => { const handleClick = useCallback(() => { toggleItem({ group, item }); + + if (typeof handleApply === 'function') { + handleApply(group, item); + } }, [group, item, toggleItem]); return ( @@ -34,7 +38,9 @@ const FilterItem = props => { ); }; -export default FilterItem; +FilterItem.defaultProps = { + handleApply: null +}; FilterItem.propTypes = { filterApi: shape({ @@ -45,5 +51,8 @@ FilterItem.propTypes = { item: shape({ title: string.isRequired, value: oneOfType([number, string]).isRequired - }).isRequired + }).isRequired, + handleApply: func }; + +export default FilterItem; diff --git a/packages/venia-ui/lib/components/FilterModal/FilterList/filterList.js b/packages/venia-ui/lib/components/FilterModal/FilterList/filterList.js index cab655d5e2..54a7fa3f4f 100644 --- a/packages/venia-ui/lib/components/FilterModal/FilterList/filterList.js +++ b/packages/venia-ui/lib/components/FilterModal/FilterList/filterList.js @@ -1,15 +1,16 @@ import React, { Fragment, useMemo } from 'react'; -import { array, shape, string } from 'prop-types'; +import { array, shape, string, func } from 'prop-types'; import setValidator from '@magento/peregrine/lib/validators/set'; import { mergeClasses } from '../../../classify'; import FilterItem from './filterItem'; import defaultClasses from './filterList.css'; +import FilterBlock from "../filterBlock"; const labels = new WeakMap(); const FilterList = props => { - const { filterApi, filterState, group, items } = props; + const { filterApi, filterState, group, items, handleApply } = props; const classes = mergeClasses(defaultClasses, props.classes); // memoize item creation @@ -28,6 +29,7 @@ const FilterList = props => { filterState={filterState} group={group} item={item} + handleApply={handleApply} /> ); @@ -48,6 +50,10 @@ const FilterList = props => { ); }; +FilterList.defaultProps = { + handleApply: null +}; + FilterList.propTypes = { classes: shape({ item: string, @@ -56,7 +62,8 @@ FilterList.propTypes = { filterApi: shape({}), filterState: setValidator, group: string, - items: array + items: array, + handleApply: func }; export default FilterList; diff --git a/packages/venia-ui/lib/components/FilterModal/filterBlock.js b/packages/venia-ui/lib/components/FilterModal/filterBlock.js index bbd7dfa22a..825b9505a2 100644 --- a/packages/venia-ui/lib/components/FilterModal/filterBlock.js +++ b/packages/venia-ui/lib/components/FilterModal/filterBlock.js @@ -1,5 +1,5 @@ -import React from 'react'; -import { arrayOf, shape, string } from 'prop-types'; +import React, { useMemo } from 'react'; +import { arrayOf, shape, string, func, bool } from 'prop-types'; import { ChevronDown as ArrowDown, ChevronUp as ArrowUp } from 'react-feather'; import { Form } from 'informed'; import { useFilterBlock } from '@magento/peregrine/lib/talons/FilterModal'; @@ -11,8 +11,13 @@ import FilterList from './FilterList'; import defaultClasses from './filterBlock.css'; const FilterBlock = props => { - const { filterApi, filterState, group, items, name } = props; - const talonProps = useFilterBlock(); + const { filterApi, filterState, group, items, name, handleApply, initialOpen } = props; + const hasSelected = useMemo(() => { + return items.some((item) => { + return filterState && filterState.has(item); + }); + }, [filterState, items]); + const talonProps = useFilterBlock(hasSelected, initialOpen); const { handleClick, isExpanded } = talonProps; const iconSrc = isExpanded ? ArrowUp : ArrowDown; const classes = mergeClasses(defaultClasses, props.classes); @@ -38,13 +43,17 @@ const FilterBlock = props => { filterState={filterState} group={group} items={items} + handleApply={handleApply} /> ); }; -export default FilterBlock; +FilterBlock.defaultProps = { + handleApply: null, + initialOpen: false +}; FilterBlock.propTypes = { classes: shape({ @@ -59,5 +68,9 @@ FilterBlock.propTypes = { filterState: setValidator, group: string.isRequired, items: arrayOf(shape({})), - name: string.isRequired + name: string.isRequired, + handleApply: func, + initialOpen: bool }; + +export default FilterBlock; diff --git a/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.css b/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.css new file mode 100644 index 0000000000..6fd81c5054 --- /dev/null +++ b/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.css @@ -0,0 +1,37 @@ +.root { + --stroke: var(--venia-global-color-border); + background-color: white; + bottom: 0; + display: none; + grid-template-rows: 1fr 7rem; + max-width: 360px; + width: 100%; + z-index: 3; +} + +.body { + overflow: auto; +} + +.action { + padding: 1rem 1.25rem 0; +} + +.action button { + font-size: var(--venia-typography-body-S-fontSize); + text-decoration: none; +} + +.blocks { + padding: 1rem 1.25rem 0; +} + +.blocks > li:last-child { + border-bottom: 2px solid rgb(var(--stroke)); +} + +@media (min-width: 1024px) { + .root { + display: grid; + } +} diff --git a/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.js b/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.js new file mode 100644 index 0000000000..42c3b9766a --- /dev/null +++ b/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.js @@ -0,0 +1,102 @@ +import React, { useMemo, Fragment } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { array, arrayOf, shape, string } from 'prop-types'; +import { useFilterModal } from '@magento/peregrine/lib/talons/FilterModal'; + +import { mergeClasses } from '../../classify'; +import LinkButton from '../LinkButton'; +import CurrentFilters from '../FilterModal/CurrentFilters'; +import FilterBlock from '../FilterModal/filterBlock'; +import defaultClasses from './filterSidebar.css'; + +const FILTERS_OPEN_COUNT = 3; + +/** + * A view that displays applicable and applied filters. + * + * @param {Object} props.filters - filters to display + */ +const FilterSidebar = props => { + const { filters } = props; + const talonProps = useFilterModal({ filters }); + const { + filterApi, + filterItems, + filterNames, + filterState, + handleApply, + handleReset + } = talonProps; + + const classes = mergeClasses(defaultClasses, props.classes); + + const filtersList = useMemo( + () => + Array.from(filterItems, ([group, items], iteration) => { + const blockState = filterState.get(group); + const groupName = filterNames.get(group); + + return ( + + ); + }), + [filterApi, filterItems, filterNames, filterState] + ); + + const clearAll = filterState.size ? ( +
+ + + +
+ ) : null; + + return ( + + + + ); +}; + +FilterSidebar.propTypes = { + classes: shape({ + action: string, + blocks: string, + body: string, + header: string, + headerTitle: string, + root: string, + root_open: string + }), + filters: arrayOf( + shape({ + attribute_code: string, + items: array + }) + ) +}; + +export default FilterSidebar; diff --git a/packages/venia-ui/lib/components/FilterSidebar/index.js b/packages/venia-ui/lib/components/FilterSidebar/index.js new file mode 100644 index 0000000000..4d35f5678e --- /dev/null +++ b/packages/venia-ui/lib/components/FilterSidebar/index.js @@ -0,0 +1 @@ +export { default } from './filterSidebar'; diff --git a/packages/venia-ui/lib/components/ProductSort/productSort.css b/packages/venia-ui/lib/components/ProductSort/productSort.css index 51c2af657a..3701555a7c 100644 --- a/packages/venia-ui/lib/components/ProductSort/productSort.css +++ b/packages/venia-ui/lib/components/ProductSort/productSort.css @@ -37,3 +37,17 @@ composes: root_lowPriority from '../../components/Button/button.css'; min-width: 6.25rem; } + +.desktopText { + display: none; +} + +@media (min-width: 1024px) { + .desktopText { + display: inline; + } + + .mobileText { + display: none; + } +} diff --git a/packages/venia-ui/lib/components/ProductSort/productSort.js b/packages/venia-ui/lib/components/ProductSort/productSort.js index ddcfd34003..655b5eb68b 100644 --- a/packages/venia-ui/lib/components/ProductSort/productSort.js +++ b/packages/venia-ui/lib/components/ProductSort/productSort.js @@ -8,6 +8,8 @@ import SortItem from './sortItem'; import defaultClasses from './productSort.css'; import Button from '../Button'; +const formatSelectValue = (sortMethod) => (`${sortMethod.attribute}--${sortMethod.sortDirection}`); + const ProductSort = props => { const classes = mergeClasses(defaultClasses); const { availableSortMethods, sortProps } = props; @@ -81,10 +83,19 @@ const ProductSort = props => { }} onClick={handleSortClick} > - + + + + + + {` ${currentSort.sortText}`} + {sortElements} diff --git a/packages/venia-ui/lib/components/SearchPage/searchPage.css b/packages/venia-ui/lib/components/SearchPage/searchPage.css index d70205bd90..604738f777 100644 --- a/packages/venia-ui/lib/components/SearchPage/searchPage.css +++ b/packages/venia-ui/lib/components/SearchPage/searchPage.css @@ -1,3 +1,7 @@ +:root { + --category-sidebar-width: 325px; +} + .root { padding: 1rem; } @@ -22,11 +26,19 @@ } .heading { + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +.searchInfo { line-height: var(--venia-global-typography-heading-lineHeight); margin: 2.5rem 0 1rem; max-width: 75vw; - overflow: hidden; - text-overflow: ellipsis; +} + +.totalPages { + margin-left: 0.5rem; } .headingHighlight { @@ -40,8 +52,58 @@ .sortContainer { font-size: 0.875rem; + margin: 1rem 0; } .sortText { font-weight: 600; } + +.sidebar { + display: none; +} + +@media (min-width: 1024px) { + .root { + display: flex; + flex-wrap: nowrap; + } + + .categoryTop { + width: 100%; + } + + .heading { + justify-content: space-between; + flex-wrap: nowrap; + align-items: center; + } + + .searchInfo { + margin: 0; + flex-basis: 100%; + } + + .headerButtons { + justify-content: flex-end; + } + + .filterButton { + display: none; + } + + .sortContainer { + display: none; + } + + .sidebar { + display: flex; + align-self: flex-start; + width: var(--category-sidebar-width); + padding-top: 4rem; + } + + .categoryContent { + flex-grow: 1; + } +} diff --git a/packages/venia-ui/lib/components/SearchPage/searchPage.js b/packages/venia-ui/lib/components/SearchPage/searchPage.js index 1a746e802e..bb007df4ad 100644 --- a/packages/venia-ui/lib/components/SearchPage/searchPage.js +++ b/packages/venia-ui/lib/components/SearchPage/searchPage.js @@ -13,6 +13,7 @@ import Button from '../Button'; import defaultClasses from './searchPage.css'; const FilterModal = React.lazy(() => import('../FilterModal')); +const FilterSidebar = React.lazy(() => import('../FilterSidebar')); const SearchPage = props => { const classes = mergeClasses(defaultClasses, props.classes); @@ -94,6 +95,7 @@ const SearchPage = props => { const maybeFilterModal = filters && filters.length ? : null; + const maybeSidebar = filters && filters.length ? : null; const maybeSortButton = totalCount ? ( @@ -135,25 +137,32 @@ const SearchPage = props => { return (
-
- - {formatMessage( - { - id: 'searchPage.totalPages', - defaultMessage: `items` - }, - { totalCount } - )} - -
- {maybeFilterButtons} - {maybeSortButton} +
+ {maybeSidebar} +
+
+
+
+ {searchResultsHeading} + + {formatMessage( + { + id: 'searchPage.totalPages', + defaultMessage: `items` + }, + { totalCount } + )} + +
+
+ {maybeFilterButtons} + {maybeSortButton} +
+ {maybeSortContainer}
- {maybeSortContainer} + {content} + {maybeFilterModal}
-
{searchResultsHeading}
- {content} - {maybeFilterModal}
); }; From 2a8e9eb7efaf673fbd5a105fdabe7e1fd8deb8ee Mon Sep 17 00:00:00 2001 From: Justin Conabree Date: Mon, 19 Apr 2021 13:32:14 -0400 Subject: [PATCH 02/15] Adding prop to FilterSidebar to allow easy changing of open filter count (#3115) --- .../components/FilterSidebar/filterSidebar.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.js b/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.js index 42c3b9766a..1e4b4b4588 100644 --- a/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.js +++ b/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.js @@ -1,6 +1,6 @@ import React, { useMemo, Fragment } from 'react'; import { FormattedMessage } from 'react-intl'; -import { array, arrayOf, shape, string } from 'prop-types'; +import { array, arrayOf, shape, string, number } from 'prop-types'; import { useFilterModal } from '@magento/peregrine/lib/talons/FilterModal'; import { mergeClasses } from '../../classify'; @@ -9,7 +9,7 @@ import CurrentFilters from '../FilterModal/CurrentFilters'; import FilterBlock from '../FilterModal/filterBlock'; import defaultClasses from './filterSidebar.css'; -const FILTERS_OPEN_COUNT = 3; +const DEFAULT_FILTERS_OPEN_COUNT = 3; /** * A view that displays applicable and applied filters. @@ -17,7 +17,7 @@ const FILTERS_OPEN_COUNT = 3; * @param {Object} props.filters - filters to display */ const FilterSidebar = props => { - const { filters } = props; + const { filters, filtersOpen } = props; const talonProps = useFilterModal({ filters }); const { filterApi, @@ -29,6 +29,7 @@ const FilterSidebar = props => { } = talonProps; const classes = mergeClasses(defaultClasses, props.classes); + const filtersToOpen = typeof filtersOpen === 'number' ? filtersOpen : DEFAULT_FILTERS_OPEN_COUNT; const filtersList = useMemo( () => @@ -45,11 +46,11 @@ const FilterSidebar = props => { items={items} name={groupName} handleApply={handleApply} - initialOpen={iteration < FILTERS_OPEN_COUNT} + initialOpen={iteration < filtersToOpen} /> ); }), - [filterApi, filterItems, filterNames, filterState] + [filterApi, filterItems, filterNames, filterState, filtersToOpen] ); const clearAll = filterState.size ? ( @@ -81,6 +82,10 @@ const FilterSidebar = props => { ); }; +FilterSidebar.defaultProps = { + filtersOpen: null +}; + FilterSidebar.propTypes = { classes: shape({ action: string, @@ -96,7 +101,8 @@ FilterSidebar.propTypes = { attribute_code: string, items: array }) - ) + ), + filtersOpen: number }; export default FilterSidebar; From e6cc96f00d5f203d67176b1d2235881b7af82654 Mon Sep 17 00:00:00 2001 From: Justin Conabree Date: Wed, 21 Apr 2021 09:37:07 -0400 Subject: [PATCH 03/15] Fix current filters, cleanup search page, change sort button styles and add icon, implement show more/less in filter list, scroll to top on apply filter, add total number of results in toolbar, add filter header --- .../i18n/fr_FR.json | 3 ++ .../peregrine/lib/talons/FilterModal/index.js | 1 + .../lib/talons/FilterModal/useFilterList.js | 14 ++++++ .../Category/useCategoryContent.js | 3 ++ packages/venia-ui/i18n/en_US.json | 3 ++ .../lib/RootComponents/Category/category.css | 50 ++++++++++++++++--- .../Category/categoryContent.js | 28 ++++++++--- .../CurrentFilters/currentFilter.js | 16 ++++-- .../CurrentFilters/currentFilters.js | 12 +++-- .../FilterModal/FilterList/filterList.css | 17 +++++++ .../FilterModal/FilterList/filterList.js | 48 ++++++++++++++---- .../FilterSidebar/filterSidebar.css | 12 +++++ .../components/FilterSidebar/filterSidebar.js | 29 +++++++++-- .../components/ProductSort/productSort.css | 26 +++++++++- .../lib/components/ProductSort/productSort.js | 23 ++++++--- .../lib/components/SearchPage/searchPage.css | 18 ++----- .../lib/components/SearchPage/searchPage.js | 2 +- 17 files changed, 246 insertions(+), 59 deletions(-) create mode 100644 packages/peregrine/lib/talons/FilterModal/useFilterList.js diff --git a/packages/extensions/venia-sample-language-packs/i18n/fr_FR.json b/packages/extensions/venia-sample-language-packs/i18n/fr_FR.json index 8b63176fda..9ff31c4f51 100644 --- a/packages/extensions/venia-sample-language-packs/i18n/fr_FR.json +++ b/packages/extensions/venia-sample-language-packs/i18n/fr_FR.json @@ -43,6 +43,7 @@ "cartTrigger.ariaLabel": "Basculer le mini-panier. Vous avez {count} articles dans votre panier.", "categoryContent.filter": "Filtre", "categoryContent.itemsSortedBy": "Articles triés par ", + "categoryContent.resultCount": "{count} Résultats", "categoryLeaf.allLabel": "Tout(e) {name}", "categoryList.noResults": "Aucune catégorie enfant trouvée.", "checkoutPage.additionalText": "Vous recevrez également un courriel avec les détails et nous vous informerons lorsque votre commande aura été expédiée", @@ -128,6 +129,8 @@ "Email Signup": "Inscription par courriel", "field.optional": "Optionnelle", "filterFooter.results": "Voir les résultats", + "filterList.showMore": "Voir plus", + "filterList.showLess": "Voir moins", "filterModal.action": "Tout effacer", "filterModal.headerTitle": "Filtres", "filterSearch.name": "Entrez un {name}", diff --git a/packages/peregrine/lib/talons/FilterModal/index.js b/packages/peregrine/lib/talons/FilterModal/index.js index e478e2ffc6..f6d7c94e95 100644 --- a/packages/peregrine/lib/talons/FilterModal/index.js +++ b/packages/peregrine/lib/talons/FilterModal/index.js @@ -2,3 +2,4 @@ export { useFilterBlock } from './useFilterBlock'; export { useFilterFooter } from './useFilterFooter'; export { useFilterModal } from './useFilterModal'; export { useFilterState } from './useFilterState'; +export { useFilterList } from './useFilterList'; diff --git a/packages/peregrine/lib/talons/FilterModal/useFilterList.js b/packages/peregrine/lib/talons/FilterModal/useFilterList.js new file mode 100644 index 0000000000..4dd701672d --- /dev/null +++ b/packages/peregrine/lib/talons/FilterModal/useFilterList.js @@ -0,0 +1,14 @@ +import { useCallback, useState } from 'react'; + +export const useFilterList = () => { + const [isExpanded, setExpanded] = useState(false); + + const handleClick = useCallback(() => { + setExpanded(value => !value); + }, [setExpanded]); + + return { + handleClick, + isExpanded + }; +}; diff --git a/packages/peregrine/lib/talons/RootComponents/Category/useCategoryContent.js b/packages/peregrine/lib/talons/RootComponents/Category/useCategoryContent.js index 5e43bf2b4e..4db59c1281 100644 --- a/packages/peregrine/lib/talons/RootComponents/Category/useCategoryContent.js +++ b/packages/peregrine/lib/talons/RootComponents/Category/useCategoryContent.js @@ -65,6 +65,8 @@ export const useCategoryContent = props => { const totalPagesFromData = data ? data.products.page_info.total_pages : null; + + const totalCount = data.products.total_count; const categoryName = data ? data.category.name : null; const categoryDescription = data ? data.category.description : null; @@ -76,6 +78,7 @@ export const useCategoryContent = props => { handleOpenFilters, items, loadFilters, + totalCount, totalPagesFromData }; }; diff --git a/packages/venia-ui/i18n/en_US.json b/packages/venia-ui/i18n/en_US.json index d1e29186eb..3eb299f98b 100644 --- a/packages/venia-ui/i18n/en_US.json +++ b/packages/venia-ui/i18n/en_US.json @@ -46,6 +46,7 @@ "cartTrigger.ariaLabel": "Toggle mini cart. You have {count} items in your cart.", "categoryContent.filter": "Filter", "categoryContent.itemsSortedBy": "Items sorted by ", + "categoryContent.resultCount": "{count} Results", "categoryLeaf.allLabel": "All {name}", "categoryList.noResults": "No child categories found.", "checkoutPage.accountSuccessfullyCreated": "Account successfully created.", @@ -140,6 +141,8 @@ "errorView.message": "Looks like something went wrong. Sorry about that.", "field.optional": "Optional", "filterFooter.results": "See Results", + "filterList.showMore": "Show More", + "filterList.showLess": "Show Less", "filterModal.action": "Clear all", "filterModal.headerTitle": "Filters", "filterSearch.name": "Enter a {name}", diff --git a/packages/venia-ui/lib/RootComponents/Category/category.css b/packages/venia-ui/lib/RootComponents/Category/category.css index fa1f501cea..7c318f3581 100644 --- a/packages/venia-ui/lib/RootComponents/Category/category.css +++ b/packages/venia-ui/lib/RootComponents/Category/category.css @@ -21,12 +21,6 @@ height: 100vh; } -.headerButtons { - display: flex; - justify-content: center; - padding-bottom: 1.5rem; -} - .categoryTitle { color: rgb(var(--venia-global-color-text)); padding-bottom: 1rem; @@ -36,6 +30,26 @@ text-align: center; } +.heading { + display: flex; + flex-wrap: wrap; + justify-content: center; + padding-bottom: 0.5rem; +} + +.categoryInfo { + line-height: var(--venia-global-typography-heading-lineHeight); + margin: 2.5rem 0 1rem; + max-width: 75vw; +} + +.headerButtons { + display: flex; + flex-basis: 100%; + justify-content: center; + padding-bottom: 1.5rem; +} + .sortContainer { color: rgb(var(--venia-global-color-text-alt)); text-align: center; @@ -43,6 +57,10 @@ padding-bottom: 1rem; } +.sortButton { + composes: sortButton from '../../components/ProductSort/productSort.css'; +} + .sortText { font-weight: 600; } @@ -74,10 +92,30 @@ display: none; } + .heading { + justify-content: space-between; + flex-wrap: nowrap; + align-items: center; + padding-bottom: 1.5rem; + } + + .headerButtons { + padding-bottom: 0; + } + + .categoryInfo { + margin: 0; + flex-basis: 100%; + } + .sortContainer { display: none; } + .sortButton { + border-radius: 6px; + } + .sidebar { display: flex; align-self: flex-start; diff --git a/packages/venia-ui/lib/RootComponents/Category/categoryContent.js b/packages/venia-ui/lib/RootComponents/Category/categoryContent.js index 33b3a340a2..21c392a5cd 100644 --- a/packages/venia-ui/lib/RootComponents/Category/categoryContent.js +++ b/packages/venia-ui/lib/RootComponents/Category/categoryContent.js @@ -33,6 +33,7 @@ const CategoryContent = props => { filters, handleOpenFilters, items, + totalCount, totalPagesFromData } = talonProps; @@ -54,7 +55,7 @@ const CategoryContent = props => { const maybeSortButton = totalPagesFromData && filters ? ( - + ) : null; const maybeSortContainer = @@ -73,6 +74,16 @@ const CategoryContent = props => { ) : null; + const categoryResultsHeading = totalCount > 0 ? ( + + ) : null; + // If you want to defer the loading of the FilterModal until user interaction // (hover, focus, click), simply add the talon's `loadFilters` prop as // part of the conditional here. @@ -111,14 +122,15 @@ const CategoryContent = props => { {sidebar}
-
- {maybeFilterButtons} - {maybeSortButton} -
- {maybeSortContainer} -
- {content} +
+
{categoryResultsHeading}
+
+ {maybeFilterButtons} + {maybeSortButton} +
+ {maybeSortContainer}
+ {content} {modal}
diff --git a/packages/venia-ui/lib/components/FilterModal/CurrentFilters/currentFilter.js b/packages/venia-ui/lib/components/FilterModal/CurrentFilters/currentFilter.js index 23a57203f4..3642e5f8e8 100644 --- a/packages/venia-ui/lib/components/FilterModal/CurrentFilters/currentFilter.js +++ b/packages/venia-ui/lib/components/FilterModal/CurrentFilters/currentFilter.js @@ -1,5 +1,5 @@ import React, { useCallback } from 'react'; -import { shape, string } from 'prop-types'; +import { shape, string, func } from 'prop-types'; import { X as Remove } from 'react-feather'; import { mergeClasses } from '../../../classify'; @@ -8,12 +8,15 @@ import Trigger from '../../Trigger'; import defaultClasses from './currentFilter.css'; const CurrentFilter = props => { - const { group, item, removeItem } = props; + const { group, item, removeItem, handleApply } = props; const classes = mergeClasses(defaultClasses, props.classes); const handleClick = useCallback(() => { removeItem({ group, item }); - }, [group, item, removeItem]); + if (typeof handleApply === 'function') { + handleApply(group, item); + } + }, [group, item, removeItem, handleApply]); return ( @@ -27,8 +30,13 @@ const CurrentFilter = props => { export default CurrentFilter; +CurrentFilter.defaultProps = { + handleApply: null +}; + CurrentFilter.propTypes = { classes: shape({ root: string - }) + }), + handleApply: func }; diff --git a/packages/venia-ui/lib/components/FilterModal/CurrentFilters/currentFilters.js b/packages/venia-ui/lib/components/FilterModal/CurrentFilters/currentFilters.js index a2051738dd..2f3237c977 100644 --- a/packages/venia-ui/lib/components/FilterModal/CurrentFilters/currentFilters.js +++ b/packages/venia-ui/lib/components/FilterModal/CurrentFilters/currentFilters.js @@ -1,12 +1,12 @@ import React, { useMemo } from 'react'; -import { shape, string } from 'prop-types'; +import { shape, string, func } from 'prop-types'; import { mergeClasses } from '../../../classify'; import CurrentFilter from './currentFilter'; import defaultClasses from './currentFilters.css'; const CurrentFilters = props => { - const { filterApi, filterState } = props; + const { filterApi, filterState, handleApply } = props; const { removeItem } = filterApi; const classes = mergeClasses(defaultClasses, props.classes); @@ -24,6 +24,7 @@ const CurrentFilters = props => { group={group} item={item} removeItem={removeItem} + handleApply={handleApply} /> ); @@ -36,13 +37,18 @@ const CurrentFilters = props => { return
    {filterElements}
; }; +CurrentFilters.defaultProps = { + handleApply: null +}; + CurrentFilters.propTypes = { classes: shape({ root: string, item: string, button: string, icon: string - }) + }), + handleApply: func }; export default CurrentFilters; diff --git a/packages/venia-ui/lib/components/FilterModal/FilterList/filterList.css b/packages/venia-ui/lib/components/FilterModal/FilterList/filterList.css index a1fcc4fb52..7b08861384 100644 --- a/packages/venia-ui/lib/components/FilterModal/FilterList/filterList.css +++ b/packages/venia-ui/lib/components/FilterModal/FilterList/filterList.css @@ -5,3 +5,20 @@ margin-left: -0.375rem; padding-bottom: 2rem; } + +.itemHidden { + display: none; +} + +.showMoreLessItem { + padding-left: 3px; +} + +.showMoreLessButton { + font-size: 14px; + text-decoration: underline; +} + +.showMoreLessButton:hover { + text-decoration: none; +} diff --git a/packages/venia-ui/lib/components/FilterModal/FilterList/filterList.js b/packages/venia-ui/lib/components/FilterModal/FilterList/filterList.js index 54a7fa3f4f..b0c6d294dc 100644 --- a/packages/venia-ui/lib/components/FilterModal/FilterList/filterList.js +++ b/packages/venia-ui/lib/components/FilterModal/FilterList/filterList.js @@ -1,29 +1,36 @@ import React, { Fragment, useMemo } from 'react'; -import { array, shape, string, func } from 'prop-types'; +import { array, shape, string, func, number } from 'prop-types'; +import { useIntl } from 'react-intl'; import setValidator from '@magento/peregrine/lib/validators/set'; +import { useFilterList } from '@magento/peregrine/lib/talons/FilterModal'; import { mergeClasses } from '../../../classify'; import FilterItem from './filterItem'; import defaultClasses from './filterList.css'; -import FilterBlock from "../filterBlock"; const labels = new WeakMap(); +const DEFAULT_SHOW_ITEMS_COUNT = 5; const FilterList = props => { - const { filterApi, filterState, group, items, handleApply } = props; + const { filterApi, filterState, group, items, handleApply, showItems } = props; const classes = mergeClasses(defaultClasses, props.classes); + const talonProps = useFilterList(); + const { isExpanded, handleClick } = talonProps; + const showItemsCount = typeof showItems === 'number' ? showItems : DEFAULT_SHOW_ITEMS_COUNT; + const { formatMessage } = useIntl(); // memoize item creation // search value is not referenced, so this array is stable const itemElements = useMemo( () => - items.map(item => { + items.map((item, index) => { const { title, value } = item; const key = `item-${group}-${value}`; + const itemClass = isExpanded || index < showItemsCount ? classes.item : classes.itemHidden; // create an element for each item const element = ( -
  • +
  • { return element; }), - [classes, filterApi, filterState, group, items] + [classes, filterApi, filterState, group, items, isExpanded, showItemsCount] ); + const showMoreLessItem = useMemo(() => { + if (items.length <= showItemsCount) { + return null; + } + + const label = isExpanded + ? formatMessage({ id: 'filterList.showLess', defaultMessage: 'Show Less' }) + : formatMessage({ id: 'filterList.showMore', defaultMessage: 'Show More' }); + + return ( +
  • + +
  • + ); + }, [isExpanded, handleClick, items, showItemsCount, formatMessage]); + return ( -
      {itemElements}
    +
      {itemElements}{showMoreLessItem}
    ); }; FilterList.defaultProps = { - handleApply: null + handleApply: null, + showItems: null }; FilterList.propTypes = { @@ -63,7 +92,8 @@ FilterList.propTypes = { filterState: setValidator, group: string, items: array, - handleApply: func + handleApply: func, + showItems: number }; export default FilterList; diff --git a/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.css b/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.css index 6fd81c5054..0155e44b0f 100644 --- a/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.css +++ b/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.css @@ -12,6 +12,18 @@ .body { overflow: auto; } +.header { + display: flex; + justify-content: space-between; + padding: 1.25rem 1.25rem 0; +} + +.headerTitle { + display: flex; + align-items: center; + font-size: var(--venia-global-typography-heading-L-fontSize); + line-height: 0.875rem; +} .action { padding: 1rem 1.25rem 0; diff --git a/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.js b/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.js index 1e4b4b4588..9dcc50e5fe 100644 --- a/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.js +++ b/packages/venia-ui/lib/components/FilterSidebar/filterSidebar.js @@ -1,4 +1,4 @@ -import React, { useMemo, Fragment } from 'react'; +import React, { useMemo, useCallback, useRef, Fragment } from 'react'; import { FormattedMessage } from 'react-intl'; import { array, arrayOf, shape, string, number } from 'prop-types'; import { useFilterModal } from '@magento/peregrine/lib/talons/FilterModal'; @@ -28,9 +28,22 @@ const FilterSidebar = props => { handleReset } = talonProps; + const filterRef = useRef(); const classes = mergeClasses(defaultClasses, props.classes); const filtersToOpen = typeof filtersOpen === 'number' ? filtersOpen : DEFAULT_FILTERS_OPEN_COUNT; + const handleApplyFilter = useCallback((...args) => { + const filterElement = filterRef.current; + if (filterElement && typeof filterElement.getBoundingClientRect === 'function') { + const filterTop = filterElement.getBoundingClientRect().top; + const offset = 150; + const windowScrollY = window.scrollY + filterTop - offset; + window.scrollTo(0, windowScrollY); + } + + handleApply(...args); + }, [handleApply, filterRef]); + const filtersList = useMemo( () => Array.from(filterItems, ([group, items], iteration) => { @@ -45,7 +58,7 @@ const FilterSidebar = props => { group={group} items={items} name={groupName} - handleApply={handleApply} + handleApply={handleApplyFilter} initialOpen={iteration < filtersToOpen} /> ); @@ -66,13 +79,21 @@ const FilterSidebar = props => { return ( -