diff --git a/polaris-react/.storybook/RenderPerformanceProfiler/RenderPerformanceProfiler.tsx b/polaris-react/.storybook/RenderPerformanceProfiler/RenderPerformanceProfiler.tsx index 4f757177afc..279e342bb69 100644 --- a/polaris-react/.storybook/RenderPerformanceProfiler/RenderPerformanceProfiler.tsx +++ b/polaris-react/.storybook/RenderPerformanceProfiler/RenderPerformanceProfiler.tsx @@ -1,4 +1,5 @@ -import React, {PropsWithChildren} from 'react'; +import React from 'react'; +import type {PropsWithChildren} from 'react'; interface Data { id: string; diff --git a/polaris-react/locales/en.json b/polaris-react/locales/en.json index 24ca91713f6..abb938d8099 100644 --- a/polaris-react/locales/en.json +++ b/polaris-react/locales/en.json @@ -177,9 +177,18 @@ "unsavedChanges": "Unsaved changes - {label}" }, "IndexFilters": { - "searchFilterTooltip": "Search and filter", - "searchFilterTooltipWithShortcut": "Search and filter (F)", - "searchFilterAccessibilityLabel": "Search and filter results", + "searchFilterTooltip": "Filter", + "searchFilterTooltipWithShortcut": "Filter (F)", + "searchFilterAccessibilityLabel": "Type a query to search the list, and keypress enter to add your query as a filter", + "editSearchFilter": "Edit search filter", + "SearchField": { + "defaultPlaceholder": "Search", + "action": { + "addAsFilter": "add as filter", + "addToFilter": "add to filter", + "accessibilityLabel": "Add search query as a list filter" + } + }, "sort": "Sort your results", "addView": "Add a new view", "newView": "Custom search", diff --git a/polaris-react/playground/OrdersPage.tsx b/polaris-react/playground/OrdersPage.tsx new file mode 100644 index 00000000000..f0cab965859 --- /dev/null +++ b/polaris-react/playground/OrdersPage.tsx @@ -0,0 +1,1320 @@ +// @ts-expect-error -- leave me alone +// @ts-nocheck +import React, {useEffect, useRef, useState, useMemo} from 'react'; +import { + ChartVerticalIcon, + AppsIcon, + PersonIcon, + DiscountIcon, + HomeIcon, + TargetIcon, + OrderIcon, + ProductIcon, + SettingsIcon, + SearchIcon, +} from '@shopify/polaris-icons'; + +import type { + TabProps, + IndexFiltersProps, + FilterInterface, + AppliedFilterInterface, +} from '../src'; +import { + Thumbnail, + Tag, + Avatar, + Box, + Icon, + InlineStack, + KeyboardKey, + ThemeProvider, + useBreakpoints, + Frame, + Layout, + Navigation, + Page, + FooterHelp, + Link, + ChoiceList, + useIndexResourceState, + IndexTable, + IndexFilters, + TextField, + Card, + Button, + useSetIndexFiltersMode, + IndexFiltersMode, + Badge, + Text, +} from '../src'; + +import {orders} from './orders'; +import type {Order} from './orders'; +import styles from './DetailsPage.module.css'; + +export const OrdersPage = { + tags: ['skip-tests'], + render() { + const skipToContentRef = useRef(null); + const [navItemActive, setNavItemActive] = useState('orders'); + + const contextControlMarkup = ( +
+ + + + + +

Spectrally yours

+
+ ); + + // ---- Navigation ---- + const navigationMarkup = ( + + { + setNavItemActive('orders'); + }, + subNavigationItems: [ + { + label: 'All orders', + matches: navItemActive.includes('orders'), + url: '#', + }, + { + url: '#', + label: 'Drafts', + matches: navItemActive === 'drafts', + disabled: true, + }, + { + url: '#', + label: 'Abandoned checkouts', + matches: navItemActive === 'abandoned', + disabled: true, + }, + ], + }, + { + label: 'Products', + icon: ProductIcon, + matches: navItemActive === 'products', + disabled: true, + url: '#', + onClick: () => { + setNavItemActive('products'); + }, + subNavigationItems: [ + { + label: 'All products', + matches: navItemActive.includes('products'), + url: '#', + }, + { + url: '#', + label: 'Inventory', + disabled: true, + matches: navItemActive === 'inventory', + }, + { + url: '#', + label: 'Transfers', + disabled: true, + matches: navItemActive === 'transfers', + }, + ], + }, + { + label: 'Customers', + icon: PersonIcon, + disabled: true, + matches: navItemActive === 'customers', + url: '#', + }, + { + label: 'Analytics', + icon: ChartVerticalIcon, + disabled: true, + matches: navItemActive === 'analytics', + url: '#', + }, + { + label: 'Marketing', + disabled: true, + icon: TargetIcon, + matches: navItemActive === 'marketing', + url: '#', + }, + { + label: 'Discounts', + disabled: true, + icon: DiscountIcon, + matches: navItemActive === 'discounts', + url: '#', + }, + { + label: 'Apps', + disabled: true, + icon: AppsIcon, + matches: navItemActive === 'apps', + url: '#', + }, + ]} + /> + + + + ); + + // ---- Skip to content target ---- + const skipToContentTarget = ( + + + Page content + + + ); + + // ---- Page markup ---- + const pageMarkup = ( + + + {skipToContentTarget} + + + + + + ); + + return ( + } + navigation={navigationMarkup} + skipToContentTarget={skipToContentRef} + > + {pageMarkup} + + + Learn more about{' '} + + fulfilling orders + + + + ); + }, +}; + +const posIcon = ``; + +function Table({orders}: {orders: Order[]}) { + const [isShowing, setIsShowing] = useState(true); + + const resourceName = { + singular: 'order', + plural: 'orders', + }; + + const {selectedResources, allResourcesSelected, handleSelectionChange} = + // @ts-expect-error -- I don't expect an error here, you're doing too much + useIndexResourceState(orders); + + const mapStatusToBadgeProps = (status: string) => { + let tone: BadgeProps['tone']; + let progress: BadgeProps['progress']; + + if (status === 'Partially fulfilled' || status === 'Payment pending') { + tone = 'warning'; + progress = 'partiallyComplete'; + } else if (status === 'Unfulfilled') { + tone = 'attention'; + progress = 'incomplete'; + } else if (status === 'Overdue') { + tone = 'critical'; + } else { + progress = 'complete'; + } + + return {tone, progress}; + }; + + const rowMarkup = orders.map( + ( + { + id, + date, + customer, + channel, + total, + paymentStatus, + fulfillmentStatus, + items, + deliveryMethod, + tags, + }, + index, + ) => ( + + + + {`#${id}`} + + + {date} + {customer} + {channel} + + + {total} + + + + + {paymentStatus.map((status) => ( + + {status} + + ))} + + + + + {fulfillmentStatus.map((status) => ( + + {status} + + ))} + + + {`${items} items`} + {deliveryMethod} + + + {tags.split(',').map((tag) => ( + {tag} + ))} + + + + ), + ); + + return ( + + {rowMarkup} + + ); +} + +type SavedViewFilter = Pick; + +function OrdersIndexTableWithFilters( + props?: Partial & { + withFilteringByDefault?: boolean; + orders: Order[]; + }, +) { + 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 [viewNames, setViewNames] = useState([ + 'All', + 'Unfulfilled', + 'Unpaid', + 'Open', + 'Archived', + 'Express shipping', + ]); + const [selectedView, setSelectedView] = useState(5); + const [sortSelected, setSortSelected] = useState(['order asc']); + const [queryValue, setQueryValue] = useState('express'); + const [status, setStatus] = useState([]); + const [paymentStatus, setPaymentStatus] = useState([]); + const [fulfillmentStatus, setFulfillmentStatus] = useState([]); + const [loading, setLoading] = useState(false); + const [filteredOrders, setFilteredOrders] = useState([ + { + id: '1052', + date: 'Aug 22, 2024 at 5:13 am', + customer: 'Esmeralda Ernser', + channel: 'TikTok', + total: '$35.58', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Unfulfilled'], + items: '22', + deliveryMethod: 'Express', + status: 'Open', + tags: 'gift wrap', + }, + { + id: '1046', + date: 'Jul 6, 2024 at 9:31 pm', + customer: 'Melany Sauer', + channel: 'Online Store', + total: '$237.28', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '949', + deliveryMethod: 'Express', + status: 'Open', + tags: '', + }, + { + id: '1034', + date: ' Dec 5, 2023', + customer: 'Dario Krajcik', + channel: 'Online Store', + total: '$865.93', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '3,464', + deliveryMethod: 'Express', + status: 'Open', + tags: '', + }, + { + id: '1028', + date: ' May 19, 2023', + customer: 'Guy Haley', + channel: 'Instagram', + total: '$361.90', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '1,448', + deliveryMethod: 'Express', + status: 'Open', + tags: '', + }, + { + id: '1023', + date: ' Feb 25, 2023', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$495.99', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '1,984', + deliveryMethod: 'Express', + status: 'Open', + tags: 'wholesale, net 30', + }, + { + id: '1017', + date: ' Nov 26, 2022', + customer: 'Kailyn Paucek', + channel: 'Online Store', + total: '$653.15', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '2,613', + deliveryMethod: 'Express', + status: 'Archived', + tags: '', + }, + ]); + const [savedViewFilters, setSavedViewFilters] = useState( + [ + [], + [ + { + key: 'status', + label: 'Status', + value: ['Open'], + }, + { + key: 'fulfillmentStatus', + label: 'Fulfillment status', + value: ['Unfulfilled', 'Partially fulfilled'], + }, + ], + [ + { + key: 'status', + label: 'Status', + value: ['Open'], + }, + { + key: 'paymentStatus', + label: 'Payment status', + value: ['Payment pending', 'Overdue'], + }, + ], + [ + { + key: 'status', + label: 'Status', + value: ['Open'], + }, + ], + [ + { + key: 'status', + label: 'Status', + value: ['Archived'], + }, + ], + [ + { + key: 'queryValue', + label: 'Search', + value: 'express', + }, + ], + ], + ); + + const {mode, setMode} = useSetIndexFiltersMode( + props?.withFilteringByDefault ? IndexFiltersMode.Filtering : undefined, + ); + + const escapeSpecialChars = (text: string) => { + return text.replace(/[.,*+?^${}()#|[\]\\]/g, ' '); + }; + + const getSearchRegex = (input: string) => { + const terms = escapeSpecialChars(input).split(/\s+/); + const regexParts = terms.map((term) => `(?=.*${term})`); + const regexPattern = regexParts.join(''); + return new RegExp(regexPattern, 'i'); + }; + + const hasTextValueMatches = (inputText: string, order: Order) => { + const regex = getSearchRegex(inputText); + const combinedFields = [ + order.id, + order.customer, + order.channel, + order.deliveryMethod, + order.total, + order.tags, + ].join(' '); + + return regex.test(combinedFields); + }; + + const hasArrayValueMatches = ( + orderValue: Set, + filterValue: Set, + ) => { + if (filterValue.size > 0) { + // @ts-expect-error -- It exists + return orderValue.intersection(filterValue).size > 0; + } + + return true; + }; + + const handleFilterOrders = (nextFilters: { + queryValue?: string; + paymentStatus?: string[]; + fulfillmentStatus?: string[]; + status?: string[]; + }) => { + setLoading(true); + const nextQueryValue = + nextFilters.queryValue !== undefined + ? nextFilters.queryValue + : queryValue; + const nextStatus = + nextFilters.status !== undefined ? nextFilters.status : status; + const nextPaymentStatus = + nextFilters.paymentStatus !== undefined + ? nextFilters.paymentStatus + : paymentStatus; + const nextFulfillmentStatus = + nextFilters.fulfillmentStatus !== undefined + ? nextFilters.fulfillmentStatus + : fulfillmentStatus; + + const statusSet = new Set(nextStatus); + const paymentStatusSet = new Set(nextPaymentStatus); + const fulfillmentStatusSet = new Set(nextFulfillmentStatus); + const result = orders.filter((order) => { + const matchesQueryValue = hasTextValueMatches(nextQueryValue, order); + + const matchesStatus = hasArrayValueMatches( + new Set([order.status]), + statusSet, + ); + + const matchesPaymentStatus = hasArrayValueMatches( + new Set(order.paymentStatus), + paymentStatusSet, + ); + + const matchesFulfillmentStatus = hasArrayValueMatches( + new Set(order.fulfillmentStatus), + fulfillmentStatusSet, + ); + + // if ( + // matchesQueryValue && + // matchesPaymentStatus && + // matchesFulfillmentStatus && + // matchesStatus + // ) { + // console.log( + // ` + // nextFilters: `, + // nextFilters, + // ` + // matchesQueryValue: ${matchesQueryValue}, + // matchesPaymentStatus: ${matchesPaymentStatus}, + // matchesFulfillmentStatus: ${matchesFulfillmentStatus}, + // matchesStatus: ${matchesStatus} + // `, + // ); + // } + setLoading(false); + return ( + matchesQueryValue && + matchesPaymentStatus && + matchesFulfillmentStatus && + matchesStatus + ); + }); + + setFilteredOrders(result); + }; + + // Psuedo loading state transitions + useEffect(() => { + if (queryValue !== '') { + setLoading(true); + } + const timeoutId = setTimeout(() => { + setLoading(false); + }, 750); + return () => clearTimeout(timeoutId); + }, [queryValue]); + + // ---- Filter input event handlers ---- + + const preProcessInput = (input: string) => { + // Insert a space between numbers and letters if they are adjacent + return input + .replace(/(\d)([a-zA-Z])/g, '$1 $2') + .replace(/([a-zA-Z])(\d)/g, '$1 $2'); + }; + + const handleQueryValueChange = (value: string) => { + const processedInput = preProcessInput(value); + setQueryValue(processedInput); + handleFilterOrders({queryValue: processedInput}); + }; + + const handleQueryValueRemove = () => { + setQueryValue(''); + handleFilterOrders({queryValue: ''}); + }; + + const handlePaymentStatusChange = (value: string[]) => { + setPaymentStatus(value); + handleFilterOrders({paymentStatus: value}); + }; + + const handlePaymentStatusRemove = (value: string[]) => { + setPaymentStatus([]); + handleFilterOrders({paymentStatus: []}); + }; + + const handleFulfillmentStatusChange = (value: string[]) => { + setFulfillmentStatus(value); + handleFilterOrders({fulfillmentStatus: value}); + }; + + const handleFulfillmentStatusRemove = (value: string[]) => { + setFulfillmentStatus([]); + handleFilterOrders({fulfillmentStatus: []}); + }; + + const handleStatusChange = (value: string[]) => { + setStatus(value); + handleFilterOrders({status: value}); + }; + + const handleStatusRemove = (value: string[]) => { + setStatus([]); + handleFilterOrders({status: []}); + }; + + function isEmpty(value: string | string[]) { + return Array.isArray(value) ? value.length === 0 : value === ''; + } + + const isUnsaved = ( + value?: string | string[], + savedValue?: string | string[], + ) => { + if (value === undefined) return false; + if (value.length && savedValue === undefined) return true; + + const isArray = Array.isArray(value) && Array.isArray(status); + const isString = + typeof value === 'string' && typeof savedValue === 'string'; + + if (isString) return value !== savedValue; + if (isArray) + return !( + savedValue?.length && + value.length === savedValue.length && + value.every((status) => savedValue.indexOf(status) > -1) + ); + }; + + const handlers = { + queryValue: { + set: setQueryValue, + change: handleQueryValueChange, + remove: handleQueryValueRemove, + emptyValue: '', + label: 'Search', + }, + status: { + set: setStatus, + change: handleStatusChange, + remove: handleStatusRemove, + label: 'Status', + emptyValue: [], + locked: selectedView > 0 && selectedView < 5, + }, + paymentStatus: { + set: setPaymentStatus, + change: handlePaymentStatusChange, + remove: handlePaymentStatusRemove, + label: 'Payment status', + emptyValue: [], + locked: selectedView === 2, + }, + fulfillmentStatus: { + set: setFulfillmentStatus, + change: handleFulfillmentStatusChange, + remove: handleFulfillmentStatusRemove, + label: 'Fulfillment status', + emptyValue: [], + locked: selectedView === 1, + }, + }; + + const handleChangeFilters = (nextFilterValues: { + queryValue?: string; + paymentStatus?: string[]; + fulfillmentStatus?: string[]; + status?: string[]; + }) => { + for (const key in nextFilterValues) { + if (key in handlers) { + handlers[key].set(nextFilterValues[key]); + } + } + + handleFilterOrders(nextFilterValues); + }; + + // ---- Applied filter event handlers ---- + const handleResetToSavedFilters = (view: number) => { + const nextFilters: { + queryValue: string; + paymentStatus: string[]; + fulfillmentStatus: string[]; + status: string[]; + } = { + queryValue: '', + paymentStatus: [], + fulfillmentStatus: [], + status: [], + }; + console.log('VIEW RESETTING TO: ', view); + savedViewFilters[view]?.forEach(({key, value}) => { + nextFilters[key] = value; + }); + + console.log( + `resetting to --- + `, + nextFilters, + ); + + return handleChangeFilters(nextFilters); + }; + + const handleClearFilters = () => { + handleChangeFilters({ + queryValue: '', + paymentStatus: [], + fulfillmentStatus: [], + status: [], + }); + }; + + const getHumanReadableValue = (label: string, value: string | string[]) => { + if (isEmpty(value)) return ''; + if (!Array.isArray(value)) { + return `${label}: ${value}`; + } + + let humanReadableValue: string; + + if (value.length === 1) { + humanReadableValue = value[0]; + } else if (value.length === 2) { + humanReadableValue = `${value[0]} or ${value[1]}`; + } else { + humanReadableValue = value + .map((text, index) => { + return index !== value.length - 1 ? text : `or ${text}`; + }) + .join(', '); + } + + return `${label}: ${humanReadableValue}`; + }; + // ---- + + const filters: FilterInterface[] = [ + { + key: 'paymentStatus', + value: paymentStatus, + label: handlers.paymentStatus.label, + filter: ( + + ), + }, + { + key: 'fulfillmentStatus', + value: fulfillmentStatus, + label: handlers.fulfillmentStatus.label, + filter: ( + + ), + }, + { + key: 'status', + value: status, + label: handlers.status.label, + filter: ( + + ), + }, + ]; + + const appliedFilters: AppliedFilterInterface[] = []; + + Object.entries({ + queryValue, + status, + paymentStatus, + fulfillmentStatus, + }).forEach(([key, value]) => { + if (isEmpty(value)) return; + + const savedValue = savedViewFilters[selectedView]?.find( + (filter) => filter.key === key, + )?.value; + + appliedFilters.push({ + key, + value, + locked: handlers[key].locked, + label: getHumanReadableValue(handlers[key].label, value), + unsavedChanges: selectedView === 0 ? true : isUnsaved(value, savedValue), + onRemove: handlers[key].locked ? undefined : handlers[key].remove, + }); + }); + + const appliedFilterMatchesSavedFilter = ( + appliedFilter: AppliedFilterInterface, + ) => { + const savedFilter = savedViewFilters[selectedView].find( + (savedFilter) => savedFilter.key === appliedFilter.key, + ); + + if (!savedFilter) { + return false; + } else if (typeof appliedFilter.value === 'string') { + return appliedFilter.value === savedFilter.value; + } else { + const hasSameArrayValue = + new Set(savedFilter.value).difference(new Set(appliedFilter.value)) + .size === 0; + + return hasSameArrayValue; + } + }; + + const hasUnsavedChanges = + (!savedViewFilters[selectedView] && appliedFilters.length > 0) || + (appliedFilters.length === 0 && + savedViewFilters[selectedView].length > 0) || + !appliedFilters.every(appliedFilterMatchesSavedFilter); + + // ---- View event handlers + const sleep = (ms: number) => { + return new Promise((resolve) => setTimeout(resolve, ms)); + }; + + const handleSelectView = async (view: number) => { + setQueryValue(''); + setMode(IndexFiltersMode.Default); + setSelectedView(view); + setLoading(true); + handleResetToSavedFilters(view); + await sleep(250); + setLoading(false); + }; + + const handleEditView = (index: number) => () => { + setMode(IndexFiltersMode.Filtering); + }; + + const handleDeleteView = (index: number) => async () => { + const nextViewNames = [...viewNames]; + const nextSavedViewFilters = [...savedViewFilters]; + nextViewNames.splice(index, 1); + nextSavedViewFilters.splice(index, 1); + setSavedViewFilters(nextSavedViewFilters); + setViewNames(nextViewNames); + await sleep(250); + handleClearFilters(); + return true; + }; + + const handleRenameView = (index: number) => async (newName: string) => { + const nextViewNames = [...viewNames]; + nextViewNames[index] = newName; + await sleep(250); + setViewNames(nextViewNames); + return true; + }; + + const handleDuplicateView = (index: number) => async (name: string) => { + setLoading(true); + const duplicateViewIndex = viewNames.length; + const duplicateViewFilters = [...savedViewFilters[index]]; + setSavedViewFilters((filters) => [...filters, duplicateViewFilters]); + const nextAppliedFilters = { + queryValue: '', + status: [], + paymentStatus: [], + fulfillmentStatus: [], + }; + + duplicateViewFilters.forEach(({key, value}) => { + nextAppliedFilters[key] = value; + }); + setViewNames((names) => [...names, name]); + await sleep(250); + setSelectedView(duplicateViewIndex); + setLoading(false); + return true; + }; + + const handleSaveViewFilters = async (index: number) => { + const nextSavedFilters = [...savedViewFilters]; + nextSavedFilters[index] = appliedFilters.map(({key, value, label}) => ({ + key, + value, + label, + locked: false, + })); + + setSavedViewFilters(nextSavedFilters); + await sleep(300); + return true; + }; + + const handleCreateNewView = async (name: string) => { + const newViewIndex = viewNames.length; + setViewNames((names) => [...names, name]); + setSavedViewFilters((filters) => [...filters, []]); + handleClearFilters(); + await sleep(250); + setMode(IndexFiltersMode.Default); + setSelectedView(newViewIndex); + return true; + }; + + const handleSaveViewAs = async (index: number, name: string) => { + setMode(IndexFiltersMode.Default); + setViewNames((names) => [...names, name]); + setSelectedView(index); + const saved = await handleSaveViewFilters(index); + return saved; + }; + + const handleSave = async (name: string) => { + let saved = false; + const index = !name ? selectedView : viewNames.indexOf(name); + setLoading(true); + + if (index < 0) { + saved = await handleSaveViewAs(viewNames.length, name); + } else { + saved = await handleSaveViewFilters(index); + } + + setLoading(false); + return saved; + }; + + const handleCancel = () => { + if (!hasUnsavedChanges) { + console.log('cancelled -- no unsaved changes'); + } else if (selectedView === 0) { + handleClearFilters(); + console.log('cancelled -- clearing all'); + } else { + handleResetToSavedFilters(selectedView); + console.log('cancelled -- resetting to saved'); + } + }; + const tabs: TabProps[] = viewNames.map((name, index) => { + return { + index, + id: `${name}-${index}`, + content: name, + isLocked: index === 0, + actions: + index === 0 + ? [] + : [ + { + type: 'rename', + onPrimaryAction: handleRenameView(index), + }, + { + type: 'duplicate', + onPrimaryAction: handleDuplicateView(index), + }, + { + type: 'edit', + onAction: handleEditView(index), + }, + { + type: 'delete', + onPrimaryAction: handleDeleteView(index), + }, + ], + onAction: () => {}, + }; + }); + + const primaryAction: IndexFiltersProps['primaryAction'] = { + type: selectedView > 4 ? 'save' : 'save-as', + onAction: handleSave, + disabled: !hasUnsavedChanges, + loading: false, + }; + + const cancelAction: IndexFiltersProps['cancelAction'] = { + onAction: handleCancel, + disabled: false, + loading: false, + }; + + const queryPlaceholder = `Searching in ${viewNames[ + selectedView + ].toLowerCase()}`; + + return ( + + + + + ); +} + +function TopBarPlaceholder({onClickMobileMenu}: {onClickMobileMenu?(): void}) { + const {mdUp} = useBreakpoints(); + + const logoMarkup = ( + + ); + + const mobileMenuActivator = ( +
+
+ ); + + const rightSlotMarkup = ( +
+ {mdUp ? logoMarkup : mobileMenuActivator} +
+ ); + + const centerSlotMarkup = ( +
+ + + + +
+ +
+ + Search + +
+ + + K + +
+
+
+
+ ); + + const secondaryMenuMarkup = ( +
+ +
+ ); + + const userMenuMarkup = ( +
+ {mdUp ? ( + + + + Unicorn Trough + + + ) : ( + + )} +
+ ); + + const leftSlotMarkup = ( + + {secondaryMenuMarkup} + {userMenuMarkup} + + ); + + return ( + + + + {rightSlotMarkup} + {centerSlotMarkup} + {leftSlotMarkup} + + + + ); +} diff --git a/polaris-react/playground/orders.ts b/polaris-react/playground/orders.ts new file mode 100644 index 00000000000..95641e658aa --- /dev/null +++ b/polaris-react/playground/orders.ts @@ -0,0 +1,666 @@ +export interface Order { + id: string; + date: string; + customer: string; + channel: string; + total: string; + paymentStatus: string[]; + fulfillmentStatus: string[]; + status: string; + items: string; + deliveryMethod: string; + tags: string; +} + +export const orders: Order[] = [ + { + id: '1053', + date: 'Aug 22, 2024 at 11:11 pm', + customer: 'Ian Nikolaus', + channel: 'Online Store', + total: '$2,051.20', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Unfulfilled'], + items: '8,205', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1052', + date: 'Aug 22, 2024 at 5:13 am', + customer: 'Esmeralda Ernser', + channel: 'TikTok', + total: '$35.58', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Unfulfilled'], + items: '22', + deliveryMethod: 'Express', + status: 'Open', + tags: 'gift wrap', + }, + { + id: '1051', + date: 'Aug 21, 2024 at 9:59 am', + customer: 'Lindsay Gorczany', + channel: 'TikTok', + total: '$79.86', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '319', + deliveryMethod: '2 Day Air', + status: 'Open', + tags: '', + }, + { + id: '1050', + date: 'Aug 20, 2024 at 11:47 am', + customer: 'Brennan Schowalter', + channel: 'TikTok', + total: '$207.24', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Partially fulfilled'], + items: '829', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1049', + date: 'Aug 20, 2024 at 2:56 am', + customer: 'Ryder Glover', + channel: 'TikTok', + total: '$438.15', + paymentStatus: ['Payment pending'], + fulfillmentStatus: ['Partially fulfilled'], + items: '1,753', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1048', + date: 'Aug 20, 2024 at 2:14 am', + customer: 'Dillon Weissnat', + channel: 'TikTok', + total: '$577.10', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Partially fulfilled'], + items: '2,308', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1047', + date: 'Jul 18, 2024 at 6:46 am', + customer: 'Patrick Gerlach', + channel: 'Online Store', + total: '$56.73', + paymentStatus: ['Payment pending', 'Overdue'], + fulfillmentStatus: ['Unfulfilled'], + items: '227', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1046', + date: 'Jul 6, 2024 at 9:31 pm', + customer: 'Melany Sauer', + channel: 'Online Store', + total: '$237.28', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '949', + deliveryMethod: 'Express', + status: 'Open', + tags: '', + }, + { + id: '1045', + date: 'Jul 4, 2024 at 8:26 am', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$527.76', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '2,111', + deliveryMethod: 'Standard', + status: 'Open', + tags: 'wholesale, net 30', + }, + { + id: '1044', + date: 'Jun 26, 2024 at 11:04 am', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$555.51', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '556', + deliveryMethod: 'Standard', + status: 'Open', + tags: 'wholesale, net 30', + }, + { + id: '1043', + date: 'Jun 21, 2024 at 10:43 pm', + customer: 'Maxine Weimann', + channel: 'Online Store', + total: '$1,413.86', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '2,828', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1042', + date: 'Jun 17, 2024 at 6:32 pm', + customer: 'Erin Torphy', + channel: 'Online Store', + total: '$1,266.79', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '5,067', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1041', + date: 'Jun 14, 2024 at 10:14 pm', + customer: 'Reginald Herzog', + channel: 'Online Store', + total: '$1,786.00', + paymentStatus: ['Refunded'], + fulfillmentStatus: ['Fulfilled'], + items: '7,144', + deliveryMethod: 'Ground', + status: 'Canceled', + tags: 'vip, wholesale, net 60, damaged in shipment', + }, + { + id: '1040', + date: 'Jun 3, 2024 at 6:17 am', + customer: 'Talia Erdman', + channel: 'TikTok', + total: '$38.81', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '49', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1039', + date: 'Apr 21, 2024 at 6:47 pm', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$1,953.14', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '7,813', + deliveryMethod: 'Standard', + status: 'Open', + tags: 'wholesale, net 30', + }, + { + id: '1038', + date: 'Feb 17, 2024 at 7:03 am', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$1,868.27', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '7,473', + deliveryMethod: 'Standard', + status: 'Open', + tags: 'wholesale, net 30', + }, + { + id: '1037', + date: 'Jan 27, 2024 at 5:31 pm', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$663.45', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '2,654', + deliveryMethod: 'Standard', + status: 'Open', + tags: 'wholesale, net 30', + }, + { + id: '1036', + date: 'Jan 11, 2024 at 10:40 am', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$2,080.11', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '8,320', + deliveryMethod: 'Standard', + status: 'Open', + tags: 'wholesale, net 30', + }, + { + id: '1035', + date: ' Dec 28, 2023', + customer: 'Alia Simonis', + channel: 'Instagram', + total: '$165.54', + paymentStatus: ['Refunded'], + fulfillmentStatus: ['Unfulfilled'], + items: '662', + deliveryMethod: 'Overnight', + status: 'Canceled', + tags: 'gift, overnight deadline missed ', + }, + { + id: '1034', + date: ' Dec 5, 2023', + customer: 'Dario Krajcik', + channel: 'Online Store', + total: '$865.93', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '3,464', + deliveryMethod: 'Express', + status: 'Open', + tags: '', + }, + { + id: '1033', + date: ' Nov 29, 2023', + customer: 'Maxine Weimann', + channel: 'Online Store', + total: '$3,091.17', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '12,365', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1032', + date: ' Sep 2, 2023', + customer: 'Kenton Luettgen', + channel: 'Online Store', + total: '$957.20', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '3,829', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1031', + date: ' Jul 17, 2023', + customer: 'Reginald Herzog', + channel: 'Online Store', + total: '$3,063.09', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '12,252', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1030', + date: ' Jun 27, 2023', + customer: 'Laverna Daniel', + channel: 'Online Store', + total: '$486.64', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '1,947', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1029', + date: ' Jun 12, 2023', + customer: 'Maxine Weimann', + channel: 'Online Store', + total: '$2,898.38', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '11,594', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1028', + date: ' May 19, 2023', + customer: 'Guy Haley', + channel: 'Instagram', + total: '$361.90', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '1,448', + deliveryMethod: 'Express', + status: 'Open', + tags: '', + }, + { + id: '1027', + date: ' May 8, 2023', + customer: 'Rico Bednar', + channel: 'Instagram', + total: '$3,839.03', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '15,356', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1026', + date: ' May 6, 2023', + customer: 'Aisha Bahringer', + channel: 'Instagram', + total: '$1,666.90', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '6,668', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1025', + date: ' Apr 14, 2023', + customer: 'Ian Nikolaus', + channel: 'Online Store', + total: '$4,081.40', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '16,326', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1024', + date: ' Mar 24, 2023', + customer: 'Reginald Herzog', + channel: 'Online Store', + total: '$1,326.84', + paymentStatus: ['Refunded'], + fulfillmentStatus: ['Fulfilled'], + items: '5,307', + deliveryMethod: 'Ground', + status: 'Canceled', + tags: 'vip, wholesale, net 60', + }, + { + id: '1023', + date: ' Feb 25, 2023', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$495.99', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '1,984', + deliveryMethod: 'Express', + status: 'Open', + tags: 'wholesale, net 30', + }, + { + id: '1022', + date: ' Feb 25, 2023', + customer: 'Maxine Weimann', + channel: 'Online Store', + total: '$1,197.61', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '4,790', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1021', + date: ' Feb 10, 2023', + customer: 'Rickey Thompson', + channel: 'Online Store', + total: '$1,575.27', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '6,301', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1020', + date: ' Jan 15, 2023', + customer: 'Erin Torphy', + channel: 'Online Store', + total: '$2,194.11', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '8,776', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1019', + date: ' Jan 8, 2023', + customer: 'Maxine Weimann', + channel: 'Online Store', + total: '$4,836.76', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '19,347', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1018', + date: ' Dec 10, 2022', + customer: 'Ian Nikolaus', + channel: 'Online Store', + total: '$5,183.79', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '20,735', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1017', + date: ' Nov 26, 2022', + customer: 'Kailyn Paucek', + channel: 'Online Store', + total: '$653.15', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '2,613', + deliveryMethod: 'Express', + status: 'Archived', + tags: '', + }, + { + id: '1016', + date: ' Oct 13, 2022', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$4,840.18', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '19,361', + deliveryMethod: 'Standard', + status: 'Archived', + tags: 'wholesale, net 30', + }, + { + id: '1015', + date: ' Sep 2, 2022', + customer: 'Reginald Herzog', + channel: 'Online Store', + total: '$4,788.04', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '19,152', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1014', + date: ' Aug 1, 2022', + customer: 'Erin Torphy', + channel: 'Online Store', + total: '$5,784.50', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '23,138', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1013', + date: ' Jul 9, 2022', + customer: 'Ian Nikolaus', + channel: 'Online Store', + total: '$4,779.71', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '19,119', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1012', + date: ' Jun 3, 2022', + customer: 'Erin Torphy', + channel: 'Online Store', + total: '$4,488.62', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '17,954', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1011', + date: ' Apr 4, 2022', + customer: 'Ian Nikolaus', + channel: 'Online Store', + total: '$6,031.40', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '24,126', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1010', + date: ' Jan 26, 2022', + customer: 'Rickey Thompson', + channel: 'Online Store', + total: '$2,072.20', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '8,289', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1009', + date: ' Dec 21, 2021', + customer: 'Ian Nikolaus', + channel: 'Online Store', + total: '$5,132.97', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '20,532', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1008', + date: ' Nov 7, 2021', + customer: 'Erin Torphy', + channel: 'Online Store', + total: '$7,152.06', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '28,608', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1007', + date: ' Oct 10, 2021', + customer: 'Sunny Harris', + channel: '', + total: '$22.83', + paymentStatus: ['Refunded'], + fulfillmentStatus: ['Unfulfilled'], + items: '91', + deliveryMethod: 'Local delivery', + status: 'Canceled', + tags: '', + }, + { + id: '1006', + date: ' Sep 28, 2021', + customer: 'Domenic Johnston', + channel: '', + total: '$33.88', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '136', + deliveryMethod: 'Local delivery', + status: 'Archived', + tags: '', + }, + { + id: '1005', + date: ' Sep 20, 2021', + customer: 'Cassie Tromp', + channel: '', + total: '$27.22', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '109', + deliveryMethod: 'Pickup', + status: 'Archived', + tags: '', + }, + { + id: '1004', + date: ' Aug 24, 2021', + customer: 'Anika Ankunding', + channel: '', + total: '$91.91', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '138', + deliveryMethod: 'Local delivery', + status: 'Archived', + tags: '', + }, +]; diff --git a/polaris-react/playground/stories.tsx b/polaris-react/playground/stories.tsx index 46b0c692d9b..6f31b26f8b1 100644 --- a/polaris-react/playground/stories.tsx +++ b/polaris-react/playground/stories.tsx @@ -1,14 +1,15 @@ import {Playground} from './Playground'; import {KitchenSink} from './KitchenSink'; import {DetailsPage} from './DetailsPage'; +import {OrdersPage} from './OrdersPage'; export default { // eslint-disable-next-line storybook/no-title-property-in-meta - title: 'Playground', + title: 'Test Pages', parameters: { layout: 'fullscreen', chromatic: {disable: true}, }, }; -export {DetailsPage, KitchenSink, Playground}; +export {Playground, DetailsPage, OrdersPage, KitchenSink}; diff --git a/polaris-react/src/components/Filters/components/FilterPill/FilterPill.module.css b/polaris-react/src/components/Filters/components/FilterPill/FilterPill.module.css index 745010abd2f..61a6666e695 100644 --- a/polaris-react/src/components/Filters/components/FilterPill/FilterPill.module.css +++ b/polaris-react/src/components/Filters/components/FilterPill/FilterPill.module.css @@ -87,6 +87,14 @@ @media (--p-breakpoints-md-up) { padding-right: 0; } + + &.locked { + padding-right: calc(var(--p-space-050) + var(--p-space-300)); + + @media (--p-breakpoints-md-up) { + padding-right: calc(var(--p-space-050) + var(--p-space-200)); + } + } } .clearButton { diff --git a/polaris-react/src/components/Filters/components/FilterPill/FilterPill.tsx b/polaris-react/src/components/Filters/components/FilterPill/FilterPill.tsx index 9a19a980191..4b846fe29be 100644 --- a/polaris-react/src/components/Filters/components/FilterPill/FilterPill.tsx +++ b/polaris-react/src/components/Filters/components/FilterPill/FilterPill.tsx @@ -116,6 +116,7 @@ export function FilterPill({ const toggleButtonClassNames = classNames( styles.PlainButton, styles.ToggleButton, + onRemove === undefined && styles.locked, ); const disclosureMarkup = !selected ? ( @@ -145,18 +146,19 @@ export function FilterPill({ ) : null; - const removeFilterButtonMarkup = selected ? ( - -
- -
-
- ) : null; + const removeFilterButtonMarkup = + selected && onRemove !== undefined ? ( + +
+ +
+
+ ) : null; const activator = (
diff --git a/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx b/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx index 3a5ca335e02..8759a9026d7 100644 --- a/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx +++ b/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx @@ -194,16 +194,19 @@ export function FiltersBar({ const pinnedFiltersMarkup = pinnedFilters.map( ({key: filterKey, ...pinnedFilter}) => { const appliedFilter = appliedFilters?.find(({key}) => key === filterKey); - const handleFilterPillRemove = () => { - setLocalPinnedFilters((currentLocalPinnedFilters) => - currentLocalPinnedFilters.filter((key) => { - const isMatchedFilters = key === filterKey; - const isPinnedFilterFromProps = pinnedFromPropsKeys.includes(key); - return !isMatchedFilters || isPinnedFilterFromProps; - }), - ); - appliedFilter?.onRemove(filterKey); - }; + const handleFilterPillRemove = appliedFilter?.locked + ? undefined + : () => { + setLocalPinnedFilters((currentLocalPinnedFilters) => + currentLocalPinnedFilters.filter((key) => { + const isMatchedFilters = key === filterKey; + const isPinnedFilterFromProps = + pinnedFromPropsKeys.includes(key); + return !isMatchedFilters || isPinnedFilterFromProps; + }), + ); + appliedFilter?.onRemove?.(filterKey); + }; return ( setQueryValue('')} onSort={setSortSelected} @@ -687,9 +687,10 @@ export const WithPinnedFilters = { sortOptions={sortOptions} sortSelected={sortSelected} queryValue={queryValue} - queryPlaceholder="Searching in all" + queryPlaceholder={`Searching in ${itemStrings[selected].toLowerCase()} + `} onQueryChange={handleFiltersQueryChange} - onQueryClear={() => setQueryValue('')} + onQueryClear={handleQueryValueRemove} onSort={setSortSelected} primaryAction={primaryAction} cancelAction={{ @@ -975,7 +976,7 @@ export const WithPrefilledFilters = { sortOptions={sortOptions} sortSelected={sortSelected} queryValue={queryValue} - queryPlaceholder="Searching in all" + queryPlaceholder={`Searching in ${itemStrings[selected].toLowerCase()}`} onQueryChange={handleFiltersQueryChange} onQueryClear={() => setQueryValue('')} onSort={setSortSelected} @@ -1275,7 +1276,7 @@ export const WithHiddenFilter = { sortOptions={sortOptions} sortSelected={sortSelected} queryValue={queryValue} - queryPlaceholder="Searching in all" + queryPlaceholder={`Searching in ${itemStrings[selected].toLowerCase()}`} onQueryChange={handleFiltersQueryChange} onQueryClear={() => setQueryValue('')} onSort={setSortSelected} @@ -1612,7 +1613,7 @@ export const WithAsyncData = { sortOptions={sortOptions} sortSelected={sortSelected} queryValue={queryValue} - queryPlaceholder="Searching in all" + queryPlaceholder={`Searching in ${itemStrings[selected].toLowerCase()}`} onQueryChange={handleFiltersQueryChange} onQueryClear={() => setQueryValue('')} onSort={setSortSelected} @@ -1918,7 +1919,7 @@ export const Disabled = { sortOptions={sortOptions} sortSelected={sortSelected} queryValue={queryValue} - queryPlaceholder="Searching in all" + queryPlaceholder={`Searching in ${itemStrings[selected].toLowerCase()}`} onQueryChange={handleFiltersQueryChange} onQueryClear={() => setQueryValue('')} onSort={setSortSelected} @@ -2087,7 +2088,7 @@ export const WithQueryFieldAndFiltersHidden = { sortOptions={sortOptions} sortSelected={sortSelected} queryValue="" - queryPlaceholder="Searching in all" + queryPlaceholder={`Searching in ${itemStrings[selected].toLowerCase()}`} onQueryChange={() => {}} onQueryClear={() => {}} onSort={setSortSelected} @@ -2336,7 +2337,7 @@ export const WithNoFilters = { sortOptions={sortOptions} sortSelected={sortSelected} queryValue={queryValue} - queryPlaceholder="Searching in all" + queryPlaceholder={`Searching in ${itemStrings[selected].toLowerCase()}`} onQueryChange={handleFiltersQueryChange} onQueryClear={() => setQueryValue('')} onSort={setSortSelected} @@ -2382,7 +2383,7 @@ export const WithNoFilters = { }, }; -export const WithOnlySearchAndSort = { +export const WithSearchAndSortOnly = { render() { const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -2514,7 +2515,7 @@ export const WithOnlySearchAndSort = { sortOptions={sortOptions} sortSelected={sortSelected} queryValue={queryValue} - queryPlaceholder="Searching in all" + queryPlaceholder={'Search'} onQueryChange={handleFiltersQueryChange} onQueryClear={() => setQueryValue('')} onSort={setSortSelected} diff --git a/polaris-react/src/components/IndexFilters/IndexFilters.tsx b/polaris-react/src/components/IndexFilters/IndexFilters.tsx index 86c119f85c3..a483a8293f8 100644 --- a/polaris-react/src/components/IndexFilters/IndexFilters.tsx +++ b/polaris-react/src/components/IndexFilters/IndexFilters.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useCallback, useRef} from 'react'; +import React, {useMemo, useEffect, useCallback, useRef, useState} from 'react'; import {Transition} from 'react-transition-group'; import {useI18n} from '../../utilities/i18n'; @@ -12,13 +12,15 @@ import {Filters} from '../Filters'; import type {FiltersProps} from '../Filters'; import {Tabs} from '../Tabs'; import type {TabsProps} from '../Tabs'; +import {TextField} from '../TextField'; import {useBreakpoints} from '../../utilities/breakpoints'; import {useIsSticky} from './hooks'; import { Container, SortButton, - SearchFilterButton, + SearchField, + FilterButton, UpdateButtons, EditColumnsButton, } from './components'; @@ -70,7 +72,7 @@ export interface IndexFiltersProps onSortKeyChange?: (value: string) => void; /** Optional callback when using saved views and changing the sort direction */ onSortDirectionChange?: (value: string) => void; - /** Callback when the add filter button is clicked, to be passed to AlphaFilters. */ + /** Callback when the add filter button is clicked, to be passed to Filters. */ onAddFilterClick?: () => void; /** The primary action to display */ primaryAction?: IndexFiltersPrimaryAction; @@ -156,6 +158,18 @@ export function IndexFilters({ const defaultRef = useRef(null); const filteringRef = useRef(null); + const [searchOnlyValue, setSearchOnlyValue] = useState(''); + const [searchFilterValue, setSearchFilterValue] = useState(queryValue); + + useEffect(() => { + if (queryValue === '') { + setSearchOnlyValue(''); + setSearchFilterValue(''); + } else if (queryValue.length > 0 && searchOnlyValue.length === 0) { + setSearchFilterValue(queryValue); + } + }, [queryValue, searchOnlyValue, searchFilterValue]); + const { value: filtersFocused, setFalse: setFiltersUnFocused, @@ -203,13 +217,6 @@ export function IndexFilters({ [onSort], ); - const handleChangeSearch = useCallback( - (value: string) => { - onQueryChange(value); - }, - [onQueryChange], - ); - const useExecutedCallback = ( action?: ExecutedCallback, afterEffect?: () => void, @@ -229,6 +236,8 @@ export function IndexFilters({ const onExecutedCancelAction = useCallback(() => { cancelAction?.onAction?.(); + // setSearchOnlyValue(''); + // setSearchFilterValue(''); setMode(IndexFiltersMode.Default); }, [cancelAction, setMode]); @@ -309,8 +318,21 @@ export function IndexFilters({ const isActionLoading = primaryAction?.loading || cancelAction?.loading; - function handleClickFilterButton() { + function handleHideFilters() { + cancelAction?.onAction(); + setMode(IndexFiltersMode.Default); + } + + const handleShowFilters = useCallback(() => { beginEdit(IndexFiltersMode.Filtering); + }, [beginEdit]); + + function handleClickFilterButton() { + if (mode === IndexFiltersMode.Filtering) { + handleHideFilters(); + } else { + handleShowFilters(); + } } const searchFilterTooltipLabelId = disableKeyboardShortcuts @@ -330,9 +352,52 @@ export function IndexFilters({ setMode(IndexFiltersMode.Default); } - function handleClearSearch() { - onQueryClear?.(); - } + const handleQueryChange = useCallback( + (input: 'searchOnly' | 'searchFilter') => (value: string) => { + if (input === 'searchOnly') { + onQueryChange(searchFilterValue ? searchFilterValue + value : value); + setSearchOnlyValue(value); + } else { + onQueryChange(searchOnlyValue ? `${value},${searchOnlyValue}` : value); + setSearchFilterValue(value); + } + }, + [searchFilterValue, searchOnlyValue, onQueryChange], + ); + + const handleAddAsFilter = useCallback(() => { + if (mode !== IndexFiltersMode.Filtering) { + handleShowFilters(); + } + if (searchOnlyValue) { + const augmentedQuery = searchFilterValue + ? `${searchFilterValue},${searchOnlyValue}` + : searchOnlyValue; + onQueryChange(augmentedQuery); + setSearchFilterValue(augmentedQuery); + setSearchOnlyValue(''); + } + }, [ + mode, + searchFilterValue, + searchOnlyValue, + onQueryChange, + handleShowFilters, + ]); + + const handleQueryClear = useCallback( + (input: 'searchOnly' | 'searchFilter') => () => { + if (input === 'searchOnly') { + setSearchOnlyValue(''); + onQueryChange(searchFilterValue); + } else { + onQueryClear?.(); + setSearchOnlyValue(''); + setSearchFilterValue(''); + } + }, + [searchFilterValue, onQueryChange, onQueryClear], + ); function handleQueryBlur() { setFiltersUnFocused(); @@ -347,9 +412,90 @@ export function IndexFilters({ if (mode !== IndexFiltersMode.Default) { return; } - beginEdit(IndexFiltersMode.Filtering); + + handleShowFilters(); } + const handleKeyDownEnter = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + onQueryChange( + searchOnlyValue + ? searchFilterValue + searchOnlyValue + : searchFilterValue, + ); + } + }, + [searchOnlyValue, searchFilterValue, onQueryChange], + ); + + const searchFilterLabel = i18n.translate( + 'Polaris.IndexFilters.SearchField.defaultPlaceholder', + ); + + const filtersWithSearch = [ + ...filters, + { + key: 'appliedSearchFilter', + label: searchFilterLabel, + hidden: true, + filter: ( +
+ +
+ ), + }, + ]; + + const getAppliedFilters = useMemo(() => { + let searchFilter; + + const supportsSavedFilters = + !hideQueryField && + !hideFilters && + primaryAction && + (primaryAction.type === 'save' || primaryAction.type === 'save-as'); + + if (queryValue && supportsSavedFilters && searchFilterValue) { + searchFilter = { + key: 'appliedSearchFilter', + label: `${searchFilterLabel}: ${searchFilterValue}`, + value: searchFilterValue, + unsavedChanges: appliedFilters?.find( + ({key}) => key.includes('search') || key.includes('query'), + )?.unsavedChanges, + onRemove: handleQueryClear('searchFilter'), + }; + } + + if (searchFilter) { + return Array.isArray(appliedFilters) + ? [...appliedFilters, searchFilter] + : [searchFilter]; + } + + return appliedFilters; + }, [ + queryValue, + hideFilters, + hideQueryField, + primaryAction, + appliedFilters, + searchFilterValue, + searchFilterLabel, + handleQueryClear, + ]); + return (
- - {(state) => ( -
- {mode !== IndexFiltersMode.Filtering ? ( - - -
-
- -
- {isLoading && mdDown && ( -
- -
- )} -
-
- {isLoading && !mdDown && ( -
- {isLoading ? : null} -
- )} - {mode === IndexFiltersMode.Default ? ( - <> - {hideFilters && hideQueryField ? null : ( - - )} - {editColumnsMarkup} - {sortMarkup} - - ) : null} - {mode === IndexFiltersMode.EditingColumns - ? updateButtonsMarkup - : null} -
-
-
- ) : null} -
- )} -
+
+ + +
+
+ +
+ {isLoading && mdDown && ( +
+ +
+ )} +
+
+ + {isLoading && !mdDown && ( +
+ {isLoading ? : null} +
+ )} + + {hideFilters ? null : ( + + )} + {editColumnsMarkup} + {sortMarkup} + {mode === IndexFiltersMode.EditingColumns + ? updateButtonsMarkup + : null} +
+
+
+
+ {mode === IndexFiltersMode.Filtering ? ( {}} + onQueryClear={() => {}} onAddFilterClick={onAddFilterClick} - filters={filters} - appliedFilters={appliedFilters} + filters={filtersWithSearch} + appliedFilters={getAppliedFilters} onClearAll={onClearAll} disableFilters={disabled} hideFilters={hideFilters} - hideQueryField={hideQueryField} - disableQueryField={disabled || disableQueryField} loading={loading || isActionLoading} focused={filtersFocused} mountedState={mdDown ? undefined : state} - borderlessQueryField closeOnChildOverlayClick={closeOnChildOverlayClick} >
diff --git a/polaris-react/src/components/IndexFilters/components/FilterButton/FilterButton.module.css b/polaris-react/src/components/IndexFilters/components/FilterButton/FilterButton.module.css new file mode 100644 index 00000000000..27ee0f393ef --- /dev/null +++ b/polaris-react/src/components/IndexFilters/components/FilterButton/FilterButton.module.css @@ -0,0 +1,6 @@ +.pressed > button { + /* stylelint-disable polaris/conventions/polaris/custom-property-allowed-list -- Filter section activator pressed state */ + background: var(--pc-button-bg_active); + color: var(--pc-button-color_active); + box-shadow: var(--pc-button-box-shadow_active); +} diff --git a/polaris-react/src/components/IndexFilters/components/SearchFilterButton/SearchFilterButton.tsx b/polaris-react/src/components/IndexFilters/components/FilterButton/FilterButton.tsx similarity index 55% rename from polaris-react/src/components/IndexFilters/components/SearchFilterButton/SearchFilterButton.tsx rename to polaris-react/src/components/IndexFilters/components/FilterButton/FilterButton.tsx index 6d218c6f836..7178e8d575a 100644 --- a/polaris-react/src/components/IndexFilters/components/SearchFilterButton/SearchFilterButton.tsx +++ b/polaris-react/src/components/IndexFilters/components/FilterButton/FilterButton.tsx @@ -1,48 +1,39 @@ import React from 'react'; -import type {CSSProperties} from 'react'; -import {SearchIcon, FilterIcon} from '@shopify/polaris-icons'; +import {FilterIcon} from '@shopify/polaris-icons'; -import {Icon} from '../../../Icon'; import {Tooltip} from '../../../Tooltip'; import {Text} from '../../../Text'; -import {InlineStack} from '../../../InlineStack'; import {Button} from '../../../Button'; -export interface SearchFilterButtonProps { +import styles from './FilterButton.module.css'; + +export interface FilterButtonProps { onClick: () => void; label: string; disabled?: boolean; + pressed?: boolean; tooltipContent: string; disclosureZIndexOverride?: number; - hideFilters?: boolean; - hideQueryField?: boolean; - style: CSSProperties; } -export function SearchFilterButton({ +export function FilterButton({ onClick, label, disabled, + pressed, tooltipContent, disclosureZIndexOverride, - style, - hideFilters, - hideQueryField, -}: SearchFilterButtonProps) { - const iconMarkup = ( - - {hideQueryField ? null : } - {hideFilters ? null : } - - ); +}: FilterButtonProps) { + const className = pressed ? styles.pressed : undefined; const activator = ( -
+
diff --git a/polaris-react/src/components/IndexFilters/components/FilterButton/index.ts b/polaris-react/src/components/IndexFilters/components/FilterButton/index.ts new file mode 100644 index 00000000000..a934688aba3 --- /dev/null +++ b/polaris-react/src/components/IndexFilters/components/FilterButton/index.ts @@ -0,0 +1 @@ +export {FilterButton} from './FilterButton'; diff --git a/polaris-react/src/components/IndexFilters/components/SearchField/SearchField.module.css b/polaris-react/src/components/IndexFilters/components/SearchField/SearchField.module.css new file mode 100644 index 00000000000..b0db19f8c58 --- /dev/null +++ b/polaris-react/src/components/IndexFilters/components/SearchField/SearchField.module.css @@ -0,0 +1,18 @@ +.AddAsFilterAction { + padding-block: var(--p-space-100); + background: none; + border: none; + color: unset; + cursor: pointer; + border-radius: var(--p-border-radius-100); + transition: color var(--p-motion-duration-150) var(--p-motion-ease-in-out); + + &:hover, + &:active { + color: var(--p-color-text); + + svg { + fill: var(--p-color-icon-secondary-hover); + } + } +} diff --git a/polaris-react/src/components/IndexFilters/components/SearchField/SearchField.tsx b/polaris-react/src/components/IndexFilters/components/SearchField/SearchField.tsx new file mode 100644 index 00000000000..d404ee91d6e --- /dev/null +++ b/polaris-react/src/components/IndexFilters/components/SearchField/SearchField.tsx @@ -0,0 +1,125 @@ +import React, {useId, useState} from 'react'; +import {SearchIcon, ReturnIcon} from '@shopify/polaris-icons'; + +import {Box} from '../../../Box'; +import {Icon} from '../../../Icon'; +import {TextField} from '../../../TextField'; +import {useBreakpoints} from '../../../../utilities/breakpoints'; +import {useI18n} from '../../../../utilities/i18n'; +import {InlineStack} from '../../../InlineStack'; +import {UnstyledButton} from '../../../UnstyledButton'; + +import styles from './SearchField.module.css'; + +export interface SearchFieldProps { + focused?: boolean; + value?: string; + placeholder?: string; + disabled?: boolean; + /** Shows a loading spinner to the right of the input */ + loading?: boolean; + onChange: (value: string) => void; + onFocus?: () => void; + onBlur?: () => void; + onClear?: () => void; + onKeyDownEnter?(): void; +} + +export function SearchField({ + focused: forceFocus = false, + value, + placeholder, + disabled, + loading, + onChange, + onClear, + onFocus, + onBlur, + onKeyDownEnter, +}: SearchFieldProps) { + const id = useId(); + const i18n = useI18n(); + const {mdUp} = useBreakpoints(); + const [focused, setFocused] = useState(forceFocus); + + function handleChange(eventValue: string) { + onChange(eventValue ?? value); + } + + function handleClear() { + if (onClear) { + onClear(); + } else { + onChange(''); + } + } + + function handleKeyDown(event: React.KeyboardEvent) { + if (event.key === 'Enter') onKeyDownEnter?.(); + } + + function handleClick() { + onKeyDownEnter?.(); + } + + function handleFocus() { + onFocus?.(); + setFocused(true); + } + + function handleBlur() { + onBlur?.(); + setFocused(false); + } + + const addAsFilterText = + value && focused ? ( + + + {i18n.translate( + 'Polaris.IndexFilters.SearchField.action.addAsFilter', + )} + + + + + + ) : undefined; + + return ( +
+ : undefined} + focused={focused} + label={ + placeholder ?? + i18n.translate('Polaris.IndexFilters.SearchField.defaultPlaceholder') + } + labelHidden + clearButton + loading={loading} + suffix={addAsFilterText} + /> +
+ ); +} diff --git a/polaris-react/src/components/IndexFilters/components/SearchField/index.ts b/polaris-react/src/components/IndexFilters/components/SearchField/index.ts new file mode 100644 index 00000000000..55415ea36b5 --- /dev/null +++ b/polaris-react/src/components/IndexFilters/components/SearchField/index.ts @@ -0,0 +1 @@ +export {SearchField} from './SearchField'; diff --git a/polaris-react/src/components/IndexFilters/components/SearchFilterButton/index.ts b/polaris-react/src/components/IndexFilters/components/SearchFilterButton/index.ts deleted file mode 100644 index 70da59a2ddc..00000000000 --- a/polaris-react/src/components/IndexFilters/components/SearchFilterButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {SearchFilterButton} from './SearchFilterButton'; diff --git a/polaris-react/src/components/IndexFilters/components/index.ts b/polaris-react/src/components/IndexFilters/components/index.ts index 956db0bf98b..9123cd62253 100644 --- a/polaris-react/src/components/IndexFilters/components/index.ts +++ b/polaris-react/src/components/IndexFilters/components/index.ts @@ -1,5 +1,6 @@ export {Container} from './Container'; -export {SearchFilterButton} from './SearchFilterButton'; +export {SearchField} from './SearchField'; +export {FilterButton} from './FilterButton'; export {SortButton} from './SortButton'; export {UpdateButtons} from './UpdateButtons'; export {EditColumnsButton} from './EditColumnsButton'; diff --git a/polaris-react/src/components/IndexFilters/tests/IndexFilters.test.tsx b/polaris-react/src/components/IndexFilters/tests/IndexFilters.test.tsx index ca857cb0f63..b9c2c007752 100644 --- a/polaris-react/src/components/IndexFilters/tests/IndexFilters.test.tsx +++ b/polaris-react/src/components/IndexFilters/tests/IndexFilters.test.tsx @@ -7,7 +7,7 @@ import {Filters} from '../../Filters'; import {IndexFilters, IndexFiltersMode} from '..'; import type {IndexFiltersProps} from '../IndexFilters'; import { - SearchFilterButton, + FilterButton, SortButton, UpdateButtons, EditColumnsButton, @@ -102,7 +102,7 @@ describe('IndexFilters', () => { , ); wrapper.act(() => { - wrapper.find(SearchFilterButton)!.trigger('onClick'); + wrapper.find(FilterButton)!.trigger('onClick'); }); expect(setMode).toHaveBeenCalledWith(IndexFiltersMode.Filtering); @@ -119,7 +119,7 @@ describe('IndexFilters', () => { />, ); wrapper.act(() => { - wrapper.find(SearchFilterButton)!.trigger('onClick'); + wrapper.find(FilterButton)!.trigger('onClick'); }); expect(onEditStart).toHaveBeenCalledWith(IndexFiltersMode.Filtering); @@ -155,15 +155,15 @@ describe('IndexFilters', () => { }); }); - it('renders the SearchFilterButton tooltipContent with keyboard shortcut by default', () => { + it('renders the FilterButton tooltipContent with keyboard shortcut by default', () => { const wrapper = mountWithApp(); - expect(wrapper).toContainReactComponent(SearchFilterButton, { + expect(wrapper).toContainReactComponent(FilterButton, { tooltipContent: 'Search and filter (F)', }); }); - it('passes the disclosureZIndexOverride to the SearchFilterButton when provided', () => { + it('passes the disclosureZIndexOverride to the FilterButton when provided', () => { const disclosureZIndexOverride = 517; const wrapper = mountWithApp( { />, ); - expect(wrapper).toContainReactComponent(SearchFilterButton, { + expect(wrapper).toContainReactComponent(FilterButton, { disclosureZIndexOverride, }); }); @@ -276,7 +276,7 @@ describe('IndexFilters', () => { expect(wrapper).not.toContainReactComponent(Filters); }); - it('does not render the SortButton or SearchFilterButton component', () => { + it('does not render the SortButton or FilterButton component', () => { const wrapper = mountWithApp( { ); expect(wrapper).not.toContainReactComponent(SortButton); - expect(wrapper).not.toContainReactComponent(SearchFilterButton); + expect(wrapper).not.toContainReactComponent(FilterButton); }); it('does not render the EditColumnsButton', () => { @@ -411,12 +411,12 @@ describe('IndexFilters', () => { }); describe('disableKeyboardShortcuts', () => { - it('renders the SearchFilterButton tooltipContent without the keyboard shortcut', () => { + it('renders the FilterButton tooltipContent without the keyboard shortcut', () => { const wrapper = mountWithApp( , ); - expect(wrapper).toContainReactComponent(SearchFilterButton, { + expect(wrapper).toContainReactComponent(FilterButton, { tooltipContent: 'Search and filter', }); }); diff --git a/polaris-react/src/components/TextField/TextField.module.css b/polaris-react/src/components/TextField/TextField.module.css index 99ed23ed5a9..f38c340d765 100644 --- a/polaris-react/src/components/TextField/TextField.module.css +++ b/polaris-react/src/components/TextField/TextField.module.css @@ -38,7 +38,7 @@ } /* stylelint-disable-next-line selector-max-specificity -- set Backdrop styles */ - &:not(.disabled):not(.error):not(.readOnly) + &:not(.disabled):not(.error):not(.readOnly):not(.borderless) > .Input:hover:not(:focus-visible) { /* stylelint-disable-next-line -- set Backdrop styles */ ~ .Backdrop { @@ -322,13 +322,25 @@ } } +/* stylelint-disable -- set secondary Backdrop styles */ .borderless { .Input, .Backdrop { - border: none; min-height: var(--p-space-800); + border-color: transparent; + + &:not(:active):not(:hover) { + border-color: var(--p-color-border-secondary); + } + + &.Input:hover:not(:focus-visible) { + ~ .Backdrop { + background-color: var(--p-color-input-bg-surface-hover); + } + } } } +/* stylelint-enable */ .slim { .Input, diff --git a/polaris-react/src/types.ts b/polaris-react/src/types.ts index 26327bf76f3..f6cafbca92f 100644 --- a/polaris-react/src/types.ts +++ b/polaris-react/src/types.ts @@ -369,14 +369,20 @@ export type NonEmptyArray = [T, ...T[]]; export type ArrayElement = T extends (infer U)[] ? U : never; export interface AppliedFilterInterface { + /** Whether or not the filter can be removed */ + locked?: boolean; /** A unique key used to identify the filter */ key: string; - /** The name of the filter */ + /** The name of the filter. + * The rendered applied filter pill label will prefix the value with the label in standardized format. For example, label 'Product vender' and value ['Tootsie Roll Industries LLC' or 'The Hershey Company'] will be parsed into human readable label 'Product vendor: Tootsie Roll Industries LLC or The Hershey Company' + */ label: string; + /** The human readable filter input value */ + value?: any; /** Whether the filter is newly applied or updated and hasn't been saved */ unsavedChanges?: boolean; /** Callback when the remove button is pressed */ - onRemove(key: string): void; + onRemove?(key: string): void; } export interface FilterInterface { @@ -384,6 +390,8 @@ export interface FilterInterface { key: string; /** The name of the filter */ label: string; + /** The current filter input value */ + value?: any; /** The markup for the given filter */ filter: React.ReactNode; /** Whether or not the filter should have a shortcut popover displayed */