From 7fa19024035f8e834e5140db09636e5dc8faf76b Mon Sep 17 00:00:00 2001 From: Marc Thomas Date: Wed, 27 Sep 2023 18:04:37 +0100 Subject: [PATCH 1/6] chore: refactor filters to not render FiltersBar when there are no filters --- .../src/components/Filters/Filters.tsx | 280 +------------- .../components/FiltersBar/FiltersBar.scss | 294 +++++++++++++++ .../components/FiltersBar/FiltersBar.tsx | 298 +++++++++++++++ .../Filters/components/FiltersBar/index.ts | 1 + .../FiltersBar/tests/FiltersBar.test.tsx | 347 ++++++++++++++++++ .../components/Filters/components/index.ts | 1 + .../components/Filters/tests/Filters.test.tsx | 323 +--------------- .../IndexFilters/IndexFilters.stories.tsx | 289 ++++++++++++++- 8 files changed, 1259 insertions(+), 574 deletions(-) create mode 100644 polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.scss create mode 100644 polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx create mode 100644 polaris-react/src/components/Filters/components/FiltersBar/index.ts create mode 100644 polaris-react/src/components/Filters/components/FiltersBar/tests/FiltersBar.test.tsx diff --git a/polaris-react/src/components/Filters/Filters.tsx b/polaris-react/src/components/Filters/Filters.tsx index 0b98b65c6d3..4210dcce7db 100644 --- a/polaris-react/src/components/Filters/Filters.tsx +++ b/polaris-react/src/components/Filters/Filters.tsx @@ -1,27 +1,15 @@ -import React, {useState, useRef, useEffect, useMemo} from 'react'; +import React, {useMemo} from 'react'; import type {ReactNode} from 'react'; -import {PlusMinor} from '@shopify/polaris-icons'; import type {TransitionStatus} from 'react-transition-group'; -import {useI18n} from '../../utilities/i18n'; -import {Popover} from '../Popover'; -import {ActionList} from '../ActionList'; -import {Text} from '../Text'; -import {UnstyledButton} from '../UnstyledButton'; import {classNames} from '../../utilities/css'; -import {useBreakpoints} from '../../utilities/breakpoints'; -import type { - ActionListItemDescriptor, - AppliedFilterInterface, - FilterInterface, -} from '../../types'; +import type {AppliedFilterInterface, FilterInterface} from '../../types'; import {InlineStack} from '../InlineStack'; import type {BoxProps} from '../Box'; import {Box} from '../Box'; import {Spinner} from '../Spinner'; -import {Button} from '../Button'; -import {FilterPill, SearchField} from './components'; +import {FiltersBar, SearchField} from './components'; import styles from './Filters.scss'; const TRANSITION_DURATION = 'var(--p-motion-duration-150)'; @@ -136,149 +124,7 @@ export function Filters({ onAddFilterClick, closeOnChildOverlayClick, }: FiltersProps) { - const i18n = useI18n(); - const {mdDown} = useBreakpoints(); - const [popoverActive, setPopoverActive] = useState(false); - const [localPinnedFilters, setLocalPinnedFilters] = useState([]); - const hasMounted = useRef(false); - - useEffect(() => { - hasMounted.current = true; - }); - - const togglePopoverActive = () => - setPopoverActive((popoverActive) => !popoverActive); - - const handleAddFilterClick = () => { - onAddFilterClick?.(); - togglePopoverActive(); - }; - const appliedFilterKeys = appliedFilters?.map(({key}) => key); - - const pinnedFiltersFromPropsAndAppliedFilters = filters.filter( - ({pinned, key}) => - (Boolean(pinned) || appliedFilterKeys?.includes(key)) && - // Filters that are pinned in local state display at the end of our list - !localPinnedFilters.find((filterKey) => filterKey === key), - ); - - useEffect(() => { - const allAppliedFilterKeysInLocalPinnedFilters = - !appliedFilterKeys || - appliedFilterKeys.every((value) => localPinnedFilters.includes(value)); - - if (!allAppliedFilterKeysInLocalPinnedFilters) { - setLocalPinnedFilters((currentLocalPinnedFilters: string[]): string[] => { - const newPinnedFilters = - appliedFilterKeys?.filter( - (filterKey) => - !currentLocalPinnedFilters.find( - (currentFilterKey) => currentFilterKey === filterKey, - ), - ) || []; - - return [...currentLocalPinnedFilters, ...newPinnedFilters]; - }); - } - }, [appliedFilterKeys, localPinnedFilters]); - - const pinnedFiltersFromLocalState = localPinnedFilters - .map((key) => filters.find((filter) => filter.key === key)) - .reduce( - (acc, filter) => (filter ? [...acc, filter] : acc), - [], - ); - - const pinnedFilters = [ - ...pinnedFiltersFromPropsAndAppliedFilters, - ...pinnedFiltersFromLocalState, - ]; - - const onFilterClick = - ({key, onAction}: FilterInterface) => - () => { - // PopoverOverlay will cause a rerender of the component and nuke the - // popoverActive state, so we set this as a microtask - setTimeout(() => { - setLocalPinnedFilters((currentLocalPinnedFilters) => [ - ...new Set([...currentLocalPinnedFilters, key]), - ]); - onAction?.(); - togglePopoverActive(); - }, 0); - }; - - const filterToActionItem = (filter: FilterInterface) => ({ - ...filter, - content: filter.label, - onAction: onFilterClick(filter), - }); - - const unpinnedFilters = filters.filter( - (filter) => !pinnedFilters.some(({key}) => key === filter.key), - ); - - const unsectionedFilters = unpinnedFilters - .filter((filter) => !filter.section) - .map(filterToActionItem); - - const sectionedFilters = unpinnedFilters - .filter((filter) => filter.section) - .reduce( - (acc, filter) => { - const filterActionItem = filterToActionItem(filter); - const sectionIndex = acc.findIndex( - (section) => section.title === filter.section, - ); - - if (sectionIndex === -1) { - acc.push({ - title: filter.section!, - items: [filterActionItem], - }); - } else { - acc[sectionIndex].items.push(filterActionItem); - } - - return acc; - }, - [] as { - title: string; - items: ActionListItemDescriptor[]; - }[], - ); - - const hasOneOrMorePinnedFilters = pinnedFilters.length >= 1; - - const labelVariant = mdDown ? 'bodyLg' : 'bodySm'; - - const addFilterActivator = ( -
- - - {i18n.translate('Polaris.Filters.addFilter')}{' '} - - - -
- ); - - const handleClearAllFilters = () => { - setLocalPinnedFilters([]); - onClearAll?.(); - }; - - const shouldShowAddButton = filters.some((filter) => !filter.pinned); + const {polarisSummerEditions2023: se23} = useFeatures(); const additionalContent = useMemo(() => { return ( @@ -351,114 +197,20 @@ export function Filters({ } : undefined; - const pinnedFiltersMarkup = pinnedFilters.map( - ({key: filterKey, ...pinnedFilter}) => { - const appliedFilter = appliedFilters?.find(({key}) => key === filterKey); - const handleFilterPillRemove = () => { - setLocalPinnedFilters((currentLocalPinnedFilters) => - currentLocalPinnedFilters.filter((key) => key !== filterKey), - ); - appliedFilter?.onRemove(filterKey); - }; - - return ( - - ); - }, - ); - - const addButton = shouldShowAddButton ? ( -
- - - -
- ) : null; - - const clearAllMarkup = - appliedFilters?.length || localPinnedFilters.length ? ( -
- -
- ) : null; - const filtersMarkup = hideFilters || filters.length === 0 ? null : ( -
-
-
- {pinnedFiltersMarkup} - {addButton} - {clearAllMarkup} -
-
- {hideQueryField ? ( - - - {additionalContent} - - - ) : null} -
+ ); return ( diff --git a/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.scss b/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.scss new file mode 100644 index 00000000000..d943042b619 --- /dev/null +++ b/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.scss @@ -0,0 +1,294 @@ +@import '../../../../styles/common'; + +.Container { + position: relative; + z-index: var(--p-z-index-1); + border-bottom: var(--p-border-width-025) solid var(--p-color-border-subdued); + border-top-left-radius: var(--p-border-radius-200); + border-top-right-radius: var(--p-border-radius-200); + background: var(--p-color-bg); +} + +.ContainerUplift { + background: none; +} + +@media #{$p-breakpoints-sm-down} { + .Container { + border-top-left-radius: 0; + border-top-right-radius: 0; + height: 57px; + + &.ContainerUplift { + height: unset; + } + } +} + +.SearchField { + flex: 1; +} + +.Spinner { + width: var(--p-space-500); + transform: translateX(var(--p-space-100)); + + svg { + display: block; + } +} + +.FiltersWrapper { + border-bottom: var(--p-border-width-025) solid var(--p-color-border-subdued); + height: 53px; + overflow: hidden; + + @media #{$p-breakpoints-sm-down} { + background: var(--p-color-bg); + } + + @media #{$p-breakpoints-md-up} { + height: auto; + overflow: visible; + } +} + +.hideQueryField .FiltersWrapper { + display: flex; + align-items: center; +} + +.FiltersInner { + overflow: auto; + white-space: nowrap; + padding: var(--p-space-300) var(--p-space-400) var(--p-space-500); +} + +.hideQueryField .FiltersInner { + flex: 1; + padding: var(--p-space-300); +} + +@media #{$p-breakpoints-md-up} { + .FiltersInner { + overflow: visible; + flex-wrap: wrap; + gap: var(--p-space-200); + // stylelint-disable-next-line -- No 6px space token + padding: 0.375rem var(--p-space-200); + } + + .hideQueryField .FiltersInner { + flex: 1; + // stylelint-disable-next-line -- No 6px space token + padding: 0.375rem var(--p-space-200); + } +} + +.AddFilter { + background: var(--p-color-bg-subdued); + border-radius: var(--p-border-radius-750); + border: var(--p-color-border-subdued) dashed var(--p-border-width-025); + padding: 0 var(--p-space-200) 0 var(--p-space-300); + height: 28px; + cursor: pointer; + color: var(--p-color-text); + display: flex; + align-items: center; + justify-content: center; + outline: inherit; + // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY + @include focus-ring($border-width: var(--p-border-width-025)); + + path { + fill: var(--p-color-icon); + } + + @media #{$p-breakpoints-md-up} { + font-size: var(--p-font-size-75); + line-height: var(--p-font-line-height-400); + height: 24px; + // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY + padding: 0 0.375rem 0 var(--p-space-200); + } + + &:hover, + &:focus { + background: var(--p-color-bg-hover); + border-color: var(--p-color-border-hover); + + path { + fill: var(--p-color-icon-hover); + } + } + + &:active { + background: var(--p-color-bg-active); + border-color: var(--p-color-border-hover); + } + + &[aria-disabled='true'] { + background: var(--p-color-bg-disabled); + border-color: var(--p-color-border-disabled); + color: var(--p-color-text-disabled); + cursor: default; + + path { + fill: var(--p-color-icon-disabled); + } + } + + &:focus-visible:not(:active) { + // stylelint-disable-next-line -- no way to set focus ring without mixin currently + @include focus-ring($style: 'focused'); + } + + &::after { + border-radius: var(--p-border-radius-750); + } + + span { + margin-right: var(--p-space-050); + + @media #{$p-breakpoints-md-up} { + margin-right: var(--p-space-025); + } + } + + svg { + width: var(--p-space-500); + + @media #{$p-breakpoints-md-up} { + width: var(--p-space-400); + } + } + + #{$se23} & { + background: var(--p-color-bg-secondary-experimental); + border-radius: var(--p-border-radius-200); + border: var(--p-color-border) dashed var(--p-border-width-025); + + &:hover { + background: transparent; + border-style: solid; + } + + &:focus { + background: transparent; + outline-offset: var(--p-border-width-050); + } + + &:active { + background: var(--p-color-bg-subdued); + + #{$se23} & { + background: var(--p-color-bg-secondary-experimental); + } + } + + &::after { + border-radius: var(--p-border-radius-200); + } + } +} + +@media #{$p-breakpoints-md-down} { + .FiltersWrapperWithAddButton { + position: relative; + + .FiltersInner { + padding: var(--p-space-200); + padding-right: 0; + } + } + + .AddFilterActivatorMultiple { + position: sticky; + z-index: var(--p-z-index-1); + top: 0; + right: 0; + display: flex; + padding: var(--p-space-100) var(--p-space-400) var(--p-space-100) 0; + background: var(--p-color-bg); + margin-left: var(--p-space-200); + + &::before { + content: ''; + position: absolute; + top: 0; + left: -12px; + width: 12px; + height: 100%; + pointer-events: none; + // stylelint-disable -- needed to create the fade effect + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0) 0%, + var(--p-color-bg) 70%, + var(--p-color-bg) 100% + ); + // stylelint-enable + } + + .AddFilter { + padding: var(--p-space-300) var(--p-space-200); + + // stylelint-disable-next-line selector-max-combinators -- required to hide the text of the button + span { + display: none; + } + } + } +} + +.FiltersStickyArea { + position: relative; + display: flex; + gap: var(--p-space-100); + flex-wrap: nowrap; + align-items: center; + justify-content: flex-start; + + @media #{$p-breakpoints-md-up} { + flex-wrap: wrap; + } +} + +.ClearAll { + margin-left: var(--p-space-100); + + #{$se23} & { + margin-left: var(--p-space-200); + + // stylelint-disable-next-line -- se23 overrides + span { + font-size: var(--p-font-size-75); + font-weight: var(--p-font-weight-medium); + } + } +} + +@media #{$p-breakpoints-md-down} { + .ClearAll { + margin-left: var(--p-space-200); + padding-right: var(--p-space-400); + + #{$se23} & { + margin-left: 0; + + // stylelint-disable-next-line -- se23 overrides + span { + font-size: var(--p-font-size-350); + font-weight: var(--p-font-weight-medium); + } + } + } + + .MultiplePinnedFilterClearAll { + transform: translateX(-8px); + position: relative; + z-index: var(--p-z-index-1); + margin-left: 0; + padding-right: var(--p-space-400); + } +} diff --git a/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx b/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx new file mode 100644 index 00000000000..ab2ed9e351f --- /dev/null +++ b/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx @@ -0,0 +1,298 @@ +import type {ReactNode} from 'react'; +import React, {useState, useRef, useEffect} from 'react'; +import {PlusMinor} from '@shopify/polaris-icons'; +import type {TransitionStatus} from 'react-transition-group'; + +import {useI18n} from '../../../../utilities/i18n'; +import {Popover} from '../../../Popover'; +import {ActionList} from '../../../ActionList'; +import {Text} from '../../../Text'; +import {UnstyledButton} from '../../../UnstyledButton'; +import {classNames} from '../../../../utilities/css'; +import {useBreakpoints} from '../../../../utilities/breakpoints'; +import {useFeatures} from '../../../../utilities/features'; +import type { + ActionListItemDescriptor, + AppliedFilterInterface, + FilterInterface, +} from '../../../../types'; +import {HorizontalStack} from '../../../HorizontalStack'; +import {Box} from '../../../Box'; +import {Button} from '../../../Button'; +import {FilterPill} from '../FilterPill'; + +import styles from './FiltersBar.scss'; + +export interface FiltersBarProps { + /** Currently entered text in the query field */ + queryValue?: string; + /** Placeholder text for the query field. */ + queryPlaceholder?: string; + /** Whether the query field is focused. */ + focused?: boolean; + /** Available filters added to the filter bar. Shortcut filters are pinned to the front of the bar. */ + filters: FilterInterface[]; + /** Applied filters which are rendered as filter pills. The remove callback is called with the respective key. */ + appliedFilters?: AppliedFilterInterface[]; + /** Callback when the reset all button is pressed. */ + onClearAll: () => void; + + /** Disable all filters. */ + disabled?: boolean; + /** Hide the query field. */ + hideQueryField?: boolean; + /** Disable the filters */ + disableFilters?: boolean; + mountedState?: TransitionStatus; + /** Callback when the add filter button is clicked. */ + onAddFilterClick?: () => void; + /** Whether the filter should close when clicking inside another Popover. */ + closeOnChildOverlayClick?: boolean; + additionalContent?: ReactNode; + mountedStateStyles?: any; +} + +export function FiltersBar({ + filters, + appliedFilters, + onClearAll, + disabled, + hideQueryField, + disableFilters, + mountedStateStyles, + additionalContent, + onAddFilterClick, + closeOnChildOverlayClick, +}: FiltersBarProps) { + const i18n = useI18n(); + const {mdDown} = useBreakpoints(); + const {polarisSummerEditions2023: se23} = useFeatures(); + const [popoverActive, setPopoverActive] = useState(false); + const hasMounted = useRef(false); + useEffect(() => { + hasMounted.current = true; + }); + + const togglePopoverActive = () => + setPopoverActive((popoverActive) => !popoverActive); + + const handleAddFilterClick = () => { + onAddFilterClick?.(); + togglePopoverActive(); + }; + const appliedFilterKeys = appliedFilters?.map(({key}) => key); + + const pinnedFiltersFromPropsAndAppliedFilters = filters.filter( + ({pinned, key}) => { + const isPinnedOrApplied = + Boolean(pinned) || appliedFilterKeys?.includes(key); + return isPinnedOrApplied; + }, + ); + const [localPinnedFilters, setLocalPinnedFilters] = useState( + pinnedFiltersFromPropsAndAppliedFilters.map(({key}) => key), + ); + + const pinnedFilters = localPinnedFilters + .map((key) => filters.find((filter) => filter.key === key)) + .reduce( + (acc, filter) => (filter ? [...acc, filter] : acc), + [], + ); + + const onFilterClick = + ({key, onAction}: FilterInterface) => + () => { + // PopoverOverlay will cause a rerender of the component and nuke the + // popoverActive state, so we set this as a microtask + setTimeout(() => { + setLocalPinnedFilters((currentLocalPinnedFilters) => [ + ...new Set([...currentLocalPinnedFilters, key]), + ]); + onAction?.(); + togglePopoverActive(); + }, 0); + }; + + const filterToActionItem = (filter: FilterInterface) => ({ + ...filter, + content: filter.label, + onAction: onFilterClick(filter), + }); + + const unpinnedFilters = filters.filter( + (filter) => !pinnedFilters.some(({key}) => key === filter.key), + ); + + const unsectionedFilters = unpinnedFilters + .filter((filter) => !filter.section) + .map(filterToActionItem); + + const sectionedFilters = unpinnedFilters + .filter((filter) => filter.section) + .reduce( + (acc, filter) => { + const filterActionItem = filterToActionItem(filter); + const sectionIndex = acc.findIndex( + (section) => section.title === filter.section, + ); + + if (sectionIndex === -1) { + acc.push({ + title: filter.section!, + items: [filterActionItem], + }); + } else { + acc[sectionIndex].items.push(filterActionItem); + } + + return acc; + }, + [] as { + title: string; + items: ActionListItemDescriptor[]; + }[], + ); + + const hasOneOrMorePinnedFilters = pinnedFilters.length >= 1; + + const se23LabelVariant = mdDown && se23 ? 'bodyLg' : 'bodySm'; + const labelVariant = mdDown ? 'bodyMd' : 'bodySm'; + + const addFilterActivator = ( +
+ + + {i18n.translate('Polaris.Filters.addFilter')}{' '} + + + +
+ ); + + const handleClearAllFilters = () => { + setLocalPinnedFilters([]); + onClearAll?.(); + }; + const shouldShowAddButton = filters.some((filter) => !filter.pinned); + + const pinnedFiltersMarkup = pinnedFilters.map( + ({key: filterKey, ...pinnedFilter}) => { + const appliedFilter = appliedFilters?.find(({key}) => key === filterKey); + const handleFilterPillRemove = () => { + setLocalPinnedFilters((currentLocalPinnedFilters) => + currentLocalPinnedFilters.filter((key) => key !== filterKey), + ); + appliedFilter?.onRemove(filterKey); + }; + + return ( + + ); + }, + ); + + const addButton = shouldShowAddButton ? ( +
+ + + +
+ ) : null; + + const clearAllMarkup = + appliedFilters?.length || localPinnedFilters.length ? ( +
+ +
+ ) : null; + + return ( +
+
+
+ {pinnedFiltersMarkup} + {addButton} + {clearAllMarkup} +
+
+ {hideQueryField ? ( + + + {additionalContent} + + + ) : null} +
+ ); +} diff --git a/polaris-react/src/components/Filters/components/FiltersBar/index.ts b/polaris-react/src/components/Filters/components/FiltersBar/index.ts new file mode 100644 index 00000000000..0bb12d6cb90 --- /dev/null +++ b/polaris-react/src/components/Filters/components/FiltersBar/index.ts @@ -0,0 +1 @@ +export * from './FiltersBar'; diff --git a/polaris-react/src/components/Filters/components/FiltersBar/tests/FiltersBar.test.tsx b/polaris-react/src/components/Filters/components/FiltersBar/tests/FiltersBar.test.tsx new file mode 100644 index 00000000000..669034455b9 --- /dev/null +++ b/polaris-react/src/components/Filters/components/FiltersBar/tests/FiltersBar.test.tsx @@ -0,0 +1,347 @@ +import React from 'react'; +import {matchMedia} from '@shopify/jest-dom-mocks'; +import {mountWithApp} from 'tests/utilities'; + +import {ActionList} from '../../../../ActionList'; +import {FiltersBar} from '../FiltersBar'; +import type {FiltersBarProps} from '../FiltersBar'; +import {FilterPill} from '../../FilterPill'; + +describe('', () => { + let originalScroll: any; + + beforeEach(() => { + originalScroll = HTMLElement.prototype.scroll; + matchMedia.mock(); + }); + + afterEach(() => { + HTMLElement.prototype.scroll = originalScroll; + matchMedia.restore(); + }); + + const defaultProps: FiltersBarProps = { + filters: [ + { + key: 'foo', + label: 'Foo', + pinned: false, + filter:
Filter
, + }, + { + key: 'bar', + label: 'Bar', + pinned: true, + filter:
Filter
, + }, + { + key: 'baz', + label: 'Baz', + pinned: false, + filter:
Filter
, + }, + ], + appliedFilters: [], + onClearAll: jest.fn(), + }; + + it('renders a list of pinned filters', () => { + const scrollSpy = jest.fn(); + HTMLElement.prototype.scroll = scrollSpy; + const wrapper = mountWithApp(); + + expect(wrapper).toContainReactComponentTimes(FilterPill, 1); + expect(wrapper).toContainReactComponent(FilterPill, { + label: defaultProps.filters[1].label, + }); + }); + + it('renders the unpinned filters inside a Popover', () => { + const scrollSpy = jest.fn(); + HTMLElement.prototype.scroll = scrollSpy; + const wrapper = mountWithApp(); + + wrapper.act(() => { + wrapper + .find('button', { + 'aria-label': 'Add filter', + })! + .trigger('onClick'); + }); + + expect(wrapper).toContainReactComponent(ActionList, { + items: [ + expect.objectContaining({content: defaultProps.filters[0].label}), + expect.objectContaining({content: defaultProps.filters[2].label}), + ], + }); + }); + + it('renders the unpinned disabled filters inside a Popover', () => { + const scrollSpy = jest.fn(); + HTMLElement.prototype.scroll = scrollSpy; + const filters = [ + ...defaultProps.filters, + { + key: 'disabled', + label: 'Disabled', + pinned: false, + disabled: true, + filter:
Filter
, + }, + ]; + + const wrapper = mountWithApp( + , + ); + + wrapper.act(() => { + wrapper + .find('button', { + 'aria-label': 'Add filter', + })! + .trigger('onClick'); + }); + + expect(wrapper).toContainReactComponent(ActionList, { + items: [ + expect.objectContaining({ + content: filters[0].label, + }), + expect.objectContaining({ + content: filters[2].label, + }), + expect.objectContaining({ + content: filters[3].label, + disabled: true, + }), + ], + }); + }); + + it('renders an applied filter', () => { + const scrollSpy = jest.fn(); + HTMLElement.prototype.scroll = scrollSpy; + const appliedFilters = [ + { + ...defaultProps.filters[2], + label: 'Bux', + value: ['Bux'], + onRemove: jest.fn(), + }, + ]; + const wrapper = mountWithApp( + , + ); + + expect(wrapper).toContainReactComponentTimes(FilterPill, 2); + expect(wrapper.findAll(FilterPill)[1]).toHaveReactProps({ + label: 'Bux', + selected: true, + }); + }); + + it('will not open the popover for an applied filter by default', () => { + const appliedFilters = [ + { + ...defaultProps.filters[2], + label: 'Bux', + value: ['Bux'], + onRemove: jest.fn(), + }, + ]; + const wrapper = mountWithApp( + , + ); + + expect(wrapper).toContainReactComponentTimes(FilterPill, 2); + expect(wrapper.findAll(FilterPill)[1]).toHaveReactProps({ + label: 'Bux', + initialActive: false, + }); + }); + + it('triggers the onAddFilterClick callback when the add filter button is clicked', () => { + const callbackFunction = jest.fn(); + const wrapper = mountWithApp( + , + ); + wrapper.act(() => { + wrapper + .find('button', { + 'aria-label': 'Add filter', + })! + .trigger('onClick'); + }); + + expect(callbackFunction).toHaveBeenCalled(); + expect(wrapper).toContainReactComponent(ActionList); + }); + + it('will not remove a pinned filter when it is removed from the applied filters array', () => { + const appliedFilters = [ + { + ...defaultProps.filters[2], + label: 'Value is Baz', + value: ['Baz'], + onRemove: jest.fn(), + }, + ]; + const wrapper = mountWithApp( + , + ); + + expect(wrapper.findAll(FilterPill)[1]).toHaveReactProps({ + label: 'Value is Baz', + selected: true, + }); + wrapper.setProps({appliedFilters: []}); + + expect(wrapper.findAll(FilterPill)[1]).toHaveReactProps({ + label: 'Baz', + selected: false, + }); + }); + + it('correctly invokes the onRemove callback when clicking on an applied filter', () => { + const scrollSpy = jest.fn(); + HTMLElement.prototype.scroll = scrollSpy; + const appliedFilters = [ + { + ...defaultProps.filters[2], + label: 'Bux', + value: ['Bux'], + onRemove: jest.fn(), + }, + ]; + const wrapper = mountWithApp( + , + ); + + wrapper.act(() => { + wrapper.findAll(FilterPill)[1].findAll('button')[1].trigger('onClick'); + }); + + expect(appliedFilters[0].onRemove).toHaveBeenCalled(); + }); + + it('will not render the add badge if all filters are pinned by default', () => { + const scrollSpy = jest.fn(); + HTMLElement.prototype.scroll = scrollSpy; + const filters = defaultProps.filters.map((filter) => ({ + ...filter, + pinned: true, + })); + + const wrapper = mountWithApp( + , + ); + + expect(wrapper).not.toContainReactComponent('div', { + className: 'AddFilterActivator', + }); + }); + + it('will not render a disabled filter if pinned', () => { + const scrollSpy = jest.fn(); + HTMLElement.prototype.scroll = scrollSpy; + const filters = [ + ...defaultProps.filters, + { + key: 'disabled', + label: 'Disabled', + pinned: true, + disabled: true, + filter:
Filter
, + }, + ]; + + const wrapper = mountWithApp( + , + ); + + expect(wrapper).toContainReactComponentTimes(FilterPill, 2); + + wrapper.act(() => { + wrapper + .find('button', { + 'aria-label': 'Add filter', + })! + .trigger('onClick'); + }); + + expect(wrapper).toContainReactComponent(ActionList, { + items: [ + expect.objectContaining({content: defaultProps.filters[0].label}), + expect.objectContaining({content: defaultProps.filters[2].label}), + ], + }); + + expect(wrapper.findAll(FilterPill)[1].domNode).toBeNull(); + }); + + it('renders filters with sections', () => { + const filtersWithSections = [ + { + key: 'sectionfilter1', + label: 'SF1', + pinned: false, + filter:
SF1
, + section: 'Section One', + }, + { + key: 'sectionfilter2', + label: 'SF2', + pinned: false, + filter:
SF1
, + section: 'Section Two', + }, + { + key: 'sectionfilter3', + label: 'SF3', + pinned: false, + filter:
SF3
, + section: 'Section One', + }, + ]; + + const wrapper = mountWithApp( + , + ); + + wrapper.act(() => { + wrapper + .find('button', { + 'aria-label': 'Add filter', + })! + .trigger('onClick'); + }); + + expect(wrapper).toContainReactComponent(ActionList, { + sections: [ + expect.objectContaining({ + title: 'Section One', + items: [ + expect.objectContaining({ + content: filtersWithSections[0].label, + }), + expect.objectContaining({ + content: filtersWithSections[2].label, + }), + ], + }), + expect.objectContaining({ + title: 'Section Two', + items: [ + expect.objectContaining({ + content: filtersWithSections[1].label, + }), + ], + }), + ], + }); + }); +}); diff --git a/polaris-react/src/components/Filters/components/index.ts b/polaris-react/src/components/Filters/components/index.ts index 2bb64cad0c9..02044c400ad 100644 --- a/polaris-react/src/components/Filters/components/index.ts +++ b/polaris-react/src/components/Filters/components/index.ts @@ -1,2 +1,3 @@ export * from './FilterPill'; export * from './SearchField'; +export * from './FiltersBar'; diff --git a/polaris-react/src/components/Filters/tests/Filters.test.tsx b/polaris-react/src/components/Filters/tests/Filters.test.tsx index 761d5d00254..ead52bed33c 100644 --- a/polaris-react/src/components/Filters/tests/Filters.test.tsx +++ b/polaris-react/src/components/Filters/tests/Filters.test.tsx @@ -2,10 +2,9 @@ import React from 'react'; import {matchMedia} from '@shopify/jest-dom-mocks'; import {mountWithApp} from 'tests/utilities'; -import {ActionList} from '../../ActionList'; import {Filters} from '../Filters'; import type {FiltersProps} from '../Filters'; -import {FilterPill} from '../components'; +import {FiltersBar, SearchField} from '../components'; describe('', () => { let originalScroll: any; @@ -47,330 +46,38 @@ describe('', () => { onClearAll: jest.fn(), }; - it('renders a list of pinned filters', () => { + it('renders the SearchField by default', () => { const scrollSpy = jest.fn(); HTMLElement.prototype.scroll = scrollSpy; const wrapper = mountWithApp(); - - expect(wrapper).toContainReactComponentTimes(FilterPill, 1); - expect(wrapper).toContainReactComponent(FilterPill, { - label: defaultProps.filters[1].label, - }); + expect(wrapper).toContainReactComponent(SearchField); }); - it('renders the unpinned filters inside a Popover', () => { + it('does not render the SearchField if hideQueryField is true', () => { const scrollSpy = jest.fn(); HTMLElement.prototype.scroll = scrollSpy; - const wrapper = mountWithApp(); - - wrapper.act(() => { - wrapper - .find('button', { - 'aria-label': 'Add filter', - })! - .trigger('onClick'); - }); - - expect(wrapper).toContainReactComponent(ActionList, { - items: [ - expect.objectContaining({content: defaultProps.filters[0].label}), - expect.objectContaining({content: defaultProps.filters[2].label}), - ], - }); + const wrapper = mountWithApp(); + expect(wrapper).not.toContainReactComponent(SearchField); }); - it('renders the unpinned disabled filters inside a Popover', () => { + it('renders the FiltersBar by default', () => { const scrollSpy = jest.fn(); HTMLElement.prototype.scroll = scrollSpy; - const filters = [ - ...defaultProps.filters, - { - key: 'disabled', - label: 'Disabled', - pinned: false, - disabled: true, - filter:
Filter
, - }, - ]; - - const wrapper = mountWithApp( - , - ); - - wrapper.act(() => { - wrapper - .find('button', { - 'aria-label': 'Add filter', - })! - .trigger('onClick'); - }); - - expect(wrapper).toContainReactComponent(ActionList, { - items: [ - expect.objectContaining({ - content: filters[0].label, - }), - expect.objectContaining({ - content: filters[2].label, - }), - expect.objectContaining({ - content: filters[3].label, - disabled: true, - }), - ], - }); - }); - - it('renders an applied filter', () => { - const scrollSpy = jest.fn(); - HTMLElement.prototype.scroll = scrollSpy; - const appliedFilters = [ - { - ...defaultProps.filters[2], - label: 'Bux', - value: ['Bux'], - onRemove: jest.fn(), - }, - ]; - const wrapper = mountWithApp( - , - ); - - expect(wrapper).toContainReactComponentTimes(FilterPill, 2); - expect(wrapper.findAll(FilterPill)[1]).toHaveReactProps({ - label: 'Bux', - selected: true, - }); - }); - - it('will not open the popover for an applied filter by default', () => { - const appliedFilters = [ - { - ...defaultProps.filters[2], - label: 'Bux', - value: ['Bux'], - onRemove: jest.fn(), - }, - ]; - const wrapper = mountWithApp( - , - ); - - expect(wrapper).toContainReactComponentTimes(FilterPill, 2); - expect(wrapper.findAll(FilterPill)[1]).toHaveReactProps({ - label: 'Bux', - initialActive: false, - }); - }); - - it('triggers the onAddFilterClick callback when the add filter button is clicked', () => { - const callbackFunction = jest.fn(); - const wrapper = mountWithApp( - , - ); - wrapper.act(() => { - wrapper - .find('button', { - 'aria-label': 'Add filter', - })! - .trigger('onClick'); - }); - - expect(callbackFunction).toHaveBeenCalled(); - expect(wrapper).toContainReactComponent(ActionList); - }); - - it('will not remove a pinned filter when it is removed from the applied filters array', () => { - const appliedFilters = [ - { - ...defaultProps.filters[2], - label: 'Value is Baz', - value: ['Baz'], - onRemove: jest.fn(), - }, - ]; - const wrapper = mountWithApp( - , - ); - - expect(wrapper.findAll(FilterPill)[1]).toHaveReactProps({ - label: 'Value is Baz', - selected: true, - }); - wrapper.setProps({appliedFilters: []}); - - expect(wrapper.findAll(FilterPill)[1]).toHaveReactProps({ - label: 'Baz', - selected: false, - }); - }); - - it('correctly adds the applied filters when updated via props', () => { - const appliedFilters = [ - { - ...defaultProps.filters[2], - label: 'Value is Baz', - value: ['Baz'], - onRemove: jest.fn(), - }, - ]; - const wrapper = mountWithApp( - , - ); - - expect(wrapper).not.toContainReactComponent(FilterPill, { - selected: true, - }); - - wrapper.setProps({ - appliedFilters, - }); - - expect(wrapper.findAll(FilterPill)[1]).toHaveReactProps({ - label: 'Value is Baz', - selected: true, - }); - }); - - it('correctly invokes the onRemove callback when clicking on an applied filter', () => { - const scrollSpy = jest.fn(); - HTMLElement.prototype.scroll = scrollSpy; - const appliedFilters = [ - { - ...defaultProps.filters[2], - label: 'Bux', - value: ['Bux'], - onRemove: jest.fn(), - }, - ]; - const wrapper = mountWithApp( - , - ); - - wrapper.act(() => { - wrapper.findAll(FilterPill)[1].findAll('button')[1].trigger('onClick'); - }); - - expect(appliedFilters[0].onRemove).toHaveBeenCalled(); + const wrapper = mountWithApp(); + expect(wrapper).toContainReactComponent(FiltersBar); }); - it('will not render the add badge if all filters are pinned by default', () => { + it('does not render the FiltersBar if hideFilters is true', () => { const scrollSpy = jest.fn(); HTMLElement.prototype.scroll = scrollSpy; - const filters = defaultProps.filters.map((filter) => ({ - ...filter, - pinned: true, - })); - - const wrapper = mountWithApp( - , - ); - - expect(wrapper).not.toContainReactComponent('div', { - className: 'AddFilterActivator', - }); + const wrapper = mountWithApp(); + expect(wrapper).not.toContainReactComponent(FiltersBar); }); - it('will not render a disabled filter if pinned', () => { + it('does not render the FiltersBar if hideFilters is falsy but there are no filters', () => { const scrollSpy = jest.fn(); HTMLElement.prototype.scroll = scrollSpy; - const filters = [ - ...defaultProps.filters, - { - key: 'disabled', - label: 'Disabled', - pinned: true, - disabled: true, - filter:
Filter
, - }, - ]; - - const wrapper = mountWithApp( - , - ); - - expect(wrapper).toContainReactComponentTimes(FilterPill, 2); - - wrapper.act(() => { - wrapper - .find('button', { - 'aria-label': 'Add filter', - })! - .trigger('onClick'); - }); - - expect(wrapper).toContainReactComponent(ActionList, { - items: [ - expect.objectContaining({content: defaultProps.filters[0].label}), - expect.objectContaining({content: defaultProps.filters[2].label}), - ], - }); - - expect(wrapper.findAll(FilterPill)[1].domNode).toBeNull(); - }); - - it('renders filters with sections', () => { - const filtersWithSections = [ - { - key: 'sectionfilter1', - label: 'SF1', - pinned: false, - filter:
SF1
, - section: 'Section One', - }, - { - key: 'sectionfilter2', - label: 'SF2', - pinned: false, - filter:
SF1
, - section: 'Section Two', - }, - { - key: 'sectionfilter3', - label: 'SF3', - pinned: false, - filter:
SF3
, - section: 'Section One', - }, - ]; - - const wrapper = mountWithApp( - , - ); - - wrapper.act(() => { - wrapper - .find('button', { - 'aria-label': 'Add filter', - })! - .trigger('onClick'); - }); - - expect(wrapper).toContainReactComponent(ActionList, { - sections: [ - expect.objectContaining({ - title: 'Section One', - items: [ - expect.objectContaining({ - content: filtersWithSections[0].label, - }), - expect.objectContaining({ - content: filtersWithSections[2].label, - }), - ], - }), - expect.objectContaining({ - title: 'Section Two', - items: [ - expect.objectContaining({ - content: filtersWithSections[1].label, - }), - ], - }), - ], - }); + const wrapper = mountWithApp(); + expect(wrapper).not.toContainReactComponent(FiltersBar); }); }); diff --git a/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx b/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx index ba3d0b4b9cb..9615ef116b5 100644 --- a/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx +++ b/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx @@ -522,9 +522,291 @@ export function WithPinnedFilters() { disabled: false, loading: false, }; - const [accountStatus, setAccountStatus] = useState([ - 'enabled', + const [accountStatus, setAccountStatus] = useState(null); + const [moneySpent, setMoneySpent] = useState(null); + const [taggedWith, setTaggedWith] = useState(''); + const [queryValue, setQueryValue] = useState(''); + + const handleAccountStatusChange = useCallback( + (value) => setAccountStatus(value), + [], + ); + const handleMoneySpentChange = useCallback( + (value) => setMoneySpent(value), + [], + ); + const handleTaggedWithChange = useCallback( + (value) => setTaggedWith(value), + [], + ); + const handleFiltersQueryChange = useCallback( + (value) => setQueryValue(value), + [], + ); + const handleAccountStatusRemove = useCallback( + () => setAccountStatus(null), + [], + ); + const handleMoneySpentRemove = useCallback(() => setMoneySpent(null), []); + const handleTaggedWithRemove = useCallback(() => setTaggedWith(''), []); + const handleQueryValueRemove = useCallback(() => setQueryValue(''), []); + const handleFiltersClearAll = useCallback(() => { + handleAccountStatusRemove(); + handleMoneySpentRemove(); + handleTaggedWithRemove(); + handleQueryValueRemove(); + }, [ + handleAccountStatusRemove, + handleMoneySpentRemove, + handleQueryValueRemove, + handleTaggedWithRemove, ]); + + const filters = [ + { + key: 'accountStatus', + label: 'Account status', + filter: ( + + ), + pinned: true, + }, + { + key: 'taggedWith', + label: 'Tagged with', + filter: ( + + ), + pinned: true, + }, + { + key: 'moneySpent', + label: 'Money spent', + filter: ( + + ), + }, + ]; + + const appliedFilters: IndexFiltersProps['appliedFilters'] = []; + if (!isEmpty(accountStatus)) { + const key = 'accountStatus'; + appliedFilters.push({ + key, + label: disambiguateLabel(key, accountStatus), + onRemove: handleAccountStatusRemove, + }); + } + if (!isEmpty(moneySpent)) { + const key = 'moneySpent'; + appliedFilters.push({ + key, + label: disambiguateLabel(key, moneySpent), + onRemove: handleMoneySpentRemove, + }); + } + if (!isEmpty(taggedWith)) { + const key = 'taggedWith'; + appliedFilters.push({ + key, + label: disambiguateLabel(key, taggedWith), + onRemove: handleTaggedWithRemove, + }); + } + + return ( + + {}} + onSort={setSortSelected} + primaryAction={primaryAction} + cancelAction={{ + onAction: onHandleCancel, + disabled: false, + loading: false, + }} + tabs={tabs} + selected={selected} + onSelect={setSelected} + canCreateNewView + onCreateNewView={onCreateNewView} + filters={filters} + appliedFilters={appliedFilters} + onClearAll={handleFiltersClearAll} + mode={mode} + setMode={setMode} + /> + + + ); + + function disambiguateLabel(key, value) { + switch (key) { + case 'moneySpent': + return `Money spent is between $${value[0]} and $${value[1]}`; + case 'taggedWith': + return `Tagged with ${value}`; + case 'accountStatus': + return value.map((val) => `Customer ${val}`).join(', '); + default: + return value; + } + } + + function isEmpty(value) { + if (Array.isArray(value)) { + return value.length === 0; + } else { + return value === '' || value == null; + } + } +} + +export function WithPrefilledFilters() { + const sleep = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + const [itemStrings, setItemStrings] = useState([ + 'All', + 'Unpaid', + 'Open', + 'Closed', + 'Local delivery', + 'Local pickup', + ]); + const deleteView = (index: number) => { + const newItemStrings = [...itemStrings]; + newItemStrings.splice(index, 1); + setItemStrings(newItemStrings); + setSelected(0); + }; + + const duplicateView = async (name: string) => { + setItemStrings([...itemStrings, name]); + setSelected(itemStrings.length); + await sleep(1); + return true; + }; + + const tabs: TabProps[] = itemStrings.map((item, index) => ({ + content: item, + index, + onAction: () => {}, + id: `${item}-${index}`, + isLocked: index === 0, + actions: + index === 0 + ? [] + : [ + { + type: 'rename', + onAction: () => {}, + onPrimaryAction: async (value: string) => { + const newItemsStrings = tabs.map((item, idx) => { + if (idx === index) { + return value; + } + return item.content; + }); + await sleep(1); + setItemStrings(newItemsStrings); + return true; + }, + }, + { + type: 'duplicate', + onPrimaryAction: async (name) => { + await sleep(1); + duplicateView(name); + return true; + }, + }, + { + type: 'edit', + }, + { + type: 'delete', + onPrimaryAction: async (id: string) => { + await sleep(1); + deleteView(index); + return true; + }, + }, + ], + })); + const [selected, setSelected] = useState(0); + const onCreateNewView = async (value: string) => { + await sleep(500); + setItemStrings([...itemStrings, value]); + setSelected(itemStrings.length); + return true; + }; + const sortOptions: IndexFiltersProps['sortOptions'] = [ + {label: 'Order', value: 'order asc', directionLabel: 'Ascending'}, + {label: 'Order', value: 'order desc', directionLabel: 'Descending'}, + {label: 'Customer', value: 'customer asc', directionLabel: 'A-Z'}, + {label: 'Customer', value: 'customer desc', directionLabel: 'Z-A'}, + {label: 'Date', value: 'date asc', directionLabel: 'A-Z'}, + {label: 'Date', value: 'date desc', directionLabel: 'Z-A'}, + {label: 'Total', value: 'total asc', directionLabel: 'Ascending'}, + {label: 'Total', value: 'total desc', directionLabel: 'Descending'}, + ]; + const [sortSelected, setSortSelected] = useState(['order asc']); + const {mode, setMode} = useSetIndexFiltersMode(); + const onHandleCancel = () => {}; + + const onHandleSave = async () => { + await sleep(1); + return true; + }; + + const primaryAction: IndexFiltersProps['primaryAction'] = + selected === 0 + ? { + type: 'save-as', + onAction: onCreateNewView, + disabled: false, + loading: false, + } + : { + type: 'save', + onAction: onHandleSave, + disabled: false, + loading: false, + }; + const [accountStatus, setAccountStatus] = useState(null); const [moneySpent, setMoneySpent] = useState(null); const [taggedWith, setTaggedWith] = useState('Returning customer'); const [queryValue, setQueryValue] = useState(''); @@ -672,6 +954,9 @@ export function WithPinnedFilters() { setMode={setMode} />
+ ); From ad1818fa74f6ff7ec8ee7a048544d9bd62e77704 Mon Sep 17 00:00:00 2001 From: Marc Thomas Date: Wed, 27 Sep 2023 18:05:33 +0100 Subject: [PATCH 2/6] chore: add changeset --- .changeset/slimy-pans-worry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/slimy-pans-worry.md diff --git a/.changeset/slimy-pans-worry.md b/.changeset/slimy-pans-worry.md new file mode 100644 index 00000000000..1698bd06680 --- /dev/null +++ b/.changeset/slimy-pans-worry.md @@ -0,0 +1,5 @@ +--- +'@shopify/polaris': minor +--- + +Updated Filters to not render or perform filters logic if the filters array is empty From adbce26652a4aa64c0e2fe2db7479fe41b949457 Mon Sep 17 00:00:00 2001 From: Marc Thomas Date: Thu, 28 Sep 2023 11:55:29 +0100 Subject: [PATCH 3/6] chore: update story --- .../IndexFilters/IndexFilters.stories.tsx | 299 +++++++++++++++++- 1 file changed, 295 insertions(+), 4 deletions(-) diff --git a/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx b/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx index 9615ef116b5..0642f38faa9 100644 --- a/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx +++ b/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx @@ -806,7 +806,9 @@ export function WithPrefilledFilters() { disabled: false, loading: false, }; - const [accountStatus, setAccountStatus] = useState(null); + const [accountStatus, setAccountStatus] = useState([ + 'enabled', + ]); const [moneySpent, setMoneySpent] = useState(null); const [taggedWith, setTaggedWith] = useState('Returning customer'); const [queryValue, setQueryValue] = useState(''); @@ -954,9 +956,298 @@ export function WithPrefilledFilters() { setMode={setMode} />
- + + ); + + function disambiguateLabel(key, value) { + switch (key) { + case 'moneySpent': + return `Money spent is between $${value[0]} and $${value[1]}`; + case 'taggedWith': + return `Tagged with ${value}`; + case 'accountStatus': + return value.map((val) => `Customer ${val}`).join(', '); + default: + return value; + } + } + + function isEmpty(value) { + if (Array.isArray(value)) { + return value.length === 0; + } else { + return value === '' || value == null; + } + } +} + +export function WithAsyncData() { + const sleep = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + const [loadData, setLoadData] = useState(false); + const [itemStrings, setItemStrings] = useState([ + 'All', + 'Unpaid', + 'Open', + 'Closed', + 'Local delivery', + 'Local pickup', + ]); + const deleteView = (index: number) => { + const newItemStrings = [...itemStrings]; + newItemStrings.splice(index, 1); + setItemStrings(newItemStrings); + setSelected(0); + }; + + const duplicateView = async (name: string) => { + setItemStrings([...itemStrings, name]); + setSelected(itemStrings.length); + await sleep(1); + return true; + }; + + const tabs: TabProps[] = itemStrings.map((item, index) => ({ + content: item, + index, + onAction: () => {}, + id: `${item}-${index}`, + isLocked: index === 0, + actions: + index === 0 + ? [] + : [ + { + type: 'rename', + onAction: () => {}, + onPrimaryAction: async (value: string) => { + const newItemsStrings = tabs.map((item, idx) => { + if (idx === index) { + return value; + } + return item.content; + }); + await sleep(1); + setItemStrings(newItemsStrings); + return true; + }, + }, + { + type: 'duplicate', + onPrimaryAction: async (name) => { + await sleep(1); + duplicateView(name); + return true; + }, + }, + { + type: 'edit', + }, + { + type: 'delete', + onPrimaryAction: async (id: string) => { + await sleep(1); + deleteView(index); + return true; + }, + }, + ], + })); + const [selected, setSelected] = useState(0); + const onCreateNewView = async (value: string) => { + await sleep(500); + setItemStrings([...itemStrings, value]); + setSelected(itemStrings.length); + return true; + }; + const sortOptions: IndexFiltersProps['sortOptions'] = [ + {label: 'Order', value: 'order asc', directionLabel: 'Ascending'}, + {label: 'Order', value: 'order desc', directionLabel: 'Descending'}, + {label: 'Customer', value: 'customer asc', directionLabel: 'A-Z'}, + {label: 'Customer', value: 'customer desc', directionLabel: 'Z-A'}, + {label: 'Date', value: 'date asc', directionLabel: 'A-Z'}, + {label: 'Date', value: 'date desc', directionLabel: 'Z-A'}, + {label: 'Total', value: 'total asc', directionLabel: 'Ascending'}, + {label: 'Total', value: 'total desc', directionLabel: 'Descending'}, + ]; + const [sortSelected, setSortSelected] = useState(['order asc']); + const {mode, setMode} = useSetIndexFiltersMode(); + const onHandleCancel = () => {}; + + const onHandleSave = async () => { + await sleep(1); + return true; + }; + + const primaryAction: IndexFiltersProps['primaryAction'] = + selected === 0 + ? { + type: 'save-as', + onAction: onCreateNewView, + disabled: false, + loading: false, + } + : { + type: 'save', + onAction: onHandleSave, + disabled: false, + loading: false, + }; + const [accountStatus, setAccountStatus] = useState([ + 'enabled', + ]); + const [moneySpent, setMoneySpent] = useState(null); + const [taggedWith, setTaggedWith] = useState('Returning customer'); + const [queryValue, setQueryValue] = useState(''); + + const handleAccountStatusChange = useCallback( + (value) => setAccountStatus(value), + [], + ); + const handleMoneySpentChange = useCallback( + (value) => setMoneySpent(value), + [], + ); + const handleTaggedWithChange = useCallback( + (value) => setTaggedWith(value), + [], + ); + const handleFiltersQueryChange = useCallback( + (value) => setQueryValue(value), + [], + ); + const handleAccountStatusRemove = useCallback( + () => setAccountStatus(null), + [], + ); + const handleMoneySpentRemove = useCallback(() => setMoneySpent(null), []); + const handleTaggedWithRemove = useCallback(() => setTaggedWith(''), []); + const handleQueryValueRemove = useCallback(() => setQueryValue(''), []); + const handleFiltersClearAll = useCallback(() => { + handleAccountStatusRemove(); + handleMoneySpentRemove(); + handleTaggedWithRemove(); + handleQueryValueRemove(); + }, [ + handleAccountStatusRemove, + handleMoneySpentRemove, + handleQueryValueRemove, + handleTaggedWithRemove, + ]); + + const filters = [ + { + key: 'accountStatus', + label: 'Account status', + filter: ( + + ), + shortcut: true, + }, + { + key: 'taggedWith', + label: 'Tagged with', + filter: ( + + ), + shortcut: true, + }, + { + key: 'moneySpent', + label: 'Money spent', + filter: ( + + ), + }, + ]; + + const appliedFilters: IndexFiltersProps['appliedFilters'] = []; + if (!isEmpty(accountStatus)) { + const key = 'accountStatus'; + appliedFilters.push({ + key, + label: disambiguateLabel(key, accountStatus), + onRemove: handleAccountStatusRemove, + }); + } + if (!isEmpty(moneySpent)) { + const key = 'moneySpent'; + appliedFilters.push({ + key, + label: disambiguateLabel(key, moneySpent), + onRemove: handleMoneySpentRemove, + }); + } + if (!isEmpty(taggedWith)) { + const key = 'taggedWith'; + appliedFilters.push({ + key, + label: disambiguateLabel(key, taggedWith), + onRemove: handleTaggedWithRemove, + }); + } + + return ( + + {}} + onSort={setSortSelected} + primaryAction={primaryAction} + cancelAction={{ + onAction: onHandleCancel, + disabled: false, + loading: false, + }} + tabs={tabs} + selected={selected} + onSelect={setSelected} + canCreateNewView + onCreateNewView={onCreateNewView} + filters={loadData ? filters : []} + appliedFilters={loadData ? appliedFilters : []} + onClearAll={handleFiltersClearAll} + mode={mode} + setMode={setMode} + /> +
+
+ +
); From a6433020c9ed81ae6084fe87c76b1390b3985ff1 Mon Sep 17 00:00:00 2001 From: Marc Thomas Date: Thu, 28 Sep 2023 12:20:19 +0100 Subject: [PATCH 4/6] chore: remove unnecessary duplicate CSS file --- .../components/FiltersBar/FiltersBar.scss | 294 ------------------ .../components/FiltersBar/FiltersBar.tsx | 3 +- 2 files changed, 1 insertion(+), 296 deletions(-) delete mode 100644 polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.scss diff --git a/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.scss b/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.scss deleted file mode 100644 index d943042b619..00000000000 --- a/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.scss +++ /dev/null @@ -1,294 +0,0 @@ -@import '../../../../styles/common'; - -.Container { - position: relative; - z-index: var(--p-z-index-1); - border-bottom: var(--p-border-width-025) solid var(--p-color-border-subdued); - border-top-left-radius: var(--p-border-radius-200); - border-top-right-radius: var(--p-border-radius-200); - background: var(--p-color-bg); -} - -.ContainerUplift { - background: none; -} - -@media #{$p-breakpoints-sm-down} { - .Container { - border-top-left-radius: 0; - border-top-right-radius: 0; - height: 57px; - - &.ContainerUplift { - height: unset; - } - } -} - -.SearchField { - flex: 1; -} - -.Spinner { - width: var(--p-space-500); - transform: translateX(var(--p-space-100)); - - svg { - display: block; - } -} - -.FiltersWrapper { - border-bottom: var(--p-border-width-025) solid var(--p-color-border-subdued); - height: 53px; - overflow: hidden; - - @media #{$p-breakpoints-sm-down} { - background: var(--p-color-bg); - } - - @media #{$p-breakpoints-md-up} { - height: auto; - overflow: visible; - } -} - -.hideQueryField .FiltersWrapper { - display: flex; - align-items: center; -} - -.FiltersInner { - overflow: auto; - white-space: nowrap; - padding: var(--p-space-300) var(--p-space-400) var(--p-space-500); -} - -.hideQueryField .FiltersInner { - flex: 1; - padding: var(--p-space-300); -} - -@media #{$p-breakpoints-md-up} { - .FiltersInner { - overflow: visible; - flex-wrap: wrap; - gap: var(--p-space-200); - // stylelint-disable-next-line -- No 6px space token - padding: 0.375rem var(--p-space-200); - } - - .hideQueryField .FiltersInner { - flex: 1; - // stylelint-disable-next-line -- No 6px space token - padding: 0.375rem var(--p-space-200); - } -} - -.AddFilter { - background: var(--p-color-bg-subdued); - border-radius: var(--p-border-radius-750); - border: var(--p-color-border-subdued) dashed var(--p-border-width-025); - padding: 0 var(--p-space-200) 0 var(--p-space-300); - height: 28px; - cursor: pointer; - color: var(--p-color-text); - display: flex; - align-items: center; - justify-content: center; - outline: inherit; - // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY - @include focus-ring($border-width: var(--p-border-width-025)); - - path { - fill: var(--p-color-icon); - } - - @media #{$p-breakpoints-md-up} { - font-size: var(--p-font-size-75); - line-height: var(--p-font-line-height-400); - height: 24px; - // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY - padding: 0 0.375rem 0 var(--p-space-200); - } - - &:hover, - &:focus { - background: var(--p-color-bg-hover); - border-color: var(--p-color-border-hover); - - path { - fill: var(--p-color-icon-hover); - } - } - - &:active { - background: var(--p-color-bg-active); - border-color: var(--p-color-border-hover); - } - - &[aria-disabled='true'] { - background: var(--p-color-bg-disabled); - border-color: var(--p-color-border-disabled); - color: var(--p-color-text-disabled); - cursor: default; - - path { - fill: var(--p-color-icon-disabled); - } - } - - &:focus-visible:not(:active) { - // stylelint-disable-next-line -- no way to set focus ring without mixin currently - @include focus-ring($style: 'focused'); - } - - &::after { - border-radius: var(--p-border-radius-750); - } - - span { - margin-right: var(--p-space-050); - - @media #{$p-breakpoints-md-up} { - margin-right: var(--p-space-025); - } - } - - svg { - width: var(--p-space-500); - - @media #{$p-breakpoints-md-up} { - width: var(--p-space-400); - } - } - - #{$se23} & { - background: var(--p-color-bg-secondary-experimental); - border-radius: var(--p-border-radius-200); - border: var(--p-color-border) dashed var(--p-border-width-025); - - &:hover { - background: transparent; - border-style: solid; - } - - &:focus { - background: transparent; - outline-offset: var(--p-border-width-050); - } - - &:active { - background: var(--p-color-bg-subdued); - - #{$se23} & { - background: var(--p-color-bg-secondary-experimental); - } - } - - &::after { - border-radius: var(--p-border-radius-200); - } - } -} - -@media #{$p-breakpoints-md-down} { - .FiltersWrapperWithAddButton { - position: relative; - - .FiltersInner { - padding: var(--p-space-200); - padding-right: 0; - } - } - - .AddFilterActivatorMultiple { - position: sticky; - z-index: var(--p-z-index-1); - top: 0; - right: 0; - display: flex; - padding: var(--p-space-100) var(--p-space-400) var(--p-space-100) 0; - background: var(--p-color-bg); - margin-left: var(--p-space-200); - - &::before { - content: ''; - position: absolute; - top: 0; - left: -12px; - width: 12px; - height: 100%; - pointer-events: none; - // stylelint-disable -- needed to create the fade effect - background: linear-gradient( - 90deg, - rgba(255, 255, 255, 0) 0%, - var(--p-color-bg) 70%, - var(--p-color-bg) 100% - ); - // stylelint-enable - } - - .AddFilter { - padding: var(--p-space-300) var(--p-space-200); - - // stylelint-disable-next-line selector-max-combinators -- required to hide the text of the button - span { - display: none; - } - } - } -} - -.FiltersStickyArea { - position: relative; - display: flex; - gap: var(--p-space-100); - flex-wrap: nowrap; - align-items: center; - justify-content: flex-start; - - @media #{$p-breakpoints-md-up} { - flex-wrap: wrap; - } -} - -.ClearAll { - margin-left: var(--p-space-100); - - #{$se23} & { - margin-left: var(--p-space-200); - - // stylelint-disable-next-line -- se23 overrides - span { - font-size: var(--p-font-size-75); - font-weight: var(--p-font-weight-medium); - } - } -} - -@media #{$p-breakpoints-md-down} { - .ClearAll { - margin-left: var(--p-space-200); - padding-right: var(--p-space-400); - - #{$se23} & { - margin-left: 0; - - // stylelint-disable-next-line -- se23 overrides - span { - font-size: var(--p-font-size-350); - font-weight: var(--p-font-weight-medium); - } - } - } - - .MultiplePinnedFilterClearAll { - transform: translateX(-8px); - position: relative; - z-index: var(--p-z-index-1); - margin-left: 0; - padding-right: var(--p-space-400); - } -} diff --git a/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx b/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx index ab2ed9e351f..db4ecf68855 100644 --- a/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx +++ b/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx @@ -20,8 +20,7 @@ import {HorizontalStack} from '../../../HorizontalStack'; import {Box} from '../../../Box'; import {Button} from '../../../Button'; import {FilterPill} from '../FilterPill'; - -import styles from './FiltersBar.scss'; +import styles from '../../Filters.scss'; export interface FiltersBarProps { /** Currently entered text in the query field */ From dc392e93be6fc7936892490de9c11e98775033a9 Mon Sep 17 00:00:00 2001 From: Marc Thomas Date: Mon, 2 Oct 2023 10:45:42 +0100 Subject: [PATCH 5/6] chore: react to changes in dynamic filters --- .../components/FiltersBar/FiltersBar.tsx | 7 +++ .../IndexFilters/IndexFilters.stories.tsx | 54 ++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx b/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx index db4ecf68855..a2e079c1914 100644 --- a/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx +++ b/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx @@ -4,6 +4,7 @@ import {PlusMinor} from '@shopify/polaris-icons'; import type {TransitionStatus} from 'react-transition-group'; import {useI18n} from '../../../../utilities/i18n'; +import {useOnValueChange} from '../../../../utilities/use-on-value-change'; import {Popover} from '../../../Popover'; import {ActionList} from '../../../ActionList'; import {Text} from '../../../Text'; @@ -92,6 +93,12 @@ export function FiltersBar({ pinnedFiltersFromPropsAndAppliedFilters.map(({key}) => key), ); + useOnValueChange(filters.length, () => { + setLocalPinnedFilters( + pinnedFiltersFromPropsAndAppliedFilters.map(({key}) => key), + ); + }); + const pinnedFilters = localPinnedFilters .map((key) => filters.find((filter) => filter.key === key)) .reduce( diff --git a/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx b/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx index 0642f38faa9..c9bbe992152 100644 --- a/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx +++ b/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx @@ -985,6 +985,7 @@ export function WithAsyncData() { const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const [loadData, setLoadData] = useState(false); + const [addAsyncFilter, setAddAsyncFilter] = useState(false); const [itemStrings, setItemStrings] = useState([ 'All', 'Unpaid', @@ -1071,7 +1072,7 @@ export function WithAsyncData() { {label: 'Total', value: 'total desc', directionLabel: 'Descending'}, ]; const [sortSelected, setSortSelected] = useState(['order asc']); - const {mode, setMode} = useSetIndexFiltersMode(); + const {mode, setMode} = useSetIndexFiltersMode(IndexFiltersMode.Filtering); const onHandleCancel = () => {}; const onHandleSave = async () => { @@ -1098,6 +1099,10 @@ export function WithAsyncData() { ]); const [moneySpent, setMoneySpent] = useState(null); const [taggedWith, setTaggedWith] = useState('Returning customer'); + const [deliveryMethod, setDeliveryMethod] = useState([ + 'local_pick_up', + 'local_delivery', + ]); const [queryValue, setQueryValue] = useState(''); const handleAccountStatusChange = useCallback( @@ -1112,6 +1117,10 @@ export function WithAsyncData() { (value) => setTaggedWith(value), [], ); + const handleDeliveryMethodChange = useCallback( + (value) => setDeliveryMethod(value), + [], + ); const handleFiltersQueryChange = useCallback( (value) => setQueryValue(value), [], @@ -1122,17 +1131,23 @@ export function WithAsyncData() { ); const handleMoneySpentRemove = useCallback(() => setMoneySpent(null), []); const handleTaggedWithRemove = useCallback(() => setTaggedWith(''), []); + const handleDeliveryMethodRemove = useCallback( + () => setDeliveryMethod(null), + [], + ); const handleQueryValueRemove = useCallback(() => setQueryValue(''), []); const handleFiltersClearAll = useCallback(() => { handleAccountStatusRemove(); handleMoneySpentRemove(); handleTaggedWithRemove(); + handleDeliveryMethodRemove(); handleQueryValueRemove(); }, [ handleAccountStatusRemove, handleMoneySpentRemove, handleQueryValueRemove, handleTaggedWithRemove, + handleDeliveryMethodRemove, ]); const filters = [ @@ -1189,6 +1204,28 @@ export function WithAsyncData() { }, ]; + if (addAsyncFilter) { + filters.push({ + key: 'delivery_method', + label: 'Delivery method', + filter: ( + + ), + }); + } + const appliedFilters: IndexFiltersProps['appliedFilters'] = []; if (!isEmpty(accountStatus)) { const key = 'accountStatus'; @@ -1214,6 +1251,16 @@ export function WithAsyncData() { onRemove: handleTaggedWithRemove, }); } + if (!isEmpty(deliveryMethod)) { + const key = 'delivery_method'; + appliedFilters.push({ + key, + label: disambiguateLabel(key, deliveryMethod), + onRemove: handleDeliveryMethodRemove, + }); + } + + console.log({addAsyncFilter, filters}); return ( @@ -1247,6 +1294,9 @@ export function WithAsyncData() { + ); @@ -1259,6 +1309,8 @@ export function WithAsyncData() { return `Tagged with ${value}`; case 'accountStatus': return value.map((val) => `Customer ${val}`).join(', '); + case 'delivery_method': + return `Delivery method: ${value.join(', ')}`; default: return value; } From 6e4db2585d36a522f18a9d57d4a31e757db1aa37 Mon Sep 17 00:00:00 2001 From: Marc Thomas Date: Mon, 2 Oct 2023 14:23:56 +0100 Subject: [PATCH 6/6] chore: rebase against v12.0.0 --- polaris-react/src/components/Filters/Filters.tsx | 2 -- .../Filters/components/FiltersBar/FiltersBar.tsx | 15 +++++---------- .../IndexFilters/IndexFilters.stories.tsx | 14 ++++++++++---- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/polaris-react/src/components/Filters/Filters.tsx b/polaris-react/src/components/Filters/Filters.tsx index 4210dcce7db..173d83f398c 100644 --- a/polaris-react/src/components/Filters/Filters.tsx +++ b/polaris-react/src/components/Filters/Filters.tsx @@ -124,8 +124,6 @@ export function Filters({ onAddFilterClick, closeOnChildOverlayClick, }: FiltersProps) { - const {polarisSummerEditions2023: se23} = useFeatures(); - const additionalContent = useMemo(() => { return ( <> diff --git a/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx b/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx index a2e079c1914..3fb5ff671cb 100644 --- a/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx +++ b/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx @@ -11,13 +11,12 @@ import {Text} from '../../../Text'; import {UnstyledButton} from '../../../UnstyledButton'; import {classNames} from '../../../../utilities/css'; import {useBreakpoints} from '../../../../utilities/breakpoints'; -import {useFeatures} from '../../../../utilities/features'; import type { ActionListItemDescriptor, AppliedFilterInterface, FilterInterface, } from '../../../../types'; -import {HorizontalStack} from '../../../HorizontalStack'; +import {InlineStack} from '../../../InlineStack'; import {Box} from '../../../Box'; import {Button} from '../../../Button'; import {FilterPill} from '../FilterPill'; @@ -36,7 +35,6 @@ export interface FiltersBarProps { appliedFilters?: AppliedFilterInterface[]; /** Callback when the reset all button is pressed. */ onClearAll: () => void; - /** Disable all filters. */ disabled?: boolean; /** Hide the query field. */ @@ -66,7 +64,6 @@ export function FiltersBar({ }: FiltersBarProps) { const i18n = useI18n(); const {mdDown} = useBreakpoints(); - const {polarisSummerEditions2023: se23} = useFeatures(); const [popoverActive, setPopoverActive] = useState(false); const hasMounted = useRef(false); useEffect(() => { @@ -162,7 +159,6 @@ export function FiltersBar({ const hasOneOrMorePinnedFilters = pinnedFilters.length >= 1; - const se23LabelVariant = mdDown && se23 ? 'bodyLg' : 'bodySm'; const labelVariant = mdDown ? 'bodyMd' : 'bodySm'; const addFilterActivator = ( @@ -178,7 +174,7 @@ export function FiltersBar({ disableFilters } > - + {i18n.translate('Polaris.Filters.addFilter')}{' '} @@ -253,10 +249,9 @@ export function FiltersBar({ > @@ -287,7 +282,7 @@ export function FiltersBar({ paddingBlockStart="200" paddingBlockEnd="200" > - {additionalContent} - + ) : null} diff --git a/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx b/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx index c9bbe992152..58a96585968 100644 --- a/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx +++ b/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx @@ -1260,8 +1260,6 @@ export function WithAsyncData() { }); } - console.log({addAsyncFilter, filters}); - return (
- -