diff --git a/.changeset/lucky-wombats-push.md b/.changeset/lucky-wombats-push.md new file mode 100644 index 00000000000..ca2b7fddbce --- /dev/null +++ b/.changeset/lucky-wombats-push.md @@ -0,0 +1,5 @@ +--- +'@shopify/polaris': minor +--- + +Add new `IndexFiltersManager` for allowing disabling of Page Header actions when in Filtering or EditingColumns mode diff --git a/polaris-react/src/components/AppProvider/AppProvider.tsx b/polaris-react/src/components/AppProvider/AppProvider.tsx index 17a3d3d142b..bc5a13ff884 100644 --- a/polaris-react/src/components/AppProvider/AppProvider.tsx +++ b/polaris-react/src/components/AppProvider/AppProvider.tsx @@ -12,6 +12,7 @@ import { ScrollLockManager, ScrollLockManagerContext, } from '../../utilities/scroll-lock-manager'; +import {IndexFiltersManager} from '../../utilities/index-filters'; import { StickyManager, StickyManagerContext, @@ -179,7 +180,7 @@ export class AppProvider extends Component { - {children} + {children} diff --git a/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx b/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx index cd49d87b73f..f30b136577e 100644 --- a/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx +++ b/polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx @@ -10,10 +10,18 @@ import { RangeSlider, TextField, Card, + Page, + Badge, + Button, + useSetIndexFiltersMode, IndexFiltersMode, } from '@shopify/polaris'; +import { + ViewMinor, + DeleteMinor, + MobileVerticalDotsMajor, +} from '@shopify/polaris-icons'; -import {useSetIndexFiltersMode} from './hooks'; import type {IndexFiltersProps} from './IndexFilters'; export default { @@ -95,7 +103,11 @@ function Table() { ); } -function BasicExample(props?: Partial) { +function BasicExample( + props?: Partial & { + withFilteringByDefault?: boolean; + }, +) { const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const [itemStrings, setItemStrings] = useState([ @@ -184,7 +196,9 @@ function BasicExample(props?: Partial) { {label: 'Total', value: 'total desc', directionLabel: 'Descending'}, ]; const [sortSelected, setSortSelected] = useState(['order asc']); - const {mode, setMode} = useSetIndexFiltersMode(); + const {mode, setMode} = useSetIndexFiltersMode( + props?.withFilteringByDefault ? IndexFiltersMode.Filtering : undefined, + ); const onHandleCancel = () => {}; const onHandleSave = async () => { @@ -385,6 +399,10 @@ export function Default() { return ; } +export function WithFilteringByDefault() { + return ; +} + export function WithoutKeyboardShortcuts() { return ; } @@ -675,292 +693,6 @@ export function WithPinnedFilters() { } } -export function WithNoPinnedAndPrefilledFilters() { - 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(IndexFiltersMode.Filtering); - 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: ( - - ), - pinned: false, - }, - { - key: 'taggedWith', - label: 'Tagged with', - filter: ( - - ), - pinned: false, - }, - { - 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 Disabled() { const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -1390,3 +1122,107 @@ export function WithQueryFieldAndFiltersHidden() { ); } + +export function WrappedInAPage() { + return ( + Paid} + subtitle="Perfect for any pet" + compactTitle + primaryAction={{content: 'Save'}} + secondaryActions={[ + { + content: 'Delete', + destructive: true, + icon: DeleteMinor, + accessibilityLabel: 'Delete action label', + onAction: () => console.log('Delete action'), + }, + { + content: 'View on your store', + icon: ViewMinor, + onAction: () => console.log('View on your store action'), + }, + ]} + actionGroups={[ + { + title: 'Promote', + icon: MobileVerticalDotsMajor, + actions: [ + { + content: 'Share on Facebook', + accessibilityLabel: 'Individual action label', + onAction: () => console.log('Share on Facebook action'), + }, + ], + }, + ]} + pagination={{ + hasPrevious: true, + hasNext: true, + }} + > + + + ); +} + +export function WrappedInAPageWithCustomActions() { + const {mode} = useSetIndexFiltersMode(); + const shouldDisableAction = mode !== IndexFiltersMode.Default; + return ( + Paid} + subtitle="Perfect for any pet" + compactTitle + primaryAction={ + + } + secondaryActions={ + + } + actionGroups={[ + { + title: 'Promote', + icon: MobileVerticalDotsMajor, + actions: [ + { + content: 'Share on Facebook', + accessibilityLabel: 'Individual action label', + onAction: () => console.log('Share on Facebook action'), + }, + ], + }, + ]} + pagination={{ + hasPrevious: true, + hasNext: true, + }} + > + + + ); +} diff --git a/polaris-react/src/components/IndexFilters/IndexFilters.tsx b/polaris-react/src/components/IndexFilters/IndexFilters.tsx index b91ab5d2008..557a4e0aa84 100644 --- a/polaris-react/src/components/IndexFilters/IndexFilters.tsx +++ b/polaris-react/src/components/IndexFilters/IndexFilters.tsx @@ -14,6 +14,7 @@ import {Tabs} from '../Tabs'; import type {TabsProps} from '../Tabs'; import {useBreakpoints} from '../../utilities/breakpoints'; import {useFeatures} from '../../utilities/features'; +import {IndexFiltersMode} from '../../utilities/index-filters'; import {useIsSticky} from './hooks'; import { @@ -27,7 +28,6 @@ import type { IndexFiltersCancelAction, SortButtonChoice, } from './types'; -import {IndexFiltersMode} from './types'; import styles from './IndexFilters.scss'; const DEFAULT_IGNORED_TAGS = ['INPUT', 'SELECT', 'TEXTAREA']; diff --git a/polaris-react/src/components/IndexFilters/hooks/index.ts b/polaris-react/src/components/IndexFilters/hooks/index.ts index 5273cb5d3a9..d2acec233ff 100644 --- a/polaris-react/src/components/IndexFilters/hooks/index.ts +++ b/polaris-react/src/components/IndexFilters/hooks/index.ts @@ -1,2 +1 @@ -export * from './useSetIndexFiltersMode'; export * from './useIsSticky'; diff --git a/polaris-react/src/components/IndexFilters/hooks/useIsSticky/tests/useIsSticky.test.tsx b/polaris-react/src/components/IndexFilters/hooks/useIsSticky/tests/useIsSticky.test.tsx index 3903f388d49..b1d2211b70e 100644 --- a/polaris-react/src/components/IndexFilters/hooks/useIsSticky/tests/useIsSticky.test.tsx +++ b/polaris-react/src/components/IndexFilters/hooks/useIsSticky/tests/useIsSticky.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {intersectionObserver} from '@shopify/jest-dom-mocks'; import {mountWithApp} from 'tests/utilities'; -import {IndexFiltersMode} from '../../../types'; +import {IndexFiltersMode} from '../../../../../utilities/index-filters'; import {useIsSticky} from '..'; interface Props { diff --git a/polaris-react/src/components/IndexFilters/hooks/useIsSticky/useIsSticky.ts b/polaris-react/src/components/IndexFilters/hooks/useIsSticky/useIsSticky.ts index 22fc3165db1..8e07d4c3ab0 100644 --- a/polaris-react/src/components/IndexFilters/hooks/useIsSticky/useIsSticky.ts +++ b/polaris-react/src/components/IndexFilters/hooks/useIsSticky/useIsSticky.ts @@ -2,7 +2,7 @@ import {useEffect, useRef, useState} from 'react'; import type {RefObject} from 'react'; import {debounce} from '../../../../utilities/debounce'; -import type {IndexFiltersMode} from '../../types'; +import type {IndexFiltersMode} from '../../../../utilities/index-filters'; const DEBOUNCE_PERIOD = 250; diff --git a/polaris-react/src/components/IndexFilters/hooks/useSetIndexFiltersMode/index.ts b/polaris-react/src/components/IndexFilters/hooks/useSetIndexFiltersMode/index.ts deleted file mode 100644 index a6211e533f6..00000000000 --- a/polaris-react/src/components/IndexFilters/hooks/useSetIndexFiltersMode/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useSetIndexFiltersMode'; diff --git a/polaris-react/src/components/IndexFilters/hooks/useSetIndexFiltersMode/useSetIndexFiltersMode.tsx b/polaris-react/src/components/IndexFilters/hooks/useSetIndexFiltersMode/useSetIndexFiltersMode.tsx deleted file mode 100644 index d3aaf23ec1b..00000000000 --- a/polaris-react/src/components/IndexFilters/hooks/useSetIndexFiltersMode/useSetIndexFiltersMode.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import {useState} from 'react'; - -import {IndexFiltersMode} from '../../types'; - -export function useSetIndexFiltersMode( - defaultMode: IndexFiltersMode = IndexFiltersMode.Default, -) { - const [mode, setMode] = useState(defaultMode); - - return {mode, setMode}; -} diff --git a/polaris-react/src/components/IndexFilters/tests/IndexFilters.test.tsx b/polaris-react/src/components/IndexFilters/tests/IndexFilters.test.tsx index db9a05c422b..1eb9e2b6373 100644 --- a/polaris-react/src/components/IndexFilters/tests/IndexFilters.test.tsx +++ b/polaris-react/src/components/IndexFilters/tests/IndexFilters.test.tsx @@ -5,7 +5,8 @@ import {matchMedia} from '@shopify/jest-dom-mocks'; import {Tabs} from '../../Tabs'; import {Filters} from '../../Filters'; -import {IndexFilters, IndexFiltersMode} from '..'; +import {IndexFilters} from '..'; +import {IndexFiltersMode} from '../../../utilities/index-filters'; import type {IndexFiltersProps} from '../IndexFilters'; import {SearchFilterButton, SortButton, UpdateButtons} from '../components'; diff --git a/polaris-react/src/components/IndexFilters/types.ts b/polaris-react/src/components/IndexFilters/types.ts index 7ef04c71daa..d1cd35e299b 100644 --- a/polaris-react/src/components/IndexFilters/types.ts +++ b/polaris-react/src/components/IndexFilters/types.ts @@ -19,9 +19,3 @@ export interface IndexFiltersCancelAction { disabled?: boolean; loading?: boolean; } - -export enum IndexFiltersMode { - Default = 'DEFAULT', - Filtering = 'FILTERING', - EditingColumns = 'EDITING_COLUMNS', -} diff --git a/polaris-react/src/components/Page/components/Header/Header.tsx b/polaris-react/src/components/Page/components/Header/Header.tsx index ffba84660e0..8fbf00182a5 100644 --- a/polaris-react/src/components/Page/components/Header/Header.tsx +++ b/polaris-react/src/components/Page/components/Header/Header.tsx @@ -26,6 +26,10 @@ import type {PaginationProps} from '../../../Pagination'; import {ActionMenu, hasGroupsWithActions} from '../../../ActionMenu'; import {isInterface} from '../../../../utilities/is-interface'; import {isReactElement} from '../../../../utilities/is-react-element'; +import { + IndexFiltersMode, + useSetIndexFiltersMode, +} from '../../../../utilities/index-filters'; import {Box} from '../../../Box'; import {HorizontalStack} from '../../../HorizontalStack'; import {useFeatures} from '../../../../utilities/features'; @@ -93,6 +97,8 @@ export function Header({ const i18n = useI18n(); const {polarisSummerEditions2023} = useFeatures(); const {isNavigationCollapsed} = useMediaQuery(); + const {mode} = useSetIndexFiltersMode(); + const disableActions = mode !== IndexFiltersMode.Default; if (additionalNavigation && process.env.NODE_ENV === 'development') { // eslint-disable-next-line no-console @@ -124,7 +130,11 @@ export function Header({ pagination && !isNavigationCollapsed ? (
- +
) : null; @@ -147,7 +157,10 @@ export function Header({ ); const primaryActionMarkup = primaryAction ? ( - + ) : null; let actionMenuMarkup: MaybeJSX = null; @@ -157,8 +170,14 @@ export function Header({ ) { actionMenuMarkup = ( ({ + ...secondaryAction, + disabled: disableActions || secondaryAction.disabled, + }))} + groups={actionGroups.map((actionGroup) => ({ + ...actionGroup, + disabled: disableActions || actionGroup.disabled, + }))} rollup={isNavigationCollapsed} rollupActionsLabel={ title @@ -271,8 +290,10 @@ export function Header({ function PrimaryActionMarkup({ primaryAction, + disabled, }: { primaryAction: PrimaryAction | React.ReactNode; + disabled: boolean; }) { const {isNavigationCollapsed} = useMediaQuery(); @@ -284,6 +305,7 @@ function PrimaryActionMarkup({ shouldShowIconOnly(isNavigationCollapsed, primaryAction), { primary, + disabled: disabled || primaryAction.disabled, }, ); diff --git a/polaris-react/src/components/Page/components/Header/tests/Header.test.tsx b/polaris-react/src/components/Page/components/Header/tests/Header.test.tsx index 6eba736a8c1..b69084d7ae7 100644 --- a/polaris-react/src/components/Page/components/Header/tests/Header.test.tsx +++ b/polaris-react/src/components/Page/components/Header/tests/Header.test.tsx @@ -12,6 +12,7 @@ import {Tooltip} from '../../../../Tooltip'; import type {LinkAction, MenuActionDescriptor} from '../../../../../types'; import {Header} from '../Header'; import type {HeaderProps} from '../Header'; +import {IndexFiltersMode} from '../../../../../utilities/index-filters'; describe('
', () => { const mockProps: HeaderProps = { @@ -115,6 +116,26 @@ describe('
', () => { }); }); + it('renders a disabled button when a non-default IndexFiltersMode is set', () => { + const primaryAction: HeaderProps['primaryAction'] = { + content: buttonContent, + }; + + const header = mountWithApp( +
, + { + indexFilters: { + mode: IndexFiltersMode.Filtering, + }, + }, + ); + + expect(header).toContainReactComponent(Button, { + disabled: true, + children: buttonContent, + }); + }); + it('renders a `ReactNode`', () => { const PrimaryAction = () => null; @@ -153,6 +174,27 @@ describe('
', () => { hasNext: true, }); }); + + it('adds false values for hasNext and hasPrevious when a non-default IndexFiltersMode is set', () => { + const pagination = { + hasNext: true, + hasPrevious: true, + }; + + const header = mountWithApp( +
, + { + indexFilters: { + mode: IndexFiltersMode.Filtering, + }, + }, + ); + + expect(header).toContainReactComponent(Pagination, { + hasNext: false, + hasPrevious: false, + }); + }); }); describe('actionGroups', () => { @@ -180,6 +222,24 @@ describe('
', () => { groups: mockActionGroups, }); }); + + it('disables actions within the actionGroups when a non-default IndexFiltersMode is set', () => { + const wrapper = mountWithApp( +
, + { + indexFilters: { + mode: IndexFiltersMode.Filtering, + }, + }, + ); + + expect(wrapper).toContainReactComponent(ActionMenu, { + groups: mockActionGroups.map((actionGroup) => ({ + ...actionGroup, + disabled: true, + })), + }); + }); }); describe('additionalNavigation', () => { diff --git a/polaris-react/src/components/PolarisTestProvider/PolarisTestProvider.tsx b/polaris-react/src/components/PolarisTestProvider/PolarisTestProvider.tsx index 3078d25240b..2e2f8c121a9 100644 --- a/polaris-react/src/components/PolarisTestProvider/PolarisTestProvider.tsx +++ b/polaris-react/src/components/PolarisTestProvider/PolarisTestProvider.tsx @@ -19,6 +19,11 @@ import type {LinkLikeComponent} from '../../utilities/link'; import {FeaturesContext} from '../../utilities/features'; import type {FeaturesConfig} from '../../utilities/features'; import {EphemeralPresenceManager} from '../EphemeralPresenceManager'; +import { + IndexFiltersMode, + IndexFiltersModeContext, +} from '../../utilities/index-filters'; +import type {IndexFiltersModeContextType} from '../../utilities/index-filters'; type FrameContextType = NonNullable>; type MediaQueryContextType = NonNullable< @@ -38,6 +43,8 @@ export interface WithPolarisTestProviderOptions { features?: FeaturesConfig; // Contexts provided by Frame frame?: Partial; + // Contexts provided by IndexFilters + indexFilters?: Partial; } export interface PolarisTestProviderProps @@ -50,6 +57,11 @@ const defaultMediaQuery: MediaQueryContextType = { isNavigationCollapsed: false, }; +const defaultIndexFilters: IndexFiltersModeContextType = { + mode: IndexFiltersMode.Default, + setMode: noop, +}; + export function PolarisTestProvider({ strict, children, @@ -58,6 +70,7 @@ export function PolarisTestProvider({ mediaQuery, features, frame, + indexFilters, }: PolarisTestProviderProps) { const Wrapper = strict ? StrictMode : Fragment; const intl = useMemo(() => new I18n(i18n || {}), [i18n]); @@ -77,6 +90,8 @@ export function PolarisTestProvider({ const mergedMediaQuery = merge(defaultMediaQuery, mediaQuery); + const mergedIndexFilters = merge(defaultIndexFilters, indexFilters); + return ( @@ -89,7 +104,11 @@ export function PolarisTestProvider({ - {children} + + {children} + diff --git a/polaris-react/src/components/index.ts b/polaris-react/src/components/index.ts deleted file mode 100644 index 1eb3969feed..00000000000 --- a/polaris-react/src/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {EphemeralPresenceManager} from './EphemeralPresenceManager'; diff --git a/polaris-react/src/index.ts b/polaris-react/src/index.ts index 0fdb30948db..30f8e87d59b 100644 --- a/polaris-react/src/index.ts +++ b/polaris-react/src/index.ts @@ -188,11 +188,7 @@ export type {IconProps} from './components/Icon'; export {Image} from './components/Image'; export type {ImageProps} from './components/Image'; -export { - IndexFilters, - useSetIndexFiltersMode, - IndexFiltersMode, -} from './components/IndexFilters'; +export {IndexFilters} from './components/IndexFilters'; export type { IndexFiltersProps, SortButtonChoice, @@ -425,6 +421,11 @@ export {WithinContentContext as _SECRET_INTERNAL_WITHIN_CONTENT_CONTEXT} from '. export {useEventListener} from './utilities/use-event-listener'; export {useTheme} from './utilities/use-theme'; export {useIndexResourceState} from './utilities/use-index-resource-state'; +export { + useSetIndexFiltersMode, + IndexFiltersMode, + IndexFiltersManager, +} from './utilities/index-filters'; export { useRowHovered as useIndexTableRowHovered, useRowSelected as useIndexTableRowSelected, diff --git a/polaris-react/src/utilities/index-filters/IndexFiltersManager.tsx b/polaris-react/src/utilities/index-filters/IndexFiltersManager.tsx new file mode 100644 index 00000000000..30969ffa9ff --- /dev/null +++ b/polaris-react/src/utilities/index-filters/IndexFiltersManager.tsx @@ -0,0 +1,29 @@ +import type {ContextType} from 'react'; +import React, {useMemo, useState} from 'react'; + +import {IndexFiltersModeContext} from './context'; +import {IndexFiltersMode} from './types'; + +export interface IndexFiltersManagerProps { + children?: React.ReactNode; +} + +type Context = NonNullable>; + +export function IndexFiltersManager({children}: IndexFiltersManagerProps) { + const [mode, setMode] = useState(IndexFiltersMode.Default); + + const value = useMemo( + () => ({ + mode, + setMode, + }), + [mode, setMode], + ); + + return ( + + {children} + + ); +} diff --git a/polaris-react/src/utilities/index-filters/context.tsx b/polaris-react/src/utilities/index-filters/context.tsx new file mode 100644 index 00000000000..84c975c3457 --- /dev/null +++ b/polaris-react/src/utilities/index-filters/context.tsx @@ -0,0 +1,12 @@ +import {createContext} from 'react'; + +import type {IndexFiltersMode} from './types'; + +export interface IndexFiltersModeContextType { + mode: IndexFiltersMode; + setMode: (mode: IndexFiltersMode) => void; +} + +export const IndexFiltersModeContext = createContext< + IndexFiltersModeContextType | undefined +>(undefined); diff --git a/polaris-react/src/utilities/index-filters/hooks.ts b/polaris-react/src/utilities/index-filters/hooks.ts new file mode 100644 index 00000000000..8ad2d5c74be --- /dev/null +++ b/polaris-react/src/utilities/index-filters/hooks.ts @@ -0,0 +1,30 @@ +import {useContext, useRef} from 'react'; + +import {IndexFiltersModeContext} from './context'; +import type {IndexFiltersMode} from './types'; + +export function useSetIndexFiltersMode(initialValue?: IndexFiltersMode) { + const indexFiltersMode = useContext(IndexFiltersModeContext); + + if (!indexFiltersMode) { + throw new Error( + 'No index filters manager was provided. Your application must be wrapped in an component. See https://polaris.shopify.com/components/app-provider for implementation instructions.', + ); + } + + const {mode, setMode} = indexFiltersMode; + + const hasMounted = useRef(false); + + if (!hasMounted.current) { + if (initialValue) { + setMode(initialValue); + } + hasMounted.current = true; + } + + return { + mode, + setMode, + }; +} diff --git a/polaris-react/src/utilities/index-filters/index.ts b/polaris-react/src/utilities/index-filters/index.ts new file mode 100644 index 00000000000..31b84d9d2b9 --- /dev/null +++ b/polaris-react/src/utilities/index-filters/index.ts @@ -0,0 +1,4 @@ +export * from './hooks'; +export * from './context'; +export * from './types'; +export * from './IndexFiltersManager'; diff --git a/polaris-react/src/utilities/index-filters/tests/IndexFiltersManager.test.tsx b/polaris-react/src/utilities/index-filters/tests/IndexFiltersManager.test.tsx new file mode 100644 index 00000000000..009d5fe31bc --- /dev/null +++ b/polaris-react/src/utilities/index-filters/tests/IndexFiltersManager.test.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import {mountWithApp} from 'tests/utilities'; + +import {IndexFiltersMode} from '../types'; +import {IndexFiltersManager} from '../IndexFiltersManager'; +import {useSetIndexFiltersMode} from '../hooks'; + +describe('', () => { + it('renders children with the context value', () => { + const ChildComponent = () => { + const {mode, setMode} = useSetIndexFiltersMode(); + return ( +
+ {mode} + + + +
+ ); + }; + + const wrapper = mountWithApp( + + + , + ); + + const modeElement = wrapper.find('span', { + id: 'mode', + }); + const defaultButton = wrapper.find('button', { + id: 'default', + }); + const filteringButton = wrapper.find('button', { + id: 'filtering', + }); + const editingButton = wrapper.find('button', { + id: 'editing-columns', + }); + expect(modeElement?.text()).toBe(IndexFiltersMode.Default); + + wrapper.act(() => { + filteringButton?.trigger('onClick'); + }); + + expect(modeElement?.text()).toBe(IndexFiltersMode.Filtering); + + wrapper.act(() => { + editingButton?.trigger('onClick'); + }); + + expect(modeElement?.text()).toBe(IndexFiltersMode.EditingColumns); + + wrapper.act(() => { + defaultButton?.trigger('onClick'); + }); + + expect(modeElement?.text()).toBe(IndexFiltersMode.Default); + }); +}); diff --git a/polaris-react/src/utilities/index-filters/tests/hooks.test.tsx b/polaris-react/src/utilities/index-filters/tests/hooks.test.tsx new file mode 100644 index 00000000000..02508f021ff --- /dev/null +++ b/polaris-react/src/utilities/index-filters/tests/hooks.test.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import {mountWithApp} from 'tests/utilities'; + +import * as hooks from '../hooks'; +import {IndexFiltersMode} from '../types'; + +describe('useSetIndexFiltersMode', () => { + it('returns mode from the provider', () => { + const spy = jest.fn(); + + function MockComponent() { + const value = hooks.useSetIndexFiltersMode(); + spy(value); + return null; + } + + mountWithApp(, { + indexFilters: { + mode: IndexFiltersMode.Filtering, + }, + }); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + mode: IndexFiltersMode.Filtering, + }), + ); + }); + + describe('with a mocked value', () => { + it('calls setMode with defaultMode inside useEffect', () => { + const initialMode = IndexFiltersMode.EditingColumns; + const useSetIndexFiltersModeMock = jest.spyOn( + hooks, + 'useSetIndexFiltersMode', + ); + + function MockComponent() { + hooks.useSetIndexFiltersMode(initialMode); + return null; + } + mountWithApp(); + + expect(useSetIndexFiltersModeMock).toHaveBeenCalledTimes(1); + expect(useSetIndexFiltersModeMock).toHaveBeenCalledWith(initialMode); + }); + }); +}); diff --git a/polaris-react/src/utilities/index-filters/types.ts b/polaris-react/src/utilities/index-filters/types.ts new file mode 100644 index 00000000000..9cde25ffa08 --- /dev/null +++ b/polaris-react/src/utilities/index-filters/types.ts @@ -0,0 +1,5 @@ +export enum IndexFiltersMode { + Default = 'DEFAULT', + Filtering = 'FILTERING', + EditingColumns = 'EDITING_COLUMNS', +}